feat(ai): 完善 MCP 新增参数指引

- 新增 npx MCP 服务模板和启动预览

- 补充 command/args 表单说明与内置指引快照

- 覆盖 MCP 参数草稿、指引和 AI 配置检查测试
This commit is contained in:
Syngnat
2026-06-10 12:13:45 +08:00
parent d6f552d539
commit 2c7962f5d3
10 changed files with 57 additions and 21 deletions

View File

@@ -40,7 +40,7 @@ describe('AIMCPServerCard', () => {
expect(markup).toContain('保存后显示给你和 AI 看的名字');
expect(markup).toContain('示例值:');
expect(markup).toContain('Filesystem / Browser / GitHub');
expect(markup).toContain('server.js / --stdio / -m / your_mcp_server');
expect(markup).toContain('-y / @modelcontextprotocol/server-filesystem / --stdio / server.js');
expect(markup).toContain('当前固定为 stdio');
expect(markup).toContain('单次工具发现或调用最多等待多久');
expect(markup).toContain('必填');
@@ -50,10 +50,12 @@ describe('AIMCPServerCard', () => {
expect(markup).toContain('自动拆分到下方字段');
expect(markup).toContain('$env:KEY=VALUE;');
expect(markup).toContain('set KEY=VALUE &&');
expect(markup).toContain('npx -y package --stdio');
expect(markup).toContain('-y、@modelcontextprotocol/server-filesystem、--stdio、server.js');
expect(markup).toContain('每个参数单独录入一个标签');
expect(markup).toContain('每行一个 KEY=VALUE');
expect(markup).toContain('没有等号或 key 含空格的行不会保存');
expect(markup).toContain('不要把 node server.js --stdio 整串都塞进这里');
expect(markup).toContain('不要把 npx -y package --stdio 或 node server.js --stdio 整串都塞进这里');
expect(markup).toContain('不要写 export');
expect(markup).toContain('当前阶段只支持 stdio');
expect(markup).toContain('实际启动命令预览');
@@ -67,6 +69,7 @@ describe('AIMCPServerCard', () => {
expect(markup).toContain('默认 20 秒');
expect(markup).toContain('稍宽松 45 秒');
expect(markup).toContain('慢启动 60 秒');
expect(markup).toContain('npx -y @modelcontextprotocol/server-filesystem --stdio');
expect(markup).toContain('node server.js --stdio');
expect(markup).toContain('$env:GITHUB_TOKEN=...; uvx mcp-server-github --stdio');
});

View File

@@ -73,11 +73,11 @@ const AIMCPServerFormPanel: React.FC<AIMCPServerFormPanelProps> = ({
options={[{ label: 'stdio', value: 'stdio' }]}
/>
</AIMCPHelpBlock>
<AIMCPHelpBlock title="启动命令" description="这里只填命令本身;如果是 node/uvx/python 这类启动器,把脚本名或模块名放到下面的参数里。不要把 node server.js --stdio 整串都塞进这里。" overlayTheme={overlayTheme} darkMode={darkMode} fieldState="required" example="node / uvx / python">
<AIMCPHelpBlock title="启动命令" description="这里只填命令本身;如果是 npx/node/uvx/python 这类启动器,把包名、脚本名或模块名放到下面的参数里。不要把 npx -y package --stdio 或 node server.js --stdio 整串都塞进这里。" overlayTheme={overlayTheme} darkMode={darkMode} fieldState="required" example="npx / node / uvx / python">
<Input
value={server.command}
onChange={(event) => onChange({ command: event.target.value })}
placeholder="启动命令例如node / uvx / python"
placeholder="启动命令,例如:npx / node / uvx / python"
style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }}
/>
</AIMCPHelpBlock>
@@ -120,12 +120,12 @@ const AIMCPServerFormPanel: React.FC<AIMCPServerFormPanelProps> = ({
</AIMCPHelpBlock>
</div>
<AIMCPHelpBlock title="命令参数" description="每个参数单独录入一个标签;命令本体不要填在这里。比如 node server.js --stdio要把 server.js 和 --stdio 分开填。不确定怎么拆时,优先回到上面的“完整命令”框自动拆分。" overlayTheme={overlayTheme} darkMode={darkMode} fieldState="optional" example="server.js、--stdio、-m、your_mcp_server">
<AIMCPHelpBlock title="命令参数" description="每个参数单独录入一个标签;命令本体不要填在这里。比如 npx -y package --stdio要把 -y、package 和 --stdio 分开填;node server.js --stdio 要把 server.js 和 --stdio 分开填。不确定怎么拆时,优先回到上面的“完整命令”框自动拆分。" overlayTheme={overlayTheme} darkMode={darkMode} fieldState="optional" example="-y、@modelcontextprotocol/server-filesystem、--stdio、server.js">
<Select
mode="tags"
value={server.args || []}
onChange={(value) => onChange({ args: value })}
placeholder="命令参数,回车录入,例如:server.js、--stdio"
placeholder="命令参数,回车录入,例如:-y、包名、--stdio"
style={{ width: '100%' }}
/>
</AIMCPHelpBlock>

View File

@@ -111,9 +111,12 @@ describe('AISettingsMCPSection', () => {
expect(markup).toContain('给 MCP Server 传入 KEY=VALUE 形式的配置');
expect(markup).toContain('单次工具发现或调用最多等待多久');
expect(markup).toContain('常见启动方式模板');
expect(markup).toContain('npx 包');
expect(markup).toContain('npx -y @modelcontextprotocol/server-filesystem --stdio');
expect(markup).toContain('Node 脚本');
expect(markup).toContain('新增 MCP 服务');
expect(markup).toContain('还没有 MCP 服务');
expect(markup).toContain('npx -y package --stdio');
});
it('renders troubleshooting hints when a server draft exists', () => {
@@ -149,16 +152,16 @@ describe('AISettingsMCPSection', () => {
onAddServer,
}));
const nodeTemplateButton = findElement(
const npxTemplateButton = findElement(
tree,
(node) => node.type === 'button' && flattenElementText(node.props?.children).includes('Node 脚本'),
(node) => node.type === 'button' && flattenElementText(node.props?.children).includes('npx 包'),
);
expect(nodeTemplateButton).toBeTruthy();
nodeTemplateButton.props.onClick();
expect(npxTemplateButton).toBeTruthy();
npxTemplateButton.props.onClick();
expect(onAddServer).toHaveBeenCalledWith(expect.objectContaining({
command: 'node',
args: ['server.js', '--stdio'],
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', '--stdio'],
}));
});
});

View File

@@ -164,7 +164,7 @@ const AISettingsMCPSection: React.FC<AISettingsMCPSectionProps> = ({
</div>
{mcpServers.length === 0 && (
<div style={{ padding: '18px 16px', borderRadius: 14, border: `1px dashed ${cardBorder}`, background: cardBg, color: overlayTheme.mutedText }}>
MCP `node server.js``uvx some-mcp-server``python -m server`
MCP `npx -y package --stdio``node server.js``uvx some-mcp-server``python -m server`
</div>
)}
{mcpServers.map((server) => (

View File

@@ -315,8 +315,10 @@ describe('aiLocalToolExecutor AI config inspection tools', () => {
expect(result.content).toContain('"supportsWholeCommandAutoSplit":true');
expect(result.content).toContain('"fullCommandPasteExample":"$env:GITHUB_TOKEN=...; uvx mcp-server-github --stdio"');
expect(result.content).toContain('"title":"启动命令"');
expect(result.content).toContain('"example":"node / uvx / python"');
expect(result.content).toContain('"example":"npx / node / uvx / python"');
expect(result.content).toContain('PowerShell $env:KEY=VALUE;');
expect(result.content).toContain('"title":"npx 包"');
expect(result.content).toContain('"exampleLaunchPreview":"npx -y @modelcontextprotocol/server-filesystem --stdio"');
expect(result.content).toContain('"title":"uvx 工具"');
expect(result.content).toContain('"exampleLaunchPreview":"uvx some-mcp-server"');
});

View File

@@ -133,14 +133,14 @@ export const BUILTIN_AI_INSPECTION_TOOL_INFO: AIBuiltinToolInfo[] = [
icon: "🧭",
desc: "查看新增 MCP 的填写指引",
detail:
"返回新增 MCP 表单里各字段的作用、推荐填写顺序、完整命令自动拆分规则,以及 Node / uvx / Python / EXE 模板样例。适合用户问“command/args/env 到底怎么填”“给我一个 node / uvx / python 示例”“为什么启动命令不能整行填”时,先读这份真实接入指引。",
"返回新增 MCP 表单里各字段的作用、推荐填写顺序、完整命令自动拆分规则,以及 npx / Node / uvx / Python / EXE 模板样例。适合用户问“command/args/env 到底怎么填”“给我一个 npx / node / uvx / python 示例”“为什么启动命令不能整行填”时,先读这份真实接入指引。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_mcp_authoring_guide",
description:
"读取 GoNavi 当前内置的 MCP 新增指引,包括推荐填写顺序、字段作用、常见命令示例、完整命令自动拆分规则,以及 Node / uvx / Python / EXE 模板样例。适用于用户提到新增 MCP 不知道 command、args、env、timeout 怎么填,或想要一个最接近的模板时,先读取这份真实前端接入指南,不要凭记忆口述。",
"读取 GoNavi 当前内置的 MCP 新增指引,包括推荐填写顺序、字段作用、常见命令示例、完整命令自动拆分规则,以及 npx / Node / uvx / Python / EXE 模板样例。适用于用户提到新增 MCP 不知道 command、args、env、timeout 怎么填,或想要一个最接近的模板时,先读取这份真实前端接入指南,不要凭记忆口述。",
parameters: { type: "object", properties: {} },
},
},

View File

@@ -70,6 +70,17 @@ describe('mcpCommandDraft helpers', () => {
});
});
it('keeps npx package launches as command plus split args', () => {
const result = parseMCPCommandDraft('npx -y @modelcontextprotocol/server-filesystem --stdio C:\\Users\\me\\workspace');
expect(result.ok).toBe(true);
expect(result.draft).toEqual({
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', '--stdio', 'C:\\Users\\me\\workspace'],
env: {},
});
});
it('reports unclosed quotes instead of producing a broken parse', () => {
expect(splitShellLikeCommand('uvx "broken command')).toEqual({
tokens: ['uvx'],

View File

@@ -15,6 +15,7 @@ describe('mcpServerGuidance', () => {
expect(symptoms).toContain('测试提示找不到命令');
expect(symptoms).toContain('认证失败、401 或 403');
expect(allGuidance).toContain('命令参数');
expect(allGuidance).toContain('command=npx');
expect(allGuidance).toContain('KEY=VALUE');
expect(allGuidance).toContain('当前只支持 stdio');
});
@@ -24,6 +25,7 @@ describe('mcpServerGuidance', () => {
expect(notes).toContain('本机配置');
expect(notes).toContain('不要把密钥写进聊天内容');
expect(notes).toContain('command 填 npx');
expect(notes).toContain('PowerShell $env:KEY=VALUE;');
expect(notes).toContain('Windows set KEY=VALUE &&');
});

View File

@@ -24,6 +24,7 @@ export interface MCPTroubleshootingGuide {
}
export const MCP_COMMAND_EXAMPLES = [
'npx -y @modelcontextprotocol/server-filesystem --stdio',
'uvx mcp-server-fetch',
'node server.js --stdio',
'python -m your_mcp_server',
@@ -68,17 +69,17 @@ export const MCP_FIELD_GUIDES: MCPFieldGuide[] = [
key: 'command',
title: '启动命令',
summary: '只填程序名或启动器本身。',
detail: '常见是 node、uvx、python脚本名和 --stdio 这类内容放到参数里。',
detail: '常见是 npx、node、uvx、python包名、脚本名和 --stdio 这类内容放到参数里。',
fieldState: 'required',
example: 'node / uvx / python',
example: 'npx / node / uvx / python',
},
{
key: 'args',
title: '命令参数',
summary: '把脚本名、模块名、开关参数拆开逐项填写。',
detail: '例如 node server.js --stdio要拆成 server.js 和 --stdio 两项。',
detail: '例如 npx -y pkg --stdio要拆成 -y、pkg 和 --stdionode server.js --stdio 要拆成 server.js 和 --stdio。',
fieldState: 'optional',
example: 'server.js / --stdio / -m / your_mcp_server',
example: '-y / @modelcontextprotocol/server-filesystem / --stdio / server.js',
},
{
key: 'env',
@@ -100,6 +101,7 @@ export const MCP_FIELD_GUIDES: MCPFieldGuide[] = [
export const MCP_AUTHORING_NOTES = [
'启动命令只填程序本身,不要把脚本名、模块名和 --stdio 混进去。',
'README 给 npx 示例时command 填 npxargs 逐项填 -y、包名和 --stdio不要把整行 npx 命令放进 command。',
'如果 README 里只给了一整行命令,优先粘到完整命令框自动拆分;支持 KEY=VALUE、env KEY=VALUE、PowerShell $env:KEY=VALUE; 和 Windows set KEY=VALUE && 这几类前缀环境变量写法。',
'环境变量每行一条 KEY=VALUE不要写 export也不要和启动命令混成一行保存。',
'密钥类环境变量会保存到本机配置,并只在启动 MCP 进程时作为进程环境传入;不要把密钥写进聊天内容。',
@@ -112,7 +114,7 @@ export const MCP_TROUBLESHOOTING_GUIDES: MCPTroubleshootingGuide[] = [
symptom: '测试提示找不到命令',
likelyCause: '启动命令填了整串命令、命令没加入 PATH或 Windows 路径里有空格但没有用真实 exe 路径。',
fix: '启动命令只填可执行程序本身;脚本名和 --stdio 放到命令参数里。命令不在 PATH 时,直接填绝对路径。',
example: 'command=node, args=server.js / --stdio',
example: 'command=npx, args=-y / @modelcontextprotocol/server-filesystem / --stdio',
},
{
key: 'timeout-or-no-tools',

View File

@@ -9,6 +9,19 @@ export interface MCPServerDraftTemplate {
}
export const MCP_SERVER_DRAFT_TEMPLATES: MCPServerDraftTemplate[] = [
{
key: 'npx',
title: 'npx 包',
description: '适合 README 里写着 `npx -y xxx --stdio` 的 npm MCP 包。',
detail: '示例会填成 `npx -y @modelcontextprotocol/server-filesystem --stdio`,把包名和路径参数改成实际值。',
seed: {
name: 'npx 包',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', '--stdio'],
env: {},
timeoutSeconds: 20,
},
},
{
key: 'uvx',
title: 'uvx 工具',