Skip to content

fix(streamable-http-server): look up tool schemas per request - #1349

Open
DaleSeo wants to merge 2 commits into
mainfrom
DaleSeo/streamable-http-server-mcp-param-validation-uses
Open

DaleSeo wants to merge 2 commits into
mainfrom
DaleSeo/streamable-http-server-mcp-param-validation-uses

Conversation

@DaleSeo

@DaleSeo DaleSeo commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Fixes #1348

Motivation and Context

For SEP-2243, Mcp-Param-* validation now asks ServerHandler::get_tool for the tool's input schema on every request, instead of reading from a per-service cache that was never cleared.

StreamableHttpService cached each tool's schema by name for the entire lifetime of the service, including None results. When a server's tool list changed and it sent notifications/tools/list_changed, header validation kept using the old schema. This caused two problems:

  • Valid requests were rejected: Renaming a tool or adding an x-mcp-header annotation caused conforming clients to receive HTTP 400 / -32020.
  • Validation was skipped: If the first tools/call for a name arrived before the tool existed, the cached None meant its Mcp-Param-* headers were never checked again. An intermediary could then route or authorize a request based on a header value that the server never compares with the body.

get_tool is already a synchronous lookup, so handlers that need caching can do it there and invalidate the cache correctly. A shared HashMap in the transport can't do that, especially in stateless mode.

How Has This Been Tested?

Add tests

Breaking Changes

None. SEP-2243 validation only applies to protocol versions 2026-07-28 and later.

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

@github-actions github-actions Bot added T-test Testing related changes T-core Core library changes T-transport Transport layer changes labels Oct 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

@bolinfest is on triage rotation this week and is the reviewer for this one.

Automated classification did not run for this one, so it needs a full manual pass.

Automated triage. Labels are a first pass and the assigned maintainer has the final say.

@DaleSeo
DaleSeo marked this pull request as ready for review October 13, 2026 17:08
@DaleSeo
DaleSeo requested a review from a team as a code owner October 13, 2026 17:08
@DaleSeo
DaleSeo requested a review from bolinfest October 14, 2026 12:41
@DaleSeo DaleSeo assigned DaleSeo and unassigned bolinfest Oct 14, 2026
};

let before: serde_json::Value = send_mismatch().await.json().await?;
assert_ne!(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think the first request reaches the handler here, does it? From what I see it gets past the header check and then validate_required_protocol_meta rejects it with -32602 (no _meta). assert_ne! would also pass on a 500 so maybe we should assert 400 and -32602 here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, thanks! The -32602 error actually comes from validate_request_protocol_version_meta because _meta is missing from the 2026-07-28 request. validate_required_protocol_meta is disabled by default, but the effect is the same. I updated the test to check for a 400 status and -32602.

let response = send_mismatch().await;
assert_eq!(response.status(), 400);
let body: serde_json::Value = response.json().await?;
assert_eq!(body["error"]["code"], -32020);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The body mentions two cases (a stale schema rejecting valid requests and a cached None skipping validation) but I think this test only covers the second one. I tried flipping annotated back to false at the end on this branch and I noticed main still returns -32020 there (the stale schema) and this branch doesn't, so it'd make a good regression test. Might be worth a second test that starts annotated?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good idea, @gocamille! Added skips_param_header_validation_once_tool_loses_annotation.

gocamille
gocamille previously approved these changes Oct 16, 2026

@gocamille gocamille left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks good to me, thanks @DaleSeo! I left a couple of small test suggestions, none blocking.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-core Core library changes T-test Testing related changes T-transport Transport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

streamable-http server: Mcp-Param-* validation uses a tool schema cache that is never invalidated

3 participants