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 { 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';
|
||||
|
||||
const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7);
|
||||
@ -41,7 +45,7 @@ registerTool({
|
||||
const generatedAt = new Date().toISOString();
|
||||
|
||||
// Step 1: collect error clusters
|
||||
let clusters: import('../../lib/platform-client.js').TelemetryCluster[] = [];
|
||||
let clusters: TelemetryCluster[] = [];
|
||||
let clusterError: string | undefined;
|
||||
try {
|
||||
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', () => {
|
||||
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', () => {
|
||||
|
||||
@ -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) */
|
||||
function zodToJsonSchema(schema: ZodTypeAny): Record<string, unknown> {
|
||||
try {
|
||||
const def = (
|
||||
schema as unknown as { _def: { typeName: string; shape?: () => Record<string, ZodTypeAny> } }
|
||||
)._def;
|
||||
const def = (schema as unknown as { _def: ZodDef })._def;
|
||||
if (def.typeName === 'ZodObject' && def.shape) {
|
||||
const shape = def.shape();
|
||||
const properties: Record<string, unknown> = {};
|
||||
const required: string[] = [];
|
||||
for (const [key, field] of Object.entries(shape)) {
|
||||
const fieldDef = (
|
||||
field as unknown as {
|
||||
_def: {
|
||||
typeName: string;
|
||||
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);
|
||||
const fieldDef = (field as unknown as { _def: ZodDef })._def;
|
||||
const { resolvedType, isRequired, description } = resolveZodDef(fieldDef);
|
||||
properties[key] = { ...resolvedType, description: description ?? key };
|
||||
if (isRequired) required.push(key);
|
||||
}
|
||||
return { type: 'object', properties, required };
|
||||
}
|
||||
@ -54,9 +49,48 @@ function zodToJsonSchema(schema: ZodTypeAny): Record<string, unknown> {
|
||||
return { type: 'object' };
|
||||
}
|
||||
|
||||
function zodTypeNameToJsonType(typeName: string): string {
|
||||
if (typeName === 'ZodNumber' || typeName === 'ZodCoerce') return 'number';
|
||||
if (typeName === 'ZodBoolean') return 'boolean';
|
||||
if (typeName === 'ZodArray') return 'array';
|
||||
return 'string'; // ZodString, ZodEnum, ZodLiteral, unknown
|
||||
/**
|
||||
* Unwrap ZodOptional / ZodDefault wrappers and resolve the JSON type for a field.
|
||||
* Returns the JSON Schema fragment plus whether the field is required.
|
||||
*/
|
||||
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