feat(ai): 显示 MCP 工具参数摘要

This commit is contained in:
Syngnat
2026-06-10 19:58:18 +08:00
parent 69f51f8ec8
commit 630044b740
3 changed files with 185 additions and 12 deletions

View File

@@ -8,6 +8,7 @@ import type { ParsedMCPEnvDraft } from '../../utils/mcpEnvDraft';
import type { MCPServerDraftValidation } from '../../utils/mcpServerValidation';
import AIMCPHelpBlock, { buildMCPHintStyle, mcpLabelStyle } from './AIMCPHelpBlock';
import AIMCPServerValidationPanel from './AIMCPServerValidationPanel';
import AIMCPToolSchemaSummary from './AIMCPToolSchemaSummary';
interface AIMCPServerFormPanelProps {
server: AIMCPServerConfig;
@@ -166,18 +167,12 @@ const AIMCPServerFormPanel: React.FC<AIMCPServerFormPanelProps> = ({
overlayTheme={overlayTheme}
/>
{serverTools.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ fontSize: 12, fontWeight: 700, color: overlayTheme.titleText }}></div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{serverTools.map((tool) => (
<span key={tool.alias} style={{ padding: '4px 8px', borderRadius: 999, background: darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)', fontSize: 12, color: overlayTheme.mutedText }}>
{tool.alias}
</span>
))}
</div>
</div>
)}
<AIMCPToolSchemaSummary
tools={serverTools}
cardBorder={cardBorder}
darkMode={darkMode}
overlayTheme={overlayTheme}
/>
<div style={{ padding: '10px 12px', borderRadius: 10, border: `1px solid ${cardBorder}`, background: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.72)' }}>
<div style={{ ...mcpLabelStyle, color: overlayTheme.titleText }}></div>

View File

@@ -0,0 +1,145 @@
import React from 'react';
import type { AIMCPToolDescriptor } from '../../types';
import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
import { buildMCPHintStyle } from './AIMCPHelpBlock';
const MAX_PARAMETER_PREVIEW = 6;
type JSONSchemaRecord = Record<string, any>;
const isRecord = (value: unknown): value is JSONSchemaRecord =>
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
const readSchemaType = (schema: JSONSchemaRecord): string => {
if (Array.isArray(schema.type)) {
return schema.type.map((item) => String(item)).filter(Boolean).join('|') || 'unknown';
}
if (typeof schema.type === 'string' && schema.type.trim()) {
return schema.type.trim();
}
if (Array.isArray(schema.enum)) {
return 'enum';
}
if (isRecord(schema.properties)) {
return 'object';
}
if (isRecord(schema.items)) {
return 'array';
}
return 'unknown';
};
const readRequiredSet = (schema: JSONSchemaRecord): Set<string> => new Set(
Array.isArray(schema.required)
? schema.required.map((item) => String(item)).filter(Boolean)
: [],
);
const summarizeToolParameters = (tool: AIMCPToolDescriptor) => {
const inputSchema = isRecord(tool.inputSchema) ? tool.inputSchema : {};
const properties = isRecord(inputSchema.properties) ? inputSchema.properties : {};
const requiredSet = readRequiredSet(inputSchema);
const parameters = Object.entries(properties).map(([name, rawSchema]) => {
const schema = isRecord(rawSchema) ? rawSchema : {};
return {
name,
required: requiredSet.has(name),
type: readSchemaType(schema),
description: String(schema.description || schema.title || '').trim(),
};
});
return {
hasInputSchema: Object.keys(inputSchema).length > 0,
parameters,
requiredCount: parameters.filter((item) => item.required).length,
truncated: parameters.length > MAX_PARAMETER_PREVIEW,
};
};
interface AIMCPToolSchemaSummaryProps {
tools: AIMCPToolDescriptor[];
cardBorder: string;
darkMode: boolean;
overlayTheme: OverlayWorkbenchTheme;
}
const AIMCPToolSchemaSummary: React.FC<AIMCPToolSchemaSummaryProps> = ({
tools,
cardBorder,
darkMode,
overlayTheme,
}) => {
if (tools.length === 0) {
return null;
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ fontSize: 12, fontWeight: 700, color: overlayTheme.titleText }}></div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 8 }}>
{tools.map((tool) => {
const summary = summarizeToolParameters(tool);
const previewParameters = summary.parameters.slice(0, MAX_PARAMETER_PREVIEW);
return (
<div
key={tool.alias}
style={{
padding: '10px 12px',
borderRadius: 10,
border: `1px solid ${cardBorder}`,
background: darkMode ? 'rgba(255,255,255,0.025)' : 'rgba(255,255,255,0.78)',
display: 'flex',
flexDirection: 'column',
gap: 6,
}}
>
<div style={{ fontSize: 12, fontWeight: 700, color: overlayTheme.titleText, overflowWrap: 'anywhere' }}>
{tool.alias}
</div>
{tool.description ? (
<div style={buildMCPHintStyle(overlayTheme.mutedText)}>{tool.description}</div>
) : null}
<div style={buildMCPHintStyle(overlayTheme.mutedText)}>
{summary.hasInputSchema
? `参数 ${summary.parameters.length} 个,必填 ${summary.requiredCount} 个;星号表示必填。`
: '未声明 inputSchema调用参数需参考服务文档或用 /mcptool 继续查看。'}
</div>
{previewParameters.length > 0 ? (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{previewParameters.map((parameter) => (
<span
key={parameter.name}
title={parameter.description || undefined}
style={{
padding: '3px 7px',
borderRadius: 999,
background: parameter.required
? (darkMode ? 'rgba(59,130,246,0.18)' : 'rgba(37,99,235,0.10)')
: (darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(15,23,42,0.05)'),
color: parameter.required ? '#2563eb' : overlayTheme.mutedText,
fontSize: 12,
fontFamily: 'var(--gn-font-mono)',
}}
>
{parameter.name}
{parameter.required ? '*' : ''}: {parameter.type}
</span>
))}
{summary.truncated ? (
<span style={{ ...buildMCPHintStyle(overlayTheme.mutedText), padding: '3px 0' }}>
{summary.parameters.length - MAX_PARAMETER_PREVIEW} 使 /mcptool schema
</span>
) : null}
</div>
) : null}
</div>
);
})}
</div>
</div>
);
};
export default AIMCPToolSchemaSummary;

View File

@@ -138,6 +138,31 @@ describe('AISettingsMCPSection', () => {
enabled: true,
timeoutSeconds: 20,
}],
mcpTools: [
{
alias: 'execute_sql',
serverId: 'mcp-local',
serverName: 'Local MCP',
originalName: 'execute_sql',
description: '执行 SQL',
inputSchema: {
type: 'object',
required: ['connectionId', 'sql'],
properties: {
connectionId: { type: 'string', description: '连接 ID' },
dbName: { type: 'string', description: '数据库名' },
sql: { type: 'string', description: 'SQL 文本' },
allowMutating: { type: 'boolean', description: '显式允许写操作' },
},
},
},
{
alias: 'legacy_tool',
serverId: 'mcp-local',
serverName: 'Local MCP',
originalName: 'legacy_tool',
},
],
})}
/>,
);
@@ -147,6 +172,14 @@ describe('AISettingsMCPSection', () => {
expect(markup).toContain('认证失败、401 或 403');
expect(markup).toContain('当前只支持 stdio');
expect(markup).toContain('不要把密钥写进聊天内容');
expect(markup).toContain('已发现工具和参数提示');
expect(markup).toContain('execute_sql');
expect(markup).toContain('参数 4 个,必填 2 个');
expect(markup).toContain('connectionId*: string');
expect(markup).toContain('sql*: string');
expect(markup).toContain('allowMutating: boolean');
expect(markup).toContain('legacy_tool');
expect(markup).toContain('未声明 inputSchema');
});
it('seeds a new draft when a launch template is selected', () => {