feat(ai): 增强 MCP 草稿参数诊断

This commit is contained in:
Syngnat
2026-06-12 07:57:38 +08:00
parent 0573155285
commit 1058da653d
3 changed files with 83 additions and 6 deletions

View File

@@ -396,6 +396,33 @@ describe('aiLocalToolExecutor AI config inspection tools', () => {
expect(result.content).toContain('"canSave":true');
});
it('returns MCP argument hints and redacts sensitive inline argument values', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_mcp_draft', {
fullCommand: 'uvx mcp-server-demo --stdio --api-key=sk-real-secret --directory D:\\Work',
}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"argumentHints"');
expect(result.content).toContain('"businessHints"');
expect(result.content).toContain('"argument":"--api-key"');
expect(result.content).toContain('"label":"API Key"');
expect(result.content).toContain('"sensitive":true');
expect(result.content).toContain('"argument":"--directory"');
expect(result.content).toContain('"label":"授权目录"');
expect(result.content).toContain('"argsRedacted":true');
expect(result.content).toContain('"--api-key=***"');
expect(result.content).not.toContain('sk-real-secret');
});
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

@@ -1,4 +1,5 @@
import type { AIMCPServerConfig } from '../../types';
import { buildMCPArgumentHintProfile } from '../../utils/mcpArgumentHints';
import { parseMCPCommandDraft, type ParseMCPCommandDraftResult } from '../../utils/mcpCommandDraft';
import { buildMCPEnvHintProfile } from '../../utils/mcpEnvHints';
import { parseMCPEnvDraft } from '../../utils/mcpEnvDraft';
@@ -31,6 +32,40 @@ const normalizeTimeoutSeconds = (value: unknown, fallback: number): number => {
const redactEnvValues = (env: Record<string, string>): Record<string, string> =>
Object.fromEntries(Object.keys(env).sort().map((key) => [key, env[key] ? '***' : '']));
const isSensitiveArgFlag = (arg: string): boolean => {
const flag = toTrimmedString(arg).split('=')[0].replace(/^-+/u, '').toLowerCase();
return /(token|api-?key|secret|password|pass|credential)/iu.test(flag);
};
const redactSensitiveArgValues = (args: string[]): string[] => {
const result: string[] = [];
let redactNext = false;
for (const arg of args) {
const text = toTrimmedString(arg);
if (!text) {
continue;
}
if (redactNext && !text.startsWith('-')) {
result.push('***');
redactNext = false;
continue;
}
redactNext = false;
if (isSensitiveArgFlag(text)) {
const equalsIndex = text.indexOf('=');
if (equalsIndex >= 0) {
result.push(`${text.slice(0, equalsIndex)}=***`);
} else {
result.push(text);
redactNext = true;
}
continue;
}
result.push(text);
}
return result;
};
const buildRedactedFullCommand = (
fullCommand: string,
parsedCommand: ParseMCPCommandDraftResult | null,
@@ -43,7 +78,7 @@ const buildRedactedFullCommand = (
}
return [
...Object.keys(parsedCommand.draft.env || {}).sort().map((key) => `${key}=***`),
buildMCPLaunchPreview(parsedCommand.draft.command, parsedCommand.draft.args),
buildMCPLaunchPreview(parsedCommand.draft.command, redactSensitiveArgValues(parsedCommand.draft.args)),
].filter(Boolean).join(' ');
};
@@ -172,6 +207,8 @@ export const buildMCPDraftInspectionSnapshot = (args: Record<string, unknown> =
const validation = validateMCPServerDraft(server, parsedEnvDraft);
const issueKeys = new Set(validation.issues.map((issue) => issue.key));
const recommendedTemplate = resolveRecommendedTemplate(command, commandArgs);
const argumentHintProfile = buildMCPArgumentHintProfile(command, commandArgs);
const redactedCommandArgs = redactSensitiveArgValues(commandArgs);
const envHintProfile = buildMCPEnvHintProfile(command, commandArgs, env);
const suggestedServerSeed = buildMCPServerDraftSeed({
name: toTrimmedString(args.name ?? args.serverName) || undefined,
@@ -192,7 +229,8 @@ export const buildMCPDraftInspectionSnapshot = (args: Record<string, unknown> =
ok: parsedCommand.ok,
error: parsedCommand.error || '',
command: parsedCommand.draft?.command || '',
args: parsedCommand.draft?.args || [],
args: redactSensitiveArgValues(parsedCommand.draft?.args || []),
argsRedacted: JSON.stringify(redactSensitiveArgValues(parsedCommand.draft?.args || [])) !== JSON.stringify(parsedCommand.draft?.args || []),
envKeys: Object.keys(parsedCommand.draft?.env || {}).sort(),
}
: {
@@ -206,9 +244,19 @@ export const buildMCPDraftInspectionSnapshot = (args: Record<string, unknown> =
name: server.name,
transport: server.transport,
command,
args: commandArgs,
args: redactedCommandArgs,
argsRedacted: JSON.stringify(redactedCommandArgs) !== JSON.stringify(commandArgs),
envKeys: Object.keys(env).sort(),
envVarCount: Object.keys(env).length,
argumentHints: argumentHintProfile ? {
commandName: argumentHintProfile.commandName,
title: argumentHintProfile.title,
summary: argumentHintProfile.summary,
orderHint: argumentHintProfile.orderHint,
steps: argumentHintProfile.steps,
businessHints: argumentHintProfile.businessHints,
nextActions: argumentHintProfile.nextActions,
} : null,
envHints: envHintProfile ? {
envVarCount: envHintProfile.envVarCount,
secretLikeCount: envHintProfile.secretLikeCount,
@@ -229,12 +277,14 @@ export const buildMCPDraftInspectionSnapshot = (args: Record<string, unknown> =
} : null,
invalidEnvLines: parsedEnvDraft?.invalidLines || [],
timeoutSeconds: server.timeoutSeconds,
launchCommandPreview: buildMCPLaunchPreview(command, commandArgs),
launchCommandPreview: buildMCPLaunchPreview(command, redactedCommandArgs),
recommendedTemplate,
suggestedServerSeed: {
...suggestedServerSeed,
args: redactSensitiveArgValues(suggestedServerSeed.args || []),
env: redactEnvValues(env),
envRedacted: Object.keys(env).length > 0,
argsRedacted: JSON.stringify(redactSensitiveArgValues(suggestedServerSeed.args || [])) !== JSON.stringify(suggestedServerSeed.args || []),
},
},
validation: {

View File

@@ -118,14 +118,14 @@ export const BUILTIN_AI_INSPECTION_MCP_TOOL_INFO: AIBuiltinToolInfo[] = [
icon: "🧪",
desc: "校验 MCP 新增草稿",
detail:
"按完整启动命令或分字段草稿试算 GoNavi 的 MCP 新增配置,返回自动拆分结果、启动预览、可应用草稿、环境变量用途提示、字段校验问题、推荐模板和下一步修复建议。适合用户贴出一整行 MCP 启动命令、问 command/args/env/timeout 该怎么拆,或保存前想确认配置有没有明显问题时使用。",
"按完整启动命令或分字段草稿试算 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 做分字段校验返回解析后的字段、启动命令预览、suggestedServerSeed、环境变量 key 的用途和风险提示、错误/告警、推荐模板和 nextActions。适用于用户贴出 MCP README 启动命令、问新增 MCP 参数怎么填、或 AI 准备指导用户保存前,先用真实校验器试算。",
"校验一份待新增的 MCP 服务草稿。支持传 fullCommand/rawCommand/commandLine 让 GoNavi 自动拆分,也支持传 command、args、envText、timeoutSeconds 和 templateKey 做分字段校验;返回解析后的字段、脱敏启动命令预览、suggestedServerSeed、命令参数用途提示、环境变量 key 的用途和风险提示、错误/告警、推荐模板和 nextActions。适用于用户贴出 MCP README 启动命令、问新增 MCP 参数怎么填、或 AI 准备指导用户保存前,先用真实校验器试算;结果不会回显 api-key/token/password 等敏感参数值。",
parameters: {
type: "object",
properties: {