Skip to content

Fix Zod object detection logic #468

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

geelen
Copy link
Contributor

@geelen geelen commented May 9, 2025

Previously (in #309), 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.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Note: In general, having two loosely structured arguments that are both optional and so can occur in the same position of a function call (i.e. server.tool(name, params, cb) and server.tool(name, annotations, cb) is really brittle. This PR improves things so the version of Zod the downstream user has installed no longer breaks things, but I would strongly recommend removing the ambiguity from the API.

It would mean that in order to declare a tool with no params but with annotations, you'd write server.tool(name, {}, annotations, cb). Those four extra characters seem a small price to pay for eliminating an entire class of errors like this.

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.
Copy link
Contributor

@ihrpr ihrpr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, the ambiguous API design is the real issue - let's fix that rather than adding workarounds.

@geelen
Copy link
Contributor Author

geelen commented May 9, 2025

Well I don't think it's an either/or, personally. Fixing the API is technically a breaking change, which might be fine but probably shouldn't be rushed out. Whereas this is a straight bugfix, which imo should get released ASAP as a patch release to stop anyone else hitting the issue.

function isZodRawShape(obj: unknown): obj is ZodRawShape {
if (typeof obj !== "object" || obj === null) return false;

const isEmptyObject = z.object({}).strict().safeParse(obj).success;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haven't tested it, would it potentially also cause an issue with a different version of Zod?

- simpler test for empty object
- only test public zod properties (parse, safeParse)

Confirmed that vendored-library tests still pass. Note that you need the --testPathIgnorePatterns to avoid the __tests__ directory (this is what was breaking the build):
$ npm test -- --testPathIgnorePatterns="vendor"
@bhosmer-ant
Copy link
Contributor

@geelen thanks a bunch for this - I've tweaked the heuristic type test, removed the dependency on safeParse for the empty-object test, and confirmed that the fix still works against the vendored library (split the commits so this is reproducible). Per @ihrpr I'd like to avoid bringing in the vendored library for testing.

Also agree that a real fix means a different API - essential considering the more complex/type-unsniffable tool configs that are sure to follow (starting with outputSchema).

@bhosmer-ant bhosmer-ant merged commit bced33d into modelcontextprotocol:main May 9, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants