feat(ai): 增加 MCP 工具 arguments 示例

This commit is contained in:
Syngnat
2026-06-10 20:07:19 +08:00
parent 630044b740
commit 55a52bb0f3
3 changed files with 104 additions and 0 deletions

View File

@@ -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":"<connectionId>","sql":"<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('{}');
});
});

View File

@@ -36,6 +36,42 @@ const readRequiredSet = (schema: JSONSchemaRecord): Set<string> => 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<Record<string, unknown>>((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<AIMCPToolSchemaSummaryProps> = ({
? `参数 ${summary.parameters.length} 个,必填 ${summary.requiredCount} 个;星号表示必填。`
: '未声明 inputSchema调用参数需参考服务文档或用 /mcptool 继续查看。'}
</div>
{summary.hasInputSchema ? (
<div style={buildMCPHintStyle(overlayTheme.mutedText)}>
arguments
{' '}
<code style={{ fontFamily: 'var(--gn-font-mono)', overflowWrap: 'anywhere' }}>
{summary.minimalArgumentsExample}
</code>
</div>
) : null}
{previewParameters.length > 0 ? (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{previewParameters.map((parameter) => (

View File

@@ -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('&quot;connectionId&quot;:&quot;&lt;connectionId&gt;&quot;');
expect(markup).toContain('&quot;sql&quot;:&quot;&lt;sql&gt;&quot;');
expect(markup).toContain('connectionId*: string');
expect(markup).toContain('sql*: string');
expect(markup).toContain('allowMutating: boolean');