mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-15 02:49:49 +08:00
✨ feat(ai): 完善 MCP Docker 启动参数指引
- 新增 Docker MCP 启动模板和参数顺序提示 - 校验 docker run、-i 和镜像名等易漏参数 - 同步 MCP 设置页说明、空状态和单元测试
This commit is contained in:
@@ -75,11 +75,11 @@ const AIMCPServerFormPanel: React.FC<AIMCPServerFormPanelProps> = ({
|
||||
options={[{ label: 'stdio', value: 'stdio' }]}
|
||||
/>
|
||||
</AIMCPHelpBlock>
|
||||
<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">
|
||||
<AIMCPHelpBlock title="启动命令" description="这里只填命令本身;如果是 npx/node/uvx/python/docker 这类启动器,把包名、脚本名、模块名或 docker run 参数放到下面的参数里。不要把 npx -y package --stdio、node server.js --stdio 或 docker run -i image 整串都塞进这里。" overlayTheme={overlayTheme} darkMode={darkMode} fieldState="required" example="npx / node / uvx / python / docker">
|
||||
<Input
|
||||
value={server.command}
|
||||
onChange={(event) => onChange({ command: event.target.value })}
|
||||
placeholder="启动命令,例如:npx / node / uvx / python"
|
||||
placeholder="启动命令,例如:npx / node / uvx / python / docker"
|
||||
style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }}
|
||||
/>
|
||||
</AIMCPHelpBlock>
|
||||
@@ -122,7 +122,7 @@ const AIMCPServerFormPanel: React.FC<AIMCPServerFormPanelProps> = ({
|
||||
</AIMCPHelpBlock>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<AIMCPHelpBlock title="命令参数" description="每个参数单独录入一个标签;命令本体不要填在这里。比如 npx -y package --stdio,要把 -y、package 和 --stdio 分开填;node server.js --stdio 要把 server.js 和 --stdio 分开填;docker run --rm -i image 要把 run、--rm、-i 和镜像名分开填。不确定怎么拆时,优先回到上面的“完整命令”框自动拆分。" overlayTheme={overlayTheme} darkMode={darkMode} fieldState="optional" example="-y、@modelcontextprotocol/server-filesystem、--stdio、server.js、run、--rm、-i、image">
|
||||
<Select
|
||||
mode="tags"
|
||||
value={server.args || []}
|
||||
|
||||
@@ -108,10 +108,10 @@ describe('AISettingsMCPSection', () => {
|
||||
expect(markup).toContain('timeout');
|
||||
expect(markup).toContain('只填程序名或启动器本身');
|
||||
expect(markup).toContain('应填:');
|
||||
expect(markup).toContain('填 npx、node、uvx、python,或某个 exe 的绝对路径');
|
||||
expect(markup).toContain('填 npx、node、uvx、python、docker,或某个 exe 的绝对路径');
|
||||
expect(markup).toContain('不要填整行命令,例如不要填 npx -y pkg --stdio');
|
||||
expect(markup).toContain('把脚本名、模块名、开关参数拆开逐项填写');
|
||||
expect(markup).toContain('不要再填 npx/node/uvx/python');
|
||||
expect(markup).toContain('不要再填 npx/node/uvx/python/docker');
|
||||
expect(markup).toContain('给 MCP Server 传入 KEY=VALUE 形式的配置');
|
||||
expect(markup).toContain('不要写 export、set 或 $env: 前缀');
|
||||
expect(markup).toContain('单次工具发现或调用最多等待多久');
|
||||
@@ -119,6 +119,8 @@ describe('AISettingsMCPSection', () => {
|
||||
expect(markup).toContain('npx 包');
|
||||
expect(markup).toContain('npx -y @modelcontextprotocol/server-filesystem --stdio');
|
||||
expect(markup).toContain('Node 脚本');
|
||||
expect(markup).toContain('Docker 镜像');
|
||||
expect(markup).toContain('docker run -i --rm image');
|
||||
expect(markup).toContain('新增 MCP 服务');
|
||||
expect(markup).toContain('还没有 MCP 服务');
|
||||
expect(markup).toContain('npx -y package --stdio');
|
||||
@@ -205,4 +207,26 @@ describe('AISettingsMCPSection', () => {
|
||||
args: ['-y', '@modelcontextprotocol/server-filesystem', '--stdio'],
|
||||
}));
|
||||
});
|
||||
|
||||
it('seeds a docker MCP draft with interactive stdio args', () => {
|
||||
const onAddServer = vi.fn();
|
||||
const tree = AISettingsMCPSection(buildMCPSectionProps({
|
||||
mcpClientStatuses: [],
|
||||
selectedMCPClientStatus: undefined,
|
||||
onAddServer,
|
||||
}));
|
||||
|
||||
const dockerTemplateButton = findElement(
|
||||
tree,
|
||||
(node) => node.type === 'button' && flattenElementText(node.props?.children).includes('Docker 镜像'),
|
||||
);
|
||||
expect(dockerTemplateButton).toBeTruthy();
|
||||
dockerTemplateButton.props.onClick();
|
||||
|
||||
expect(onAddServer).toHaveBeenCalledWith(expect.objectContaining({
|
||||
command: 'docker',
|
||||
args: ['run', '--rm', '-i', 'mcp/server-fetch:latest'],
|
||||
timeoutSeconds: 45,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,7 +123,7 @@ const AISettingsMCPSection: React.FC<AISettingsMCPSectionProps> = ({
|
||||
>
|
||||
<div style={{ fontWeight: 700, fontSize: 14, color: overlayTheme.titleText }}>常见启动方式模板</div>
|
||||
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.7 }}>
|
||||
不确定命令和参数怎么拆时,先选一个最接近的启动方式。GoNavi 会自动带入示例值,你再改成自己的脚本名、模块名或 exe 路径即可。
|
||||
不确定命令和参数怎么拆时,先选一个最接近的启动方式。GoNavi 会自动带入示例值,你再改成自己的脚本名、模块名、Docker 镜像或 exe 路径即可。
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 10 }}>
|
||||
{MCP_SERVER_DRAFT_TEMPLATES.map((template) => (
|
||||
@@ -154,7 +154,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 服务。常见形式是 `npx -y package --stdio`、`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`、`docker run --rm -i image`。
|
||||
</div>
|
||||
)}
|
||||
{mcpServers.map((server) => (
|
||||
|
||||
@@ -28,6 +28,15 @@ describe('mcpArgumentHints', () => {
|
||||
expect(profile?.nextActions).toContain('补充 模块名,示例:your_mcp_server');
|
||||
});
|
||||
|
||||
it('guides docker users to keep stdin and provide an image', () => {
|
||||
const profile = buildMCPArgumentHintProfile('docker', ['run', '--rm']);
|
||||
|
||||
expect(profile?.title).toContain('Docker');
|
||||
expect(profile?.orderHint).toContain('run -> --rm -> -i');
|
||||
expect(profile?.nextActions).toContain('补充 保持标准输入,示例:-i');
|
||||
expect(profile?.nextActions).toContain('补充 镜像名,示例:mcp/server-fetch:latest');
|
||||
});
|
||||
|
||||
it('falls back to executable guidance for custom binaries', () => {
|
||||
const profile = buildMCPArgumentHintProfile('D:\\tools\\acme-mcp-server.exe', []);
|
||||
|
||||
|
||||
@@ -53,6 +53,31 @@ const hasPythonModuleArg = (args: string[]): boolean => {
|
||||
return moduleFlagIndex >= 0 && Boolean(args[moduleFlagIndex + 1]);
|
||||
};
|
||||
|
||||
const hasDockerRunArg = (args: string[]): boolean =>
|
||||
args.some((arg) => arg.toLowerCase() === 'run');
|
||||
|
||||
const hasDockerInteractiveArg = (args: string[]): boolean =>
|
||||
hasArg(args, '-i') || hasArg(args, '--interactive');
|
||||
|
||||
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) {
|
||||
const arg = candidates[index];
|
||||
if (!arg || arg.startsWith('-')) {
|
||||
const lower = arg.toLowerCase();
|
||||
if (['-e', '--env', '--name', '--network', '-v', '--volume'].includes(lower)) {
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg.includes('=') || arg.includes(':') || arg.includes('/')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const buildStep = (
|
||||
key: string,
|
||||
label: string,
|
||||
@@ -149,6 +174,24 @@ export const buildMCPArgumentHintProfile = (
|
||||
};
|
||||
}
|
||||
|
||||
if (commandName === 'docker') {
|
||||
const steps = [
|
||||
buildStep('run', '运行子命令', 'run', 'Docker MCP 通常要以 docker run 启动容器。', true, hasDockerRunArg(normalizedArgs)),
|
||||
buildStep('interactive', '保持标准输入', '-i', 'MCP 需要 stdio 持续连接,Docker 容器必须保留 stdin。', true, hasDockerInteractiveArg(normalizedArgs)),
|
||||
buildStep('cleanup', '退出后清理容器', '--rm', '测试和日常使用建议自动删除临时容器,避免残留。', false, hasArg(normalizedArgs, '--rm')),
|
||||
buildStep('image', '镜像名', 'mcp/server-fetch:latest', 'README 里的 Docker 镜像名,放在 docker run 选项之后。', true, hasDockerImageArg(normalizedArgs)),
|
||||
buildStep('container-env', '容器环境变量', '-e API_KEY=...', '容器内应用需要的 token 通常要用 -e/--env 传给容器。', false, normalizedArgs.some((arg) => arg === '-e' || arg === '--env' || arg.startsWith('-e='))),
|
||||
];
|
||||
return {
|
||||
commandName,
|
||||
title: 'Docker MCP 参数顺序建议',
|
||||
summary: 'Docker 场景 command 只填 docker,run、-i、--rm、镜像名和容器参数都放到 args 里。',
|
||||
orderHint: '推荐顺序:run -> --rm -> -i -> -e KEY=VALUE -> 镜像名 -> 服务自己的业务参数',
|
||||
steps,
|
||||
nextActions: buildNextActions(steps),
|
||||
};
|
||||
}
|
||||
|
||||
const steps = [
|
||||
buildStep('stdio', 'stdio 模式参数', 'stdio 或 --stdio', '多数本机 MCP 二进制需要显式 stdio 参数;以 README 为准。', false, hasStdioArg(normalizedArgs)),
|
||||
buildStep('business', '业务参数', '--config ./config.json', '二进制自己的配置文件、工作目录、端口或模式参数。', false, normalizedArgs.length > 0),
|
||||
|
||||
@@ -30,6 +30,7 @@ export const MCP_COMMAND_EXAMPLES = [
|
||||
'uvx mcp-server-fetch',
|
||||
'node server.js --stdio',
|
||||
'python -m your_mcp_server',
|
||||
'docker run --rm -i mcp/server-fetch:latest',
|
||||
];
|
||||
|
||||
export const MCP_COMMAND_PARSE_EXAMPLE = '$env:GITHUB_TOKEN=...; uvx mcp-server-github --stdio';
|
||||
@@ -38,7 +39,7 @@ export const MCP_SERVER_FILL_STEPS: MCPFillStep[] = [
|
||||
{ step: '1', title: '模板 / 完整命令', detail: '优先选最接近的模板,或先粘一整行命令让 GoNavi 自动拆分。' },
|
||||
{ step: '2', title: '服务名称', detail: '命名成 Browser、GitHub、Filesystem 这类一眼能认出的用途名。' },
|
||||
{ step: '3', title: '启动命令', detail: '这里只填程序名或启动器本身,不要把整行命令塞进去。' },
|
||||
{ step: '4', title: '命令参数', detail: '把脚本名、模块名和 --stdio 这类参数拆开逐项填写。' },
|
||||
{ step: '4', title: '命令参数', detail: '把脚本名、模块名、Docker run 参数和 --stdio 这类参数拆开逐项填写。' },
|
||||
{ step: '5', title: '环境变量 / 超时', detail: '只有在服务确实需要额外配置时再补,不需要可以留空。' },
|
||||
];
|
||||
|
||||
@@ -77,21 +78,21 @@ export const MCP_FIELD_GUIDES: MCPFieldGuide[] = [
|
||||
key: 'command',
|
||||
title: '启动命令',
|
||||
summary: '只填程序名或启动器本身。',
|
||||
detail: '常见是 npx、node、uvx、python;包名、脚本名和 --stdio 这类内容放到参数里。',
|
||||
fill: '填 npx、node、uvx、python,或某个 exe 的绝对路径。',
|
||||
detail: '常见是 npx、node、uvx、python、docker;包名、脚本名、run、-i 和 --stdio 这类内容放到参数里。',
|
||||
fill: '填 npx、node、uvx、python、docker,或某个 exe 的绝对路径。',
|
||||
avoid: '不要填整行命令,例如不要填 npx -y pkg --stdio。',
|
||||
fieldState: 'required',
|
||||
example: 'npx / node / uvx / python',
|
||||
example: 'npx / node / uvx / python / docker',
|
||||
},
|
||||
{
|
||||
key: 'args',
|
||||
title: '命令参数',
|
||||
summary: '把脚本名、模块名、开关参数拆开逐项填写。',
|
||||
detail: '例如 npx -y pkg --stdio,要拆成 -y、pkg 和 --stdio;node server.js --stdio 要拆成 server.js 和 --stdio。',
|
||||
fill: '逐项填 -y、包名、脚本名、-m、--stdio 等参数。',
|
||||
avoid: '不要再填 npx/node/uvx/python,也不要把多个参数粘成一个长字符串。',
|
||||
detail: '例如 npx -y pkg --stdio,要拆成 -y、pkg 和 --stdio;docker run --rm -i image 要拆成 run、--rm、-i 和 image。',
|
||||
fill: '逐项填 -y、包名、脚本名、-m、--stdio、run、--rm、-i、镜像名等参数。',
|
||||
avoid: '不要再填 npx/node/uvx/python/docker,也不要把多个参数粘成一个长字符串。',
|
||||
fieldState: 'optional',
|
||||
example: '-y / @modelcontextprotocol/server-filesystem / --stdio / server.js',
|
||||
example: '-y / @modelcontextprotocol/server-filesystem / --stdio / server.js / run / --rm / -i / image',
|
||||
},
|
||||
{
|
||||
key: 'env',
|
||||
@@ -118,6 +119,7 @@ export const MCP_FIELD_GUIDES: MCPFieldGuide[] = [
|
||||
export const MCP_AUTHORING_NOTES = [
|
||||
'启动命令只填程序本身,不要把脚本名、模块名和 --stdio 混进去。',
|
||||
'README 给 npx 示例时,command 填 npx,args 逐项填 -y、包名和 --stdio;不要把整行 npx 命令放进 command。',
|
||||
'README 给 Docker 示例时,command 填 docker,args 逐项填 run、--rm、-i、镜像名和容器参数;容器内 token 通常用 -e KEY=VALUE 传给容器。',
|
||||
'如果 README 里只给了一整行命令,优先粘到完整命令框自动拆分;支持 KEY=VALUE、env KEY=VALUE、PowerShell $env:KEY=VALUE; 和 Windows set KEY=VALUE && 这几类前缀环境变量写法。',
|
||||
'环境变量每行一条 KEY=VALUE,不要写 export,也不要和启动命令混成一行保存。',
|
||||
'密钥类环境变量会保存到本机配置,并只在启动 MCP 进程时作为进程环境传入;不要把密钥写进聊天内容。',
|
||||
@@ -135,9 +137,9 @@ export const MCP_TROUBLESHOOTING_GUIDES: MCPTroubleshootingGuide[] = [
|
||||
{
|
||||
key: 'timeout-or-no-tools',
|
||||
symptom: '测试超时或发现 0 个工具',
|
||||
likelyCause: '服务启动慢、缺少 stdio 参数,或填成了只支持 HTTP/SSE 的 MCP 服务。',
|
||||
fix: '先确认这个服务支持 stdio,再补齐 --stdio 等参数;启动慢时把超时调到 45 或 60 秒。',
|
||||
example: 'args=--stdio, timeout=45',
|
||||
likelyCause: '服务启动慢、缺少 stdio 参数,Docker 容器缺少 -i,或填成了只支持 HTTP/SSE 的 MCP 服务。',
|
||||
fix: '先确认这个服务支持 stdio,再补齐 --stdio 或 Docker -i 等参数;启动慢时把超时调到 45 或 60 秒。',
|
||||
example: 'args=--stdio 或 docker run --rm -i image, timeout=45',
|
||||
},
|
||||
{
|
||||
key: 'auth-failed',
|
||||
|
||||
@@ -61,6 +61,19 @@ export const MCP_SERVER_DRAFT_TEMPLATES: MCPServerDraftTemplate[] = [
|
||||
timeoutSeconds: 20,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'docker',
|
||||
title: 'Docker 镜像',
|
||||
description: '适合 README 里写着 `docker run -i --rm image` 的容器化 MCP。本机需要已安装 Docker。',
|
||||
detail: '示例会填成 `docker run --rm -i mcp/server-fetch:latest`;容器内 token 通常用 -e KEY=VALUE 放到参数里。',
|
||||
seed: {
|
||||
name: 'Docker MCP',
|
||||
command: 'docker',
|
||||
args: ['run', '--rm', '-i', 'mcp/server-fetch:latest'],
|
||||
env: {},
|
||||
timeoutSeconds: 45,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'exe',
|
||||
title: '本机 EXE',
|
||||
|
||||
@@ -61,4 +61,36 @@ describe('mcpServerValidation', () => {
|
||||
expect(validation.canSave).toBe(true);
|
||||
expect(validation.errorCount).toBe(0);
|
||||
});
|
||||
|
||||
it('warns when docker MCP launch args miss run, stdin, or image', () => {
|
||||
const validation = validateMCPServerDraft({
|
||||
name: 'Docker MCP',
|
||||
transport: 'stdio',
|
||||
command: 'docker',
|
||||
args: ['--rm'],
|
||||
timeoutSeconds: 45,
|
||||
}, { invalidLines: [] });
|
||||
|
||||
expect(validation.canTest).toBe(true);
|
||||
expect(validation.warningCount).toBeGreaterThanOrEqual(3);
|
||||
expect(validation.issues.map((issue) => issue.key)).toContain('docker-run-missing');
|
||||
expect(validation.issues.map((issue) => issue.key)).toContain('docker-interactive-missing');
|
||||
expect(validation.issues.map((issue) => issue.key)).toContain('docker-image-missing');
|
||||
});
|
||||
|
||||
it('accepts complete docker MCP launch args without docker-specific warnings', () => {
|
||||
const validation = validateMCPServerDraft({
|
||||
name: 'Docker MCP',
|
||||
transport: 'stdio',
|
||||
command: 'docker',
|
||||
args: ['run', '--rm', '-i', '-e', 'API_KEY=...', 'mcp/server-fetch:latest'],
|
||||
timeoutSeconds: 45,
|
||||
}, { invalidLines: [] });
|
||||
|
||||
expect(validation.canTest).toBe(true);
|
||||
expect(validation.canSave).toBe(true);
|
||||
expect(validation.issues.map((issue) => issue.key)).not.toContain('docker-run-missing');
|
||||
expect(validation.issues.map((issue) => issue.key)).not.toContain('docker-interactive-missing');
|
||||
expect(validation.issues.map((issue) => issue.key)).not.toContain('docker-image-missing');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ const KNOWN_LAUNCHER_COMMANDS = new Set([
|
||||
'py',
|
||||
'uv',
|
||||
'uvx',
|
||||
'docker',
|
||||
'go',
|
||||
'java',
|
||||
'cmd',
|
||||
@@ -74,7 +75,33 @@ const argsContainEnvOrShellGlue = (args: string[]): boolean =>
|
||||
|
||||
const launcherUsuallyNeedsArgs = (command: string): boolean => {
|
||||
const firstToken = firstShellToken(command);
|
||||
return ['node', 'python', 'python3', 'py', 'uvx', 'npx', 'bun', 'deno', 'go', 'java'].includes(firstToken);
|
||||
return ['node', 'python', 'python3', 'py', 'uvx', 'npx', 'bun', 'deno', 'docker', 'go', 'java'].includes(firstToken);
|
||||
};
|
||||
|
||||
const isDockerCommand = (command: string): boolean =>
|
||||
firstShellToken(command) === 'docker';
|
||||
|
||||
const hasDockerRunArg = (args: string[]): boolean =>
|
||||
args.some((arg) => arg.toLowerCase() === 'run');
|
||||
|
||||
const hasDockerInteractiveArg = (args: string[]): boolean =>
|
||||
args.some((arg) => arg.toLowerCase() === '-i' || arg.toLowerCase() === '--interactive');
|
||||
|
||||
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) {
|
||||
const arg = candidates[index];
|
||||
if (!arg || arg.startsWith('-')) {
|
||||
const lower = arg.toLowerCase();
|
||||
if (['-e', '--env', '--name', '--network', '-v', '--volume'].includes(lower)) {
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const validateMCPServerDraft = (
|
||||
@@ -129,6 +156,33 @@ export const validateMCPServerDraft = (
|
||||
});
|
||||
}
|
||||
|
||||
if (command && isDockerCommand(command)) {
|
||||
if (!hasDockerRunArg(args)) {
|
||||
issues.push({
|
||||
key: 'docker-run-missing',
|
||||
severity: 'warning',
|
||||
title: 'Docker 参数缺少 run',
|
||||
detail: 'Docker MCP 通常需要 command=docker,args 里单独填写 run、--rm、-i、镜像名和服务参数。',
|
||||
});
|
||||
}
|
||||
if (!hasDockerInteractiveArg(args)) {
|
||||
issues.push({
|
||||
key: 'docker-interactive-missing',
|
||||
severity: 'warning',
|
||||
title: 'Docker 参数缺少 -i',
|
||||
detail: 'MCP 需要持续读取标准输入;docker run 场景请加 -i 或 --interactive,否则工具发现可能立即断开。',
|
||||
});
|
||||
}
|
||||
if (!hasDockerImageArg(args)) {
|
||||
issues.push({
|
||||
key: 'docker-image-missing',
|
||||
severity: 'warning',
|
||||
title: 'Docker 参数可能缺少镜像名',
|
||||
detail: '请在 docker run 选项之后填写 README 提供的镜像名,例如 mcp/server-fetch:latest。',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (argsContainEnvOrShellGlue(args)) {
|
||||
issues.push({
|
||||
key: 'args-contain-env-or-shell-glue',
|
||||
|
||||
Reference in New Issue
Block a user