From c0fcbe284977f044eb0c583d7fe09d992dd2e7f5 Mon Sep 17 00:00:00 2001 From: zhaoxi Date: Sat, 6 Jun 2026 04:43:08 +0000 Subject: [PATCH] =?UTF-8?q?feat(mcp):=20=E5=AE=8C=E6=95=B4=E8=90=BD?= =?UTF-8?q?=E5=9C=B0=20MCP=20=E6=9C=8D=E5=8A=A1=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dockerfile 后端 stage 加 nodejs/npm,支持 stdio MCP server - 新增前端 MCP 服务管理 UI(MCPSettings.tsx),支持 stdio/sse/http 三种 transport - PluginLayout 改双 tab(技能 / MCP 服务) - i18n 补 MCP 相关 zh/en 文案 - send_file 工具的 trace_id 改可选,聊天场景退化为返回内容 - system_workflow 工具集纳入 send_file Co-Authored-By: Claude Opus 4.7 --- Dockerfile | 3 + .../src/components/Plugin/MCPSettings.tsx | 378 ++++++++++++++++++ .../src/components/Plugin/PluginLayout.tsx | 34 +- frontend/src/i18n/locales/en.json | 29 +- frontend/src/i18n/locales/zh.json | 29 +- .../global_state_machine.py | 4 +- .../plugin/tool_plugin/send_file/send_file.py | 15 +- 7 files changed, 480 insertions(+), 12 deletions(-) create mode 100644 frontend/src/components/Plugin/MCPSettings.tsx diff --git a/Dockerfile b/Dockerfile index 80e805e..00ef40f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,10 +13,13 @@ FROM python:3.13-slim WORKDIR /app # Install system dependencies (for building PostgreSQL drivers and other native extensions) +# nodejs/npm are needed for stdio-mode MCP servers (e.g. npx -y @modelcontextprotocol/...) RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ libpq-dev \ git \ + nodejs \ + npm \ && rm -rf /var/lib/apt/lists/* # Install uv package manager diff --git a/frontend/src/components/Plugin/MCPSettings.tsx b/frontend/src/components/Plugin/MCPSettings.tsx new file mode 100644 index 0000000..1af6085 --- /dev/null +++ b/frontend/src/components/Plugin/MCPSettings.tsx @@ -0,0 +1,378 @@ +import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Plus, Trash2, Server, Loader2, Plug, ChevronDown, ChevronRight } from 'lucide-react'; +import apiClient from '../../api/client'; + +type Transport = 'stdio' | 'sse' | 'http'; + +interface MCPServer { + server_id: string; + name: string; + transport: Transport; + command?: string; + args?: string[]; + url?: string; + tool_prefix?: string; + env?: Record; +} + +interface MCPToolInfo { + server_id: string; + name: string; + transport: string; + tools: string[]; + error?: string; +} + +export function MCPSettings() { + const { t } = useTranslation(); + const [servers, setServers] = useState([]); + const [toolsByServer, setToolsByServer] = useState>({}); + const [loading, setLoading] = useState(true); + const [expanded, setExpanded] = useState(null); + + // Form state + const [name, setName] = useState(''); + const [transport, setTransport] = useState('stdio'); + const [command, setCommand] = useState(''); + const [argsText, setArgsText] = useState(''); + const [url, setUrl] = useState(''); + const [toolPrefix, setToolPrefix] = useState(''); + const [envText, setEnvText] = useState(''); + const [submitting, setSubmitting] = useState(false); + const [message, setMessage] = useState(''); + const [error, setError] = useState(''); + + const fetchServers = async () => { + setLoading(true); + try { + const response = await apiClient.get('/api/v1/resource/mcp'); + setServers(response.data.servers || []); + } catch (err) { + console.error('Failed to fetch MCP servers:', err); + } finally { + setLoading(false); + } + }; + + const fetchToolsForAll = async () => { + try { + const response = await apiClient.get('/api/v1/resource/tool'); + const mcpTools: MCPToolInfo[] = response.data.mcp_servers || []; + const map: Record = {}; + mcpTools.forEach((t) => { + if (t.server_id) map[t.server_id] = t; + }); + setToolsByServer(map); + } catch (err) { + console.error('Failed to fetch MCP tools:', err); + } + }; + + useEffect(() => { + fetchServers(); + fetchToolsForAll(); + }, []); + + const resetForm = () => { + setName(''); + setCommand(''); + setArgsText(''); + setUrl(''); + setToolPrefix(''); + setEnvText(''); + }; + + const parseEnv = (text: string): Record => { + const env: Record = {}; + text.split('\n').forEach((line) => { + const trimmed = line.trim(); + if (!trimmed) return; + const eq = trimmed.indexOf('='); + if (eq > 0) { + env[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim(); + } + }); + return env; + }; + + const handleAdd = async (e: React.FormEvent) => { + e.preventDefault(); + if (!name) return; + setSubmitting(true); + setMessage(''); + setError(''); + + const payload: any = { name, transport, tool_prefix: toolPrefix || null }; + if (transport === 'stdio') { + payload.command = command; + payload.args = argsText + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + } else { + payload.url = url; + } + const env = parseEnv(envText); + if (Object.keys(env).length > 0) payload.env = env; + + try { + await apiClient.post('/api/v1/resource/mcp', payload); + setMessage(t('plugin.mcpAddSuccess')); + resetForm(); + await fetchServers(); + await fetchToolsForAll(); + } catch (err: any) { + setError(err.response?.data?.detail || t('plugin.mcpAddFailed')); + } finally { + setSubmitting(false); + } + }; + + const handleDelete = async (server: MCPServer) => { + if (!confirm(t('plugin.mcpDeleteConfirm', { name: server.name }))) return; + try { + await apiClient.delete(`/api/v1/resource/mcp/${server.server_id}`); + await fetchServers(); + await fetchToolsForAll(); + } catch { + alert(t('plugin.mcpDeleteFailed')); + } + }; + + return ( +
+
+
+ +

{t('plugin.mcpManagement')}

+
+

{t('plugin.mcpDesc')}

+
+ +
+
+
+ +
+

{t('plugin.mcpAdd')}

+
+
+
+
+
+ + setName(e.target.value)} + placeholder={t('plugin.mcpNamePlaceholder')} + className="w-full px-3.5 py-2.5 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent placeholder:text-text-muted/50" + /> +
+
+ + +
+
+ + {transport === 'stdio' ? ( +
+
+ + setCommand(e.target.value)} + placeholder={t('plugin.mcpCommandPlaceholder')} + className="w-full px-3.5 py-2.5 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent placeholder:text-text-muted/50" + /> +
+
+ + setArgsText(e.target.value)} + placeholder={t('plugin.mcpArgsPlaceholder')} + className="w-full px-3.5 py-2.5 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent placeholder:text-text-muted/50" + /> +
+
+ ) : ( +
+ + setUrl(e.target.value)} + placeholder={t('plugin.mcpUrlPlaceholder')} + className="w-full px-3.5 py-2.5 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent placeholder:text-text-muted/50" + /> +
+ )} + +
+ + setToolPrefix(e.target.value)} + placeholder={t('plugin.mcpToolPrefixPlaceholder')} + className="w-full px-3.5 py-2.5 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent placeholder:text-text-muted/50" + /> +
+ +
+ +