diff --git a/frontend/src/components/ai/aiLocalToolExecutor.aiConfigInspection.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.aiConfigInspection.test.ts index 737fe4a..5f78eac 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.aiConfigInspection.test.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.aiConfigInspection.test.ts @@ -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', { diff --git a/frontend/src/components/ai/aiMCPDraftInspectionInsights.ts b/frontend/src/components/ai/aiMCPDraftInspectionInsights.ts index d3132fe..4820d6c 100644 --- a/frontend/src/components/ai/aiMCPDraftInspectionInsights.ts +++ b/frontend/src/components/ai/aiMCPDraftInspectionInsights.ts @@ -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): Record => 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 = 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 = 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 = 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 = } : 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: { diff --git a/frontend/src/utils/aiBuiltinInspectionMcpToolInfo.ts b/frontend/src/utils/aiBuiltinInspectionMcpToolInfo.ts index b0463e8..89ca237 100644 --- a/frontend/src/utils/aiBuiltinInspectionMcpToolInfo.ts +++ b/frontend/src/utils/aiBuiltinInspectionMcpToolInfo.ts @@ -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: {