feat(mcp): 支持一键补齐缺失启动参数

- 在 MCP 参数提示中按当前 command 生成缺失必填参数

- 支持一键追加 docker、npx、python 等常见启动参数

- 增加组件交互测试覆盖参数补齐行为
This commit is contained in:
Syngnat
2026-06-12 02:16:26 +08:00
parent 97f062773b
commit 4daaa22cba
3 changed files with 86 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
import React from 'react';
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
import { describe, expect, it, vi } from 'vitest';
import { buildOverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
import AIMCPArgumentHints from './AIMCPArgumentHints';
const flattenRendererText = (node: any): string => {
if (node == null || typeof node === 'boolean') {
return '';
}
if (typeof node === 'string' || typeof node === 'number') {
return String(node);
}
if (Array.isArray(node)) {
return node.map((item) => flattenRendererText(item)).join('');
}
return flattenRendererText(node.children ?? node.props?.children);
};
describe('AIMCPArgumentHints', () => {
it('can append missing required args from command-specific hints', async () => {
const onArgsChange = vi.fn();
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(
<AIMCPArgumentHints
command="docker"
args={['run', '--rm']}
onArgsChange={onArgsChange}
cardBorder="rgba(0,0,0,0.08)"
darkMode={false}
overlayTheme={buildOverlayWorkbenchTheme(false)}
/>,
);
});
const buttons = renderer.root.findAll(
(node) => node.type === 'button' && flattenRendererText(node).includes('一键补齐缺失必填参数'),
);
expect(buttons.length).toBe(1);
expect(flattenRendererText(buttons[0])).toContain('-i / mcp/server-fetch:latest');
await act(async () => {
buttons[0].props.onClick();
});
expect(onArgsChange).toHaveBeenCalledWith(['run', '--rm', '-i', 'mcp/server-fetch:latest']);
});
});

View File

@@ -2,19 +2,31 @@ import React from 'react';
import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
import { buildMCPArgumentHintProfile } from '../../utils/mcpArgumentHints';
import { splitShellLikeCommand } from '../../utils/mcpCommandDraft';
import { buildMCPHintStyle, mcpLabelStyle } from './AIMCPHelpBlock';
interface AIMCPArgumentHintsProps {
command: string;
args?: string[];
onArgsChange?: (args: string[]) => void;
cardBorder: string;
darkMode: boolean;
overlayTheme: OverlayWorkbenchTheme;
}
const buildMissingRequiredArgs = (
profile: NonNullable<ReturnType<typeof buildMCPArgumentHintProfile>>,
): string[] =>
profile.steps
.filter((step) => step.required && !step.satisfied)
.flatMap((step) => splitShellLikeCommand(step.example).tokens)
.map((item) => item.trim())
.filter(Boolean);
const AIMCPArgumentHints: React.FC<AIMCPArgumentHintsProps> = ({
command,
args,
onArgsChange,
cardBorder,
darkMode,
overlayTheme,
@@ -23,6 +35,8 @@ const AIMCPArgumentHints: React.FC<AIMCPArgumentHintsProps> = ({
if (!profile) {
return null;
}
const missingRequiredArgs = buildMissingRequiredArgs(profile);
const canApplyMissingArgs = Boolean(onArgsChange && missingRequiredArgs.length > 0);
return (
<div
@@ -74,6 +88,25 @@ const AIMCPArgumentHints: React.FC<AIMCPArgumentHintsProps> = ({
README
</div>
)}
{canApplyMissingArgs ? (
<button
type="button"
onClick={() => onArgsChange?.([...(args || []), ...missingRequiredArgs])}
style={{
alignSelf: 'flex-start',
padding: '5px 11px',
borderRadius: 999,
border: `1px solid ${cardBorder}`,
background: darkMode ? 'rgba(245,158,11,0.14)' : 'rgba(245,158,11,0.10)',
color: '#b45309',
fontSize: 12,
fontWeight: 700,
cursor: 'pointer',
}}
>
{missingRequiredArgs.join(' / ')}
</button>
) : null}
</div>
);
};

View File

@@ -134,6 +134,7 @@ const AIMCPServerFormPanel: React.FC<AIMCPServerFormPanelProps> = ({
<AIMCPArgumentHints
command={server.command}
args={server.args}
onArgsChange={(args) => onChange({ args })}
cardBorder={cardBorder}
darkMode={darkMode}
overlayTheme={overlayTheme}