diff --git a/frontend/src/components/ai/AIMCPServerFormPanel.tsx b/frontend/src/components/ai/AIMCPServerFormPanel.tsx index 147c877..c722dbc 100644 --- a/frontend/src/components/ai/AIMCPServerFormPanel.tsx +++ b/frontend/src/components/ai/AIMCPServerFormPanel.tsx @@ -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 = ({ overlayTheme={overlayTheme} /> - {serverTools.length > 0 && ( -
-
已发现工具
-
- {serverTools.map((tool) => ( - - {tool.alias} - - ))} -
-
- )} +
操作说明
diff --git a/frontend/src/components/ai/AIMCPToolSchemaSummary.tsx b/frontend/src/components/ai/AIMCPToolSchemaSummary.tsx new file mode 100644 index 0000000..4f66b82 --- /dev/null +++ b/frontend/src/components/ai/AIMCPToolSchemaSummary.tsx @@ -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; + +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 => 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 = ({ + tools, + cardBorder, + darkMode, + overlayTheme, +}) => { + if (tools.length === 0) { + return null; + } + + return ( +
+
已发现工具和参数提示
+
+ {tools.map((tool) => { + const summary = summarizeToolParameters(tool); + const previewParameters = summary.parameters.slice(0, MAX_PARAMETER_PREVIEW); + return ( +
+
+ {tool.alias} +
+ {tool.description ? ( +
{tool.description}
+ ) : null} +
+ {summary.hasInputSchema + ? `参数 ${summary.parameters.length} 个,必填 ${summary.requiredCount} 个;星号表示必填。` + : '未声明 inputSchema,调用参数需参考服务文档或用 /mcptool 继续查看。'} +
+ {previewParameters.length > 0 ? ( +
+ {previewParameters.map((parameter) => ( + + {parameter.name} + {parameter.required ? '*' : ''}: {parameter.type} + + ))} + {summary.truncated ? ( + + 还有 {summary.parameters.length - MAX_PARAMETER_PREVIEW} 个参数,使用 /mcptool 查看完整 schema + + ) : null} +
+ ) : null} +
+ ); + })} +
+
+ ); +}; + +export default AIMCPToolSchemaSummary; diff --git a/frontend/src/components/ai/AISettingsMCPSection.test.tsx b/frontend/src/components/ai/AISettingsMCPSection.test.tsx index 12505e6..307354e 100644 --- a/frontend/src/components/ai/AISettingsMCPSection.test.tsx +++ b/frontend/src/components/ai/AISettingsMCPSection.test.tsx @@ -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', () => {