From 7eb086cade9a9d21e5d2436f01d205c30ad6ecb9 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 10 Jun 2026 17:17:37 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(ai):=20=E6=96=B0=E5=A2=9E=20MC?= =?UTF-8?q?P=20=E8=8D=89=E7=A8=BF=E6=A0=A1=E9=AA=8C=E6=8E=A2=E9=92=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 inspect_mcp_draft 内置工具,支持完整命令和分字段草稿校验 - 补充 MCP 新增指引、斜杠菜单和工具目录流程 - 增加工具注册、执行器和系统提示相关测试 --- .../ai/AIBuiltinToolsCatalog.test.tsx | 2 + .../components/ai/AIBuiltinToolsCatalog.tsx | 4 +- ...calToolExecutor.aiConfigInspection.test.ts | 24 +++ .../ai/aiMCPDraftInspectionInsights.test.ts | 50 +++++ .../ai/aiMCPDraftInspectionInsights.ts | 194 ++++++++++++++++++ .../src/components/ai/aiSlashCommands.test.ts | 7 + frontend/src/components/ai/aiSlashCommands.ts | 3 +- ...iSnapshotInspectionAIConfigToolExecutor.ts | 7 + .../ai/aiSystemContextMessages.test.ts | 3 +- .../ai/aiSystemInspectionGuidance.ts | 8 +- .../messageBubble/AIMessageStatusBlocks.tsx | 1 + .../src/utils/aiBuiltinInspectionToolInfo.ts | 34 +++ frontend/src/utils/aiToolRegistry.test.ts | 9 + 13 files changed, 341 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/ai/aiMCPDraftInspectionInsights.test.ts create mode 100644 frontend/src/components/ai/aiMCPDraftInspectionInsights.ts diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx index c3c9c66..89f95ab 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx @@ -42,6 +42,8 @@ describe('AIBuiltinToolsCatalog', () => { expect(markup).toContain('inspect_mcp_setup'); expect(markup).toContain('新增 MCP 填写指引'); expect(markup).toContain('inspect_mcp_authoring_guide'); + expect(markup).toContain('inspect_mcp_draft'); + expect(markup).toContain('真实校验器试算'); expect(markup).toContain('查看 MCP 工具参数'); expect(markup).toContain('inspect_mcp_tool_schema'); expect(markup).toContain('inputSchema'); diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx index 5cf4e33..feba9dd 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx @@ -77,8 +77,8 @@ const BUILTIN_TOOL_FLOWS = [ }, { title: '新增 MCP 填写指引', - steps: 'inspect_mcp_authoring_guide → inspect_mcp_setup', - description: '适合先读真实字段说明、模板样例和整行命令拆分规则,再结合当前 MCP 配置现状判断应该新增哪种启动方式。', + steps: 'inspect_mcp_authoring_guide → inspect_mcp_draft → inspect_mcp_setup', + description: '适合先读真实字段说明、模板样例和整行命令拆分规则,再把用户贴出的命令或草稿交给真实校验器试算,最后结合当前 MCP 配置现状判断应该新增哪种启动方式。', }, { title: '查看 MCP 工具参数', diff --git a/frontend/src/components/ai/aiLocalToolExecutor.aiConfigInspection.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.aiConfigInspection.test.ts index 4357cbb..760849e 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.aiConfigInspection.test.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.aiConfigInspection.test.ts @@ -323,6 +323,30 @@ describe('aiLocalToolExecutor AI config inspection tools', () => { expect(result.content).toContain('"exampleLaunchPreview":"uvx some-mcp-server"'); }); + it('validates an mcp draft with the real command splitter and server validator', async () => { + const result = await executeLocalAIToolCall({ + toolCall: buildToolCall('inspect_mcp_draft', { + fullCommand: '$env:GITHUB_TOKEN="ghp test"; uvx mcp-server-github --stdio', + timeoutSeconds: 45, + }), + connections: [buildConnection()], + mcpTools: [], + toolContextMap: new Map(), + runtime: { + getDatabases: vi.fn(), + getTables: vi.fn(), + }, + }); + + expect(result.success).toBe(true); + expect(result.content).toContain('"command":"uvx"'); + expect(result.content).toContain('"args":["mcp-server-github","--stdio"]'); + expect(result.content).toContain('"envKeys":["GITHUB_TOKEN"]'); + expect(result.content).toContain('"launchCommandPreview":"uvx mcp-server-github --stdio"'); + expect(result.content).toContain('"recommendedTemplate":{"key":"uvx"'); + expect(result.content).toContain('"canSave":true'); + }); + it('returns mcp tool input schemas so the model can build arguments from discovered tool metadata', async () => { const result = await executeLocalAIToolCall({ toolCall: buildToolCall('inspect_mcp_tool_schema', { diff --git a/frontend/src/components/ai/aiMCPDraftInspectionInsights.test.ts b/frontend/src/components/ai/aiMCPDraftInspectionInsights.test.ts new file mode 100644 index 0000000..ef1a4d7 --- /dev/null +++ b/frontend/src/components/ai/aiMCPDraftInspectionInsights.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; + +import { buildMCPDraftInspectionSnapshot } from './aiMCPDraftInspectionInsights'; + +describe('aiMCPDraftInspectionInsights', () => { + it('parses a full MCP launch command and returns reusable field values', () => { + const snapshot = buildMCPDraftInspectionSnapshot({ + fullCommand: '$env:GITHUB_TOKEN="ghp test"; uvx mcp-server-github --stdio', + timeoutSeconds: 45, + }); + + expect(snapshot.parse).toMatchObject({ + ok: true, + command: 'uvx', + args: ['mcp-server-github', '--stdio'], + envKeys: ['GITHUB_TOKEN'], + }); + expect(snapshot.draft.launchCommandPreview).toBe('uvx mcp-server-github --stdio'); + expect(snapshot.draft.envKeys).toEqual(['GITHUB_TOKEN']); + expect(snapshot.draft.timeoutSeconds).toBe(45); + expect(snapshot.draft.recommendedTemplate).toMatchObject({ + key: 'uvx', + title: 'uvx 工具', + confidence: 'high', + }); + expect(snapshot.validation.canSave).toBe(true); + expect(snapshot.nextActions).toContain('当前草稿可以保存并测试工具发现;如果发现 0 个工具,再检查服务是否支持 stdio。'); + }); + + it('validates split fields and returns concrete next actions for common mistakes', () => { + const snapshot = buildMCPDraftInspectionSnapshot({ + command: 'npx -y @modelcontextprotocol/server-filesystem --stdio', + args: ['env', 'GITHUB_TOKEN=abc'], + envText: 'export TOKEN=abc', + timeoutSeconds: 1, + }); + + expect(snapshot.draft.command).toBe('npx -y @modelcontextprotocol/server-filesystem --stdio'); + expect(snapshot.validation.errorCount).toBe(1); + expect(snapshot.validation.warningCount).toBeGreaterThanOrEqual(3); + expect(snapshot.validation.issues.map((issue) => issue.key)).toEqual(expect.arrayContaining([ + 'command-whole-line', + 'args-contain-env-or-shell-glue', + 'env-invalid-lines', + 'timeout-out-of-range', + ])); + expect(snapshot.nextActions.join('\n')).toContain('把整行命令放到完整命令框自动拆分'); + expect(snapshot.nextActions.join('\n')).toContain('环境变量改成每行 KEY=VALUE'); + }); +}); diff --git a/frontend/src/components/ai/aiMCPDraftInspectionInsights.ts b/frontend/src/components/ai/aiMCPDraftInspectionInsights.ts new file mode 100644 index 0000000..f900813 --- /dev/null +++ b/frontend/src/components/ai/aiMCPDraftInspectionInsights.ts @@ -0,0 +1,194 @@ +import type { AIMCPServerConfig } from '../../types'; +import { parseMCPCommandDraft } from '../../utils/mcpCommandDraft'; +import { parseMCPEnvDraft } from '../../utils/mcpEnvDraft'; +import { buildMCPLaunchPreview } from '../../utils/mcpServerGuidance'; +import { MCP_SERVER_DRAFT_TEMPLATES } from '../../utils/mcpServerTemplates'; +import { validateMCPServerDraft } from '../../utils/mcpServerValidation'; + +const toTrimmedString = (value: unknown): string => String(value ?? '').trim(); + +const normalizeArgs = (value: unknown): string[] => { + if (Array.isArray(value)) { + return value.map(toTrimmedString).filter(Boolean); + } + const text = toTrimmedString(value); + if (!text) { + return []; + } + return text + .split(/\r?\n|,/u) + .map(toTrimmedString) + .filter(Boolean); +}; + +const normalizeTimeoutSeconds = (value: unknown, fallback: number): number => { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallback; +}; + +const getTemplateSeed = (templateKey: unknown): Partial => { + const normalizedKey = toTrimmedString(templateKey).toLowerCase(); + if (!normalizedKey) { + return {}; + } + return MCP_SERVER_DRAFT_TEMPLATES.find((template) => template.key === normalizedKey)?.seed || {}; +}; + +const resolveRecommendedTemplate = (command: string, args: string[]) => { + const normalizedCommand = toTrimmedString(command).toLowerCase(); + if (!normalizedCommand) { + return null; + } + + const commandTemplate = MCP_SERVER_DRAFT_TEMPLATES.find((template) => { + const seedCommand = toTrimmedString(template.seed.command).toLowerCase(); + if (seedCommand === normalizedCommand) { + return true; + } + return template.key === 'exe' && /\.(exe|cmd|bat)$/iu.test(normalizedCommand); + }); + if (!commandTemplate) { + return null; + } + + return { + key: commandTemplate.key, + title: commandTemplate.title, + description: commandTemplate.description, + exampleLaunchPreview: buildMCPLaunchPreview( + toTrimmedString(commandTemplate.seed.command), + Array.isArray(commandTemplate.seed.args) ? commandTemplate.seed.args : [], + ), + confidence: args.length > 0 ? 'high' : 'medium', + }; +}; + +const buildNextActions = (params: { + errorCount: number; + warningCount: number; + issueKeys: Set; + hasFullCommand: boolean; +}): string[] => { + const { errorCount, warningCount, issueKeys, hasFullCommand } = params; + const actions: string[] = []; + + if (issueKeys.has('command-missing')) { + actions.push('先粘贴 README 里的完整启动命令,或至少填写 node、npx、uvx、python、exe 之一作为 command。'); + } + if (issueKeys.has('command-whole-line')) { + actions.push('把整行命令放到完整命令框自动拆分;command 只保留可执行程序,脚本名、包名和 --stdio 放到 args。'); + } + if (issueKeys.has('args-missing-for-launcher')) { + actions.push('给启动器补齐参数:npx 通常需要 -y 和包名,node 需要 server.js,python 需要 -m 模块名,uvx 需要包名。'); + } + if (issueKeys.has('args-contain-env-or-shell-glue') || issueKeys.has('env-invalid-lines')) { + actions.push('环境变量改成每行 KEY=VALUE;不要把 export、set、env、&& 或 $env:KEY=VALUE; 放进 args。'); + } + if (issueKeys.has('timeout-out-of-range')) { + actions.push('把 timeout 调整到 20 秒;慢启动服务可改成 45 或 60 秒。'); + } + if (errorCount === 0 && warningCount === 0) { + actions.push('当前草稿可以保存并测试工具发现;如果发现 0 个工具,再检查服务是否支持 stdio。'); + } else if (errorCount === 0) { + actions.push('当前草稿可以测试,但建议先处理 warning,避免工具发现超时或发现 0 个工具。'); + } + if (!hasFullCommand) { + actions.push('如果仍不确定怎么拆,优先把原始完整命令传给 fullCommand 让 GoNavi 试算。'); + } + + return actions; +}; + +export const buildMCPDraftInspectionSnapshot = (args: Record = {}) => { + const templateSeed = getTemplateSeed(args.templateKey); + const fullCommand = toTrimmedString(args.fullCommand ?? args.commandLine ?? args.rawCommand); + const parsedCommand = fullCommand ? parseMCPCommandDraft(fullCommand) : null; + const envDraftText = toTrimmedString(args.envText ?? args.envDraft); + const parsedEnvDraft = envDraftText ? parseMCPEnvDraft(envDraftText) : undefined; + + const baseName = toTrimmedString(templateSeed.name) || 'MCP 草稿'; + let command = toTrimmedString(templateSeed.command); + let commandArgs = Array.isArray(templateSeed.args) ? templateSeed.args.map(toTrimmedString).filter(Boolean) : []; + let env: Record = { ...(templateSeed.env || {}) }; + + if (parsedCommand?.ok && parsedCommand.draft) { + command = parsedCommand.draft.command; + commandArgs = parsedCommand.draft.args; + env = { + ...env, + ...parsedCommand.draft.env, + }; + } + if (args.command !== undefined) { + command = toTrimmedString(args.command); + } + if (args.args !== undefined) { + commandArgs = normalizeArgs(args.args); + } + if (parsedEnvDraft) { + env = { + ...env, + ...parsedEnvDraft.env, + }; + } + + const server: Pick = { + name: toTrimmedString(args.name ?? args.serverName) || baseName, + transport: 'stdio', + command, + args: commandArgs, + timeoutSeconds: normalizeTimeoutSeconds(args.timeoutSeconds, Number(templateSeed.timeoutSeconds) || 20), + }; + const validation = validateMCPServerDraft(server, parsedEnvDraft); + const issueKeys = new Set(validation.issues.map((issue) => issue.key)); + const recommendedTemplate = resolveRecommendedTemplate(command, commandArgs); + + return { + input: { + hasFullCommand: Boolean(fullCommand), + templateKey: toTrimmedString(args.templateKey), + fullCommand, + }, + parse: parsedCommand + ? { + ok: parsedCommand.ok, + error: parsedCommand.error || '', + command: parsedCommand.draft?.command || '', + args: parsedCommand.draft?.args || [], + envKeys: Object.keys(parsedCommand.draft?.env || {}).sort(), + } + : { + ok: false, + error: '未提供 fullCommand,已按分字段草稿校验。', + command: '', + args: [], + envKeys: [], + }, + draft: { + name: server.name, + transport: server.transport, + command, + args: commandArgs, + envKeys: Object.keys(env).sort(), + envVarCount: Object.keys(env).length, + invalidEnvLines: parsedEnvDraft?.invalidLines || [], + timeoutSeconds: server.timeoutSeconds, + launchCommandPreview: buildMCPLaunchPreview(command, commandArgs), + recommendedTemplate, + }, + validation: { + errorCount: validation.errorCount, + warningCount: validation.warningCount, + infoCount: validation.infoCount, + canTest: validation.canTest, + canSave: validation.canSave, + issues: validation.issues, + }, + nextActions: buildNextActions({ + errorCount: validation.errorCount, + warningCount: validation.warningCount, + issueKeys, + hasFullCommand: Boolean(fullCommand), + }), + }; +}; diff --git a/frontend/src/components/ai/aiSlashCommands.test.ts b/frontend/src/components/ai/aiSlashCommands.test.ts index 6605778..c27717f 100644 --- a/frontend/src/components/ai/aiSlashCommands.test.ts +++ b/frontend/src/components/ai/aiSlashCommands.test.ts @@ -14,6 +14,7 @@ describe('aiSlashCommands', () => { expect(commands.some((command) => command.cmd === '/health')).toBe(true); expect(commands.some((command) => command.cmd === '/mcp')).toBe(true); expect(commands.some((command) => command.cmd === '/mcpadd')).toBe(true); + expect(commands.some((command) => command.cmd === '/mcpdraft')).toBe(true); expect(commands.some((command) => command.cmd === '/mcptool')).toBe(true); expect(commands.some((command) => command.cmd === '/connfail')).toBe(true); expect(commands.some((command) => command.cmd === '/shortcuts')).toBe(true); @@ -54,6 +55,12 @@ describe('aiSlashCommands', () => { expect(filterAISlashCommands('/mcpt').map((command) => command.cmd)).toContain('/mcptool'); }); + it('supports filtering mcp draft validation diagnostics by keyword and command prefix', () => { + expect(filterAISlashCommands('MCP草稿').map((command) => command.cmd)).toContain('/mcpdraft'); + expect(filterAISlashCommands('启动命令').map((command) => command.cmd)).toContain('/mcpdraft'); + expect(filterAISlashCommands('/mcpd').map((command) => command.cmd)).toContain('/mcpdraft'); + }); + it('groups commands by configured category order', () => { const groups = groupAISlashCommands(filterAISlashCommands('/')); diff --git a/frontend/src/components/ai/aiSlashCommands.ts b/frontend/src/components/ai/aiSlashCommands.ts index 473bcdc..3ee6dea 100644 --- a/frontend/src/components/ai/aiSlashCommands.ts +++ b/frontend/src/components/ai/aiSlashCommands.ts @@ -49,7 +49,8 @@ export const DEFAULT_AI_SLASH_COMMANDS: AISlashCommandDefinition[] = [ { cmd: '/index', label: '📊 索引建议', desc: '推荐最优索引方案', prompt: '请基于当前表结构和常见查询场景,推荐最优的索引方案并给出建表语句:', category: 'review', keywords: ['index', '索引', '慢查询'] }, { cmd: '/health', label: '🩺 AI 配置体检', desc: '调用体检探针总览当前 AI 配置', prompt: '请先调用 inspect_ai_setup_health,对当前 GoNavi AI 配置做一次完整体检,然后总结 blockers、warnings 和 nextActions。', category: 'diagnose', featured: true, keywords: ['health', '体检', 'ai配置', '探针'] }, { cmd: '/mcp', label: '🪛 排查 MCP 接入', desc: '检查 MCP 服务和外部客户端状态', prompt: '请先调用 inspect_mcp_setup,帮我盘点当前 MCP 服务、工具发现结果,以及 Claude Code / Codex 的接入状态。', category: 'diagnose', featured: true, keywords: ['mcp', 'codex', 'claude', '外部客户端'] }, - { cmd: '/mcpadd', label: '🧭 新增 MCP 指引', desc: '查看 command、args、env 和模板怎么填', prompt: '请先调用 inspect_mcp_authoring_guide,再结合 inspect_mcp_setup,告诉我新增 GoNavi MCP 服务时 command、args、env、timeout 应该怎么填,以及最接近的模板应该选哪个。', category: 'diagnose', featured: true, keywords: ['mcp新增', 'command', 'args', 'env', '模板'] }, + { cmd: '/mcpadd', label: '🧭 新增 MCP 指引', desc: '查看 command、args、env 和模板怎么填', prompt: '请先调用 inspect_mcp_authoring_guide;如果我贴了完整启动命令或草稿,再调用 inspect_mcp_draft 试算字段和校验问题;最后结合 inspect_mcp_setup,告诉我新增 GoNavi MCP 服务时 command、args、env、timeout 应该怎么填,以及最接近的模板应该选哪个。', category: 'diagnose', featured: true, keywords: ['mcp新增', 'command', 'args', 'env', '模板'] }, + { cmd: '/mcpdraft', label: '🧪 MCP 草稿校验', desc: '校验一条 MCP 启动命令怎么拆', prompt: '请先调用 inspect_mcp_draft 校验我提供的 MCP fullCommand 或 command/args/env/timeout 草稿,返回自动拆分结果、启动预览、错误/告警和 nextActions;如果还缺字段说明,再补充调用 inspect_mcp_authoring_guide。', category: 'diagnose', keywords: ['mcp草稿', 'mcp校验', 'fullcommand', '启动命令', '参数拆分', 'command', 'args', 'env'] }, { cmd: '/mcptool', label: '🧩 MCP 工具参数', desc: '查看 MCP 工具 schema 和 arguments 写法', prompt: '请先调用 inspect_mcp_setup 找到当前已发现的 MCP 工具 alias;如果我已经给了工具名或关键词,再调用 inspect_mcp_tool_schema 读取对应 inputSchema,告诉我必填参数、字段类型、枚举值、嵌套路径,以及 arguments JSON 应该怎么写。', category: 'diagnose', keywords: ['mcp工具', 'mcp工具参数', 'schema', 'arguments', '参数', '工具调用', 'inputschema'] }, { cmd: '/connfail', label: '🧯 连接失败探针', desc: '总结最近连接失败、冷却和验证异常', prompt: '请先调用 inspect_recent_connection_failures,帮我总结最近数据库连接失败、连接冷却、验证失败和 SSH 隧道异常的真实日志结论;如果已经有明确地址或类型,再结合 inspect_current_connection 或 inspect_saved_connections 继续缩小范围。', category: 'diagnose', featured: true, keywords: ['连接失败', '冷却', '验证失败', 'ssh', 'mysql'] }, { cmd: '/shortcuts', label: '⌨️ 快捷键探针', desc: '读取当前 Win/Mac 快捷键配置', prompt: '请先调用 inspect_shortcuts,告诉我当前 GoNavi 的快捷键配置,尤其是执行 SQL、切换结果区、打开 AI 面板和 AI 发送消息这些动作在当前平台和另一平台分别怎么按,是否改过默认值。', category: 'diagnose', keywords: ['快捷键', 'shortcuts', '结果区', 'mac', 'windows'] }, diff --git a/frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts b/frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts index 51a32e4..94fd88f 100644 --- a/frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts +++ b/frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts @@ -13,6 +13,7 @@ import { buildAIProviderSnapshot } from './aiProviderInsights'; import { buildAIRuntimeSnapshot } from './aiRuntimeInsights'; import { buildAISafetySnapshot } from './aiSafetyInsights'; import { buildMCPAuthoringGuideSnapshot } from './aiMCPAuthoringGuideInsights'; +import { buildMCPDraftInspectionSnapshot } from './aiMCPDraftInspectionInsights'; import { buildAISetupHealthSnapshot } from './aiSetupHealthInsights'; import { buildMCPSetupSnapshot } from './aiMCPInsights'; import { buildMCPToolSchemaSnapshot } from './aiMCPToolSchemaInsights'; @@ -171,6 +172,11 @@ export async function executeAIConfigSnapshotToolCall( content: JSON.stringify(buildMCPAuthoringGuideSnapshot()), success: true, }; + case 'inspect_mcp_draft': + return { + content: JSON.stringify(buildMCPDraftInspectionSnapshot(args)), + success: true, + }; case 'inspect_mcp_tool_schema': return { content: JSON.stringify(buildMCPToolSchemaSnapshot({ @@ -203,6 +209,7 @@ export async function executeAIConfigSnapshotToolCall( inspect_ai_chat_readiness: '读取 AI 聊天发送前置状态失败', inspect_mcp_setup: '读取 MCP 配置状态失败', inspect_mcp_authoring_guide: '读取 MCP 新增填写指引失败', + inspect_mcp_draft: '校验 MCP 新增草稿失败', inspect_mcp_tool_schema: '读取 MCP 工具参数 schema 失败', inspect_ai_guidance: '读取当前 AI 提示与技能配置失败', }[toolName] || '读取 AI 配置探针失败'; diff --git a/frontend/src/components/ai/aiSystemContextMessages.test.ts b/frontend/src/components/ai/aiSystemContextMessages.test.ts index 3fabbc7..148d06d 100644 --- a/frontend/src/components/ai/aiSystemContextMessages.test.ts +++ b/frontend/src/components/ai/aiSystemContextMessages.test.ts @@ -68,7 +68,7 @@ describe('buildAISystemContextMessages', () => { connections: [connections[0]], tabs: [], activeTabId: null, - availableToolNames: ['inspect_workspace_tabs', 'inspect_app_health', 'inspect_ai_setup_health', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', 'inspect_mcp_authoring_guide', 'inspect_mcp_tool_schema', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_connection_capabilities', 'inspect_saved_connections', 'inspect_external_sql_directories', 'inspect_external_sql_file', 'inspect_recent_sql_activity', 'inspect_sql_risk', 'inspect_recent_connection_failures', 'inspect_app_logs', 'inspect_ai_last_render_error', 'inspect_ai_message_flow', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'], + availableToolNames: ['inspect_workspace_tabs', 'inspect_app_health', 'inspect_ai_setup_health', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', 'inspect_mcp_authoring_guide', 'inspect_mcp_draft', 'inspect_mcp_tool_schema', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_connection_capabilities', 'inspect_saved_connections', 'inspect_external_sql_directories', 'inspect_external_sql_file', 'inspect_recent_sql_activity', 'inspect_sql_risk', 'inspect_recent_connection_failures', 'inspect_app_logs', 'inspect_ai_last_render_error', 'inspect_ai_message_flow', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'], skills, userPromptSettings, }); @@ -83,6 +83,7 @@ describe('buildAISystemContextMessages', () => { expect(joined).toContain('inspect_ai_chat_readiness 读取真实发送前置状态'); expect(joined).toContain('inspect_mcp_setup 读取真实 MCP 配置'); expect(joined).toContain('inspect_mcp_authoring_guide 读取真实新增指引和模板'); + expect(joined).toContain('inspect_mcp_draft 返回自动拆分、启动预览、配置错误/告警和 nextActions'); expect(joined).toContain('inspect_mcp_tool_schema 读取真实 inputSchema'); expect(joined).toContain('inspect_ai_guidance 读取真实提示与技能配置'); expect(joined).toContain('inspect_ai_context 读取当前挂载的表结构上下文'); diff --git a/frontend/src/components/ai/aiSystemInspectionGuidance.ts b/frontend/src/components/ai/aiSystemInspectionGuidance.ts index dda9061..dd9f8e2 100644 --- a/frontend/src/components/ai/aiSystemInspectionGuidance.ts +++ b/frontend/src/components/ai/aiSystemInspectionGuidance.ts @@ -90,7 +90,13 @@ export const appendDatabaseInspectionGuidanceMessages = ( messages, availableToolNames, 'inspect_mcp_authoring_guide', - '如果用户提到“新增 MCP 不知道 command/args/env/timeout 怎么填”“给我一个 node / uvx / python 模板”“为什么启动命令不能直接填整行”,优先调用 inspect_mcp_authoring_guide 读取真实新增指引和模板,再结合 inspect_mcp_setup 判断当前配置现状,不要凭记忆口述。', + '如果用户提到“新增 MCP 不知道 command/args/env/timeout 怎么填”“给我一个 node / uvx / python 模板”“为什么启动命令不能直接填整行”,优先调用 inspect_mcp_authoring_guide 读取真实新增指引和模板;如果用户已经贴出命令或草稿,再调用 inspect_mcp_draft 用真实校验器试算,不要凭记忆口述。', + ); + appendGuidanceIfToolAvailable( + messages, + availableToolNames, + 'inspect_mcp_draft', + '如果用户贴出 MCP README 启动命令、command/args/env/timeout 草稿,或问“这条 MCP 命令在 GoNavi 里怎么填”,优先调用 inspect_mcp_draft 返回自动拆分、启动预览、配置错误/告警和 nextActions,再给用户具体填写结果。', ); appendGuidanceIfToolAvailable( messages, diff --git a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx index 25136ea..8c516ed 100644 --- a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx +++ b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx @@ -29,6 +29,7 @@ const TOOL_ACTION_LABELS: Record = { inspect_ai_chat_readiness: '读取当前 AI 聊天发送前置状态', inspect_mcp_setup: '读取当前 MCP 配置状态', inspect_mcp_authoring_guide: '读取 MCP 新增填写指引', + inspect_mcp_draft: '校验 MCP 新增草稿', inspect_mcp_tool_schema: '读取 MCP 工具参数 schema', inspect_ai_guidance: '读取当前 AI 提示与技能配置', get_connections: '获取可用连接信息', diff --git a/frontend/src/utils/aiBuiltinInspectionToolInfo.ts b/frontend/src/utils/aiBuiltinInspectionToolInfo.ts index 5c52298..3356689 100644 --- a/frontend/src/utils/aiBuiltinInspectionToolInfo.ts +++ b/frontend/src/utils/aiBuiltinInspectionToolInfo.ts @@ -145,6 +145,40 @@ export const BUILTIN_AI_INSPECTION_TOOL_INFO: AIBuiltinToolInfo[] = [ }, }, }, + { + name: "inspect_mcp_draft", + icon: "🧪", + desc: "校验 MCP 新增草稿", + detail: + "按完整启动命令或分字段草稿试算 GoNavi 的 MCP 新增配置,返回自动拆分结果、启动预览、字段校验问题、推荐模板和下一步修复建议。适合用户贴出一整行 MCP 启动命令、问 command/args/env/timeout 该怎么拆,或保存前想确认配置有没有明显问题时使用。", + params: "fullCommand?, command?, args?, envText?, timeoutSeconds?, templateKey?, name?", + tool: { + type: "function", + function: { + name: "inspect_mcp_draft", + description: + "校验一份待新增的 MCP 服务草稿。支持传 fullCommand/rawCommand/commandLine 让 GoNavi 自动拆分,也支持传 command、args、envText、timeoutSeconds 和 templateKey 做分字段校验;返回解析后的字段、启动命令预览、错误/告警、推荐模板和 nextActions。适用于用户贴出 MCP README 启动命令、问新增 MCP 参数怎么填、或 AI 准备指导用户保存前,先用真实校验器试算。", + parameters: { + type: "object", + properties: { + fullCommand: { type: "string", description: "可选,README 或用户贴出的一整行 MCP 启动命令,例如 $env:GITHUB_TOKEN=...; uvx mcp-server-github --stdio" }, + command: { type: "string", description: "可选,分字段草稿里的启动命令,只应是 npx、node、uvx、python 或 exe 路径本身" }, + args: { + oneOf: [ + { type: "array", items: { type: "string" } }, + { type: "string" }, + ], + description: "可选,分字段草稿里的命令参数;数组更准确,也可传逗号或换行分隔字符串", + }, + envText: { type: "string", description: "可选,环境变量草稿,每行 KEY=VALUE;不要传 export、set 或 $env: 前缀" }, + timeoutSeconds: { type: "number", description: "可选,单次工具发现或调用超时秒数;推荐 20,慢启动服务可用 45 或 60" }, + templateKey: { type: "string", enum: ["npx", "uvx", "node", "python", "exe"], description: "可选,先套用一个内置模板再覆盖用户传入字段" }, + name: { type: "string", description: "可选,MCP 服务名称,例如 GitHub、Filesystem、Browser" }, + }, + }, + }, + }, + }, { name: "inspect_mcp_tool_schema", icon: "🧩", diff --git a/frontend/src/utils/aiToolRegistry.test.ts b/frontend/src/utils/aiToolRegistry.test.ts index 0638f39..8353278 100644 --- a/frontend/src/utils/aiToolRegistry.test.ts +++ b/frontend/src/utils/aiToolRegistry.test.ts @@ -38,6 +38,14 @@ describe('aiToolRegistry', () => { expect(info?.tool.function.description).toContain('command、args、env、timeout'); }); + it('registers the mcp-draft inspector as a builtin tool', () => { + const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_mcp_draft'); + expect(info).toBeTruthy(); + expect(info?.desc).toContain('MCP 新增草稿'); + expect(info?.tool.function.description).toContain('真实校验器试算'); + expect(info?.tool.function.parameters?.properties?.fullCommand?.description).toContain('一整行 MCP 启动命令'); + }); + it('registers the mcp-tool-schema inspector as a builtin tool', () => { const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_mcp_tool_schema'); expect(info).toBeTruthy(); @@ -191,6 +199,7 @@ describe('aiToolRegistry', () => { expect(tools.some((item) => item.function.name === 'inspect_ai_chat_readiness')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_mcp_setup')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_mcp_authoring_guide')).toBe(true); + expect(tools.some((item) => item.function.name === 'inspect_mcp_draft')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_mcp_tool_schema')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_ai_guidance')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_current_connection')).toBe(true);