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:
saravanakumardb1 2026-03-05 12:35:27 -08:00
parent c04c78aff4
commit 3e37b3d5a0
3 changed files with 95 additions and 24 deletions

View File

@ -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);

View File

@ -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', () => {

View File

@ -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' };
}
}