fix(mcp-server): handle ZodDefault in zodToJsonSchema so boolean/object fields with defaults emit correct JSON Schema types; clean up inline import in debug-pack
This commit is contained in:
parent
c04c78aff4
commit
3e37b3d5a0
@ -1,6 +1,10 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { registerTool } from '../tools/registry.js';
|
import { registerTool } from '../tools/registry.js';
|
||||||
import { telemetryClusters, diagnosticsCreateSession } from '../../lib/platform-client.js';
|
import {
|
||||||
|
telemetryClusters,
|
||||||
|
diagnosticsCreateSession,
|
||||||
|
type TelemetryCluster,
|
||||||
|
} from '../../lib/platform-client.js';
|
||||||
import type { McpToolRequest } from '../tools/types.js';
|
import type { McpToolRequest } from '../tools/types.js';
|
||||||
|
|
||||||
const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7);
|
const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7);
|
||||||
@ -41,7 +45,7 @@ registerTool({
|
|||||||
const generatedAt = new Date().toISOString();
|
const generatedAt = new Date().toISOString();
|
||||||
|
|
||||||
// Step 1: collect error clusters
|
// Step 1: collect error clusters
|
||||||
let clusters: import('../../lib/platform-client.js').TelemetryCluster[] = [];
|
let clusters: TelemetryCluster[] = [];
|
||||||
let clusterError: string | undefined;
|
let clusterError: string | undefined;
|
||||||
try {
|
try {
|
||||||
const result = await telemetryClusters({ from: args.from, to: args.to }, opts);
|
const result = await telemetryClusters({ from: args.from, to: args.to }, opts);
|
||||||
|
|||||||
@ -30,6 +30,39 @@ describe('tool registry', () => {
|
|||||||
it('getTool returns undefined for unknown tool', () => {
|
it('getTool returns undefined for unknown tool', () => {
|
||||||
expect(getTool('does.not.exist')).toBeUndefined();
|
expect(getTool('does.not.exist')).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('zodToJsonSchema emits correct types for boolean/number/array fields', () => {
|
||||||
|
registerTool({
|
||||||
|
name: 'test.types',
|
||||||
|
description: 'Type test',
|
||||||
|
requiredRole: 'viewer',
|
||||||
|
inputSchema: z.object({
|
||||||
|
flag: z.boolean(),
|
||||||
|
count: z.coerce.number(),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
label: z.string().optional(),
|
||||||
|
enabled: z.boolean().default(false),
|
||||||
|
}),
|
||||||
|
async execute(args) {
|
||||||
|
return args;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const tool = listTools().find(t => t.name === 'test.types');
|
||||||
|
const props = (tool?.inputSchema as { properties: Record<string, { type: string }> })
|
||||||
|
.properties;
|
||||||
|
expect(props.flag.type).toBe('boolean');
|
||||||
|
expect(props.count.type).toBe('number');
|
||||||
|
expect(props.tags.type).toBe('array');
|
||||||
|
expect(props.label.type).toBe('string');
|
||||||
|
// ZodDefault should unwrap to boolean, not 'string'
|
||||||
|
expect(props.enabled.type).toBe('boolean');
|
||||||
|
// optional + default fields should NOT be in required
|
||||||
|
const required = tool?.inputSchema.required as string[];
|
||||||
|
expect(required).toContain('flag');
|
||||||
|
expect(required).toContain('count');
|
||||||
|
expect(required).not.toContain('label');
|
||||||
|
expect(required).not.toContain('enabled');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('tool execute', () => {
|
describe('tool execute', () => {
|
||||||
|
|||||||
@ -20,31 +20,26 @@ export function listTools(): McpToolMeta[] {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ZodDef = {
|
||||||
|
typeName: string;
|
||||||
|
description?: string;
|
||||||
|
innerType?: { _def: ZodDef };
|
||||||
|
shape?: () => Record<string, ZodTypeAny>;
|
||||||
|
};
|
||||||
|
|
||||||
/** Minimal Zod → JSON Schema converter (object shapes only — sufficient for tool docs) */
|
/** Minimal Zod → JSON Schema converter (object shapes only — sufficient for tool docs) */
|
||||||
function zodToJsonSchema(schema: ZodTypeAny): Record<string, unknown> {
|
function zodToJsonSchema(schema: ZodTypeAny): Record<string, unknown> {
|
||||||
try {
|
try {
|
||||||
const def = (
|
const def = (schema as unknown as { _def: ZodDef })._def;
|
||||||
schema as unknown as { _def: { typeName: string; shape?: () => Record<string, ZodTypeAny> } }
|
|
||||||
)._def;
|
|
||||||
if (def.typeName === 'ZodObject' && def.shape) {
|
if (def.typeName === 'ZodObject' && def.shape) {
|
||||||
const shape = def.shape();
|
const shape = def.shape();
|
||||||
const properties: Record<string, unknown> = {};
|
const properties: Record<string, unknown> = {};
|
||||||
const required: string[] = [];
|
const required: string[] = [];
|
||||||
for (const [key, field] of Object.entries(shape)) {
|
for (const [key, field] of Object.entries(shape)) {
|
||||||
const fieldDef = (
|
const fieldDef = (field as unknown as { _def: ZodDef })._def;
|
||||||
field as unknown as {
|
const { resolvedType, isRequired, description } = resolveZodDef(fieldDef);
|
||||||
_def: {
|
properties[key] = { ...resolvedType, description: description ?? key };
|
||||||
typeName: string;
|
if (isRequired) required.push(key);
|
||||||
description?: string;
|
|
||||||
innerType?: { _def: { typeName: string } };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)._def;
|
|
||||||
const isOptional = fieldDef.typeName === 'ZodOptional';
|
|
||||||
const innerTypeName = isOptional ? fieldDef.innerType?._def.typeName : fieldDef.typeName;
|
|
||||||
const jsonType = zodTypeNameToJsonType(innerTypeName ?? '');
|
|
||||||
properties[key] = { type: jsonType, description: fieldDef.description ?? key };
|
|
||||||
if (!isOptional) required.push(key);
|
|
||||||
}
|
}
|
||||||
return { type: 'object', properties, required };
|
return { type: 'object', properties, required };
|
||||||
}
|
}
|
||||||
@ -54,9 +49,48 @@ function zodToJsonSchema(schema: ZodTypeAny): Record<string, unknown> {
|
|||||||
return { type: 'object' };
|
return { type: 'object' };
|
||||||
}
|
}
|
||||||
|
|
||||||
function zodTypeNameToJsonType(typeName: string): string {
|
/**
|
||||||
if (typeName === 'ZodNumber' || typeName === 'ZodCoerce') return 'number';
|
* Unwrap ZodOptional / ZodDefault wrappers and resolve the JSON type for a field.
|
||||||
if (typeName === 'ZodBoolean') return 'boolean';
|
* Returns the JSON Schema fragment plus whether the field is required.
|
||||||
if (typeName === 'ZodArray') return 'array';
|
*/
|
||||||
return 'string'; // ZodString, ZodEnum, ZodLiteral, unknown
|
function resolveZodDef(def: ZodDef): {
|
||||||
|
resolvedType: Record<string, unknown>;
|
||||||
|
isRequired: boolean;
|
||||||
|
description?: string;
|
||||||
|
} {
|
||||||
|
const description = def.description;
|
||||||
|
if (def.typeName === 'ZodOptional') {
|
||||||
|
const inner = def.innerType?._def;
|
||||||
|
return {
|
||||||
|
resolvedType: inner ? zodDefToJsonType(inner) : { type: 'string' },
|
||||||
|
isRequired: false,
|
||||||
|
description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (def.typeName === 'ZodDefault') {
|
||||||
|
const inner = def.innerType?._def;
|
||||||
|
return {
|
||||||
|
resolvedType: inner ? zodDefToJsonType(inner) : { type: 'string' },
|
||||||
|
isRequired: false, // has default → not required in JSON Schema
|
||||||
|
description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { resolvedType: zodDefToJsonType(def), isRequired: true, description };
|
||||||
|
}
|
||||||
|
|
||||||
|
function zodDefToJsonType(def: ZodDef): Record<string, unknown> {
|
||||||
|
switch (def.typeName) {
|
||||||
|
case 'ZodObject':
|
||||||
|
return { type: 'object' };
|
||||||
|
case 'ZodArray':
|
||||||
|
return { type: 'array' };
|
||||||
|
case 'ZodBoolean':
|
||||||
|
return { type: 'boolean' };
|
||||||
|
case 'ZodNumber':
|
||||||
|
return { type: 'number' };
|
||||||
|
case 'ZodEnum':
|
||||||
|
return { type: 'string' };
|
||||||
|
default:
|
||||||
|
return { type: 'string' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user