Repository navigation
fix(model): keep _meta-only results as tool results when decoding custom results - #6
Closed
preston-footprint wants to merge 96 commits into
Conversation
…1186) * fix: allow concurrent streamable http requests * fix: keep streamable http recovery responsive Keep cancellation and replies available while old session POSTs finish. Bound the wait for old POSTs and the replacement initialization handshake. Do not retry interrupted POSTs because the server may have processed them. Add regressions for recovery, queued cancellation, control timeouts, and server replies needed by active requests. * fix: preserve response and cancellation ordering * refactor: clarify streamable http control flow * fix: match stream responses against pending request ids Match responses against all pending requests before removing a stream registration. Keep distinct numeric and string ids separate while preserving the existing fallback for servers that stringify numeric ids. Add a mixed-id regression and keep a separate registration owner alive in the abandoned-cancellation test. * feat: make streamable http control timeouts configurable
…odelcontextprotocol#1211) * fix(transport): fall back after HTTP discover rejection * fix(transport): limit legacy fallback to sessionless probes * style: apply nightly rustfmt import ordering
* feat: add request-state key rotation * docs: streamline request-state codec documentation * fix: harden request-state fallback verification * refactor: refine request-state keyring API * docs: make request-state rotation guidance self-contained * docs: streamline request-state keyring rustdocs * test: streamline request-state keyring coverage * docs: restore request-state key rotation doctest * chore: remove manual changelog entry
…lcontextprotocol#1230) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.85.13 to 2.86.7. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](taiki-e/install-action@82cd3e7...b6ff580) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.86.7 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…elcontextprotocol#1232) * feat(auth): coordinate refreshes through credential stores Add an optional owned guard covering credential reload, refresh, and save. Keep credential-store failures distinct from reauthorization, including reactive refresh after an HTTP 401. Existing stores retain their default uncoordinated behavior. Cover guard ordering, concurrent rotation, storage failures, and client identity checks. * docs(auth): clarify refresh guard coordination
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…ontextprotocol#1229) Updates the requirements on [process-wrap](https://github.com/watchexec/process-wrap) to permit the latest version. - [Changelog](https://github.com/watchexec/process-wrap/blob/main/CHANGELOG.md) - [Commits](watchexec/process-wrap@v9.0.0...v10.0.0) --- updated-dependencies: - dependency-name: process-wrap dependency-version: 10.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…xtprotocol#1238) Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 7.6.0 to 10.0.1. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](astral-sh/setup-uv@37802ad...20cfd1b) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: 10.0.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…ontextprotocol#1234) * feat(auth): request ID-JAGs with enterprise refresh tokens * feat(auth): exchange ID-JAGs for MCP access tokens Redeem ID-JAGs at the approved resource authorization server and return its bearer token, lifetime, and effective granted scopes. Preserve scope narrowing and redacted diagnostics, and reuse the default HTTP adapter. Support independently configured client authentication at both servers. Document the exchange profile and test redirects and staged failures. Partially addresses modelcontextprotocol#531. * test(conformance): cover EMA refresh-token exchange
…col#1236) * fix(auth): unify refresh checks and error handling Claude-Session: https://claude.ai/code/session_011pHFfoTygeG84mCDXzCcmw * test(auth): add live authorization-server checks Claude-Session: https://claude.ai/code/session_011pHFfoTygeG84mCDXzCcmw
…contextprotocol#1250) Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.86.7 to 2.87.6. - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](taiki-e/install-action@b6ff580...7b8d471) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.87.6 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1a51d4b5a03bb97576af186c813af67e9137ba7c to d43b6c087ac471e2ea7b8af622ff15f05c0c365b. - [Release notes](https://github.com/crate-ci/typos/releases) - [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md) - [Commits](crate-ci/typos@1a51d4b...d43b6c0) --- updated-dependencies: - dependency-name: crate-ci/typos dependency-version: d43b6c087ac471e2ea7b8af622ff15f05c0c365b dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…nic (modelcontextprotocol#1231) * fix(sse): saturate exponential reconnect backoff ExponentialBackoff::retry computed the reconnect multiplier with 2u32.pow(current_times). With max_times unset, current_times can reach the bit width, panicking in debug builds and wrapping to a zero delay in release builds for long-lived SSE clients. Use saturating_pow and Duration::saturating_mul so the delay stays monotonic and panic-free. * fix(sse): cap exponential reconnect backoff at a bounded max delay Saturating the multiplier alone can still yield decades-long sleeps once current_times reaches the bit width, pinning the stream in tokio::time::sleep without reconnecting or terminating. Add an optional max_delay (default 30s) that clamps the computed delay, keeping the backoff monotonic and panic-free while guaranteeing the client retries. * fix(sse): default max_delay to None to preserve unbounded backoff Per maintainer feedback, leave ExponentialBackoff::default() unbounded so the fix stays a pure overflow bug-fix. The saturating multiplier removes the debug panic / release wrap from modelcontextprotocol#1198, while max_delay stays opt-in for callers that want a bounded reconnect delay.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…1192) Return HTTP 403 for malformed and non-UTF-8 Origin headers when validation is enabled. Add enforce_origin_validation() for strict empty-allowlist validation while preserving the legacy default and non-empty allowlist behavior. Configure the conformance server to opt in explicitly. Signed-off-by: lucarlig <luca.carlig@ibm.com>
…HTTP 400 (modelcontextprotocol#1259) `jsonrpc_http_status` maps several protocol errors (unsupported protocol version, missing required client capability, invalid params) to HTTP 400 on the modern per-request Streamable HTTP path, but fell through to the default HTTP 200 for `ErrorCode::HEADER_MISMATCH`. When a `ServerHandler` returns `Err(ErrorData::header_mismatch(...))`, RMCP serialized the correct JSON-RPC -32020 error but sent it with HTTP 200. The legacy/transport-generated header validation path (SEP-2243 Mcp-Method/Mcp-Name/Mcp-Param-* mismatches) was unaffected, since `header_mismatch_jsonrpc_response` constructs an HTTP 400 response directly rather than going through `jsonrpc_http_status`. Only handler-generated HeaderMismatch errors on the modern path hit the bug. Add `ErrorCode::HEADER_MISMATCH` to the existing `BAD_REQUEST` match arm, matching the Streamable HTTP server-validation rules in the 2026-07-28 spec. Added unit tests covering the mapper directly (header-mismatch to 400, method-not-found to 404, an unmapped code defaulting to 200). Closes modelcontextprotocol#1225 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ontextprotocol#1208) * fix: do not treat malformed JSON 200 as Accepted for requests Streamable HTTP mapped any JSON body that failed to parse as ServerJsonRpcMessage to Accepted, which is correct for notification POSTs. For a request the worker then waits on SSE forever, so call_tool hangs when the server returns 200 application/json with an empty or non-JSON-RPC body. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> * fix: include a short body preview in JSON-RPC parse errors Request POST parse failures now include a 256-byte prefix of the unusable body so empty, HTML, and `{}` are distinguishable from the serde diagnostic. HTTP reads bytes() then from_slice, matching Unix. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> --------- Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
…delcontextprotocol#1361) * fix(streamable-http-client): handle error responses to initialize expect_initialized only stopped draining an SSE response on a result, so an error event for initialize was skipped. serve() then reported ConnectionClosed, or hung if the server kept the stream open. perform_reinitialization also ignored an error reply and went on to send notifications/initialized and retry the expired request without a session. It now returns the error, which fails the pending requests. Fixes modelcontextprotocol#1360 * fix(streamable-http-client): keep ErrorData on re-initialization failure * chore: drop extra blank line in Cargo.toml
…otocol#1352) * feat(auth): add initial authorization scope selection * refactor(auth): share initial scope discovery helpers * test(auth): trim redundant initial scope assertions * fix(auth): address initial scope selection review * test(auth): cover reactive initial scope selection * refactor(auth): use one metadata discovery entry point
…s/read (modelcontextprotocol#1344) * feat(client): cancel an abandoned tools/call, prompts/get or resources/read A caller that drops `call_tool` (a `tokio::time::timeout`, a lost `select!` branch) abandoned the request locally while the server kept working on it. Send a best-effort `notifications/cancelled` for the request in flight when that happens. A drop guard, modelled on `PendingSubscriptionRequest`, is armed while the response is awaited and disarmed as soon as it arrives, so dropping the future while the client handler fulfils MRTR input requests cancels nothing. Closes modelcontextprotocol#1338 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(client): cancel any abandoned client request, per review Every request the client peer sends now goes through the drop guard, not only tools/call, prompts/get and resources/read. `initialize` is not sent through `Peer`, and the helper is client-only: a client must never cancel `initialize`, and from 2026-07-28 a server may send `notifications/cancelled` only to end a subscriptions/listen stream. The guard no longer unregisters a subscription that these requests never register, and the test helper fails when the call never reaches the point it is to be abandoned at instead of dropping the call anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(client): keep server/discover off cancel-on-drop; borrow the peer in the guard Per review: server/discover is part of the handshake like initialize, so it goes back to plain send_request. AbandonedRequestGuard borrows the peer instead of cloning it, and the helper's doc keeps only the constraint a future editor must not break. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…rotocol#1359) * fix(transport): accept nested x-mcp-header annotations * docs: document nested x-mcp-header and schemars inline * fix(transport): walk schema maps by value and pin path pop
…delcontextprotocol#1366) * fix(client): answer a server ping during the initialize handshake expect_response dropped a server ping while waiting for the initialize response, so the ping never got a reply. Answer it with an empty result during the initialize wait, the same way the server answers an early client ping. The server/discover wait still leaves it unanswered. Refs modelcontextprotocol#745 Fixes modelcontextprotocol#1362 * test(client): drop duplicated discover ping comment * refactor(client): replace answer_ping bool with PreInitPing enum * test(client): assert notifications/initialized follows the ping response * refactor(client): match on PreInitPing instead of comparing it The compiler now flags this spot if PreInitPing gains a variant, and the PartialEq and Eq derives are no longer needed.
…odelcontextprotocol#1346) * fix(model): read a request id written in float form as that integer A JSON-RPC id of 1.0 or 1e2 is an integer written the way some encoders write every number. It used to be rejected, so the request got no response. NumberOrString now accepts a float that an f64 represents exactly as an integer (up to 2^53) and reads it as that integer. modelcontextprotocol#1287 already makes a message with an id and a method parse only as a request, so the earlier change to JsonRpcNotification is dropped; the tests for an unusable id stay as regression tests. Refs modelcontextprotocol#1345 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(model): reject 2^53 as a float-form id; it cannot say which integer was sent 2^53 + 1 written in float form parses to the same f64 as 2^53, so accepting 2^53 silently turned 9007199254740993.0 into 9007199254740992. The exact-integer range is now strictly below 2^53. Added that id to request_with_unusable_id_is_rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…T with 400 (modelcontextprotocol#1377) In legacy session mode a POST without an Mcp-Session-Id header whose message is not initialize got 422 Unprocessable Entity. The Streamable HTTP transport's Session Management section says servers that require a session ID SHOULD respond to requests without one (other than initialization) with 400 Bad Request, which GET and DELETE already do. Return the same 400 "Bad Request: Session ID is required" response from both session-less POST branches, through one helper shared with GET and DELETE. Signed-off-by: Jens Holdgaard Pedersen <Jens@holdgaard.org> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…treamable HTTP client (modelcontextprotocol#1367) * fix(transport): follow same-origin 307/308 redirects in the default Streamable HTTP client The default reqwest client used `Policy::none()`, so any redirect from an MCP server failed with `UnexpectedServerResponse("HTTP 307: ...")`. It now follows 307 and 308 when the target has the same origin as the original request, up to 10 hops. Those keep the method and body, so the JSON-RPC message and the session, auth and custom headers arrive intact. 301, 302 and 303 are still returned to the caller, because following them turns the POST into a bodyless GET. Cross-origin redirects are still returned too, which keeps the header-leak guard. Refs modelcontextprotocol#936 Reported-by: Mo (@trickyfalcon) * docs(transport): describe redirect handling in default_http_client Document on `from_uri` and `from_config` which redirects the default client follows, that a 308 does not change the stored URI, and that a client passed to `with_client` keeps its own redirect policy. * docs(transport): move redirect rationale to same_origin_redirect_policy The rationale explains the policy, so it now sits on `same_origin_redirect_policy`, and the `default_http_client` doc points there instead of repeating the `from_uri` section. * test(transport): drop unused Default derive on CapturedRequest
…ontextprotocol#1349) * fix(streamable-http-server): look up tool schemas per request * test: cover stale tool schemas in header tests
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…eta-keep-tool-result
preston-footprint
force-pushed
the
fix/custom-result-meta-keep-tool-result
branch
from
October 20, 2026 23:44
3f2472e to
8046a0e
Compare
…tom results
ServerResult is an untagged enum that tries CallToolResult before
TaskAckResult and CustomResult. Dropping _meta from CallToolResult's
known-field check fixes custom results that carry _meta, but it breaks
the legacy {"_meta": ...} tool result covered by
server_result_should_accept_legacy_call_tool_result_when_only_meta_present
(modelcontextprotocol#1103).
Count _meta as a tool-result field only when it is the only field
present. A bare _meta still decodes as CallToolResult, while _meta
alongside resultType or other unknown fields falls through to
TaskAckResult or CustomResult.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
preston-footprint
force-pushed
the
fix/custom-result-meta-keep-tool-result
branch
from
October 20, 2026 23:45
8046a0e to
39c0838
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This keeps the fix for custom results that carry
_meta, but brings back the legacy_meta-only case thatserver_result_should_accept_legacy_call_tool_result_when_only_meta_present(modelcontextprotocol#1103) covers.ServerResulttriesCallToolResultbeforeCustomResult, and with_metadropped from the field check entirely, a bare{"_meta": …}no longer decodes as a tool result.Here
_metacounts as a tool-result field only when it's the only field present. So the legacy{"_meta": …}still decodes asCallToolResult, while_metanext toresultTypeor other unknown fields falls through, toTaskAckResult(atasks/updateortasks/cancelack carrying_meta) or toCustomResult. Your two tests stay as they are, and I added tests for custom results and task acks with_meta, tool results with_meta, and otherServerResultvariants keeping their variant when_metais added. I also merged currentmaininto the branch, as a merge rather than a rebase, so your history is untouched.For modelcontextprotocol#1197.
AI disclosure
This PR and its tests were written primarily by Claude Code, under my direction and review.
🤖 Generated with Claude Code