mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-15 19:19:35 +08:00
✨ feat(ai): 增强 MCP 草稿参数诊断
This commit is contained in:
@@ -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', {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user