From 6f4e80c749cdddbee1acd2ed59ee7070d86f034f Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 11 Jun 2026 22:01:26 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(mcp):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=9C=9F=E5=A4=B1=E8=B4=A5=E8=AF=8A=E6=96=AD?= =?UTF-8?q?=E6=8E=A2=E9=92=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 解析 gonavi.log 中 MCP 启动、发现和调用失败信号 - 结合已保存 MCP 服务与工具发现状态输出原因和 nextActions - 补充系统引导、工具目录、状态标签和回归测试 --- .../ai/AIBuiltinToolsCatalog.test.tsx | 2 + ...ecutor.mcpRuntimeFailureInspection.test.ts | 89 ++++++ .../ai/aiMCPRuntimeFailureInsights.test.ts | 118 ++++++++ .../ai/aiMCPRuntimeFailureInsights.ts | 266 ++++++++++++++++++ ...apshotInspectionDiagnosticsToolExecutor.ts | 28 ++ .../ai/aiSnapshotInspectionToolExecutor.ts | 1 + .../ai/aiSystemContextMessages.test.ts | 3 +- .../ai/aiSystemInspectionGuidance.ts | 6 + .../messageBubble/AIMessageStatusBlocks.tsx | 1 + .../utils/aiBuiltinInspectionMcpToolInfo.ts | 25 ++ frontend/src/utils/aiBuiltinToolCatalog.ts | 8 +- frontend/src/utils/aiToolRegistry.test.ts | 9 + 12 files changed, 551 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/ai/aiLocalToolExecutor.mcpRuntimeFailureInspection.test.ts create mode 100644 frontend/src/components/ai/aiMCPRuntimeFailureInsights.test.ts create mode 100644 frontend/src/components/ai/aiMCPRuntimeFailureInsights.ts diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx index 5089274..a40fd06 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx @@ -49,6 +49,8 @@ describe('AIBuiltinToolsCatalog', () => { expect(markup).toContain('请求体预览'); expect(markup).toContain('排查 MCP 接入状态'); expect(markup).toContain('inspect_mcp_setup'); + expect(markup).toContain('inspect_mcp_runtime_failures'); + expect(markup).toContain('运行期失败日志'); expect(markup).toContain('新增 MCP 填写指引'); expect(markup).toContain('inspect_mcp_authoring_guide'); expect(markup).toContain('inspect_mcp_draft'); diff --git a/frontend/src/components/ai/aiLocalToolExecutor.mcpRuntimeFailureInspection.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.mcpRuntimeFailureInspection.test.ts new file mode 100644 index 0000000..4afc8e8 --- /dev/null +++ b/frontend/src/components/ai/aiLocalToolExecutor.mcpRuntimeFailureInspection.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it, vi } from 'vitest'; + +import type { AIToolCall } from '../../types'; +import { executeLocalAIToolCall } from './aiLocalToolExecutor'; + +const buildToolCall = ( + name: string, + args: Record, +): AIToolCall => ({ + id: `call-${name}`, + type: 'function', + function: { + name, + arguments: JSON.stringify(args), + }, +}); + +describe('aiLocalToolExecutor inspect_mcp_runtime_failures', () => { + it('returns structured MCP runtime failure diagnostics from gonavi.log and configured servers', async () => { + const readAppLogTail = vi.fn().mockResolvedValue({ + success: true, + data: { + logPath: 'C:/Users/demo/.GoNavi/Logs/gonavi.log', + keyword: 'GitHub', + requestedLineLimit: 160, + lines: [ + '2026/06/11 10:00:00.000000 [WARN] 列出 MCP 工具失败(server=GitHub): exec: "uvx": executable file not found in %PATH%', + ], + }, + }); + const getMCPServers = vi.fn().mockResolvedValue([{ + id: 'github', + name: 'GitHub', + transport: 'stdio', + command: 'uvx', + args: ['mcp-server-github', '--stdio'], + env: { GITHUB_TOKEN: 'secret-value' }, + enabled: true, + timeoutSeconds: 20, + }]); + + const result = await executeLocalAIToolCall({ + toolCall: buildToolCall('inspect_mcp_runtime_failures', { + serverName: 'GitHub', + }), + connections: [], + mcpTools: [], + toolContextMap: new Map(), + runtime: { + getDatabases: vi.fn(), + getTables: vi.fn(), + readAppLogTail, + getMCPServers, + }, + }); + + expect(result.success).toBe(true); + expect(readAppLogTail).toHaveBeenCalledWith(160, 'GitHub'); + expect(getMCPServers).toHaveBeenCalledTimes(1); + expect(result.content).toContain('"failureEventCount":1'); + expect(result.content).toContain('"list_tools_failed":1'); + expect(result.content).toContain('"command_not_found":1'); + expect(result.content).toContain('"name":"GitHub"'); + expect(result.content).toContain('"envKeys":["GITHUB_TOKEN"]'); + expect(result.content).toContain('检查 command 是否只填可执行程序本身'); + expect(result.content).not.toContain('secret-value'); + }); + + it('returns a clear failure when app logs cannot be read', async () => { + const result = await executeLocalAIToolCall({ + toolCall: buildToolCall('inspect_mcp_runtime_failures', {}), + connections: [], + mcpTools: [], + toolContextMap: new Map(), + runtime: { + getDatabases: vi.fn(), + getTables: vi.fn(), + readAppLogTail: vi.fn().mockResolvedValue({ + success: false, + message: 'log file missing', + }), + }, + }); + + expect(result.success).toBe(false); + expect(result.content).toContain('读取 MCP 运行期失败日志失败'); + expect(result.content).toContain('log file missing'); + }); +}); diff --git a/frontend/src/components/ai/aiMCPRuntimeFailureInsights.test.ts b/frontend/src/components/ai/aiMCPRuntimeFailureInsights.test.ts new file mode 100644 index 0000000..a2ad60b --- /dev/null +++ b/frontend/src/components/ai/aiMCPRuntimeFailureInsights.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it } from 'vitest'; + +import { buildMCPRuntimeFailureSnapshot } from './aiMCPRuntimeFailureInsights'; + +describe('buildMCPRuntimeFailureSnapshot', () => { + it('classifies MCP list-tools failures and joins them with configured servers', () => { + const snapshot = buildMCPRuntimeFailureSnapshot({ + readResult: { + data: { + logPath: 'C:/Users/demo/.GoNavi/Logs/gonavi.log', + keyword: 'MCP', + requestedLineLimit: 160, + lines: [ + '2026/06/11 10:00:00.000000 [WARN] 列出 MCP 工具失败(server=GitHub): exec: "uvx": executable file not found in %PATH%', + '2026/06/11 10:00:01.000000 [WARN] 列出 MCP 工具失败(server=DockerFetch): context deadline exceeded', + ], + }, + }, + mcpServers: [ + { + id: 'github', + name: 'GitHub', + transport: 'stdio', + command: 'uvx', + args: ['mcp-server-github', '--stdio'], + env: { GITHUB_TOKEN: 'secret-value' }, + enabled: true, + timeoutSeconds: 20, + }, + { + id: 'docker-fetch', + name: 'DockerFetch', + transport: 'stdio', + command: 'docker', + args: ['run', '--rm', '-i', 'mcp/server-fetch:latest'], + env: {}, + enabled: true, + timeoutSeconds: 20, + }, + ], + mcpTools: [], + }); + + expect(snapshot.failureEventCount).toBe(2); + expect(snapshot.breakdown).toMatchObject({ + list_tools_failed: 2, + command_not_found: 1, + timeout: 1, + }); + expect(snapshot.failureServerNames).toEqual(['GitHub', 'DockerFetch']); + expect(snapshot.serverSummaries.find((server) => server.name === 'DockerFetch')).toMatchObject({ + name: 'DockerFetch', + discoveredToolCount: 0, + recentFailureCount: 1, + probableCauses: ['timeout'], + }); + expect(snapshot.nextActions.join('\n')).toContain('检查 command 是否只填可执行程序本身'); + expect(snapshot.nextActions.join('\n')).toContain('提高 timeoutSeconds 到 45 或 60'); + expect(JSON.stringify(snapshot)).not.toContain('secret-value'); + }); + + it('detects HTTP MCP process failures and redacts secret-like log values', () => { + const snapshot = buildMCPRuntimeFailureSnapshot({ + readResult: { + data: { + lines: [ + '2026/06/11 10:00:00.000000 [ERROR] GoNavi MCP HTTP 服务启动失败:listen tcp 127.0.0.1:8765: bind: permission denied GONAVI_MCP_HTTP_TOKEN=abcdef1234567890', + '2026/06/11 10:00:01.000000 [ERROR] GoNavi MCP HTTP 服务异常退出:exit status 1', + ], + }, + }, + includeLines: true, + }); + + expect(snapshot.failureEventCount).toBe(2); + expect(snapshot.breakdown).toMatchObject({ + http_start_failed: 1, + http_process_exited: 1, + permission: 1, + process_exit: 1, + }); + expect(snapshot.events[0].linePreview).toContain('GONAVI_MCP_HTTP_TOKEN=***'); + expect(snapshot.lines?.join('\n')).not.toContain('abcdef1234567890'); + }); + + it('returns an actionable empty state when no MCP failures are found', () => { + const snapshot = buildMCPRuntimeFailureSnapshot({ + readResult: { + data: { + lines: [ + '2026/06/11 10:00:00.000000 [INFO] GoNavi MCP HTTP 服务已启动', + ], + }, + }, + mcpServers: [{ + id: 'ok', + name: 'OK', + transport: 'stdio', + command: 'node', + args: ['server.js', '--stdio'], + env: {}, + enabled: true, + timeoutSeconds: 20, + }], + mcpTools: [{ + alias: 'mcp__ok__ping', + serverId: 'ok', + serverName: 'OK', + originalName: 'ping', + }], + }); + + expect(snapshot.failureEventCount).toBe(0); + expect(snapshot.message).toContain('没有发现 MCP 启动、工具发现或工具调用失败信号'); + expect(snapshot.nextActions.join('\n')).toContain('扩大 lineLimit'); + expect(snapshot.serverSummaries[0].discoveredToolCount).toBe(1); + }); +}); diff --git a/frontend/src/components/ai/aiMCPRuntimeFailureInsights.ts b/frontend/src/components/ai/aiMCPRuntimeFailureInsights.ts new file mode 100644 index 0000000..9a9d19c --- /dev/null +++ b/frontend/src/components/ai/aiMCPRuntimeFailureInsights.ts @@ -0,0 +1,266 @@ +import type { AIMCPServerConfig, AIMCPToolDescriptor } from '../../types'; +import { buildMCPLaunchPreview } from '../../utils/mcpServerGuidance'; + +const DEFAULT_MCP_RUNTIME_LOG_LIMIT = 160; +const MAX_MCP_RUNTIME_LOG_LIMIT = 200; + +type MCPRuntimeFailureKind = + | 'list_tools_failed' + | 'tool_call_failed' + | 'http_start_failed' + | 'http_process_exited' + | 'configuration_error' + | 'mcp_warning' + | 'mcp_error'; + +type MCPRuntimeCause = + | 'command_not_found' + | 'timeout' + | 'permission' + | 'auth' + | 'network' + | 'stdio_closed' + | 'process_exit' + | 'argument_error' + | 'transport' + | 'unknown'; + +interface MCPRuntimeFailureEvent { + kind: MCPRuntimeFailureKind; + cause: MCPRuntimeCause; + level: 'INFO' | 'WARN' | 'ERROR' | 'OTHER'; + serverName: string; + linePreview: string; + nextAction: string; +} + +const secretLikeValuePatterns = [ + /bearer\s+[a-z0-9._~+/=-]{8,}/giu, + /\bsk-[a-z0-9._-]{8,}/giu, + /\bgh[pousr]_[a-z0-9_]{8,}/giu, + /\bxox[baprs]-[a-z0-9-]{8,}/giu, + /([A-Za-z_][A-Za-z0-9_]*(?:TOKEN|SECRET|PASSWORD|API[_-]?KEY)[A-Za-z0-9_]*)=([^\s;&]+)/giu, +]; + +const normalizeLimit = (input: unknown): number => { + const value = Math.floor(Number(input) || DEFAULT_MCP_RUNTIME_LOG_LIMIT); + if (value < 1) return 1; + if (value > MAX_MCP_RUNTIME_LOG_LIMIT) return MAX_MCP_RUNTIME_LOG_LIMIT; + return value; +}; + +const normalizeLogLines = (input: unknown): string[] => + Array.isArray(input) + ? input.map((line) => String(line || '').trim()).filter(Boolean) + : []; + +const redactLogLine = (line: string): string => { + let next = line; + secretLikeValuePatterns.forEach((pattern) => { + next = next.replace(pattern, (_match, key) => (typeof key === 'string' && key ? `${key}=***` : '[REDACTED]')); + }); + return next.length > 2000 ? `${next.slice(0, 2000)}...[truncated ${next.length - 2000} chars]` : next; +}; + +const detectLevel = (line: string): MCPRuntimeFailureEvent['level'] => { + if (line.includes('[ERROR]')) return 'ERROR'; + if (line.includes('[WARN]')) return 'WARN'; + if (line.includes('[INFO]')) return 'INFO'; + return 'OTHER'; +}; + +const extractServerName = (line: string): string => { + const match = line.match(/server=([^)]+)\)/iu); + return String(match?.[1] || '').trim(); +}; + +const detectFailureKind = (line: string): MCPRuntimeFailureKind | null => { + if (line.includes('列出 MCP 工具失败')) return 'list_tools_failed'; + if (line.includes('调用 MCP 工具失败') || line.includes('MCP 工具调用失败')) return 'tool_call_failed'; + if (line.includes('GoNavi MCP HTTP 服务启动失败')) return 'http_start_failed'; + if (line.includes('GoNavi MCP HTTP 服务异常退出') || line.includes('MCP HTTP 子进程已退出')) return 'http_process_exited'; + if (line.includes('MCP 服务命令不能为空') || line.includes('暂不支持的 MCP transport')) return 'configuration_error'; + if (line.toLowerCase().includes('mcp') && line.includes('[ERROR]')) return 'mcp_error'; + if (line.toLowerCase().includes('mcp') && line.includes('[WARN]')) return 'mcp_warning'; + return null; +}; + +const detectCause = (line: string): MCPRuntimeCause => { + const lower = line.toLowerCase(); + if (/(executable file not found|not found|no such file|cannot find|找不到|无法找到)/iu.test(line)) { + return 'command_not_found'; + } + if (/(context deadline exceeded|timeout|timed out|超时|deadline)/iu.test(line)) { + return 'timeout'; + } + if (/(permission denied|access is denied|operation not permitted|权限)/iu.test(line)) { + return 'permission'; + } + if (/(401|403|unauthorized|forbidden|authentication|认证|鉴权)/iu.test(line)) { + return 'auth'; + } + if (/(connection refused|connectex|econnrefused|network|dial tcp|refused|连接被拒绝)/iu.test(line)) { + return 'network'; + } + if (/(stdio|eof|closed pipe|broken pipe|stdin|stdout|标准输入|标准输出)/iu.test(line)) { + return 'stdio_closed'; + } + if (/(exit status|exited|异常退出|子进程已退出|process exited)/iu.test(line)) { + return 'process_exit'; + } + if (/(invalid character|invalid json|arguments|参数|schema|unmarshal)/iu.test(line)) { + return 'argument_error'; + } + if (lower.includes('transport')) { + return 'transport'; + } + return 'unknown'; +}; + +const causeNextAction: Record = { + command_not_found: '检查 command 是否只填可执行程序本身,并确认该命令在 PATH 中或使用绝对路径。', + timeout: '提高 timeoutSeconds 到 45 或 60,并确认服务启动后会保持 stdio 连接。', + permission: '检查可执行文件权限、杀毒/系统拦截和工作目录访问权限。', + auth: '检查环境变量里的 Token/API Key 是否已配置、未过期且权限范围足够。', + network: '检查 MCP 依赖的远端地址、代理、VPN 或本机端口是否可达。', + stdio_closed: '确认 README 要求的 --stdio/stdin 参数已填写;Docker 场景确认 args 包含 -i。', + process_exit: '单独在终端运行启动命令,查看进程启动后为什么立即退出。', + argument_error: '先调用 inspect_mcp_tool_schema 读取真实 inputSchema,再修正工具 arguments JSON。', + transport: '当前 GoNavi 新增 MCP 只支持 stdio,HTTP MCP 请使用 GoNavi HTTP 服务或对应远程接入说明。', + unknown: '结合 inspect_mcp_setup 查看配置,再调用 inspect_app_logs 扩大日志窗口确认原始错误。', +}; + +const parseFailureEvent = (line: string): MCPRuntimeFailureEvent | null => { + const kind = detectFailureKind(line); + if (!kind) { + return null; + } + const cause = detectCause(line); + return { + kind, + cause, + level: detectLevel(line), + serverName: extractServerName(line), + linePreview: redactLogLine(line), + nextAction: causeNextAction[cause], + }; +}; + +const buildBreakdown = (events: MCPRuntimeFailureEvent[]) => + events.reduce>((acc, event) => { + acc[event.kind] = (acc[event.kind] || 0) + 1; + acc[event.cause] = (acc[event.cause] || 0) + 1; + return acc; + }, {}); + +const toTrimmedString = (value: unknown): string => String(value ?? '').trim(); + +const getServerToolCount = (serverId: string, tools: AIMCPToolDescriptor[]): number => + tools.filter((tool) => tool.serverId === serverId).length; + +const matchesServer = (event: MCPRuntimeFailureEvent, server: AIMCPServerConfig): boolean => { + const serverName = event.serverName.toLowerCase(); + if (!serverName) return false; + return serverName === toTrimmedString(server.name).toLowerCase() + || serverName === toTrimmedString(server.id).toLowerCase(); +}; + +const buildServerSummaries = ( + servers: AIMCPServerConfig[], + tools: AIMCPToolDescriptor[], + events: MCPRuntimeFailureEvent[], +) => servers.map((server) => { + const serverEvents = events.filter((event) => matchesServer(event, server)); + return { + id: server.id, + name: server.name, + enabled: server.enabled !== false, + transport: server.transport, + launchCommandPreview: redactLogLine(buildMCPLaunchPreview(server.command, server.args)), + timeoutSeconds: server.timeoutSeconds, + envKeys: Object.keys(server.env || {}).sort(), + discoveredToolCount: getServerToolCount(server.id, tools), + recentFailureCount: serverEvents.length, + recentFailureKinds: Array.from(new Set(serverEvents.map((event) => event.kind))), + probableCauses: Array.from(new Set(serverEvents.map((event) => event.cause))), + }; +}); + +const collectNextActions = ( + events: MCPRuntimeFailureEvent[], + serverSummaries: ReturnType, +): string[] => { + const actions = Array.from(new Set(events.map((event) => event.nextAction))); + const enabledServersWithoutTools = serverSummaries.filter((server) => server.enabled && server.discoveredToolCount === 0); + if (enabledServersWithoutTools.length > 0) { + actions.push('有已启用 MCP 服务暂未发现工具,优先点击“测试工具发现”刷新并确认启动命令可独立运行。'); + } + if (events.some((event) => event.kind === 'list_tools_failed')) { + actions.push('工具列表为空时先修复启动/发现失败,再排查单个工具 arguments。'); + } + if (actions.length === 0) { + actions.push('最近日志未发现 MCP 失败信号;如果刚刚复现过问题,请扩大 lineLimit 或改用 serverName 精确过滤。'); + } + return actions; +}; + +export const buildMCPRuntimeFailureSnapshot = (params: { + readResult?: any; + mcpServers?: AIMCPServerConfig[]; + mcpTools?: AIMCPToolDescriptor[]; + keyword?: unknown; + serverName?: unknown; + lineLimit?: unknown; + includeLines?: unknown; +}) => { + const data = params.readResult?.data && typeof params.readResult.data === 'object' + ? params.readResult.data as Record + : {}; + const keyword = toTrimmedString(data.keyword || params.serverName || params.keyword || 'MCP'); + const requestedLineLimit = normalizeLimit(data.requestedLineLimit ?? params.lineLimit); + const serverNameFilter = toTrimmedString(params.serverName).toLowerCase(); + const textFilter = toTrimmedString(params.keyword).toLowerCase(); + const includeLines = params.includeLines === true; + const lines = normalizeLogLines(data.lines); + const events = lines + .map(parseFailureEvent) + .filter((event): event is MCPRuntimeFailureEvent => Boolean(event)) + .filter((event) => !serverNameFilter || event.serverName.toLowerCase().includes(serverNameFilter) || event.linePreview.toLowerCase().includes(serverNameFilter)) + .filter((event) => !textFilter || event.linePreview.toLowerCase().includes(textFilter)); + const serverSummaries = buildServerSummaries( + Array.isArray(params.mcpServers) ? params.mcpServers : [], + Array.isArray(params.mcpTools) ? params.mcpTools : [], + events, + ); + const failureServerNames = Array.from(new Set(events.map((event) => event.serverName).filter(Boolean))); + const warnings: string[] = []; + + if (events.length > 0) { + warnings.push(`最近日志中发现 ${events.length} 条 MCP 运行期异常信号。`); + } + const serversWithoutTools = serverSummaries.filter((server) => server.enabled && server.discoveredToolCount === 0).length; + if (serversWithoutTools > 0) { + warnings.push(`有 ${serversWithoutTools} 个已启用 MCP 服务当前未发现工具。`); + } + + return { + logPath: String(data.logPath || ''), + keyword, + serverNameFilter: toTrimmedString(params.serverName), + requestedLineLimit, + returnedLineCount: lines.length, + fileWindowTruncated: data.fileWindowTruncated === true, + matchedLinesTruncated: data.matchedLinesTruncated === true, + failureEventCount: events.length, + failureServerNames, + breakdown: buildBreakdown(events), + events, + serverSummaries, + warnings, + nextActions: collectNextActions(events, serverSummaries), + lines: includeLines ? lines.map(redactLogLine) : undefined, + message: events.length > 0 + ? `最近日志中发现 ${events.length} 条 MCP 运行期异常信号` + : '最近日志里没有发现 MCP 启动、工具发现或工具调用失败信号。', + }; +}; diff --git a/frontend/src/components/ai/aiSnapshotInspectionDiagnosticsToolExecutor.ts b/frontend/src/components/ai/aiSnapshotInspectionDiagnosticsToolExecutor.ts index 713c5c2..8b0fe91 100644 --- a/frontend/src/components/ai/aiSnapshotInspectionDiagnosticsToolExecutor.ts +++ b/frontend/src/components/ai/aiSnapshotInspectionDiagnosticsToolExecutor.ts @@ -29,6 +29,7 @@ import { buildSqlEditorTransactionSnapshot } from './aiSqlEditorTransactionInsig import { buildShortcutSnapshot } from './aiShortcutInsights'; import { buildAILastRenderErrorSnapshot } from './aiLastRenderErrorInsights'; import { buildRecentConnectionFailureSnapshot } from './aiConnectionFailureInsights'; +import { buildMCPRuntimeFailureSnapshot } from './aiMCPRuntimeFailureInsights'; import type { AISnapshotInspectionRuntime, SnapshotInspectionResult, @@ -215,6 +216,33 @@ export async function executeDiagnosticsSnapshotToolCall({ success: true, }; } + case 'inspect_mcp_runtime_failures': { + const keyword = String(args.serverName || args.keyword || 'MCP').trim(); + const readResult = typeof runtime?.readAppLogTail === 'function' + ? await runtime.readAppLogTail(Number(args.lineLimit) || 160, keyword) + : { success: false, message: '当前环境暂不支持读取 GoNavi 应用日志' }; + if (!readResult?.success) { + return { + content: `读取 MCP 运行期失败日志失败: ${readResult?.message || '未知错误'}`, + success: false, + }; + } + const mcpServers = typeof runtime?.getMCPServers === 'function' + ? await runtime.getMCPServers().catch(() => undefined) + : undefined; + return { + content: JSON.stringify(buildMCPRuntimeFailureSnapshot({ + readResult, + mcpServers: Array.isArray(mcpServers) ? mcpServers : [], + mcpTools, + keyword: args.keyword, + serverName: args.serverName, + lineLimit: args.lineLimit, + includeLines: args.includeLines === true, + })), + success: true, + }; + } case 'inspect_ai_last_render_error': return { content: JSON.stringify(buildAILastRenderErrorSnapshot()), diff --git a/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts index f06596e..0170345 100644 --- a/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts +++ b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts @@ -176,6 +176,7 @@ export async function executeSnapshotInspectionToolCall( inspect_app_logs: '读取 GoNavi 应用日志失败', inspect_ai_upstream_logs: '读取 AI 上游请求日志失败', inspect_recent_connection_failures: '汇总最近连接失败记录失败', + inspect_mcp_runtime_failures: '读取 MCP 运行期失败诊断失败', inspect_ai_last_render_error: '读取最近一次 AI 渲染异常失败', inspect_ai_message_flow: '读取 AI 消息流诊断失败', inspect_ai_context_budget: '读取 AI 上下文体量诊断失败', diff --git a/frontend/src/components/ai/aiSystemContextMessages.test.ts b/frontend/src/components/ai/aiSystemContextMessages.test.ts index b16d677..3e03318 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_support_bundle', 'inspect_ai_setup_health', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_ai_upstream_logs', 'inspect_ai_tool_catalog', '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_editor_transaction', 'inspect_sql_risk', 'inspect_recent_connection_failures', 'inspect_app_logs', 'inspect_ai_last_render_error', 'inspect_ai_message_flow', 'inspect_ai_context_budget', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'], + availableToolNames: ['inspect_workspace_tabs', 'inspect_app_health', 'inspect_ai_support_bundle', 'inspect_ai_setup_health', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_ai_upstream_logs', 'inspect_ai_tool_catalog', 'inspect_mcp_setup', 'inspect_mcp_runtime_failures', '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_editor_transaction', 'inspect_sql_risk', 'inspect_recent_connection_failures', 'inspect_app_logs', 'inspect_ai_last_render_error', 'inspect_ai_message_flow', 'inspect_ai_context_budget', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'], skills, userPromptSettings, }); @@ -85,6 +85,7 @@ describe('buildAISystemContextMessages', () => { expect(joined).toContain('inspect_ai_upstream_logs 读取脱敏后的真实请求日志'); expect(joined).toContain('inspect_ai_tool_catalog 按关键词读取真实工具目录'); expect(joined).toContain('inspect_mcp_setup 读取真实 MCP 配置'); + expect(joined).toContain('inspect_mcp_runtime_failures 读取真实 MCP 运行期失败日志'); expect(joined).toContain('inspect_mcp_authoring_guide 读取真实新增指引和模板'); expect(joined).toContain('inspect_mcp_draft 返回自动拆分、启动预览、suggestedServerSeed、配置错误/告警和 nextActions'); expect(joined).toContain('inspect_mcp_tool_schema 读取真实 inputSchema'); diff --git a/frontend/src/components/ai/aiSystemInspectionGuidance.ts b/frontend/src/components/ai/aiSystemInspectionGuidance.ts index 0700fa7..18147bd 100644 --- a/frontend/src/components/ai/aiSystemInspectionGuidance.ts +++ b/frontend/src/components/ai/aiSystemInspectionGuidance.ts @@ -104,6 +104,12 @@ export const appendDatabaseInspectionGuidanceMessages = ( 'inspect_mcp_setup', '如果用户提到“我现在配了哪些 MCP”“Claude/Codex 有没有接入 GoNavi MCP”“为什么外部客户端用不了”“当前 MCP 服务启用了哪些”,优先调用 inspect_mcp_setup 读取真实 MCP 配置和外部客户端接入状态,不要凭记忆猜测。', ); + appendGuidanceIfToolAvailable( + messages, + availableToolNames, + 'inspect_mcp_runtime_failures', + '如果用户提到“新增 MCP 测试失败”“工具发现 0 个”“MCP 工具调用失败”“stdio 断开”“Docker MCP 退出”或“HTTP MCP 启动失败”,优先调用 inspect_mcp_runtime_failures 读取真实 MCP 运行期失败日志和当前服务发现状态,再决定是否下钻 inspect_mcp_draft、inspect_mcp_docker_setup 或 inspect_mcp_setup。', + ); appendGuidanceIfToolAvailable( messages, availableToolNames, diff --git a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx index 9f7cbcb..f95993f 100644 --- a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx +++ b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx @@ -30,6 +30,7 @@ const TOOL_ACTION_LABELS: Record = { inspect_ai_tool_catalog: '读取 AI 工具目录和参数提示', inspect_ai_support_bundle: '生成 AI 排障支持包', inspect_mcp_setup: '读取当前 MCP 配置状态', + inspect_mcp_runtime_failures: '诊断 MCP 运行期失败', inspect_mcp_authoring_guide: '读取 MCP 新增填写指引', inspect_mcp_draft: '校验 MCP 新增草稿', inspect_mcp_tool_schema: '读取 MCP 工具参数 schema', diff --git a/frontend/src/utils/aiBuiltinInspectionMcpToolInfo.ts b/frontend/src/utils/aiBuiltinInspectionMcpToolInfo.ts index 491b715..b0463e8 100644 --- a/frontend/src/utils/aiBuiltinInspectionMcpToolInfo.ts +++ b/frontend/src/utils/aiBuiltinInspectionMcpToolInfo.ts @@ -48,6 +48,31 @@ export const BUILTIN_AI_INSPECTION_MCP_TOOL_INFO: AIBuiltinToolInfo[] = [ }, }, }, + { + name: "inspect_mcp_runtime_failures", + icon: "🧯", + desc: "诊断 MCP 启动与调用失败", + detail: + "读取 gonavi.log 中最近的 MCP 启动、工具发现、工具调用和 HTTP MCP 子进程异常,结合当前已保存 MCP 服务与已发现工具,返回失败类型、疑似原因、涉及服务和下一步修复动作。适合用户反馈“新增 MCP 测试失败”“工具发现 0 个”“MCP 工具调用失败”“HTTP MCP 启动失败”时先调用。", + params: "serverName?, keyword?, lineLimit?(默认 160), includeLines?(默认 false)", + tool: { + type: "function", + function: { + name: "inspect_mcp_runtime_failures", + description: + "读取 GoNavi 应用日志中的 MCP 运行期失败信号,归类 MCP 服务启动失败、工具发现失败、工具调用失败和 HTTP MCP 子进程异常,并结合当前 MCP 服务配置与已发现工具数量返回疑似原因和 nextActions。适用于用户提到新增 MCP 测试失败、工具发现 0 个、MCP 工具调用失败、stdio 断开、命令找不到、Docker MCP 退出或 HTTP MCP 启动失败时,先读取该工具,不要只凭弹窗文案猜测。", + parameters: { + type: "object", + properties: { + serverName: { type: "string", description: "可选,只看某个 MCP 服务名或日志中的 server= 名称,例如 GitHub、Browser、DockerFetch" }, + keyword: { type: "string", description: "可选,在 MCP 相关日志里继续按关键词过滤,例如 timeout、stdio、permission、401、docker" }, + lineLimit: { type: "number", description: "可选,最多读取多少行日志尾部,默认 160,最大 200" }, + includeLines: { type: "boolean", description: "可选,是否附带脱敏后的 MCP 日志原文行,默认 false;需要引用原文时再开启" }, + }, + }, + }, + }, + }, { name: "inspect_mcp_authoring_guide", icon: "🧭", diff --git a/frontend/src/utils/aiBuiltinToolCatalog.ts b/frontend/src/utils/aiBuiltinToolCatalog.ts index 74b4824..5dd6c5b 100644 --- a/frontend/src/utils/aiBuiltinToolCatalog.ts +++ b/frontend/src/utils/aiBuiltinToolCatalog.ts @@ -86,8 +86,8 @@ export const BUILTIN_TOOL_FLOWS: AIBuiltinToolFlow[] = [ }, { title: '排查 MCP 接入状态', - steps: 'inspect_mcp_setup -> inspect_ai_runtime', - description: '适合先确认当前配置了哪些 MCP 服务、哪些已启用、外部客户端有没有写入当前 GoNavi 路径,再结合运行时工具列表判断为什么某个工具没暴露出来。', + steps: 'inspect_mcp_setup -> inspect_mcp_runtime_failures -> inspect_ai_runtime', + description: '适合先确认当前配置了哪些 MCP 服务、哪些已启用、外部客户端有没有写入当前 GoNavi 路径,再结合 MCP 运行期失败日志判断为什么某个工具没暴露出来。', }, { title: '远程 Agent 接入 GoNavi MCP', @@ -101,8 +101,8 @@ export const BUILTIN_TOOL_FLOWS: AIBuiltinToolFlow[] = [ }, { title: '排查 Docker MCP 启动', - steps: 'inspect_mcp_docker_setup -> inspect_mcp_draft -> inspect_mcp_setup', - description: '适合用户按 Docker README 新增 MCP 后发现 0 个工具、容器一启动就退出,或不确定 docker run 参数是否拆对时,先检查 run、-i、镜像名和超时设置。', + steps: 'inspect_mcp_runtime_failures -> inspect_mcp_docker_setup -> inspect_mcp_draft', + description: '适合用户按 Docker README 新增 MCP 后发现 0 个工具、容器一启动就退出,或不确定 docker run 参数是否拆对时,先看运行期失败原因,再检查 run、-i、镜像名和超时设置。', }, { title: '查看 MCP 工具参数', diff --git a/frontend/src/utils/aiToolRegistry.test.ts b/frontend/src/utils/aiToolRegistry.test.ts index 3a03c2b..5239887 100644 --- a/frontend/src/utils/aiToolRegistry.test.ts +++ b/frontend/src/utils/aiToolRegistry.test.ts @@ -47,6 +47,14 @@ describe('aiToolRegistry', () => { expect(info?.tool.function.parameters?.properties?.exposeStrategy?.enum).toContain('cloudflare_tunnel'); }); + it('registers the mcp-runtime-failure inspector as a builtin tool', () => { + const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_mcp_runtime_failures'); + expect(info).toBeTruthy(); + expect(info?.desc).toContain('启动与调用失败'); + expect(info?.tool.function.description).toContain('工具发现失败'); + expect(info?.tool.function.parameters?.properties?.serverName?.description).toContain('MCP 服务名'); + }); + it('registers the mcp-authoring-guide inspector as a builtin tool', () => { const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_mcp_authoring_guide'); expect(info).toBeTruthy(); @@ -256,6 +264,7 @@ describe('aiToolRegistry', () => { expect(tools.some((item) => item.function.name === 'inspect_ai_tool_catalog')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_mcp_setup')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_mcp_remote_access')).toBe(true); + expect(tools.some((item) => item.function.name === 'inspect_mcp_runtime_failures')).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);