diff --git a/package.json b/package.json index a6a8c5d8a..35154c75f 100644 --- a/package.json +++ b/package.json @@ -160,6 +160,7 @@ "@langchain/community": "^1.0.0", "@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch", "@langchain/openai": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch", + "@mcp-ui/client": "^5.14.1", "@mistralai/mistralai": "^1.7.5", "@modelcontextprotocol/sdk": "^1.17.5", "@mozilla/readability": "^0.6.0", diff --git a/src/main/mcpServers/factory.ts b/src/main/mcpServers/factory.ts index 2323701e4..1e5cd88b3 100644 --- a/src/main/mcpServers/factory.ts +++ b/src/main/mcpServers/factory.ts @@ -8,6 +8,7 @@ import DiDiMcpServer from './didi-mcp' import DifyKnowledgeServer from './dify-knowledge' import FetchServer from './fetch' import FileSystemServer from './filesystem' +import MCPUIDemoServer from './mcp-ui-demo' import MemoryServer from './memory' import PythonServer from './python' import ThinkingServer from './sequentialthinking' @@ -48,6 +49,9 @@ export function createInMemoryMCPServer( const apiKey = envs.DIDI_API_KEY return new DiDiMcpServer(apiKey).server } + case BuiltinMCPServerNames.mcpUIDemo: { + return new MCPUIDemoServer().server + } default: throw new Error(`Unknown in-memory MCP server: ${name}`) } diff --git a/src/main/mcpServers/mcp-ui-demo.ts b/src/main/mcpServers/mcp-ui-demo.ts new file mode 100644 index 000000000..2b56bdeb0 --- /dev/null +++ b/src/main/mcpServers/mcp-ui-demo.ts @@ -0,0 +1,433 @@ +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js' + +const server = new Server( + { + name: 'mcp-ui-demo', + version: '1.0.0' + }, + { + capabilities: { + tools: {} + } + } +) + +// HTML templates for different UIs +const getHelloWorldUI = () => + ` + + + + + + +
+

🎉 Hello from MCP UI!

+

This is a simple MCP UI Resource rendered in Cherry Studio

+
+ + +`.trim() + +const getInteractiveUI = () => + ` + + + + + + +
+

Interactive MCP UI Demo

+

Click the buttons to interact with MCP tools:

+ + + + + +
+ +
+ This UI can communicate with the host application through postMessage API. +
+
+ + + + +`.trim() + +const getFormUI = () => + ` + + + + + + +
+

📝 Form UI Demo

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+
+ + + + +`.trim() + +// List available tools +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: 'demo_echo', + description: 'Echo back the message sent from UI', + inputSchema: { + type: 'object', + properties: { + message: { + type: 'string', + description: 'Message to echo back' + } + }, + required: ['message'] + } + }, + { + name: 'show_hello_ui', + description: 'Display a simple hello world UI with gradient background', + inputSchema: { + type: 'object', + properties: {} + } + }, + { + name: 'show_interactive_ui', + description: + 'Display an interactive UI demo with buttons for calling tools, getting timestamps, and opening links', + inputSchema: { + type: 'object', + properties: {} + } + }, + { + name: 'show_form_ui', + description: 'Display a form UI demo with input fields for name, email, and message', + inputSchema: { + type: 'object', + properties: {} + } + } + ] + } +}) + +// Handle tool calls +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params + + if (name === 'demo_echo') { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + echo: args?.message || 'No message provided', + timestamp: new Date().toISOString() + }) + } + ] + } + } + + if (name === 'show_hello_ui') { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + type: 'resource', + resource: { + uri: 'ui://demo/hello', + mimeType: 'text/html', + text: getHelloWorldUI() + } + }) + } + ] + } + } + + if (name === 'show_interactive_ui') { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + type: 'resource', + resource: { + uri: 'ui://demo/interactive', + mimeType: 'text/html', + text: getInteractiveUI() + } + }) + } + ] + } + } + + if (name === 'show_form_ui') { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + type: 'resource', + resource: { + uri: 'ui://demo/form', + mimeType: 'text/html', + text: getFormUI() + } + }) + } + ] + } + } + + throw new Error(`Unknown tool: ${name}`) +}) + +class MCPUIDemoServer { + public server: Server + constructor() { + this.server = server + } +} + +export default MCPUIDemoServer diff --git a/src/renderer/src/components/MCPUIRenderer/MCPUIRenderer.tsx b/src/renderer/src/components/MCPUIRenderer/MCPUIRenderer.tsx new file mode 100644 index 000000000..1951a3f6f --- /dev/null +++ b/src/renderer/src/components/MCPUIRenderer/MCPUIRenderer.tsx @@ -0,0 +1,157 @@ +import { loggerService } from '@logger' +import type { UIActionResult } from '@mcp-ui/client' +import { UIResourceRenderer } from '@mcp-ui/client' +import type { EmbeddedResource } from '@modelcontextprotocol/sdk/types.js' +import { isUIResource } from '@renderer/types' +import type { FC } from 'react' +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +const logger = loggerService.withContext('MCPUIRenderer') + +interface Props { + resource: EmbeddedResource + serverId?: string + serverName?: string + onToolCall?: (toolName: string, params: any) => Promise +} + +const MCPUIRenderer: FC = ({ resource, onToolCall }) => { + const { t } = useTranslation() + const [error] = useState(null) + + const handleUIAction = useCallback( + async (result: UIActionResult): Promise => { + logger.debug('UI Action received:', result) + + try { + switch (result.type) { + case 'tool': { + // Handle tool call from UI + if (onToolCall) { + const { toolName, params } = result.payload + logger.info(`UI requesting tool call: ${toolName}`, { params }) + const response = await onToolCall(toolName, params) + + // Check if the response contains a UIResource + try { + if (response && response.content && Array.isArray(response.content)) { + const firstContent = response.content[0] + if (firstContent && firstContent.type === 'text' && firstContent.text) { + const parsedText = JSON.parse(firstContent.text) + if (isUIResource(parsedText)) { + // Return the UIResource directly for rendering in the iframe + logger.info('Tool response contains UIResource:', { uri: parsedText.resource.uri }) + return { status: 'success', data: parsedText } + } + } + } + } catch (parseError) { + // Not a UIResource, return the original response + logger.debug('Tool response is not a UIResource') + } + + return { status: 'success', data: response } + } else { + logger.warn('Tool call requested but no handler provided') + return { status: 'error', message: 'Tool call handler not available' } + } + } + + case 'intent': { + // Handle user intent + logger.info('UI intent:', result.payload) + window.toast.info(t('message.mcp.ui.intent_received')) + return { status: 'acknowledged' } + } + + case 'notify': { + // Handle notification from UI + logger.info('UI notification:', result.payload) + window.toast.info(result.payload.message || t('message.mcp.ui.notification')) + return { status: 'acknowledged' } + } + + case 'prompt': { + // Handle prompt request from UI + logger.info('UI prompt request:', result.payload) + // TODO: Integrate with prompt system + return { status: 'error', message: 'Prompt execution not yet implemented' } + } + + case 'link': { + // Handle navigation request + const { url } = result.payload + logger.info('UI navigation request:', { url }) + window.open(url, '_blank') + return { status: 'acknowledged' } + } + + default: + logger.warn('Unknown UI action type:', { result }) + return { status: 'error', message: 'Unknown action type' } + } + } catch (err) { + logger.error('Error handling UI action:', err as Error) + return { + status: 'error', + message: err instanceof Error ? err.message : 'Unknown error' + } + } + }, + [onToolCall, t] + ) + + if (error) { + return ( + + {t('message.mcp.ui.error')} + {error} + + ) + } + + return ( + + + + ) +} + +const UIContainer = styled.div` + width: 100%; + min-height: 400px; + border-radius: 8px; + overflow: hidden; + background: var(--color-background); + border: 1px solid var(--color-border); + + iframe { + width: 100%; + border: none; + min-height: 400px; + height: 600px; + } +` + +const ErrorContainer = styled.div` + padding: 16px; + border-radius: 8px; + background: var(--color-error-bg, #fee); + border: 1px solid var(--color-error-border, #fcc); + color: var(--color-error-text, #c33); +` + +const ErrorTitle = styled.div` + font-weight: 600; + margin-bottom: 8px; + font-size: 14px; +` + +const ErrorMessage = styled.div` + font-size: 13px; + opacity: 0.9; +` + +export default MCPUIRenderer diff --git a/src/renderer/src/components/MCPUIRenderer/index.ts b/src/renderer/src/components/MCPUIRenderer/index.ts new file mode 100644 index 000000000..eb5cb63b2 --- /dev/null +++ b/src/renderer/src/components/MCPUIRenderer/index.ts @@ -0,0 +1 @@ +export { default as MCPUIRenderer } from './MCPUIRenderer' diff --git a/src/renderer/src/i18n/label.ts b/src/renderer/src/i18n/label.ts index bd74ecd45..2ec4644bd 100644 --- a/src/renderer/src/i18n/label.ts +++ b/src/renderer/src/i18n/label.ts @@ -342,7 +342,8 @@ const builtInMcpDescriptionKeyMap: Record = { [BuiltinMCPServerNames.filesystem]: 'settings.mcp.builtinServersDescriptions.filesystem', [BuiltinMCPServerNames.difyKnowledge]: 'settings.mcp.builtinServersDescriptions.dify_knowledge', [BuiltinMCPServerNames.python]: 'settings.mcp.builtinServersDescriptions.python', - [BuiltinMCPServerNames.didiMCP]: 'settings.mcp.builtinServersDescriptions.didi_mcp' + [BuiltinMCPServerNames.didiMCP]: 'settings.mcp.builtinServersDescriptions.didi_mcp', + [BuiltinMCPServerNames.mcpUIDemo]: 'settings.mcp.builtinServersDescriptions.mcp_ui_demo' } as const export const getBuiltInMcpServerDescriptionLabel = (key: string): string => { diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 329ec7879..5fb05b240 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1824,6 +1824,13 @@ "preparing": "Preparing to export to Notion..." } }, + "mcp": { + "ui": { + "error": "UI Render Error", + "intent_received": "Intent received from UI", + "notification": "Notification from UI" + } + }, "mention": { "title": "Switch model answer" }, @@ -3851,6 +3858,7 @@ "fetch": "MCP server for retrieving URL web content", "filesystem": "A Node.js server implementing the Model Context Protocol (MCP) for file system operations. Requires configuration of directories allowed for access.", "mcp_auto_install": "Automatically install MCP service (beta)", + "mcp_ui_demo": "MCP UI Demo server with interactive UI resources showcase. Demonstrates HTML-based UI resources with buttons, forms, and tool call integration", "memory": "Persistent memory implementation based on a local knowledge graph. This enables the model to remember user-related information across different conversations. Requires configuring the MEMORY_FILE_PATH environment variable.", "no": "No description", "python": "Execute Python code in a secure sandbox environment. Run Python with Pyodide, supporting most standard libraries and scientific computing packages", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 0d44039a1..43b3d700e 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1824,6 +1824,13 @@ "preparing": "正在准备导出到 Notion..." } }, + "mcp": { + "ui": { + "error": "[to be translated]:UI Render Error", + "intent_received": "[to be translated]:Intent received from UI", + "notification": "[to be translated]:Notification from UI" + } + }, "mention": { "title": "切换模型回答" }, @@ -3851,6 +3858,7 @@ "fetch": "用于获取 URL 网页内容的 MCP 服务器", "filesystem": "实现文件系统操作的模型上下文协议(MCP)的 Node.js 服务器。需要配置允许访问的目录", "mcp_auto_install": "自动安装 MCP 服务(测试版)", + "mcp_ui_demo": "[to be translated]:MCP UI Demo server with interactive UI resources showcase. Demonstrates HTML-based UI resources with buttons, forms, and tool call integration", "memory": "基于本地知识图谱的持久性记忆基础实现。这使得模型能够在不同对话间记住用户的相关信息。需要配置 MEMORY_FILE_PATH 环境变量。", "no": "无描述", "python": "在安全的沙盒环境中执行 Python 代码。使用 Pyodide 运行 Python,支持大多数标准库和科学计算包", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 58f036ce7..cfda6d53c 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1824,6 +1824,13 @@ "preparing": "正在準備匯出到 Notion..." } }, + "mcp": { + "ui": { + "error": "[to be translated]:UI Render Error", + "intent_received": "[to be translated]:Intent received from UI", + "notification": "[to be translated]:Notification from UI" + } + }, "mention": { "title": "切換模型回答" }, @@ -3851,6 +3858,7 @@ "fetch": "用於獲取 URL 網頁內容的 MCP 伺服器", "filesystem": "實現文件系統操作的模型上下文協議(MCP)的 Node.js 伺服器。需要配置允許訪問的目錄", "mcp_auto_install": "自動安裝 MCP 服務(測試版)", + "mcp_ui_demo": "[to be translated]:MCP UI Demo server with interactive UI resources showcase. Demonstrates HTML-based UI resources with buttons, forms, and tool call integration", "memory": "基於本地知識圖譜的持久性記憶基礎實現。這使得模型能夠在不同對話間記住使用者的相關資訊。需要配置 MEMORY_FILE_PATH 環境變數。", "no": "無描述", "python": "在安全的沙盒環境中執行 Python 代碼。使用 Pyodide 運行 Python,支援大多數標準庫和科學計算套件", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index 4cdadd638..4c8844916 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -1824,6 +1824,13 @@ "preparing": "Export nach Notion wird vorbereitet..." } }, + "mcp": { + "ui": { + "error": "[to be translated]:UI Render Error", + "intent_received": "[to be translated]:Intent received from UI", + "notification": "[to be translated]:Notification from UI" + } + }, "mention": { "title": "Modellantwort wechseln" }, @@ -3851,6 +3858,7 @@ "fetch": "MCP-Server zum Abrufen von Webseiteninhalten", "filesystem": "MCP-Server für Dateisystemoperationen (Node.js), der den Zugriff auf bestimmte Verzeichnisse ermöglicht", "mcp_auto_install": "MCP-Service automatisch installieren (Beta-Version)", + "mcp_ui_demo": "[to be translated]:MCP UI Demo server with interactive UI resources showcase. Demonstrates HTML-based UI resources with buttons, forms, and tool call integration", "memory": "MCP-Server mit persistenter Erinnerungsbasis auf lokalem Wissensgraphen, der Informationen über verschiedene Dialoge hinweg speichert. MEMORY_FILE_PATH-Umgebungsvariable muss konfiguriert werden", "no": "Keine Beschreibung", "python": "Python-Code in einem sicheren Sandbox-Umgebung ausführen. Verwendung von Pyodide für Python, Unterstützung für die meisten Standardbibliotheken und wissenschaftliche Pakete", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 5175611ba..09f5e2ed3 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -1824,6 +1824,13 @@ "preparing": "Ετοιμάζεται η εξαγωγή στο Notion..." } }, + "mcp": { + "ui": { + "error": "[to be translated]:UI Render Error", + "intent_received": "[to be translated]:Intent received from UI", + "notification": "[to be translated]:Notification from UI" + } + }, "mention": { "title": "Εναλλαγή απάντησης αστρόναυτη" }, @@ -3851,6 +3858,7 @@ "fetch": "Εξυπηρετητής MCP για λήψη περιεχομένου ιστοσελίδας URL", "filesystem": "Εξυπηρετητής Node.js για το πρωτόκολλο περιβάλλοντος μοντέλου (MCP) που εφαρμόζει λειτουργίες συστήματος αρχείων. Απαιτείται διαμόρφωση για την επιτροπή πρόσβασης σε καταλόγους", "mcp_auto_install": "Αυτόματη εγκατάσταση υπηρεσίας MCP (προβολή)", + "mcp_ui_demo": "[to be translated]:MCP UI Demo server with interactive UI resources showcase. Demonstrates HTML-based UI resources with buttons, forms, and tool call integration", "memory": "Βασική υλοποίηση μόνιμης μνήμης με βάση τοπικό γράφημα γνώσης. Αυτό επιτρέπει στο μοντέλο να θυμάται πληροφορίες σχετικές με τον χρήστη ανάμεσα σε διαφορετικές συνομιλίες. Απαιτείται η ρύθμιση της μεταβλητής περιβάλλοντος MEMORY_FILE_PATH.", "no": "Χωρίς περιγραφή", "python": "Εκτελέστε κώδικα Python σε ένα ασφαλές περιβάλλον sandbox. Χρησιμοποιήστε το Pyodide για να εκτελέσετε Python, υποστηρίζοντας την πλειονότητα των βιβλιοθηκών της τυπικής βιβλιοθήκης και των πακέτων επιστημονικού υπολογισμού", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 9b3923cf9..8c24c98e4 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -1824,6 +1824,13 @@ "preparing": "Preparando para exportar a Notion..." } }, + "mcp": { + "ui": { + "error": "[to be translated]:UI Render Error", + "intent_received": "[to be translated]:Intent received from UI", + "notification": "[to be translated]:Notification from UI" + } + }, "mention": { "title": "Cambiar modelo de respuesta" }, @@ -3851,6 +3858,7 @@ "fetch": "Servidor MCP para obtener el contenido de la página web de una URL", "filesystem": "Servidor Node.js que implementa el protocolo de contexto del modelo (MCP) para operaciones del sistema de archivos. Requiere configuración del directorio permitido para el acceso", "mcp_auto_install": "Instalación automática del servicio MCP (versión beta)", + "mcp_ui_demo": "[to be translated]:MCP UI Demo server with interactive UI resources showcase. Demonstrates HTML-based UI resources with buttons, forms, and tool call integration", "memory": "Implementación básica de memoria persistente basada en un grafo de conocimiento local. Esto permite que el modelo recuerde información relevante del usuario entre diferentes conversaciones. Es necesario configurar la variable de entorno MEMORY_FILE_PATH.", "no": "sin descripción", "python": "Ejecuta código Python en un entorno sandbox seguro. Usa Pyodide para ejecutar Python, compatible con la mayoría de las bibliotecas estándar y paquetes de cálculo científico.", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 8212e2787..9c8d2c5a9 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -1824,6 +1824,13 @@ "preparing": "Préparation pour l'exportation vers Notion..." } }, + "mcp": { + "ui": { + "error": "[to be translated]:UI Render Error", + "intent_received": "[to be translated]:Intent received from UI", + "notification": "[to be translated]:Notification from UI" + } + }, "mention": { "title": "Changer le modèle de réponse" }, @@ -3851,6 +3858,7 @@ "fetch": "serveur MCP utilisé pour récupérer le contenu des pages web URL", "filesystem": "Serveur Node.js implémentant le protocole de contexte de modèle (MCP) pour les opérations de système de fichiers. Nécessite une configuration des répertoires autorisés à être accédés.", "mcp_auto_install": "Installation automatique du service MCP (version bêta)", + "mcp_ui_demo": "[to be translated]:MCP UI Demo server with interactive UI resources showcase. Demonstrates HTML-based UI resources with buttons, forms, and tool call integration", "memory": "Implémentation de base de mémoire persistante basée sur un graphe de connaissances local. Cela permet au modèle de se souvenir des informations relatives à l'utilisateur entre différentes conversations. Nécessite la configuration de la variable d'environnement MEMORY_FILE_PATH.", "no": "sans description", "python": "Exécutez du code Python dans un environnement bac à sable sécurisé. Utilisez Pyodide pour exécuter Python, prenant en charge la plupart des bibliothèques standard et des packages de calcul scientifique.", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 46817adcc..df491e68b 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -1824,6 +1824,13 @@ "preparing": "Notionへのエクスポートを準備中..." } }, + "mcp": { + "ui": { + "error": "[to be translated]:UI Render Error", + "intent_received": "[to be translated]:Intent received from UI", + "notification": "[to be translated]:Notification from UI" + } + }, "mention": { "title": "モデルを切り替える" }, @@ -3851,6 +3858,7 @@ "fetch": "URLのウェブページコンテンツを取得するためのMCPサーバー", "filesystem": "Node.jsサーバーによるファイルシステム操作を実現するモデルコンテキストプロトコル(MCP)。アクセスを許可するディレクトリの設定が必要です", "mcp_auto_install": "MCPサービスの自動インストール(ベータ版)", + "mcp_ui_demo": "[to be translated]:MCP UI Demo server with interactive UI resources showcase. Demonstrates HTML-based UI resources with buttons, forms, and tool call integration", "memory": "ローカルのナレッジグラフに基づく永続的なメモリの基本的な実装です。これにより、モデルは異なる会話間でユーザーの関連情報を記憶できるようになります。MEMORY_FILE_PATH 環境変数の設定が必要です。", "no": "説明なし", "python": "安全なサンドボックス環境でPythonコードを実行します。Pyodideを使用してPythonを実行し、ほとんどの標準ライブラリと科学計算パッケージをサポートしています。", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 805c2e837..82677b8f0 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -1824,6 +1824,13 @@ "preparing": "Preparando exportação para Notion..." } }, + "mcp": { + "ui": { + "error": "[to be translated]:UI Render Error", + "intent_received": "[to be translated]:Intent received from UI", + "notification": "[to be translated]:Notification from UI" + } + }, "mention": { "title": "Alternar modelo de resposta" }, @@ -3851,6 +3858,7 @@ "fetch": "servidor MCP para obter o conteúdo da página web do URL", "filesystem": "Servidor Node.js do protocolo de contexto de modelo (MCP) para implementar operações de sistema de ficheiros. Requer configuração do diretório permitido para acesso", "mcp_auto_install": "Instalação automática do serviço MCP (beta)", + "mcp_ui_demo": "[to be translated]:MCP UI Demo server with interactive UI resources showcase. Demonstrates HTML-based UI resources with buttons, forms, and tool call integration", "memory": "Implementação base de memória persistente baseada em grafos de conhecimento locais. Isso permite que o modelo lembre informações relevantes do utilizador entre diferentes conversas. É necessário configurar a variável de ambiente MEMORY_FILE_PATH.", "no": "sem descrição", "python": "Executar código Python num ambiente sandbox seguro. Utilizar Pyodide para executar Python, suportando a maioria das bibliotecas padrão e pacotes de computação científica", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 82bde21a8..e01427e56 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -1824,6 +1824,13 @@ "preparing": "Подготовка к экспорту в Notion..." } }, + "mcp": { + "ui": { + "error": "[to be translated]:UI Render Error", + "intent_received": "[to be translated]:Intent received from UI", + "notification": "[to be translated]:Notification from UI" + } + }, "mention": { "title": "Переключить модель ответа" }, @@ -3851,6 +3858,7 @@ "fetch": "MCP-сервер для получения содержимого веб-страниц по URL", "filesystem": "Node.js-сервер протокола контекста модели (MCP) для реализации операций файловой системы. Требуется настройка каталогов, к которым разрешён доступ", "mcp_auto_install": "Автоматическая установка службы MCP (бета-версия)", + "mcp_ui_demo": "[to be translated]:MCP UI Demo server with interactive UI resources showcase. Demonstrates HTML-based UI resources with buttons, forms, and tool call integration", "memory": "реализация постоянной памяти на основе локального графа знаний. Это позволяет модели запоминать информацию о пользователе между различными диалогами. Требуется настроить переменную среды MEMORY_FILE_PATH.", "no": "без описания", "python": "Выполняйте код Python в безопасной песочнице. Запускайте Python с помощью Pyodide, поддерживается большинство стандартных библиотек и пакетов для научных вычислений", diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx index 455e64d05..6a77e2bd1 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx @@ -1,10 +1,11 @@ import { loggerService } from '@logger' import { CopyIcon, LoadingIcon } from '@renderer/components/Icons' +import { MCPUIRenderer } from '@renderer/components/MCPUIRenderer' import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { useMCPServers } from '@renderer/hooks/useMCPServers' import { useSettings } from '@renderer/hooks/useSettings' import { useTimer } from '@renderer/hooks/useTimer' -import type { MCPToolResponse } from '@renderer/types' +import { isUIResource, type MCPToolResponse } from '@renderer/types' import type { ToolMessageBlock } from '@renderer/types/newMessage' import { isToolAutoApproved } from '@renderer/utils/mcp-tools' import { cancelToolAction, confirmToolAction } from '@renderer/utils/userConfirmation' @@ -342,26 +343,54 @@ const MessageMcpTool: FC = ({ block }) => { try { logger.debug(`renderPreview: ${content}`) const parsedResult = JSON.parse(content) - switch (parsedResult.content[0]?.type) { + + // Handle regular MCP tool responses + const mcpResponse: any = parsedResult + switch (mcpResponse.content?.[0]?.type) { case 'text': try { - return ( - - ) + // Try to parse the text content to check if it's a UIResource + const textContent = JSON.parse(mcpResponse.content[0].text) + if (isUIResource(textContent)) { + logger.info('Rendering UI Resource from MCP text response:', { uri: textContent.resource.uri }) + return ( + { + // Handle tool call from UI + logger.info(`Tool call from UI: ${toolName}`, params) + try { + const server = mcpServers.find((s) => s.id === tool.serverId) + if (!server) { + throw new Error('Server not found') + } + const result = await window.api.mcp.callTool({ + server, + name: toolName, + args: params + }) + return result + } catch (error) { + logger.error('Error calling tool from UI:', error as Error) + throw error + } + }} + /> + ) + } + // Regular JSON text content + return } catch (e) { + // Not JSON or parsing failed, display as string return ( - + ) } default: - return + return } } catch (e) { logger.error('failed to render the preview of mcp results:', e as Error) diff --git a/src/renderer/src/store/mcp.ts b/src/renderer/src/store/mcp.ts index aef38bcbe..8a5816f55 100644 --- a/src/renderer/src/store/mcp.ts +++ b/src/renderer/src/store/mcp.ts @@ -174,6 +174,16 @@ export const builtinMCPServers: BuiltinMCPServer[] = [ provider: 'CherryAI', installSource: 'builtin', isTrusted: true + }, + { + id: nanoid(), + name: BuiltinMCPServerNames.mcpUIDemo, + type: 'inMemory', + isActive: false, + shouldConfig: true, + provider: 'CherryAI', + installSource: 'builtin', + isTrusted: true } ] as const diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index f82fec8f0..b178c851e 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -731,7 +731,8 @@ export const BuiltinMCPServerNames = { filesystem: '@cherry/filesystem', difyKnowledge: '@cherry/dify-knowledge', python: '@cherry/python', - didiMCP: '@cherry/didi-mcp' + didiMCP: '@cherry/didi-mcp', + mcpUIDemo: '@cherry/mcp-ui-demo' } as const export type BuiltinMCPServerName = (typeof BuiltinMCPServerNames)[keyof typeof BuiltinMCPServerNames] @@ -841,6 +842,22 @@ export interface GetResourceResponse { contents: MCPResource[] } +// MCP UI Resource types +export type UIResourceMimeType = 'text/html' | 'text/uri-list' | 'application/vnd.mcp-ui.remote-dom' + +export interface UIResource { + type: 'resource' + resource: { + uri: string // starts with 'ui://' + mimeType: UIResourceMimeType + text?: string + blob?: string + } +} + +// Re-export isUIResource from @mcp-ui/client +export { isUIResource } from '@mcp-ui/client' + export interface QuickPhrase { id: string title: string diff --git a/yarn.lock b/yarn.lock index d1810fac7..5282cdf98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4657,6 +4657,22 @@ __metadata: languageName: node linkType: hard +"@mcp-ui/client@npm:^5.14.1": + version: 5.14.1 + resolution: "@mcp-ui/client@npm:5.14.1" + dependencies: + "@modelcontextprotocol/sdk": "npm:*" + "@quilted/threads": "npm:^3.1.3" + "@r2wc/react-to-web-component": "npm:^2.0.4" + "@remote-dom/core": "npm:^1.8.0" + "@remote-dom/react": "npm:^1.2.2" + peerDependencies: + react: ^18 || ^19 + react-dom: ^18 || ^19 + checksum: 10c0/8045c7ccb3e9333b53b23ed432b3b667d715bb90e799980865e03f65ccc8e566b09d93195825ffda3bcfb47ffa48db5464d981bcbbb8099b6101b865ebdbd55f + languageName: node + linkType: hard + "@mermaid-js/parser@npm:^0.6.2": version: 0.6.2 resolution: "@mermaid-js/parser@npm:0.6.2" @@ -4684,6 +4700,32 @@ __metadata: languageName: node linkType: hard +"@modelcontextprotocol/sdk@npm:*": + version: 1.22.0 + resolution: "@modelcontextprotocol/sdk@npm:1.22.0" + dependencies: + ajv: "npm:^8.17.1" + ajv-formats: "npm:^3.0.1" + content-type: "npm:^1.0.5" + cors: "npm:^2.8.5" + cross-spawn: "npm:^7.0.5" + eventsource: "npm:^3.0.2" + eventsource-parser: "npm:^3.0.0" + express: "npm:^5.0.1" + express-rate-limit: "npm:^7.5.0" + pkce-challenge: "npm:^5.0.0" + raw-body: "npm:^3.0.0" + zod: "npm:^3.23.8" + zod-to-json-schema: "npm:^3.24.1" + peerDependencies: + "@cfworker/json-schema": ^4.1.1 + peerDependenciesMeta: + "@cfworker/json-schema": + optional: true + checksum: 10c0/71f4bef238715c248aa197ce820f95ac4fc75c7c311fc7d88c2e01d12692bffc5c9b9e3fe4c266ae0cca5df08c5b2fb6d60ab05d8905399665b67417d297903e + languageName: node + linkType: hard + "@modelcontextprotocol/sdk@npm:^1.17.5": version: 1.17.5 resolution: "@modelcontextprotocol/sdk@npm:1.17.5" @@ -5392,6 +5434,13 @@ __metadata: languageName: node linkType: hard +"@preact/signals-core@npm:^1.8.0": + version: 1.12.1 + resolution: "@preact/signals-core@npm:1.12.1" + checksum: 10c0/06e73a9b6b90ef5967687eb64003cc7c1abae1950ade55941c3eefeca5d4f642010bce3f267f00663703d7f1509c51ced4751733b7ef5dcba29707fe38d375a1 + languageName: node + linkType: hard + "@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": version: 1.1.2 resolution: "@protobufjs/aspromise@npm:1.1.2" @@ -5474,6 +5523,48 @@ __metadata: languageName: node linkType: hard +"@quilted/events@npm:^2.1.3": + version: 2.1.3 + resolution: "@quilted/events@npm:2.1.3" + dependencies: + "@preact/signals-core": "npm:^1.8.0" + checksum: 10c0/7f0906c7f15d956f0e9aaa7e736c97b0a130e7cf651efb3b64ffc5868b352197d029acfd55787a36abf41064608421c5653de958b1490e6fb00948a942ee829d + languageName: node + linkType: hard + +"@quilted/threads@npm:^3.1.3": + version: 3.3.1 + resolution: "@quilted/threads@npm:3.3.1" + dependencies: + "@quilted/events": "npm:^2.1.3" + peerDependencies: + "@preact/signals-core": ^1.8.0 + peerDependenciesMeta: + "@preact/signals-core": + optional: true + checksum: 10c0/ca9ed767aa65d5052c30f144a607c0f39721ab1cb91675b9fd0d05195605010ab182a50a946858b33d6c6d4f3947b3296245d6e05f9e4646d6f37bdc39175e70 + languageName: node + linkType: hard + +"@r2wc/core@npm:^1.3.0": + version: 1.3.0 + resolution: "@r2wc/core@npm:1.3.0" + checksum: 10c0/cfa561f8f872c9f3337b963ca23ce0810157019c202a5144c4f45b8257ec348f1f3ccced4875f0ba633097663e3cfd8a1bd55d25b37cad76aeb526e8a596eacf + languageName: node + linkType: hard + +"@r2wc/react-to-web-component@npm:^2.0.4": + version: 2.1.0 + resolution: "@r2wc/react-to-web-component@npm:2.1.0" + dependencies: + "@r2wc/core": "npm:^1.3.0" + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + checksum: 10c0/04fd1f45afd8ffa0567763abf5aa67c0d1b09be75f36390b90e6b4cd6e5e1a80c6793387868f9df600998367c86caee55dd9a8ed0c73be9d662e2925de2c0faf + languageName: node + linkType: hard + "@radix-ui/primitive@npm:1.1.3": version: 1.1.3 resolution: "@radix-ui/primitive@npm:1.1.3" @@ -6103,6 +6194,46 @@ __metadata: languageName: node linkType: hard +"@remote-dom/core@npm:^1.7.0, @remote-dom/core@npm:^1.8.0": + version: 1.10.1 + resolution: "@remote-dom/core@npm:1.10.1" + dependencies: + "@remote-dom/polyfill": "npm:^1.5.1" + htm: "npm:^3.1.1" + peerDependencies: + "@preact/signals-core": ^1.3.0 + peerDependenciesMeta: + "@preact/signals-core": + optional: true + preact: + optional: true + checksum: 10c0/53db62196471f8201665accfad2bc1ca8db4b305d2485bdd57cd700975e93d1ce02f0abf95a90ad1d36dd8c06ff91e81e153bb9dd2ab487a5e7a6f827b3b8145 + languageName: node + linkType: hard + +"@remote-dom/polyfill@npm:^1.5.1": + version: 1.5.1 + resolution: "@remote-dom/polyfill@npm:1.5.1" + checksum: 10c0/753836a286214e9ac8d6246501d4d7824fe600ae4b49c3fe6ac5467934b5a15aeb0f80e773f7c7219f829ac5758be077468ede6da9a5de14260f3627ffcd6e1f + languageName: node + linkType: hard + +"@remote-dom/react@npm:^1.2.2": + version: 1.2.2 + resolution: "@remote-dom/react@npm:1.2.2" + dependencies: + "@remote-dom/core": "npm:^1.7.0" + "@types/react": "npm:^18.0.0" + htm: "npm:^3.1.1" + peerDependencies: + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + checksum: 10c0/87883e06dab4a2a52cf9aa6f397166a25054aebf0a9f401bfb67831038dac59fb549754054a297617ba7ca8f33c8967765570fed3cf27a280d921d8f73d2dd64 + languageName: node + linkType: hard + "@replit/codemirror-lang-nix@npm:^6.0.1": version: 6.0.1 resolution: "@replit/codemirror-lang-nix@npm:6.0.1" @@ -8749,6 +8880,13 @@ __metadata: languageName: node linkType: hard +"@types/prop-types@npm:*": + version: 15.7.15 + resolution: "@types/prop-types@npm:15.7.15" + checksum: 10c0/b59aad1ad19bf1733cf524fd4e618196c6c7690f48ee70a327eb450a42aab8e8a063fbe59ca0a5701aebe2d92d582292c0fb845ea57474f6a15f6994b0e260b2 + languageName: node + linkType: hard + "@types/qs@npm:*": version: 6.14.0 resolution: "@types/qs@npm:6.14.0" @@ -8808,6 +8946,16 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^18.0.0": + version: 18.3.27 + resolution: "@types/react@npm:18.3.27" + dependencies: + "@types/prop-types": "npm:*" + csstype: "npm:^3.2.2" + checksum: 10c0/a761d2f58de03d0714806cc65d32bb3d73fb33a08dd030d255b47a295e5fff2a775cf1c20b786824d8deb6454eaccce9bc6998d9899c14fc04bbd1b0b0b72897 + languageName: node + linkType: hard + "@types/react@npm:^19.0.12": version: 19.1.2 resolution: "@types/react@npm:19.1.2" @@ -9982,6 +10130,7 @@ __metadata: "@langchain/openai": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch" "@libsql/client": "npm:0.14.0" "@libsql/win32-x64-msvc": "npm:^0.4.7" + "@mcp-ui/client": "npm:^5.14.1" "@mistralai/mistralai": "npm:^1.7.5" "@modelcontextprotocol/sdk": "npm:^1.17.5" "@mozilla/readability": "npm:^0.6.0" @@ -10341,6 +10490,20 @@ __metadata: languageName: node linkType: hard +"ajv-formats@npm:^3.0.1": + version: 3.0.1 + resolution: "ajv-formats@npm:3.0.1" + dependencies: + ajv: "npm:^8.0.0" + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: 10c0/168d6bca1ea9f163b41c8147bae537e67bd963357a5488a1eaf3abe8baa8eec806d4e45f15b10767e6020679315c7e1e5e6803088dfb84efa2b4e9353b83dd0a + languageName: node + linkType: hard + "ajv-keywords@npm:^3.4.1": version: 3.5.2 resolution: "ajv-keywords@npm:3.5.2" @@ -10362,7 +10525,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.6.3": +"ajv@npm:^8.0.0, ajv@npm:^8.17.1, ajv@npm:^8.6.3": version: 8.17.1 resolution: "ajv@npm:8.17.1" dependencies: @@ -12325,6 +12488,13 @@ __metadata: languageName: node linkType: hard +"csstype@npm:^3.2.2": + version: 3.2.3 + resolution: "csstype@npm:3.2.3" + checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce + languageName: node + linkType: hard + "csv-parse@npm:^5.6.0": version: 5.6.0 resolution: "csv-parse@npm:5.6.0" @@ -16017,6 +16187,13 @@ __metadata: languageName: node linkType: hard +"htm@npm:^3.1.1": + version: 3.1.1 + resolution: "htm@npm:3.1.1" + checksum: 10c0/0de4c8fff2b8e76c162235ae80dbf93ca5eef1575bd50596a06ce9bebf1a6da5efc467417c53034a9ffa2ab9ecff819cbec041dc9087894b2b900ad4de26c7e7 + languageName: node + linkType: hard + "html-encoding-sniffer@npm:^4.0.0": version: 4.0.0 resolution: "html-encoding-sniffer@npm:4.0.0"