mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 02:19:58 +08:00
✨ feat(mcp): 新增运行期失败诊断探针
- 解析 gonavi.log 中 MCP 启动、发现和调用失败信号 - 结合已保存 MCP 服务与工具发现状态输出原因和 nextActions - 补充系统引导、工具目录、状态标签和回归测试
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
118
frontend/src/components/ai/aiMCPRuntimeFailureInsights.test.ts
Normal file
118
frontend/src/components/ai/aiMCPRuntimeFailureInsights.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
266
frontend/src/components/ai/aiMCPRuntimeFailureInsights.ts
Normal file
266
frontend/src/components/ai/aiMCPRuntimeFailureInsights.ts
Normal 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 只支持 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<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 启动、工具发现或工具调用失败信号。',
|
||||
};
|
||||
};
|
||||
@@ -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()),
|
||||
|
||||
@@ -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 上下文体量诊断失败',
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: "🧭",
|
||||
|
||||
@@ -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 工具参数',
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user