mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 10:29:52 +08:00
✨ feat(mcp): 增强新增服务参数逐项提示
- 新增 MCP 参数逐项说明,覆盖未知参数、位置参数和常见运行时参数 - 对敏感参数值做脱敏展示,避免提示区泄露 token 或 api key - 将逐项说明拆分到独立 util,并接入 inspect_mcp_draft 诊断输出
This commit is contained in:
@@ -115,12 +115,40 @@ describe('AIMCPArgumentHints', () => {
|
||||
});
|
||||
|
||||
const text = flattenRendererText(renderer.toJSON());
|
||||
expect(text).toContain('参数逐项说明');
|
||||
expect(text).toContain('已识别业务参数');
|
||||
expect(text).toContain('--api-key');
|
||||
expect(text).toContain('API Key');
|
||||
expect(text).toContain('不要截图真实值');
|
||||
expect(text).toContain('--directory');
|
||||
expect(text).toContain('授权目录');
|
||||
expect(text).toContain('值已脱敏');
|
||||
expect(text).not.toContain('sk-real-secret');
|
||||
});
|
||||
|
||||
it('renders fallback explanations for unknown MCP args', async () => {
|
||||
let renderer!: ReactTestRenderer;
|
||||
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
<AIMCPArgumentHints
|
||||
command="acme-mcp-server"
|
||||
args={['--tenant', 'prod', 'target-a']}
|
||||
cardBorder="rgba(0,0,0,0.08)"
|
||||
darkMode={false}
|
||||
overlayTheme={buildOverlayWorkbenchTheme(false)}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const text = flattenRendererText(renderer.toJSON());
|
||||
expect(text).toContain('参数逐项说明');
|
||||
expect(text).toContain('--tenant');
|
||||
expect(text).toContain('未识别参数');
|
||||
expect(text).toContain('GoNavi 不能从参数名 --tenant 准确判断业务含义');
|
||||
expect(text).toContain('prod');
|
||||
expect(text).toContain('未识别参数的值');
|
||||
expect(text).toContain('target-a');
|
||||
expect(text).toContain('位置参数');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
|
||||
import { buildMCPArgumentDetailHints } from '../../utils/mcpArgumentDetailHints';
|
||||
import { buildMCPArgumentHintProfile } from '../../utils/mcpArgumentHints';
|
||||
import { splitShellLikeCommand } from '../../utils/mcpCommandDraft';
|
||||
import { buildMCPHintStyle, mcpLabelStyle } from './AIMCPHelpBlock';
|
||||
@@ -72,6 +73,10 @@ const AIMCPArgumentHints: React.FC<AIMCPArgumentHintsProps> = ({
|
||||
return null;
|
||||
}
|
||||
const missingRequiredArgs = buildMissingRequiredArgs(profile);
|
||||
const argumentHints = buildMCPArgumentDetailHints(profile.commandName, [
|
||||
...profile.inlineArgs,
|
||||
...(args || []),
|
||||
]);
|
||||
const canApplyMissingArgs = Boolean(onArgsChange && missingRequiredArgs.length > 0 && profile.inlineArgs.length === 0);
|
||||
const canSplitInlineArgs = Boolean(onCommandArgsChange && profile.inlineArgs.length > 0);
|
||||
|
||||
@@ -119,6 +124,53 @@ const AIMCPArgumentHints: React.FC<AIMCPArgumentHintsProps> = ({
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{argumentHints.length > 0 ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ ...mcpLabelStyle, color: overlayTheme.titleText }}>
|
||||
参数逐项说明
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 8 }}>
|
||||
{argumentHints.map((hint) => (
|
||||
<div
|
||||
key={hint.key}
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
borderRadius: 10,
|
||||
border: `1px solid ${cardBorder}`,
|
||||
background: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.82)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 5,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
|
||||
<code style={{ fontFamily: 'var(--gn-font-mono)', fontSize: 12, color: overlayTheme.titleText }}>
|
||||
{hint.argument}
|
||||
</code>
|
||||
<span
|
||||
style={{
|
||||
padding: '1px 7px',
|
||||
borderRadius: 999,
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
color: businessHintCategoryColor[hint.category],
|
||||
background: darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(15,23,42,0.05)',
|
||||
}}
|
||||
>
|
||||
{businessHintCategoryLabel[hint.category]}
|
||||
</span>
|
||||
{hint.sensitive ? <span style={buildMCPHintStyle('#b45309')}>值已脱敏</span> : null}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: overlayTheme.titleText }}>{hint.label}</div>
|
||||
<div style={buildMCPHintStyle(overlayTheme.mutedText)}>{hint.detail}</div>
|
||||
<div style={buildMCPHintStyle(hint.sensitive ? '#b45309' : overlayTheme.mutedText)}>
|
||||
应填:{hint.valueHint}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{profile.businessHints.length > 0 ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ ...mcpLabelStyle, color: overlayTheme.titleText }}>
|
||||
|
||||
@@ -412,12 +412,14 @@ describe('aiLocalToolExecutor AI config inspection tools', () => {
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.content).toContain('"argumentHints"');
|
||||
expect(result.content).toContain('"argumentDetailHints"');
|
||||
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('"label":"授权目录的值"');
|
||||
expect(result.content).toContain('"argsRedacted":true');
|
||||
expect(result.content).toContain('"--api-key=***"');
|
||||
expect(result.content).not.toContain('sk-real-secret');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AIMCPServerConfig } from '../../types';
|
||||
import { buildMCPArgumentDetailHints } from '../../utils/mcpArgumentDetailHints';
|
||||
import { buildMCPArgumentHintProfile } from '../../utils/mcpArgumentHints';
|
||||
import { parseMCPCommandDraft, type ParseMCPCommandDraftResult } from '../../utils/mcpCommandDraft';
|
||||
import { buildMCPEnvHintProfile } from '../../utils/mcpEnvHints';
|
||||
@@ -254,6 +255,7 @@ export const buildMCPDraftInspectionSnapshot = (args: Record<string, unknown> =
|
||||
summary: argumentHintProfile.summary,
|
||||
orderHint: argumentHintProfile.orderHint,
|
||||
steps: argumentHintProfile.steps,
|
||||
argumentDetailHints: buildMCPArgumentDetailHints(argumentHintProfile.commandName, commandArgs),
|
||||
businessHints: argumentHintProfile.businessHints,
|
||||
nextActions: argumentHintProfile.nextActions,
|
||||
} : null,
|
||||
|
||||
281
frontend/src/utils/mcpArgumentDetailHints.ts
Normal file
281
frontend/src/utils/mcpArgumentDetailHints.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import {
|
||||
type BusinessArgumentHintTemplate,
|
||||
type MCPBusinessArgumentHintCategory,
|
||||
hasDockerImageArg,
|
||||
hasPackageLikeArg,
|
||||
normalizeFlagName,
|
||||
resolveBusinessArgumentHintTemplate,
|
||||
sanitizeFlagForDisplay,
|
||||
toTrimmedString,
|
||||
} from './mcpArgumentHints';
|
||||
|
||||
export interface MCPArgumentDetailHint {
|
||||
key: string;
|
||||
argument: string;
|
||||
category: MCPBusinessArgumentHintCategory;
|
||||
label: string;
|
||||
detail: string;
|
||||
valueHint: string;
|
||||
sensitive: boolean;
|
||||
}
|
||||
|
||||
const VALUE_ARG_FLAGS = new Set([
|
||||
'api-key',
|
||||
'token',
|
||||
'access-token',
|
||||
'password',
|
||||
'secret',
|
||||
'config',
|
||||
'config-file',
|
||||
'c',
|
||||
'directory',
|
||||
'dir',
|
||||
'root',
|
||||
'workspace',
|
||||
'path',
|
||||
'url',
|
||||
'endpoint',
|
||||
'base-url',
|
||||
'host',
|
||||
'port',
|
||||
'transport',
|
||||
'mode',
|
||||
'profile',
|
||||
'tenant',
|
||||
'project',
|
||||
'account',
|
||||
'executable-path',
|
||||
'repo',
|
||||
'e',
|
||||
'env',
|
||||
'name',
|
||||
'network',
|
||||
'v',
|
||||
'volume',
|
||||
'p',
|
||||
'publish',
|
||||
'entrypoint',
|
||||
'w',
|
||||
'workdir',
|
||||
'u',
|
||||
'user',
|
||||
'platform',
|
||||
'h',
|
||||
'hostname',
|
||||
]);
|
||||
|
||||
const flagExpectsValue = (flag: string): boolean => VALUE_ARG_FLAGS.has(flag);
|
||||
|
||||
const fallbackArgumentHint = (flag: string): BusinessArgumentHintTemplate => ({
|
||||
category: 'generic',
|
||||
label: '未识别参数',
|
||||
detail: `GoNavi 不能从参数名 --${flag} 准确判断业务含义,但会按当前顺序原样传给 MCP 进程。`,
|
||||
valueHint: '请对照 MCP README 确认这个参数是否需要值;需要值时把值作为下一个参数标签,或使用 --name=value。',
|
||||
sensitive: false,
|
||||
});
|
||||
|
||||
const sanitizeArgumentValueForDisplay = (value: string, sensitive = false): string => {
|
||||
const text = toTrimmedString(value);
|
||||
if (!text) return '';
|
||||
if (sensitive) return '<已隐藏>';
|
||||
if (/^(.{0,24})=(.*)$/u.test(text) && /(token|api[-_]?key|secret|password|credential)/iu.test(text.split('=')[0])) {
|
||||
return `${text.split('=')[0]}=<已隐藏>`;
|
||||
}
|
||||
if (/(sk-[a-z0-9_-]{8,}|ghp_[a-z0-9_]{8,}|xox[baprs]-[a-z0-9-]{8,})/iu.test(text)) {
|
||||
return '<疑似密钥,已隐藏>';
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const buildArgumentDetail = (
|
||||
key: string,
|
||||
argument: string,
|
||||
template: BusinessArgumentHintTemplate,
|
||||
): MCPArgumentDetailHint => ({
|
||||
key,
|
||||
argument,
|
||||
category: template.category,
|
||||
label: template.label,
|
||||
detail: template.detail,
|
||||
valueHint: template.valueHint,
|
||||
sensitive: template.sensitive,
|
||||
});
|
||||
|
||||
const runtimeArgumentTemplate = (
|
||||
commandName: string,
|
||||
args: string[],
|
||||
arg: string,
|
||||
index: number,
|
||||
): BusinessArgumentHintTemplate | null => {
|
||||
const text = toTrimmedString(arg);
|
||||
const lower = text.toLowerCase();
|
||||
|
||||
if (lower === '--stdio' || lower === 'stdio') {
|
||||
return {
|
||||
category: 'mode',
|
||||
label: 'stdio 通信模式',
|
||||
detail: '让 MCP Server 通过标准输入输出和 GoNavi 保持通信。',
|
||||
valueHint: '这是开关参数,一般不需要额外值。',
|
||||
sensitive: false,
|
||||
};
|
||||
}
|
||||
if (lower === '-y' && ['npx', 'npm', 'pnpm', 'yarn'].includes(commandName)) {
|
||||
return {
|
||||
category: 'runtime',
|
||||
label: '跳过安装确认',
|
||||
detail: '避免 npx 首次启动包时等待交互确认,适合后台工具发现。',
|
||||
valueHint: '这是开关参数,不需要额外值。',
|
||||
sensitive: false,
|
||||
};
|
||||
}
|
||||
if (lower === '-m' && ['python', 'python3', 'py'].includes(commandName)) {
|
||||
return {
|
||||
category: 'runtime',
|
||||
label: 'Python 模块启动',
|
||||
detail: '表示后一个参数是 Python 模块名,而不是脚本文件路径。',
|
||||
valueHint: '后面补模块名,例如 your_mcp_server。',
|
||||
sensitive: false,
|
||||
};
|
||||
}
|
||||
if (commandName === 'docker') {
|
||||
if (lower === 'run') {
|
||||
return {
|
||||
category: 'runtime',
|
||||
label: 'Docker 运行子命令',
|
||||
detail: '表示启动一个容器来运行 MCP Server。',
|
||||
valueHint: '通常放在 docker 后面的第一个参数。',
|
||||
sensitive: false,
|
||||
};
|
||||
}
|
||||
if (lower === '-i' || lower === '--interactive') {
|
||||
return {
|
||||
category: 'runtime',
|
||||
label: '保持标准输入',
|
||||
detail: 'MCP stdio 需要容器 stdin 持续打开,否则工具发现可能启动后立刻断开。',
|
||||
valueHint: '这是 Docker MCP 的关键参数。',
|
||||
sensitive: false,
|
||||
};
|
||||
}
|
||||
if (lower === '--rm') {
|
||||
return {
|
||||
category: 'runtime',
|
||||
label: '退出后清理容器',
|
||||
detail: '测试和日常使用后自动删除临时容器,避免残留。',
|
||||
valueHint: '这是开关参数,不需要额外值。',
|
||||
sensitive: false,
|
||||
};
|
||||
}
|
||||
if (!text.startsWith('-') && hasDockerImageArg(args.slice(0, index + 1))) {
|
||||
return {
|
||||
category: 'runtime',
|
||||
label: 'Docker 镜像或容器参数',
|
||||
detail: '这是 docker run 中的镜像名或传给容器内 MCP 服务的位置参数。',
|
||||
valueHint: '镜像名应来自 MCP README;镜像后的参数会传给容器入口程序。',
|
||||
sensitive: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!text.startsWith('-')) {
|
||||
if (['npx', 'npm', 'pnpm', 'yarn'].includes(commandName) && hasPackageLikeArg([text])) {
|
||||
return {
|
||||
category: 'runtime',
|
||||
label: 'MCP 包名或位置参数',
|
||||
detail: '通常是 README 里的 npm 包名,也可能是包自己的业务参数。',
|
||||
valueHint: '包名一般放在 -y 后、--stdio 前;业务参数以 README 为准。',
|
||||
sensitive: false,
|
||||
};
|
||||
}
|
||||
if (commandName === 'uvx' || commandName === 'uv') {
|
||||
return {
|
||||
category: 'runtime',
|
||||
label: 'Python MCP 包名或位置参数',
|
||||
detail: 'uvx 后面通常跟 MCP 包名;后续位置参数会传给该 MCP 服务。',
|
||||
valueHint: '第一个位置参数应是 README 里的包名。',
|
||||
sensitive: false,
|
||||
};
|
||||
}
|
||||
if (['node', 'bun', 'deno'].includes(commandName)) {
|
||||
return {
|
||||
category: /\.(c?m?[jt]s)$/iu.test(text) || /[\\/]/u.test(text) ? 'path' : 'runtime',
|
||||
label: '脚本或位置参数',
|
||||
detail: '通常是本地 MCP Server 的入口脚本;脚本后的值会作为业务参数传入。',
|
||||
valueHint: '入口脚本建议使用本机可访问的相对或绝对路径。',
|
||||
sensitive: false,
|
||||
};
|
||||
}
|
||||
if (['python', 'python3', 'py'].includes(commandName)) {
|
||||
return {
|
||||
category: args[index - 1] === '-m' ? 'runtime' : 'path',
|
||||
label: args[index - 1] === '-m' ? 'Python 模块名' : 'Python 脚本或位置参数',
|
||||
detail: args[index - 1] === '-m'
|
||||
? '这是 -m 后面的模块名,不要带 .py 后缀。'
|
||||
: '通常是本地 Python MCP 脚本路径,或传给脚本的位置参数。',
|
||||
valueHint: '以 README 的启动示例为准。',
|
||||
sensitive: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const buildMCPArgumentDetailHints = (commandName: string, args: string[]): MCPArgumentDetailHint[] => {
|
||||
const result: MCPArgumentDetailHint[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const text = toTrimmedString(args[index]);
|
||||
if (!text) continue;
|
||||
|
||||
const previousFlag = index > 0 ? normalizeFlagName(args[index - 1]) : '';
|
||||
const previousHasInlineValue = index > 0 && toTrimmedString(args[index - 1]).includes('=');
|
||||
if (previousFlag && !previousHasInlineValue && flagExpectsValue(previousFlag) && !text.startsWith('-')) {
|
||||
const template = resolveBusinessArgumentHintTemplate(previousFlag, true) || fallbackArgumentHint(previousFlag);
|
||||
result.push(buildArgumentDetail(
|
||||
`value-${index}-${previousFlag}`,
|
||||
sanitizeArgumentValueForDisplay(text, template.sensitive),
|
||||
{
|
||||
...template,
|
||||
label: `${template.label}的值`,
|
||||
detail: template.sensitive
|
||||
? `这是前一个 ${sanitizeFlagForDisplay(args[index - 1])} 的敏感值,提示中已脱敏。`
|
||||
: `这是前一个 ${sanitizeFlagForDisplay(args[index - 1])} 参数的值。`,
|
||||
},
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
const runtimeTemplate = runtimeArgumentTemplate(commandName, args, text, index);
|
||||
if (runtimeTemplate) {
|
||||
result.push(buildArgumentDetail(
|
||||
`runtime-${index}-${text}`,
|
||||
sanitizeArgumentValueForDisplay(text, runtimeTemplate.sensitive),
|
||||
runtimeTemplate,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
const flag = normalizeFlagName(text);
|
||||
if (flag) {
|
||||
const template = resolveBusinessArgumentHintTemplate(flag, true) || fallbackArgumentHint(flag);
|
||||
result.push(buildArgumentDetail(
|
||||
`flag-${index}-${flag}`,
|
||||
sanitizeFlagForDisplay(text),
|
||||
template,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push(buildArgumentDetail(
|
||||
`positional-${index}`,
|
||||
sanitizeArgumentValueForDisplay(text),
|
||||
{
|
||||
category: 'generic',
|
||||
label: '位置参数',
|
||||
detail: '这是没有参数名的位置参数,GoNavi 会按当前顺序原样传入 MCP 进程。',
|
||||
valueHint: '请对照 README 判断它是包名、路径、镜像名还是业务参数。',
|
||||
sensitive: false,
|
||||
},
|
||||
));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildMCPArgumentDetailHints } from './mcpArgumentDetailHints';
|
||||
import { buildMCPArgumentHintProfile } from './mcpArgumentHints';
|
||||
|
||||
describe('mcpArgumentHints', () => {
|
||||
@@ -86,13 +87,54 @@ describe('mcpArgumentHints', () => {
|
||||
]));
|
||||
});
|
||||
|
||||
it('builds per-argument explanations for unknown flags and positional values', () => {
|
||||
const hints = buildMCPArgumentDetailHints('acme-mcp-server', [
|
||||
'--tenant',
|
||||
'prod',
|
||||
'--workspace',
|
||||
'D:\\Work',
|
||||
'extra-target',
|
||||
]);
|
||||
|
||||
expect(hints).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
argument: '--tenant',
|
||||
label: '未识别参数',
|
||||
category: 'generic',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
argument: 'prod',
|
||||
label: '未识别参数的值',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
argument: '--workspace',
|
||||
label: '工作区目录',
|
||||
category: 'path',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
argument: 'D:\\Work',
|
||||
label: '工作区目录的值',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
argument: 'extra-target',
|
||||
label: '位置参数',
|
||||
}),
|
||||
]));
|
||||
});
|
||||
|
||||
it('sanitizes sensitive inline argument values in hints', () => {
|
||||
const profile = buildMCPArgumentHintProfile('uvx', [
|
||||
const args = [
|
||||
'mcp-server-demo',
|
||||
'--api-key=sk-real-secret',
|
||||
'--token',
|
||||
'ghp_real-secret-token',
|
||||
'--endpoint',
|
||||
'https://api.example.com',
|
||||
];
|
||||
const profile = buildMCPArgumentHintProfile('uvx', [
|
||||
...args,
|
||||
]);
|
||||
const argumentHints = buildMCPArgumentDetailHints('uvx', args);
|
||||
|
||||
expect(profile?.businessHints).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -107,5 +149,18 @@ describe('mcpArgumentHints', () => {
|
||||
}),
|
||||
]));
|
||||
expect(JSON.stringify(profile?.businessHints)).not.toContain('sk-real-secret');
|
||||
expect(argumentHints).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
argument: '--api-key',
|
||||
sensitive: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
argument: '<已隐藏>',
|
||||
label: 'Token的值',
|
||||
sensitive: true,
|
||||
}),
|
||||
]));
|
||||
expect(JSON.stringify(argumentHints)).not.toContain('sk-real-secret');
|
||||
expect(JSON.stringify(argumentHints)).not.toContain('ghp_real-secret-token');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface MCPArgumentHintProfile {
|
||||
nextActions: string[];
|
||||
}
|
||||
|
||||
const toTrimmedString = (value: unknown): string => String(value ?? '').trim();
|
||||
export const toTrimmedString = (value: unknown): string => String(value ?? '').trim();
|
||||
|
||||
const parseCommandField = (command: string): { normalizedCommand: string; commandName: string; inlineArgs: string[] } => {
|
||||
const { tokens } = splitShellLikeCommand(command);
|
||||
@@ -65,7 +65,7 @@ const hasArg = (args: string[], expected: string): boolean =>
|
||||
const hasStdioArg = (args: string[]): boolean =>
|
||||
hasArg(args, '--stdio') || hasArg(args, 'stdio');
|
||||
|
||||
const hasPackageLikeArg = (args: string[]): boolean =>
|
||||
export const hasPackageLikeArg = (args: string[]): boolean =>
|
||||
args.some((arg) => {
|
||||
const text = arg.trim();
|
||||
if (!text || text.startsWith('-')) return false;
|
||||
@@ -86,7 +86,7 @@ const hasDockerRunArg = (args: string[]): boolean =>
|
||||
const hasDockerInteractiveArg = (args: string[]): boolean =>
|
||||
hasArg(args, '-i') || hasArg(args, '--interactive');
|
||||
|
||||
const hasDockerImageArg = (args: string[]): boolean => {
|
||||
export const hasDockerImageArg = (args: string[]): boolean => {
|
||||
const runIndex = args.findIndex((arg) => arg.toLowerCase() === 'run');
|
||||
const candidates = runIndex >= 0 ? args.slice(runIndex + 1) : args;
|
||||
for (let index = 0; index < candidates.length; index += 1) {
|
||||
@@ -143,7 +143,7 @@ const buildNextActions = (steps: MCPArgumentHintStep[]): string[] =>
|
||||
.filter((step) => step.required && !step.satisfied)
|
||||
.map((step) => `补充 ${step.label},示例:${step.example}`);
|
||||
|
||||
type BusinessArgumentHintTemplate = Omit<MCPBusinessArgumentHint, 'key' | 'argument'>;
|
||||
export type BusinessArgumentHintTemplate = Omit<MCPBusinessArgumentHint, 'key' | 'argument'>;
|
||||
|
||||
const BUSINESS_ARGUMENT_HINTS: Record<string, BusinessArgumentHintTemplate> = {
|
||||
'api-key': {
|
||||
@@ -330,7 +330,7 @@ const BUSINESS_ARGUMENT_HINTS: Record<string, BusinessArgumentHintTemplate> = {
|
||||
},
|
||||
};
|
||||
|
||||
const normalizeFlagName = (arg: string): string => {
|
||||
export const normalizeFlagName = (arg: string): string => {
|
||||
const text = toTrimmedString(arg);
|
||||
if (!text.startsWith('-') || text === '-' || text === '--') {
|
||||
return '';
|
||||
@@ -339,7 +339,7 @@ const normalizeFlagName = (arg: string): string => {
|
||||
return withoutValue.replace(/^-+/u, '').trim().toLowerCase();
|
||||
};
|
||||
|
||||
const sanitizeFlagForDisplay = (arg: string): string => {
|
||||
export const sanitizeFlagForDisplay = (arg: string): string => {
|
||||
const text = toTrimmedString(arg);
|
||||
const withoutValue = text.split('=')[0];
|
||||
return withoutValue || text;
|
||||
@@ -383,6 +383,17 @@ const inferBusinessArgumentHint = (flag: string): BusinessArgumentHintTemplate |
|
||||
return null;
|
||||
};
|
||||
|
||||
const buildGenericArgumentHint = (flag: string): BusinessArgumentHintTemplate => ({
|
||||
category: 'generic',
|
||||
label: '未识别参数',
|
||||
detail: `GoNavi 不能从参数名 --${flag} 准确判断业务含义,但会按当前顺序原样传给 MCP 进程。`,
|
||||
valueHint: '请对照 MCP README 确认这个参数是否需要值;需要值时把值作为下一个参数标签,或使用 --name=value。',
|
||||
sensitive: false,
|
||||
});
|
||||
|
||||
export const resolveBusinessArgumentHintTemplate = (flag: string, fallbackGeneric = false): BusinessArgumentHintTemplate | null =>
|
||||
BUSINESS_ARGUMENT_HINTS[flag] || inferBusinessArgumentHint(flag) || (fallbackGeneric && flag ? buildGenericArgumentHint(flag) : null);
|
||||
|
||||
const buildBusinessArgumentHints = (args: string[]): MCPBusinessArgumentHint[] => {
|
||||
const result: MCPBusinessArgumentHint[] = [];
|
||||
const seen = new Set<string>();
|
||||
@@ -391,7 +402,7 @@ const buildBusinessArgumentHints = (args: string[]): MCPBusinessArgumentHint[] =
|
||||
if (!flag || flag === 'stdio') {
|
||||
continue;
|
||||
}
|
||||
const template = BUSINESS_ARGUMENT_HINTS[flag] || inferBusinessArgumentHint(flag);
|
||||
const template = resolveBusinessArgumentHintTemplate(flag);
|
||||
if (!template) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user