diff --git a/packages/mcp-common/src/json-schema-sanitize.spec.ts b/packages/mcp-common/src/json-schema-sanitize.spec.ts new file mode 100644 index 00000000..ad0b7dca --- /dev/null +++ b/packages/mcp-common/src/json-schema-sanitize.spec.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' + +import { stripNotEmptyJsonSchema } from './json-schema-sanitize' + +describe('stripNotEmptyJsonSchema', () => { + it('removes not:{} optional arms from anyOf', () => { + const input = { + properties: { + hint: { + anyOf: [{ not: {} }, { type: 'string', enum: ['wnam', 'enam'] }], + }, + }, + } + expect(stripNotEmptyJsonSchema(input)).toEqual({ + properties: { + hint: { type: 'string', enum: ['wnam', 'enam'] }, + }, + }) + }) + + it('leaves schemas without not:{} unchanged', () => { + const input = { + properties: { + page: { type: 'number' }, + }, + } + expect(stripNotEmptyJsonSchema(input)).toEqual(input) + }) +}) diff --git a/packages/mcp-common/src/json-schema-sanitize.ts b/packages/mcp-common/src/json-schema-sanitize.ts new file mode 100644 index 00000000..060a9813 --- /dev/null +++ b/packages/mcp-common/src/json-schema-sanitize.ts @@ -0,0 +1,42 @@ +/** + * Remove zod-to-json-schema optional arms like `{ "not": {} }` that break strict + * LLM function-calling validators (Kimi, Gemini, OpenAI strict mode). + */ +export function stripNotEmptyJsonSchema(node: T): T { + if (Array.isArray(node)) { + return node.map((item) => stripNotEmptyJsonSchema(item)) as T + } + if (node === null || typeof node !== 'object') { + return node + } + + const obj = { ...(node as Record) } + + if (Array.isArray(obj.anyOf)) { + const kept = obj.anyOf.filter((member) => !isNotEmptySchema(member)) + if (kept.length === 1) { + const inlined = stripNotEmptyJsonSchema(kept[0]) as Record + const { anyOf: _removed, ...rest } = obj + return { ...inlined, ...rest } as T + } + obj.anyOf = kept.map((member) => stripNotEmptyJsonSchema(member)) + } + + for (const key of Object.keys(obj)) { + if (key === 'anyOf') continue + obj[key] = stripNotEmptyJsonSchema(obj[key]) + } + + return obj as T +} + +function isNotEmptySchema(member: unknown): boolean { + if (!member || typeof member !== 'object') return false + const notValue = (member as Record).not + return ( + Object.keys(member as object).length === 1 && + notValue !== null && + typeof notValue === 'object' && + Object.keys(notValue as object).length === 0 + ) +} diff --git a/packages/mcp-common/src/server.ts b/packages/mcp-common/src/server.ts index 0cdf0269..9239f612 100644 --- a/packages/mcp-common/src/server.ts +++ b/packages/mcp-common/src/server.ts @@ -4,9 +4,22 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { type ZodRawShape } from 'zod' import { MetricsTracker, SessionStart, ToolCall } from '../../mcp-observability/src' +import * as zodJsonSchemaCompat from '@modelcontextprotocol/sdk/server/zod-json-schema-compat.js' + import { buildAccountTool } from './account-tool' +import { stripNotEmptyJsonSchema } from './json-schema-sanitize' import { McpError } from './mcp-error' +const patchJsonSchemaCompat = zodJsonSchemaCompat as typeof zodJsonSchemaCompat & { + __cfSanitizePatched?: boolean +} +if (!patchJsonSchemaCompat.__cfSanitizePatched) { + const originalToJsonSchema = patchJsonSchemaCompat.toJsonSchemaCompat + patchJsonSchemaCompat.toJsonSchemaCompat = (schema, opts) => + stripNotEmptyJsonSchema(originalToJsonSchema(schema, opts)) + patchJsonSchemaCompat.__cfSanitizePatched = true +} + import type { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js' import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js' import type { diff --git a/packages/mcp-common/src/tools/d1.tools.ts b/packages/mcp-common/src/tools/d1.tools.ts index e4463a9a..b3d91bf3 100644 --- a/packages/mcp-common/src/tools/d1.tools.ts +++ b/packages/mcp-common/src/tools/d1.tools.ts @@ -16,7 +16,7 @@ export function registerD1Tools(agent: CloudflareMcpAgent) { 'd1_databases_list', 'List all of the D1 databases in your Cloudflare account', { - name: D1DatabaseNameParam.nullable().optional(), + name: D1DatabaseNameParam.optional(), page: PaginationPageParam, per_page: PaginationPerPageParam, }, @@ -65,7 +65,7 @@ export function registerD1Tools(agent: CloudflareMcpAgent) { 'Create a new D1 database in your Cloudflare account', { name: D1DatabaseNameParam, - primary_location_hint: D1DatabasePrimaryLocationHintParam.nullable().optional(), + primary_location_hint: D1DatabasePrimaryLocationHintParam, }, { title: 'Create D1 database', diff --git a/packages/mcp-common/src/tools/hyperdrive.tools.ts b/packages/mcp-common/src/tools/hyperdrive.tools.ts index 70da37a5..d8ffd473 100644 --- a/packages/mcp-common/src/tools/hyperdrive.tools.ts +++ b/packages/mcp-common/src/tools/hyperdrive.tools.ts @@ -177,16 +177,15 @@ export function registerHyperdriveTools(agent: CloudflareMcpAgent) { 'Edit (patch) a Hyperdrive configuration in your Cloudflare account', { hyperdrive_id: HyperdriveConfigIdSchema, - name: HyperdriveConfigNameSchema.optional().nullable(), - database: HyperdriveOriginDatabaseSchema.optional().nullable(), - host: HyperdriveOriginHostSchema.optional().nullable(), - port: HyperdriveOriginPortSchema.optional().nullable(), - scheme: HyperdriveOriginSchemeSchema.optional().nullable(), - user: HyperdriveOriginUserSchema.optional().nullable(), - caching_disabled: HyperdriveCachingDisabledSchema.optional().nullable(), - caching_max_age: HyperdriveCachingMaxAgeSchema.optional().nullable(), - caching_stale_while_revalidate: - HyperdriveCachingStaleWhileRevalidateSchema.optional().nullable(), + name: HyperdriveConfigNameSchema.optional(), + database: HyperdriveOriginDatabaseSchema.optional(), + host: HyperdriveOriginHostSchema.optional(), + port: HyperdriveOriginPortSchema.optional(), + scheme: HyperdriveOriginSchemeSchema.optional(), + user: HyperdriveOriginUserSchema.optional(), + caching_disabled: HyperdriveCachingDisabledSchema.optional(), + caching_max_age: HyperdriveCachingMaxAgeSchema.optional(), + caching_stale_while_revalidate: HyperdriveCachingStaleWhileRevalidateSchema.optional(), }, { title: 'Edit Hyperdrive config', diff --git a/packages/mcp-common/src/types/shared.types.ts b/packages/mcp-common/src/types/shared.types.ts index fdf88dbc..9621f8d1 100644 --- a/packages/mcp-common/src/types/shared.types.ts +++ b/packages/mcp-common/src/types/shared.types.ts @@ -1,7 +1,7 @@ import { z } from 'zod' -export const PaginationPerPageParam = z.number().nullable().optional() -export const PaginationPageParam = z.number().nullable().optional() +export const PaginationPerPageParam = z.number().optional() +export const PaginationPageParam = z.number().optional() export const PaginationLimitParam = z.number().optional() export const PaginationOffsetParam = z.number().optional()