Skip to content

Conversation

@abloomston
Copy link

@abloomston abloomston commented Aug 5, 2025

Add optional strictInputSchemaValidation parameter to registerTool() config that enables Zod's strict validation to reject unknown parameters instead of silently ignoring them.

Motivation and Context

The MCP TypeScript SDK currently silently ignores unknown parameters in tool calls, which causes issues where parameter name typos are silently dropped, leading to confusing behavior where tools execute with missing data.

How Has This Been Tested?

  • Added comprehensive test case demonstrating the issue and fix

Breaking Changes

None. The strictInputSchemaValidation parameter defaults to false for backward compatibility. Existing code will continue to work unchanged.

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

Additional context

You should consider this PR in concert with #792 which makes changes that may be related.

Implementation Details:

  • Added strictInputSchemaValidation?: boolean to registerTool config interface
  • When strictInputSchemaValidation: true, applies z.object(inputSchema).strict() to reject unknown parameters
  • When strictInputSchemaValidation: false (default), maintains current lenient behavior
  • Only available in registerTool() method - legacy tool() method uses lenient validation for compatibility

Error Handling:

Use Cases:

  • Development: Catch parameter name typos early (returned as visible error responses)
  • Production APIs: Ensure clients send only expected parameters
  • Security-sensitive tools: Prevent injection of unexpected data

The feature follows existing patterns in the codebase and maintains full backward compatibility while solving a real pain point for developers.

LLM Disclosure

This PR was created with the assistance of Claude Code, Claude Haiku 3.5, Claude Sonnet 4.0, Claude Haiku 4.5, Claude Sonnet 4.5, and Chat-GPT 4.1 nano.

@abloomston abloomston requested review from a team and domdomegg August 5, 2025 17:45
@domdomegg domdomegg requested review from ochafik and removed request for domdomegg August 25, 2025 16:38
Copy link
Contributor

@ochafik ochafik left a comment

Choose a reason for hiding this comment

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

Thanks @abloomston !

README.md Outdated
strict: true // Reject { username: "test", itemcount: 42 }
}, handler);

// Lenient validation for production - handles client variations gracefully
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure about advising leniency in prod tbh, or maybe explain why (e.g. to allow some kind of forward compatibility maybe, although I'm struggling to imagine a valid use case)

Copy link
Author

Choose a reason for hiding this comment

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

@ochafik I did this to maintain backwards compatibility for users of this sdk. Let me know which (or another) you'd like me to do:

  1. Revise this comment, maintaining backwards compatibility
  2. Do not maintain backwards compatibility, making strict the default

Copy link
Author

Choose a reason for hiding this comment

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

I will revise this comment, maintaining backwards compatibility

inputSchema: ZodRawShape | undefined,
outputSchema: ZodRawShape | undefined,
annotations: ToolAnnotations | undefined,
strict: boolean | undefined,
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd name this more explicitly, e.g. rejectUnexpectedInputs, given this isn't turning registerTool as fully strict (e.g. I'd expect structuredContent to be checked against outputSchema, and potentially content text to be checked against structuredContent)

Copy link
Author

Choose a reason for hiding this comment

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

@ochafik would the variable name strictInputSchemaValidation work for you?

Copy link
Author

Choose a reason for hiding this comment

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

I will proceed with the variable name strictInputSchemaValidation

Adam Bloomston added 3 commits October 7, 2025 10:23
  This test demonstrates the current issue where tools accept unknown
  parameters (e.g. incorrect capitalization) without validation errors.
  The test uses 'username' and 'itemcount' instead of the expected
  'userName' and 'itemCount' parameters, which should be rejected
  but currently aren't.

  This test is expected to fail until strict validation is implemented.
  Add strict parameter to registerTool config that defaults to false for
  backward compatibility. When strict=true, applies .strict() to Zod
  schema creation for tool input validation to throw errors on unknown
  parameters instead of silently ignoring them.

  - Add strict?: boolean to registerTool config interface
  - Modify _createRegisteredTool to accept and use strict parameter
  - Apply z.object(inputSchema).strict() when strict=true
  - Update legacy tool() method to pass strict=false (backward compatibility)
  - Update test to verify strict validation rejects unknown parameters
  - All existing tests continue to pass (no breaking changes)

  This fixes the issue where parameter name typos are silently dropped,
  leading to confusing behavior where tools execute with missing data.
  Add Advanced Usage section explaining the strict parameter for registerTool:
  - Show examples of strict vs lenient validation
  - Explain when to use each mode (development vs production)
  - Document that strict parameter is only available in registerTool()
  - Note that legacy tool() method uses lenient validation for compatibility

  This helps developers understand when and how to use strict validation
  to catch parameter name typos and unexpected data.
@abloomston abloomston force-pushed the adam-bloomston--strict-zod-validation branch from 1753939 to 57dacc4 Compare October 7, 2025 16:30
@abloomston abloomston requested a review from a team as a code owner October 7, 2025 16:30
Adam Bloomston and others added 3 commits October 7, 2025 10:31
Revised comment to explain that lenient validation (strict=false) maintains
backwards compatibility with existing clients rather than advising leniency
for production. The updated comment clarifies that extra parameters are
accepted to support clients that may send additional fields.

Changes linted and tested.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Renamed parameter from `strict` to `strictInputSchemaValidation` for clarity.
The longer name makes it more explicit that this validation applies specifically
to the input schema and helps distinguish it from other types of strictness.

Updated:
- src/server/mcp.ts: Parameter in registerTool config and _createRegisteredTool signature
- src/server/mcp.test.ts: Test using the renamed parameter
- README.md: Documentation examples and notes

Changes linted and tested.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Changed test from expecting failure to properly testing that lenient validation
(default behavior) accepts unknown parameters for backwards compatibility.
Renamed test from "should fail..." to "should accept unknown parameters when
strict validation is disabled (default)" to clarify it's testing the expected
lenient behavior.

This ensures all tests pass while documenting both validation modes:
- Lenient (default): accepts unknown parameters
- Strict: rejects unknown parameters

Changes linted and tested - all 756 tests now pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@abloomston abloomston requested a review from ochafik October 7, 2025 18:06
Adam Bloomston and others added 2 commits October 7, 2025 12:08
Applied prettier auto-formatting to README.md and src/server/mcp.ts
to fix code style issues.

Changes linted and tested.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Renamed example from prod-tool to lenient-tool to better reflect that
lenient validation is about backwards compatibility rather than being
specifically for production environments.

Changes linted and tested.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@abloomston
Copy link
Author

@ochafik I took a pass at changes based on what I expect you to have wanted, please take a look. Thanks!

@abloomston
Copy link
Author

Hi @ochafik wondering if I can get your 👀 on this PR? Looking to close this task out on my end :) Thanks!

@felixweinberger felixweinberger added this to the zod milestone Oct 23, 2025
abloomston and others added 3 commits October 30, 2025 10:18
…esponses

When strictInputSchemaValidation is enabled, validation errors now properly
throw (reject the request) instead of being caught and returned as error
responses with isError: true.

Changes:
- Add strictInputSchemaValidation field to RegisteredTool type
- Store strictInputSchemaValidation flag when creating tools
- Modify error handling to rethrow validation errors when strict mode enabled

This ensures tools with strict validation fail fast on invalid input,
maintaining backwards compatibility for tools without strict validation
where errors are returned as error responses.

Changes linted and tested.

<em>🤖 Created with Claude Code via Airchat</em>
…col#1044 pattern

Change strict validation errors to return CallToolResult with isError: true
instead of throwing protocol-level errors, aligning with the pattern established
in PR modelcontextprotocol#1044 (commit 7387c44) where all validation errors return error responses
rather than throwing.

This ensures consistent error handling across all validation scenarios and
enables LLMs to see validation errors and attempt self-correction, as specified
in the MCP protocol specification.

Changes:
- Remove conditional throw for strict validation input errors
- Update test expectations to check for error responses instead of thrown errors
- Keep strictInputSchemaValidation field in RegisteredTool for future use

The strictInputSchemaValidation field is retained as it still serves to apply
.strict() to the Zod schema, rejecting unknown parameters. The difference is
now the rejection returns an error response visible to LLMs rather than a
protocol-level error.

Changes linted and tested.

<em>🤖 Created with Claude Code via Airchat</em>
@abloomston abloomston changed the title feat: add optional strict parameter to registerTool for Zod validation feat: add optional strictInputSchemaValidation parameter to registerTool for Zod validation Oct 30, 2025
@abloomston
Copy link
Author

Update: Aligned Error Handling with PR #1044

I've updated this PR to align with the error handling pattern established in PR #1044 (commit 7387c44), which was merged to main on October 28, 2025.

What Changed

Before: The original implementation had strict validation errors throw protocol-level McpError exceptions, making them invisible to LLMs.

After: Strict validation errors now return CallToolResult with isError: true, consistent with all other validation errors in the SDK.

Why This Change

PR #1044 established a uniform pattern where all validation errors (tool not found, disabled tools, input validation, output validation) return error responses rather than throwing protocol-level errors. This follows the MCP specification guidance:

Any errors that originate from the tool SHOULD be reported inside the result object, with isError set to true, not as an MCP protocol-level error response. Otherwise, the LLM would not be able to see that an error occurred and self-correct.

Impact

  • Consistent error handling across all validation scenarios
  • MCP spec compliance - errors visible to LLMs for self-correction
  • Core functionality preserved - strictInputSchemaValidation: true still applies .strict() to reject unknown parameters
  • All tests passing - updated test expectations to match new behavior

Commits

The feature still provides the same value (catching parameter typos, preventing unexpected data) while maintaining consistency with the SDK's error handling approach.

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