diff --git a/frontend/src/components/ai/AIMCPToolSchemaSummary.test.tsx b/frontend/src/components/ai/AIMCPToolSchemaSummary.test.tsx new file mode 100644 index 0000000..dea5d70 --- /dev/null +++ b/frontend/src/components/ai/AIMCPToolSchemaSummary.test.tsx @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; + +import type { AIMCPToolDescriptor } from '../../types'; +import { buildMCPToolMinimalArgumentsExample } from './AIMCPToolSchemaSummary'; + +const buildTool = (inputSchema: AIMCPToolDescriptor['inputSchema']): AIMCPToolDescriptor => ({ + alias: 'execute_sql', + serverId: 'gonavi', + serverName: 'GoNavi', + originalName: 'execute_sql', + inputSchema, +}); + +describe('AIMCPToolSchemaSummary', () => { + it('builds a minimal arguments example from required schema fields', () => { + const example = buildMCPToolMinimalArgumentsExample(buildTool({ + type: 'object', + required: ['connectionId', 'sql', 'allowMutating'], + properties: { + connectionId: { type: 'string' }, + dbName: { type: 'string' }, + sql: { type: 'string' }, + allowMutating: { type: 'boolean', default: true }, + }, + })); + + expect(example).toBe('{"connectionId":"","sql":"","allowMutating":true}'); + }); + + it('uses enum, array, object, and number placeholders when defaults are absent', () => { + const example = buildMCPToolMinimalArgumentsExample(buildTool({ + type: 'object', + required: ['mode', 'limit', 'filters', 'tags'], + properties: { + mode: { enum: ['safe', 'force'] }, + limit: { type: 'number' }, + filters: { type: 'object', properties: { status: { type: 'string' } } }, + tags: { type: 'array', items: { type: 'string' } }, + }, + })); + + expect(example).toBe('{"mode":"safe","limit":0,"filters":{},"tags":[]}'); + }); + + it('returns an empty object when no required parameters are declared', () => { + const example = buildMCPToolMinimalArgumentsExample(buildTool({ + type: 'object', + properties: { + keyword: { type: 'string' }, + }, + })); + + expect(example).toBe('{}'); + }); +}); diff --git a/frontend/src/components/ai/AIMCPToolSchemaSummary.tsx b/frontend/src/components/ai/AIMCPToolSchemaSummary.tsx index 4f66b82..6893403 100644 --- a/frontend/src/components/ai/AIMCPToolSchemaSummary.tsx +++ b/frontend/src/components/ai/AIMCPToolSchemaSummary.tsx @@ -36,6 +36,42 @@ const readRequiredSet = (schema: JSONSchemaRecord): Set => new Set( : [], ); +const readExampleValue = (name: string, schema: JSONSchemaRecord): unknown => { + if (Object.prototype.hasOwnProperty.call(schema, 'default')) { + const value = schema.default; + if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) { + return value; + } + } + if (Array.isArray(schema.enum) && schema.enum.length > 0) { + const value = schema.enum[0]; + if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) { + return value; + } + } + + const type = readSchemaType(schema); + if (type.includes('boolean')) return false; + if (type.includes('number') || type.includes('integer')) return 0; + if (type.includes('array')) return []; + if (type.includes('object')) return {}; + return `<${name}>`; +}; + +export const buildMCPToolMinimalArgumentsExample = (tool: AIMCPToolDescriptor): string => { + const inputSchema = isRecord(tool.inputSchema) ? tool.inputSchema : {}; + const properties = isRecord(inputSchema.properties) ? inputSchema.properties : {}; + const requiredSet = readRequiredSet(inputSchema); + const example = Object.entries(properties).reduce>((acc, [name, rawSchema]) => { + if (!requiredSet.has(name)) { + return acc; + } + acc[name] = readExampleValue(name, isRecord(rawSchema) ? rawSchema : {}); + return acc; + }, {}); + return JSON.stringify(example); +}; + const summarizeToolParameters = (tool: AIMCPToolDescriptor) => { const inputSchema = isRecord(tool.inputSchema) ? tool.inputSchema : {}; const properties = isRecord(inputSchema.properties) ? inputSchema.properties : {}; @@ -54,6 +90,7 @@ const summarizeToolParameters = (tool: AIMCPToolDescriptor) => { hasInputSchema: Object.keys(inputSchema).length > 0, parameters, requiredCount: parameters.filter((item) => item.required).length, + minimalArgumentsExample: buildMCPToolMinimalArgumentsExample(tool), truncated: parameters.length > MAX_PARAMETER_PREVIEW, }; }; @@ -106,6 +143,15 @@ const AIMCPToolSchemaSummary: React.FC = ({ ? `参数 ${summary.parameters.length} 个,必填 ${summary.requiredCount} 个;星号表示必填。` : '未声明 inputSchema,调用参数需参考服务文档或用 /mcptool 继续查看。'} + {summary.hasInputSchema ? ( +
+ 最小 arguments 示例: + {' '} + + {summary.minimalArgumentsExample} + +
+ ) : null} {previewParameters.length > 0 ? (
{previewParameters.map((parameter) => ( diff --git a/frontend/src/components/ai/AISettingsMCPSection.test.tsx b/frontend/src/components/ai/AISettingsMCPSection.test.tsx index 307354e..c2e5a48 100644 --- a/frontend/src/components/ai/AISettingsMCPSection.test.tsx +++ b/frontend/src/components/ai/AISettingsMCPSection.test.tsx @@ -175,6 +175,9 @@ describe('AISettingsMCPSection', () => { expect(markup).toContain('已发现工具和参数提示'); expect(markup).toContain('execute_sql'); expect(markup).toContain('参数 4 个,必填 2 个'); + expect(markup).toContain('最小 arguments 示例'); + expect(markup).toContain('"connectionId":"<connectionId>"'); + expect(markup).toContain('"sql":"<sql>"'); expect(markup).toContain('connectionId*: string'); expect(markup).toContain('sql*: string'); expect(markup).toContain('allowMutating: boolean');