feat(ai): 新增 MCP 草稿校验探针

- 新增 inspect_mcp_draft 内置工具,支持完整命令和分字段草稿校验

- 补充 MCP 新增指引、斜杠菜单和工具目录流程

- 增加工具注册、执行器和系统提示相关测试
This commit is contained in:
Syngnat
2026-06-10 17:17:37 +08:00
parent c9d0bce153
commit 7eb086cade
13 changed files with 341 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<AIMCPServerConfig> => {
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<string>;
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.jspython 需要 -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<string, unknown> = {}) => {
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<string, string> = { ...(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<AIMCPServerConfig, 'name' | 'transport' | 'command' | 'args' | 'timeoutSeconds'> = {
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),
}),
};
};

View File

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

View File

@@ -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'] },

View File

@@ -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 配置探针失败';

View File

@@ -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 读取当前挂载的表结构上下文');

View File

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

View File

@@ -29,6 +29,7 @@ const TOOL_ACTION_LABELS: Record<string, string> = {
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: '获取可用连接信息',

View File

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

View File

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