Skip to content

Commit 464e8f8

Browse files
committed
fix: add Zod v3/v4 compatibility for MCP SDK 1.23.0
MCP SDK 1.23.0 now supports both Zod v3 and v4, which have different internal structures. This caused context parameter injection to fail. Changes: - Add zod-compat.ts with version-agnostic detection and utilities - Update context-parameters.ts to use zod-compat utilities - Update tracingV2.ts to use zod-compat for schema shape access - Update Zod dependency to support both v3 and v4 - Add comprehensive tests for zod-compat utilities Tested with MCP SDK versions 1.21.2 and 1.23.0.
1 parent 580f1fc commit 464e8f8

File tree

6 files changed

+530
-73
lines changed

6 files changed

+530
-73
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"packageManager": "pnpm@10.11.0",
5555
"devDependencies": {
5656
"@changesets/cli": "^2.29.4",
57-
"@modelcontextprotocol/sdk": "1.11",
57+
"@modelcontextprotocol/sdk": "~1.23.0",
5858
"@types/node": "^22.15.21",
5959
"@typescript-eslint/eslint-plugin": "^8.32.1",
6060
"@typescript-eslint/parser": "^8.32.1",
@@ -75,7 +75,7 @@
7575
"@opentelemetry/otlp-transformer": "^0.203.0",
7676
"mcpcat-api": "0.1.6",
7777
"redact-pii": "3.4.0",
78-
"zod": "3.25.30"
78+
"zod": "^3.25 || ^4.0"
7979
},
8080
"lint-staged": {
8181
"*.{ts,js}": [

pnpm-lock.yaml

Lines changed: 34 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/modules/context-parameters.ts

Lines changed: 30 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,12 @@
11
import { RegisteredTool } from "../types";
22
import { z } from "zod";
33
import { DEFAULT_CONTEXT_PARAMETER_DESCRIPTION } from "./constants";
4-
5-
// Detect if something is a Zod schema (has _def and parse methods)
6-
function isZodSchema(schema: any): boolean {
7-
return (
8-
schema &&
9-
typeof schema === "object" &&
10-
"_def" in schema &&
11-
typeof schema.parse === "function"
12-
);
13-
}
14-
15-
// Detect if it's shorthand Zod syntax (object with z.* values)
16-
function isShorthandZodSyntax(schema: any): boolean {
17-
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
18-
return false;
19-
}
20-
21-
// Check if any value is a Zod schema
22-
return Object.values(schema).some((value) => isZodSchema(value));
23-
}
4+
import {
5+
isZodSchema,
6+
isShorthandZodSyntax,
7+
schemaHasProperty,
8+
extendObjectSchema,
9+
} from "./zod-compat";
2410

2511
export function addContextParameterToTool(
2612
tool: RegisteredTool,
@@ -43,36 +29,25 @@ export function addContextParameterToTool(
4329
return modifiedTool;
4430
}
4531

46-
// Handle Zod z.object() schemas
32+
const contextDescription =
33+
customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION;
34+
35+
// Handle Zod z.object() schemas (both v3 and v4)
4736
if (isZodSchema(modifiedTool.inputSchema)) {
4837
// Check if context already exists in Zod schema shape
49-
if (
50-
modifiedTool.inputSchema.shape &&
51-
"context" in modifiedTool.inputSchema.shape
52-
) {
38+
if (schemaHasProperty(modifiedTool.inputSchema, "context")) {
5339
return modifiedTool;
5440
}
55-
// It's a Zod schema, augment it with context
56-
const contextSchema = z.object({
57-
context: z
58-
.string()
59-
.describe(
60-
customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
61-
),
62-
});
63-
64-
// Use extend to add context to the schema
65-
if (typeof modifiedTool.inputSchema.extend === "function") {
66-
modifiedTool.inputSchema = modifiedTool.inputSchema.extend(
67-
contextSchema.shape,
68-
);
69-
} else if (typeof modifiedTool.inputSchema.augment === "function") {
70-
modifiedTool.inputSchema =
71-
modifiedTool.inputSchema.augment(contextSchema);
72-
} else {
73-
// Fallback: merge with new z.object
74-
modifiedTool.inputSchema = contextSchema.merge(modifiedTool.inputSchema);
75-
}
41+
42+
// Extend the schema with context using our compat layer
43+
const contextShape = {
44+
context: z.string().describe(contextDescription),
45+
};
46+
47+
modifiedTool.inputSchema = extendObjectSchema(
48+
modifiedTool.inputSchema,
49+
contextShape,
50+
);
7651

7752
return modifiedTool;
7853
}
@@ -84,18 +59,15 @@ export function addContextParameterToTool(
8459
return modifiedTool;
8560
}
8661

87-
// Create a new Zod schema with context
88-
const contextField = z
89-
.string()
90-
.describe(
91-
customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
92-
);
62+
// Extend using our compat layer (handles both v3 and v4)
63+
const contextShape = {
64+
context: z.string().describe(contextDescription),
65+
};
9366

94-
// Create new z.object with context and all original fields
95-
modifiedTool.inputSchema = z.object({
96-
context: contextField,
97-
...modifiedTool.inputSchema,
98-
});
67+
modifiedTool.inputSchema = extendObjectSchema(
68+
modifiedTool.inputSchema,
69+
contextShape,
70+
);
9971

10072
return modifiedTool;
10173
}
@@ -115,8 +87,7 @@ export function addContextParameterToTool(
11587

11688
modifiedTool.inputSchema.properties.context = {
11789
type: "string",
118-
description:
119-
customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
90+
description: contextDescription,
12091
};
12192

12293
// Add context to required array if it exists

src/modules/tracingV2.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { PublishEventRequestEventTypeEnum } from "mcpcat-api";
1414
import { publishEvent } from "./eventQueue.js";
1515
import { addContextParameterToTool } from "./context-parameters.js";
1616
import { handleReportMissing } from "./tools.js";
17+
import { getObjectShape, getLiteralValue } from "./zod-compat.js";
1718
import { setupInitializeTracing, setupListToolsTracing } from "./tracing.js";
1819
import { captureException } from "./exceptions.js";
1920

@@ -332,7 +333,8 @@ function setupToolsCallHandlerWrapping(server: HighLevelMCPServerLike): void {
332333
requestSchema: any,
333334
handler: any,
334335
) {
335-
const method = requestSchema?.shape?.method?.value;
336+
const shape = getObjectShape(requestSchema);
337+
const method = shape?.method ? getLiteralValue(shape.method) : undefined;
336338

337339
// Only wrap tools/call handler
338340
if (method === "tools/call") {

0 commit comments

Comments
 (0)