mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 10:29:52 +08:00
✨ feat(ai): 增加 MCP 工具 arguments 示例
This commit is contained in:
55
frontend/src/components/ai/AIMCPToolSchemaSummary.test.tsx
Normal file
55
frontend/src/components/ai/AIMCPToolSchemaSummary.test.tsx
Normal 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('{}');
|
||||
});
|
||||
});
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user