Skip to content

Commit b6617dc

Browse files
committed
Fix Zod object detection logic
Previously, this check was `value instanceof ZodType`, which breaks if the user is using a different version of Zod than the library. Instead, do the check using the structure of the input object rather than its prototype chain. Additionally, this adds a vendored version of Zod to use for tests, which ensures that regressions like this would be caught next time.
1 parent 35fe98a commit b6617dc

File tree

4 files changed

+35
-11
lines changed

4 files changed

+35
-11
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as _zod from 'zod';
2+
3+
// This makes the vendored zod use the same types as the actual zod package
4+
export declare const z: typeof _zod.z;
5+
export default _zod.default;

src/server/__tests__/vendor/[email protected]

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

src/server/mcp.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { McpServer } from "./mcp.js";
22
import { Client } from "../client/index.js";
33
import { InMemoryTransport } from "../inMemory.js";
4-
import { z } from "zod";
54
import {
65
ListToolsResultSchema,
76
CallToolResultSchema,
@@ -17,6 +16,9 @@ import {
1716
import { ResourceTemplate } from "./mcp.js";
1817
import { completable } from "./completable.js";
1918
import { UriTemplate } from "../shared/uriTemplate.js";
19+
// Note: deliberately using a different Zod version to the one bundled
20+
// with the SDK as that's what our users are likely to be doing
21+
import { z } from "./__tests__/vendor/[email protected]";
2022

2123
describe("McpServer", () => {
2224
test("should expose underlying Server instance", () => {

src/server/mcp.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -659,16 +659,6 @@ export class McpServer {
659659
if (this._registeredTools[name]) {
660660
throw new Error(`Tool ${name} is already registered`);
661661
}
662-
663-
// Helper to check if an object is a Zod schema (ZodRawShape)
664-
const isZodRawShape = (obj: unknown): obj is ZodRawShape => {
665-
if (typeof obj !== "object" || obj === null) return false;
666-
667-
const isEmptyObject = z.object({}).strict().safeParse(obj).success;
668-
669-
// Check if object is empty or at least one property is a ZodType instance
670-
return isEmptyObject || Object.values(obj as object).some(v => v instanceof ZodType);
671-
};
672662

673663
let description: string | undefined;
674664
if (typeof rest[0] === "string") {
@@ -931,6 +921,25 @@ const EMPTY_OBJECT_JSON_SCHEMA = {
931921
type: "object" as const,
932922
};
933923

924+
// Helper to check if an object is a Zod schema (ZodRawShape)
925+
function isZodRawShape(obj: unknown): obj is ZodRawShape {
926+
if (typeof obj !== "object" || obj === null) return false;
927+
928+
const isEmptyObject = z.object({}).strict().safeParse(obj).success;
929+
930+
// Check if object is empty or at least one property is a ZodType instance
931+
return isEmptyObject || Object.values(obj as object).some(isZodType);
932+
}
933+
934+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
935+
function isZodType(value: any): value is ZodType {
936+
return value !== null &&
937+
typeof value === 'object' &&
938+
typeof value.parse === 'function' &&
939+
typeof value.safeParse === 'function' &&
940+
typeof value._def === 'object';
941+
}
942+
934943
/**
935944
* Additional, optional information for annotating a resource.
936945
*/

0 commit comments

Comments
 (0)