A stateless streamable-HTTP request is one-shot (no session, no resumption),
so if the client drops the response before the handler finishes, the request
is terminal and should be cancelled. Previously the handler kept running with
its `RequestContext::ct` never firing, so long-running or destructive tools
could not observe a client disconnect.
Give each stateless request its own cancellation token via
`serve_directly_with_ct` and cancel it when the client disconnects. This covers
both stateless paths: `serve_negotiated_request_directly` (per-request version
negotiation) and the non-negotiated path. In each:
- A disconnect while the handler is still producing its first message cancels
it. The guard is disarmed once the handler emits anything, so a normal
response is never cancelled.
- The SSE response stream is wrapped in a guard that cancels the handler if the
stream is dropped before it ends naturally.
- When a negotiated request replies directly (JSON mode, or a non-OK status),
the receiver is dropped, so a still-running handler is cancelled rather than
left emitting into a closed channel. Without this its terminal send fails
before adding the termination permit and the serve loop parks forever.
Stateful (resumable) mode is intentionally left unchanged: there a disconnect
may be recovered via `Last-Event-ID`, so cancelling on disconnect would break
resumption.
Adds a regression test covering both stateless sub-modes (SSE and JSON).
* fix(auth): validate discovered metadata issuer
Validate authorization server metadata issuers against the issuer implied by
standard discovery URLs before trusting advertised endpoints.
fixes#983
* test(auth): add required issuer to authorization-server mock metadata
---------
Co-authored-by: Alex Hancock <alexhancock@block.xyz>
* feat!: add MRTR behavior support
* feat: harden SEP-2322 MRTR support
* ci: diff public API only on features common to base and head
cargo public-api builds both revisions with the same feature set, so a
feature introduced (or removed) by a PR broke the build of the other
revision. Restrict the all-features diff to features present in both the
base and head revisions.
* fix: address PR review - schema_for_output no longer validates or returns Result
- Add strip_output() that strips title/description without validating type (Dale #1)
- Change schema_for_output to return Arc<JsonObject> instead of Result (Dale #2)
- Cache only Arc<JsonObject> success values, not Result (Dale #3)
- Remove dead unwrap_or_else panic paths in with_output_schema, ToolBase, and macros
- Tighten test assertions from contains to assert_eq on type field (Dale #4)
- Update test_schema_for_output_rejects_primitive to accept_primitive (SEP-2106)
Co-authored-by: Orca <help@stably.ai>
* test(rmcp): add non-object output schema tests for SEP-2106
Add tests verifying schema_for_output accepts non-object types:
- test_tool_builder_methods: primitive (i32), array (Vec<String>), option
- test_structured_output: tool returning Json<Vec<T>> and Json<i32>
- test_json_schema_detection: Json<Vec<T>>, Result<Json<Vec<T>>,E>, Json<String>
- tool_traits: ToolBase::output_schema with Vec<AddOutput> output type
* test(rmcp): add missing edge case tests from code review
Add tests identified during code review:
- description stripping for primitive types
- composition types (Option<String> with anyOf/oneOf/null)
- cache correctness (Arc::ptr_eq for repeated calls)
- schema_for_input rejecting array types (not just primitives)
- schema_for_output accepting unit type ()
* feat!: mark schema_for_output return-type change as breaking
This introduces SEP-2106: schema_for_output no longer validates or
returns Result. The public signature changed, so bump major.
* fix: address Dale's PR review - direct schema.get assertions, remove ArrayTool
- Replace loose schema_str.contains(...) assertions with direct
schema.get("type") equality checks in test_tool_builder_methods.rs
and test_structured_output.rs
- Remove redundant ArrayTool fixture and its round-trip
serde_json::from_str test from tool_traits.rs since schema is
already Arc<JsonObject>
- Drop dead schema_str variable in test_structured_output.rs
---------
Co-authored-by: Brandon Bennett <brandonbennett@macbookair.myfiosgateway.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Brandon Bennett <brandonbennett@Pursuits-Air.lan>
Re-applies #919 (reverted by #932): ToolResultContent.structured_content
becomes Option<Value> so non-object structured content is accepted,
matching CallToolResult and SEP-2106.
BREAKING CHANGE: ToolResultContent.structured_content changes from
Option<JsonObject> to Option<Value>.
* fix: interpret task ttl as milliseconds
* fix: use text/plain for default text mime type
* fix: include resource param in token refresh
* test: align conformance prompt args with runner
* ci: run server conformance suite on PRs
* ci: build client bin and gate pending scenarios
`receive()` reads incoming lines with `read_until`, which is not
cancellation-safe, and it is polled inside the service loop's `select!`.
When another branch (e.g. an outgoing response) becomes ready while a
request line is only partially read, the `receive()` future is dropped
and the next call's `self.line_buf.clear()` discards the partial bytes,
so that incoming request is silently lost and never gets a response.
Under many concurrent large responses this intermittently drops requests.
Keep the partially-read bytes in `line_buf` across calls and clear it
only after a whole line has been consumed, so a cancelled read resumes
the same line instead of dropping it. The buffer is cleared (retaining
capacity) rather than reallocated per message.
Adds a regression test that fires 200 concurrent 64 KiB tool responses
over real stdio pipes and asserts no response id goes missing.
This also covers the child-process client transport, which reuses
`AsyncRwTransport`.
Expose typed accessors for the per-request protocol version, client info,
client capabilities, and log level entries carried in _meta without making
those fields required for older peers.
Fixes#869
The spec's prompt-message ContentBlock union is
`text | image | audio | resource_link | resource`, but
PromptMessageContent omitted `Audio`. Because the enum is
`#[serde(tag = "type")]` with no catch-all, a spec-conformant
`{"type":"audio",...}` content block failed to deserialize with
"unknown variant `audio`", breaking prompts/get for any server that
returns audio prompt content (the audio analogue of #842 / #843).
The supporting AudioContent type already existed, and Audio was
already a variant of the general RawContent enum (tool results,
sampling) -- only PromptMessageContent lacked it.
Add the flattened Audio variant (mirroring Image), a
PromptMessage::new_audio constructor (mirroring new_image), and
serialization + deserialization regression tests.
Fixes#864.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(elicitation): preserve enumNames through ElicitationSchema serde round-trip
UntitledSingleSelectEnumSchema lacked deny_unknown_fields, so a legacy
enum payload containing enumNames was silently matched by that variant
(ignoring the field) rather than falling through to LegacyEnumSchema.
The enumNames array was lost on re-serialization.
Add deny_unknown_fields to UntitledSingleSelectEnumSchema so that any
unknown field (including enumNames) causes serde to try the next
untagged variant, reaching LegacyEnumSchema correctly.
Also add skip_serializing_if = "Option::is_none" to
LegacyEnumSchema::enum_names so that an untitled legacy enum without
enumNames does not serialize "enumNames": null.
Fixes#903
* test(elicitation): regenerate server schema snapshot for deny_unknown_fields
deny_unknown_fields on UntitledSingleSelectEnumSchema makes schemars
emit additionalProperties: false for that definition. Regenerate the
golden schema fixtures to match (UPDATE_SCHEMA=1 cargo test -p rmcp
--test test_message_schema --all-features).
* feat: implement SEP-2164 resource not found errors
* test: update protocol version utility expectations
* feat: gate not-found code at server boundary
---------
Co-authored-by: Michael Neale <michael.neale@gmail.com>
SEP-2577 deprecates the Roots, Sampling, and Logging features. The
deprecation is advisory: the features stay fully functional and there is
no wire-level change. Mark the corresponding Rust APIs as deprecated so
downstream users get compiler warnings and migration guidance.
- Forward attributes through the service `method!` macros and deprecate
`Peer::create_message`, `Peer::list_roots`, `Peer::set_level`, and
`Peer::notify_logging_message`.
- Forward per-field attributes through the capability `builder!` macro and
deprecate the generated `enable_roots`, `enable_sampling`, and
`enable_logging` builders, plus the hand-written
`enable_roots_list_changed`, `enable_sampling_tools`, and
`enable_sampling_context`.
- Document the deprecation on the capability types and fields, and in the
README feature sections.
- Allow `deprecated` at the crate's own call sites so the build stays
warning-clean, and refresh the message schema snapshots.
The Resource variant of PromptMessageContent was missing #[serde(flatten)],
causing the embedded resource content block to serialize as a double-nested
shape `{ "type": "resource", "resource": { "resource": {...} } }` instead of
the spec-compliant flat shape `{ "type": "resource", "resource": {uri, mimeType, text} }`.
This caused Zod-based MCP clients (e.g. Claude Code) to reject prompts/get
responses containing embedded resource messages with InvalidUnion errors.
The Image and ResourceLink variants already use #[serde(flatten)] correctly;
only Resource was missing it.
Fix: add #[serde(flatten)] so EmbeddedResource (=Annotated<RawEmbeddedResource>)
fields _meta / annotations / resource are flattened to the content-block level,
matching the MCP spec for prompts embedded resources.
Regression test: test_prompt_message_resource_serialization_is_flat verifies
content.resource.uri is reachable and content.resource.resource is absent.
Schema snapshots regenerated via UPDATE_SCHEMA=1.
* fix(transport): downgrade idle timeout log from error to debug
Idle keep-alive timeout is normal zombie-session cleanup, not a transport failure.
Route it through a dedicated WorkerQuitReason::IdleTimeout variant.
Log it at debug level instead of treating it as a fatal error.
Remove the unused LocalSessionWorkerError::KeepAliveTimeout variant.
Closes#817
* fix(session): tolerate dead worker in close_session
Swallow SessionServiceTerminated in close_session when the worker has already exited.
This prevents a spurious ERROR log during the post-exit cleanup path in
spawn_session_worker.
* fix(transport): address PR review feedback
- deprecate KeepAliveTimeout
- harden tests
* feat(router): support runtime disabling of tools
Add methods to disable/enable tools at runtime.
Disabled tools are hidden from listing, lookup,
and execution, including in composed routers.
Closes#477
* fix(router): simplify disable tool api
* feat(router): auto-send tools/list_changed on disable/enable
* refactor(router): simplify disable_route and notifier call