feat(mcp): 新增运行期失败诊断探针

- 解析 gonavi.log 中 MCP 启动、发现和调用失败信号

- 结合已保存 MCP 服务与工具发现状态输出原因和 nextActions

- 补充系统引导、工具目录、状态标签和回归测试
This commit is contained in:
Syngnat
2026-06-11 22:01:26 +08:00
parent a9eed57cf7
commit 6f4e80c749
12 changed files with 551 additions and 5 deletions

View File

@@ -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');

View File

@@ -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<string, unknown>,
): 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');
});
});

View File

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

View File

@@ -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<MCPRuntimeCause, string> = {
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 只支持 stdioHTTP 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<Record<string, number>>((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<typeof buildServerSummaries>,
): 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<string, unknown>
: {};
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 启动、工具发现或工具调用失败信号。',
};
};

View File

@@ -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()),

View File

@@ -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 上下文体量诊断失败',

View File

@@ -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');

View File

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

View File

@@ -30,6 +30,7 @@ const TOOL_ACTION_LABELS: Record<string, string> = {
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',

View File

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

View File

@@ -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 工具参数',

View File

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