Compare commits

...

1 Commits

Author SHA1 Message Date
suyao
22ed2b605e feat: Add MCP UI Demo server and integrate UI rendering capabilities 2025-11-24 12:41:03 +08:00
20 changed files with 926 additions and 16 deletions

View File

@@ -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",

View File

@@ -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}`)
}

View File

@@ -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 = () =>
`
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
}
.container {
text-align: center;
}
h1 {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
p {
font-size: 1.2em;
opacity: 0.9;
}
</style>
</head>
<body>
<div class="container">
<h1>🎉 Hello from MCP UI!</h1>
<p>This is a simple MCP UI Resource rendered in Cherry Studio</p>
</div>
</body>
</html>
`.trim()
const getInteractiveUI = () =>
`
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
padding: 20px;
background: #f5f5f5;
margin: 0;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h2 {
color: #333;
margin-bottom: 20px;
}
button {
background: #667eea;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
margin: 5px;
transition: background 0.2s;
}
button:hover {
background: #5568d3;
}
#output {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 6px;
border: 1px solid #e0e0e0;
min-height: 50px;
}
.info {
color: #666;
font-size: 14px;
margin-top: 15px;
}
</style>
</head>
<body>
<div class="container">
<h2>Interactive MCP UI Demo</h2>
<p>Click the buttons to interact with MCP tools:</p>
<button onclick="callEchoTool()">Call Echo Tool</button>
<button onclick="getTimestamp()">Get Timestamp</button>
<button onclick="openLink()">Open External Link</button>
<div id="output"></div>
<div class="info">
This UI can communicate with the host application through postMessage API.
</div>
</div>
<script>
function callEchoTool() {
const output = document.getElementById('output');
output.innerHTML = '<p style="color: #0066cc;">Calling echo tool...</p>';
window.parent.postMessage({
type: 'tool',
payload: {
toolName: 'demo_echo',
params: {
message: 'Hello from MCP UI! Time: ' + new Date().toLocaleTimeString()
}
}
}, '*');
}
function getTimestamp() {
const output = document.getElementById('output');
const now = new Date();
output.innerHTML = \`
<p style="color: #00aa00;">
<strong>Current Timestamp:</strong><br/>
\${now.toLocaleString()}<br/>
Unix: \${Math.floor(now.getTime() / 1000)}
</p>
\`;
}
function openLink() {
window.parent.postMessage({
type: 'link',
payload: {
url: 'https://github.com/idosal/mcp-ui'
}
}, '*');
}
// Listen for responses
window.addEventListener('message', (event) => {
if (event.data.type === 'ui-message-response') {
const output = document.getElementById('output');
const { response, error } = event.data.payload;
if (error) {
output.innerHTML = \`<p style="color: #cc0000;">Error: \${error}</p>\`;
} else {
output.innerHTML = \`<p style="color: #00aa00;">Response: \${JSON.stringify(response, null, 2)}</p>\`;
}
}
});
</script>
</body>
</html>
`.trim()
const getFormUI = () =>
`
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
padding: 20px;
background: #f5f5f5;
margin: 0;
}
.container {
max-width: 500px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h2 {
color: #333;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: 500;
}
input, textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
}
textarea {
min-height: 100px;
resize: vertical;
}
button {
background: #667eea;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
width: 100%;
margin-top: 10px;
}
button:hover {
background: #5568d3;
}
#result {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 6px;
display: none;
}
</style>
</head>
<body>
<div class="container">
<h2>📝 Form UI Demo</h2>
<form id="demoForm" onsubmit="handleSubmit(event)">
<div class="form-group">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required placeholder="Enter your name">
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required placeholder="your@email.com">
</div>
<div class="form-group">
<label for="message">Message:</label>
<textarea id="message" name="message" required placeholder="Enter your message here..."></textarea>
</div>
<button type="submit">Submit Form</button>
</form>
<div id="result"></div>
</div>
<script>
function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = Object.fromEntries(formData.entries());
const result = document.getElementById('result');
result.style.display = 'block';
result.innerHTML = '<p style="color: #0066cc;">Submitting form...</p>';
// Send form data to host
window.parent.postMessage({
type: 'notify',
payload: {
message: 'Form submitted with data: ' + JSON.stringify(data)
}
}, '*');
// Display result
result.innerHTML = \`
<p style="color: #00aa00;"><strong>Form Submitted!</strong></p>
<pre style="background: white; padding: 10px; border-radius: 4px; overflow-x: auto;">\${JSON.stringify(data, null, 2)}</pre>
\`;
}
</script>
</body>
</html>
`.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

View File

@@ -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<any>
}
const MCPUIRenderer: FC<Props> = ({ resource, onToolCall }) => {
const { t } = useTranslation()
const [error] = useState<string | null>(null)
const handleUIAction = useCallback(
async (result: UIActionResult): Promise<any> => {
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 (
<ErrorContainer>
<ErrorTitle>{t('message.mcp.ui.error')}</ErrorTitle>
<ErrorMessage>{error}</ErrorMessage>
</ErrorContainer>
)
}
return (
<UIContainer>
<UIResourceRenderer resource={resource} onUIAction={handleUIAction} />
</UIContainer>
)
}
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

View File

@@ -0,0 +1 @@
export { default as MCPUIRenderer } from './MCPUIRenderer'

View File

@@ -342,7 +342,8 @@ const builtInMcpDescriptionKeyMap: Record<BuiltinMCPServerName, string> = {
[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 => {

View File

@@ -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",

View File

@@ -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支持大多数标准库和科学计算包",

View File

@@ -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支援大多數標準庫和科學計算套件",

View File

@@ -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",

View File

@@ -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, υποστηρίζοντας την πλειονότητα των βιβλιοθηκών της τυπικής βιβλιοθήκης και των πακέτων επιστημονικού υπολογισμού",

View File

@@ -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.",

View File

@@ -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.",

View File

@@ -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を実行し、ほとんどの標準ライブラリと科学計算パッケージをサポートしています。",

View File

@@ -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",

View File

@@ -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, поддерживается большинство стандартных библиотек и пакетов для научных вычислений",

View File

@@ -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<Props> = ({ 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 (
<CollapsedContent
isExpanded={true}
resultString={JSON.stringify(JSON.parse(parsedResult.content[0].text), null, 2)}
/>
)
// 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 (
<MCPUIRenderer
resource={textContent.resource}
serverId={tool.serverId}
serverName={tool.serverName}
onToolCall={async (toolName, params) => {
// 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 <CollapsedContent isExpanded={true} resultString={JSON.stringify(textContent, null, 2)} />
} catch (e) {
// Not JSON or parsing failed, display as string
return (
<CollapsedContent
isExpanded={true}
resultString={JSON.stringify(parsedResult.content[0].text, null, 2)}
/>
<CollapsedContent isExpanded={true} resultString={JSON.stringify(mcpResponse.content[0].text, null, 2)} />
)
}
default:
return <CollapsedContent isExpanded={true} resultString={JSON.stringify(parsedResult, null, 2)} />
return <CollapsedContent isExpanded={true} resultString={JSON.stringify(mcpResponse, null, 2)} />
}
} catch (e) {
logger.error('failed to render the preview of mcp results:', e as Error)

View File

@@ -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

View File

@@ -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

179
yarn.lock
View File

@@ -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"