fixes#1001.
Restores explicit dispatch arms for the ten client scenarios orphaned by #991:
standard auth flow:
- `auth/iss-*`
- `auth/offline-access-*`
- `auth/metadata-issuer-mismatch`
- `auth/authorization-server-migration`
plain connect/list client:
`json-schema-ref-no-deref`
`run_auth_client` now negotiates via `ClientLifecycleMode::Discover` —
`auth/authorization-server-migration` tracked in #879
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>
SEP-2350 [1] clarifies that scope accumulation is a client-side
responsibility: during re-authorization the client requests the union of
its previously requested scopes and the newly challenged scopes, because
servers report only the scopes needed for the current operation in
403 / insufficient_scope challenges (RFC 6750 §3.1), not the union of
everything granted so far.
select_base_scopes returned a single source, so a 403 challenge replaced
the previously requested scopes instead of widening them, dropping prior
permissions across step-up rounds. I make it union the previously
requested scopes, the WWW-Authenticate challenge, and the protected
resource metadata scopes (RFC 9728), treating each server-reported set as
an operational requirement for the current operation rather than an
exclusive directive; AS metadata and caller defaults only seed the
request when nothing has been requested or challenged yet.
exchange_code_for_token treats an explicit scope list as authoritative,
so a server may still narrow the grant. When the server omits scope it
has granted exactly what the client requested (RFC 6749 §5.1), so the
grant has to fall back to the scopes requested in this round, not the
previously granted set; otherwise a step-up that the server confirms by
omitting scope would silently drop the just-added permission and the
client would loop on the same 403. The widened request was not persisted
anywhere the exchange could read it, so I record it on
StoredAuthorizationState per authorization (defaulting empty for states
stored before this field existed) and resolve the grant from there. This
addresses review feedback [2] that the earlier fallback returned the
previous grant rather than the request. Deduplication preserves
first-seen order for stable, testable output. Tests cover multi-round
accumulation, dedup, resource-metadata unioning, and grant resolution
when the response omits scope. Implements [3].
[1]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/draft/basic/authorization.mdx#L682
[2]: https://github.com/modelcontextprotocol/rust-sdk/pull/888#pullrequestreview-4454854582
[3]: https://github.com/modelcontextprotocol/rust-sdk/issues/877
Signed-off-by: Stefano Amorelli <stefano@amorelli.tech>
* 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(auth): support oauth metadata fallbacks
* ci: run client conformance scenarios
* fix: pass full client conformance suite
* ci: run full client conformance suite
* fix: update SSE stream constructor
* 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`.
Per RFC 6749 section 6 and OAuth 2.1: when the server does not issue a new
refresh_token on a refresh response, the client MUST keep the existing one.
AuthorizationManager::refresh_token() was persisting the response verbatim,
dropping the previous refresh_token and forcing full re-authorization on
the next refresh.
Match the fix from python-sdk#2270: preserve the existing refresh_token
when the response omits it, replace when the server rotates.
Fixes#921
Signed-off-by: SarthakB11 <sarthak.bhardwaj21b@iiitg.ac.in>