Compare commits

...
Author SHA1 Message Date
dependabot[bot]
dbd2382755
chore(deps): update rstest requirement from 0.26.1 to 0.27.0 (#1276)
Some checks failed
CI / Lint Commit Messages (push) Has been cancelled
CI / Code Formatting (push) Has been cancelled
CI / Lint with Clippy (push) Has been cancelled
CI / SemVer Check (push) Has been cancelled
CI / Public API Check (push) Has been cancelled
CI / spell check with typos (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
CI / Run Tests (no local feature) (push) Has been cancelled
CI / Code Coverage (push) Has been cancelled
CI / Example test (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Generate Documentation (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
CodeQL / Analyze-1 (push) Has been cancelled
CodeQL / Analyze-2 (push) Has been cancelled
CodeQL / Analyze-3 (push) Has been cancelled
Conformance / server (push) Has been cancelled
Conformance / client (push) Has been cancelled
Release-plz / Release-plz release (push) Has been cancelled
Release-plz / Release-plz PR (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / Release crates (push) Has been cancelled
Updates the requirements on [rstest](https://github.com/la10736/rstest) to permit the latest version.
- [Release notes](https://github.com/la10736/rstest/releases)
- [Changelog](https://github.com/la10736/rstest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/la10736/rstest/compare/v0.26.1...v0.27.0)

---
updated-dependencies:
- dependency-name: rstest
  dependency-version: 0.27.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>
2026-09-18 13:47:35 -04:00
dependabot[bot]
9f7ace357a
chore(deps): bump the codeql-action group with 4 updates (#1277)
Bumps the codeql-action group with 4 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/autobuild](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.37.9 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](cdf488f595...b96794f015)

Updates `github/codeql-action/autobuild` from 4.37.9 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](cdf488f595...b96794f015)

Updates `github/codeql-action/analyze` from 4.37.9 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](cdf488f595...b96794f015)

Updates `github/codeql-action/upload-sarif` from 4.37.3 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](e4fba868fa...b96794f015)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: codeql-action
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: codeql-action
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: codeql-action
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: codeql-action
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-18 13:47:13 -04:00
dependabot[bot]
011f281ced
chore(deps): bump astral-sh/setup-uv from 10.0.1 to 10.1.0 (#1278)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 10.0.1 to 10.1.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](20cfd1bf94...bec219d24c)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.1.0
  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>
2026-09-18 13:46:56 -04:00
dependabot[bot]
0cfbe7f453
chore(deps): bump taiki-e/install-action from 2.87.6 to 2.87.12 (#1279)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.87.6 to 2.87.12.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](7b8d4719ee...3f74d7c16a)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.87.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-18 13:46:37 -04:00
github-actions[bot]
fd7811fdaa
chore: release v3.4.0 (#1258)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-15 11:37:39 -04:00
Dale Seo
0550e13c1b
fix(rmcp): use ServerConfig in cancellation test (#1273) 2026-09-15 11:18:33 -04:00
Auro
b037c0fa88
fix(rmcp): route peer cancellation by lifecycle (#1262)
* fix(rmcp): route peer cancellation by lifecycle

Cancel inbound legacy requests by their exact request ID. Preserve modern
subscription waiter cleanup using the peer lifecycle and outbound request kind.

Suppress cancelled handler results and errors during EOF draining without
dropping uncancelled responses during graceful shutdown.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* test(rmcp): parameterize reverse cancellation cases

Use rstest to report each startup and request ID combination independently.
Keep the existing per-case timeout and cancellation assertions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(rmcp): use canonical initialize result in cancellation test

Replace the stale ServerInfo reference after merging the upstream alias migration.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Hunter Sadler <aurokin@github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-14 16:22:57 -04:00
Dale Seo
d481cb0eec
fix: run first pre-init request in service loop (#1263) 2026-09-14 15:45:05 -04:00
Dale Seo
47fcdf4b57
fix(auth): let url origin decide prm discovery (#1264) 2026-09-14 15:30:52 -04:00
Dale Seo
165ff92df1
feat(model): add ServerConfig and ClientConfig (#1266) 2026-09-14 15:30:41 -04:00
Jack Amadeo
fbfc5cc96e
ci: add OpenSSF Scorecard analysis (#1214) 2026-09-14 12:01:32 -04:00
Teddy Tennant
3075dc9152
fix(model): deprecate ServerInfo and ClientInfo aliases (#1156)
* fix(model): deprecate ServerInfo and ClientInfo aliases

Those names collide with the protocol's serverInfo/clientInfo fields,
which are only Implementation identity. Prefer InitializeResult and
InitializeRequestParams. Handler trait signatures, the tool_handler
macro output, tests, examples and docs now use the canonical types; the
aliases remain as deprecated shims for the rest of 3.x.

Fixes #1082

* fix(model): keep public signatures on the deprecated aliases

The public API check compares rendered signatures, and rustdoc records a
type alias by the name it was written with. Respelling
`ServerHandler::get_info`, `ClientHandler::get_info`,
`DiscoverResult::from_server_info` and `impl ClientHandler for ClientInfo`
onto `InitializeResult`/`InitializeRequestParams` therefore reads as
changed public items even though the aliases resolve to those exact types
and nothing downstream breaks.

Put those four signatures back on the aliases so the published API is
byte-identical, and note why at the alias definitions. The deprecation
still steers new code, and the macro output, tests, examples and docs
keep using the canonical names.

* fix(model): migrate leftover ClientInfo test sites

Two tests landed on main after the alias deprecation and still named
ClientInfo. Clippy -D warnings and the no-local feature test job both
failed on them.
2026-09-12 12:11:30 -04:00
easyinplay
a5d1169e20
fix(auth): ignore non-metadata JSON when probing for protected resource metadata (#1204)
* fix(auth): ignore non-metadata JSON when probing for protected resource metadata

The base URL is probed first when looking for RFC 9728 protected resource
metadata, and any 200 there is taken to mean "this URL is the metadata
document". Every field of ResourceServerMetadata is optional, so an unrelated
JSON object deserializes into an all-None value and validation then fails hard
with "Protected resource metadata missing required resource field". The error
propagates out of resolve_metadata, so the .well-known fallbacks never run.

Servers that answer GET / with a JSON health payload hit this even when they
publish valid metadata at both well-known locations.

Treat a parsed document that carries none of resource, authorization_server or
authorization_servers as a soft failure, the same way this function already
treats a non-200 status and a body that is not JSON. A document carrying any of
those fields still goes through validate_resource_metadata_resource unchanged.

This is the JSON-object half of #810, which made a non-JSON body at the base URL
a soft failure for the same reason.

* fix(auth): stop a 200 from the resource ending metadata discovery

probe_resource_metadata_url treats any 200 as "this url is the metadata
document". That holds for the .well-known candidates it is called with in the
loop, and not for the first call, which is passed the resource itself. RFC 9728
publishes the document at the well-known URI and advertises it through the
resource_metadata parameter of a WWW-Authenticate challenge, so a 200 from the
resource is the resource answering and nothing more.

Because that first probe returned Some(base_url), discovery ended before the
.well-known candidates were tried, and a valid document published there was
never reached. Rejecting the body later could not recover it: by then the
candidates had already been skipped.

Split the first probe into probe_resource_endpoint_for_challenge, which reads
only the 401 branch. The .well-known probe keeps its behaviour.

The check added in the previous commit stays. A .well-known url can also answer
200 with something that is not a metadata document, and every field of
ResourceServerMetadata being optional makes that deserialize into an all-None
value that then fails validation fatally.

resolve_metadata_reaches_the_well_known_document_past_a_non_metadata_base_url
asserts the well-known url is actually requested; without this change it fails
with the base url requested twice and the protected-resource candidate never
probed. resolve_metadata_ignores_a_well_known_url_that_is_not_a_metadata_document
covers the remaining guard; without it the run ends in the original
"Protected resource metadata missing required resource field".

* fix(auth): keep trying the well-known candidates past a non-metadata document

discover_resource_metadata_url returned the first .well-known candidate that
answered 200 and left the loop; the document itself was fetched afterwards,
outside the loop. A candidate answering 200 with something that is not metadata
is only recognised at that point, by which time the remaining candidates have
been skipped and discovery gives up with nothing.

The probe already had the body in hand and threw it away, so the winning
candidate was requested twice. Read the body where the candidate is probed
instead: a candidate that is not the document costs one request and the loop
moves on to the next one, and the candidate that is the document is requested
once.

The second half of the old function, which walks the authorization servers a
document names, is unchanged; it now takes the document as an argument so the
challenge path keeps sharing it.

resolve_metadata_tries_the_next_well_known_candidate_past_a_non_metadata_document
covers the loop; without this change the second candidate is never requested and
the run ends on the issuer of an authorization server it was never meant to
reach.

* fix(auth): report an advertised url that is not the metadata document

A WWW-Authenticate challenge naming a resource_metadata url is the server
saying the document is there. The previous commits made a document carrying
neither resource nor an authorization server reference a soft failure
everywhere, which on that path drops out of resolve_metadata_from_challenge,
continues with authorization server discovery and settles on the legacy
endpoints, silently losing the RFC 8707 resource binding the document was
supposed to carry. Before those commits it surfaced as
"Protected resource metadata missing required resource field".

Where the url came from decides what that document means. A .well-known
candidate is a guess, so its answer only rules out that candidate and the loop
goes on. An advertised url has no better alternative to move on to, so say what
the server got wrong instead of degrading quietly.

Reading a non-200 or a body that is not JSON stays a soft failure on both
paths.

resolve_metadata_from_challenge_reports_an_advertised_url_without_metadata
covers the challenge path; without this change it resolves to
LegacyEndpointFallback.

* fix(auth): let a candidate fail validation without ending discovery

parse_resource_metadata decides what a body that is not the metadata document
means from where the url came from, and it decides it on the presence of
resource, authorization_server and authorization_servers alone. What comes
after is fatal whatever the origin: validate_resource_metadata_resource
rejects a missing resource, a resource that is not a URL, one carrying a
fragment, and one that does not match the base url, and each of those
propagates out of resolve_metadata.

A .well-known candidate handled by a catch-all route reaches it. The
{"error":"not_found","resource":"/.well-known/oauth-protected-resource"} such
a route answers with carries resource, so the presence check lets it through,
and the relative path then fails to parse as a URL. That is the shape this
branch opened on, one field further along: the remaining candidates are
skipped, authorization server discovery is skipped, and the run ends on an
error instead of the legacy endpoints.

Read the body and validate it in the same place, now read_resource_metadata,
so the origin governing the first decision governs the second one as well. A
guess that fails validation rules out that candidate and the loop goes on; an
advertised url still reports what the server got wrong.
authorization_metadata_from_resource_metadata takes a document that has
already been accepted.

resolve_metadata_tries_the_next_well_known_candidate_past_an_unusable_document
covers it; without this change it fails with "Protected resource metadata
resource field is not a valid URL".

* fix(auth): keep probing the candidates when an advertised url answers nothing

An advertised url reading a non-200 or a body that is not JSON is a soft
failure: the server said the document is at that url and nothing is being
served there, which rules out the url and not the document. The .well-known
candidates are derived from the base url rather than from that pointer, so
they are still worth probing, but the pointer was read ahead of the loop and
returned out of discover_resource_metadata either way. A challenge naming
https://host/.well-known/oauth-protected-resource that 404s takes the run
straight to authorization server discovery, while the document sits unread on
https://host/mcp/.well-known/oauth-protected-resource.

Fall through to the loop instead. Carrying on past a candidate's own 401
already let one url be requested twice, because the pointer that challenge
names can be a later candidate of the same run, and it is requested again when
the loop reaches it; not returning on the first pointer adds the same overlap.
Keep the urls this run has requested and skip the ones already read.

resolve_metadata_probes_the_candidates_past_an_advertised_url_that_is_not_served
covers the fall-through; without it the run reaches the authorization server
metadata of a server it was never pointed at.
resolve_metadata_requests_an_advertised_url_that_is_also_a_candidate_once
covers the repeat.

* fix(auth): walk an authorization server named by both fields once

A protected resource metadata document can name its authorization server in
authorization_servers, and servers written against the earlier draft also fill
the singular authorization_server. Filling both with the same value is common,
and the two are concatenated into the candidate list unfiltered, so every
well-known form of that one server's discovery url is requested twice before
the walk gives up on it.

well_known_paths already keeps its candidates distinct. Do the same here,
comparing the trimmed value the loop goes on to use.

resolve_metadata_requests_an_authorization_server_named_twice_once covers it;
without this change the discovery url is requested twice.

---------

Co-authored-by: easyinplay <easyinplay@users.noreply.github.com>
2026-09-12 11:21:03 -04:00
Sebastien Tardif
4fa84a5130
fix: do not treat malformed JSON 200 as Accepted for requests (#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>
2026-09-12 10:42:12 -04:00
voidstack
b86995aebd
fix(streamable-http-server): map handler-generated HeaderMismatch to HTTP 400 (#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 #1225

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 20:54:40 -04:00
Luca
e27c5d1c24
fix(http): enforce Origin validation semantics (#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>
2026-09-10 21:38:27 -04:00
github-actions[bot]
3e636cab26
chore: release v3.3.0 (#1252)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-10 11:23:32 -04:00
Jack Amadeo
b2c90b1edc
ci: pin commitlint dependencies (#1217) 2026-09-10 10:05:55 -04:00
ump45nose
46db531df9
fix(sse): saturate exponential reconnect backoff to avoid overflow panic (#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 #1198, while max_delay stays opt-in
for callers that want a bounded reconnect delay.
2026-09-09 21:24:29 -04:00
dependabot[bot]
744b9f904c
chore(deps): bump crate-ci/typos (#1249)
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](1a51d4b5a0...d43b6c087a)

---
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>
2026-09-08 20:24:44 -04:00
dependabot[bot]
e8a0d7099a
chore(deps): bump taiki-e/install-action from 2.86.7 to 2.87.6 (#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](b6ff580856...7b8d4719ee)

---
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>
2026-09-08 20:24:09 -04:00
Dale Seo
6fa40536de
fix: resolve clippy warnings across workspace (#1195)
* fix: resolve clippy warnings across workspace

* ci: also run clippy on rmcp with all features except local

--all-features enables the local feature, which cfg-gates out
tower.rs and most of the test suite, so the existing clippy step
never actually lints them.
2026-09-08 19:05:29 -04:00
Dale Seo
f9a68bfa5c
feat: add ServerHandler::negotiate_initialize (#1247) 2026-09-08 19:05:04 -04:00
Dale Seo
7d14f3a8a2
feat(macros): reject empty tool_router (#1233)
Closes #1174

Claude-Session: https://claude.ai/code/session_017tyTJfwGpE8fidbZSRi6uU
2026-09-08 19:04:40 -04:00
Dale Seo
04c2f3fac9
fix(auth): unify refresh checks and error handling (#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
2026-09-08 19:04:15 -04:00
Dale Seo
add9cbeab5
ci: bump codeql-action to 4.37.9 and group updates (#1240) 2026-09-08 10:38:30 -04:00
Nick Steele
302319861a
feat(auth): add enterprise refresh-token and ID-JAG exchanges (#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/rust-sdk#531.

* test(conformance): cover EMA refresh-token exchange
2026-09-04 15:04:08 -04:00
dependabot[bot]
3b5ca4dd3f
chore(deps): bump astral-sh/setup-uv from 7.6.0 to 10.0.1 (#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](37802adc94...20cfd1bf94)

---
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>
2026-09-02 07:01:50 +09:00
dependabot[bot]
42cf8d9fa3
chore(deps): update process-wrap requirement from 9.0 to 10.0 (#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](https://github.com/watchexec/process-wrap/compare/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>
2026-09-01 23:13:12 +09:00
github-actions[bot]
51ccb42993
chore: release v3.2.0 (#1227)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-31 19:08:18 -04:00
Dale Seo
ad9832ec21
fix: keep initialize on legacy protocol versions (#1228)
Claude-Session: https://claude.ai/code/session_01EPcBfiUWJJueJn5cve1sjp
2026-09-01 07:38:57 +09:00
Matthew Zeng
4e74499783
feat(auth): coordinate OAuth refreshes through credential stores (#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
2026-08-31 16:59:37 -04:00
dependabot[bot]
3a2ebbc920
chore(deps): bump taiki-e/install-action from 2.85.13 to 2.86.7 (#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](82cd3e7658...b6ff580856)

---
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>
2026-08-30 08:26:41 +09:00
Dale Seo
1afde5b4fc
docs: use auto lifecycle in HTTP example (#1187) 2026-08-29 07:42:20 +09:00
Dale Seo
ec041b7430
ci: reduce dependabot update noise (#1184) 2026-08-29 07:42:11 +09:00
camillelawrence
3ef34e64e7
feat: add request-state key rotation (#1128)
* 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
2026-08-28 22:19:23 +09:00
Jack Amadeo
12db02833f
ci: pin release-plz fork revision (#1221) 2026-08-27 12:33:32 -04:00
Jack Amadeo
c8bb1c6f7d
ci: remove coverage job write permission (#1219) 2026-08-27 19:02:06 +09:00
Jack Amadeo
3501f3e646
ci: default workflow tokens to read-only contents (#1218) 2026-08-27 19:01:29 +09:00
Jack Amadeo
103bf239b3
ci: scope release token permissions to jobs (#1220) 2026-08-27 18:52:18 +09:00
Jack Amadeo
78cf9f1cb6
ci: pin GitHub Actions to commit SHAs (#1216) 2026-08-25 14:50:29 -04:00
King Star
02decfb682
fix(transport): fall back after sessionless HTTP discover rejections (#1211)
* fix(transport): fall back after HTTP discover rejection

* fix(transport): limit legacy fallback to sessionless probes

* style: apply nightly rustfmt import ordering
2026-08-25 12:16:49 -04:00
Nick Cooper
697c497724
fix: allow concurrent streamable http requests (#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
2026-08-24 22:43:35 +09:00
github-actions[bot]
4a738b9dd9
chore: release v3.1.4 (#1185)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-20 12:40:17 +09:00
Recoordinate
6f8dcde04f
fix(rmcp): preserve elicitation requestedSchema $schema dialect (#1176)
* fix(rmcp): preserve elicitation requestedSchema $schema dialect

ElicitationSchema round-trips through ElicitationSchemaWire, which had no
field for the top-level `$schema` keyword. A dialect declared on an incoming
requestedSchema (allowed since protocol revision 2025-11-25) was therefore
accepted but silently dropped on re-serialization.

Add a `schema` field to both the public struct and the wire bridge, thread it
through the two `From` conversions, and expose a `with_schema` setter. The
field is `skip_serializing_if = "Option::is_none"`, so schemas without a
dialect serialize unchanged.

Refs #1168

* test(rmcp): regenerate server message schema snapshot for elicitation $schema

The $schema dialect field added to ElicitationSchema changes the
schemars-generated ServerJsonRpcMessage schema. Regenerate the golden
snapshot so test_server_json_rpc_message_schema passes.

* fix(rmcp): generate elicitation $schema as optional string in schemars

Use schemars(with = "String") on ElicitationSchema::$schema so
schemars generates "type": "string" instead of ["string", "null"].
2026-08-19 00:02:45 +09:00
Dale Seo
d5c46b3fe0
fix: harden signing key handling (#1166)
* fix: harden signing key handling

* fix: validate static keys at compile time
2026-08-18 08:56:58 -04:00
Dale Seo
38428f66bc
fix: report pre-init metadata errors (#1160) 2026-08-18 09:34:54 +09:00
Dale Seo
018ab24fac
fix: handle declined MRTR elicitation (#1151) 2026-08-18 09:34:35 +09:00
dependabot[bot]
f9b39f3c13
chore(deps): bump taiki-e/install-action from 2.85.11 to 2.85.13 (#1183)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.85.11 to 2.85.13.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.85.11...v2.85.13)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.85.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 09:33:52 +09:00
dependabot[bot]
0f164cd67e
chore(deps): bump github/codeql-action from 4.37.6 to 4.37.7 (#1182)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.37.6 to 4.37.7.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v4.37.6...v4.37.7)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.37.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 09:33:32 +09:00
github-actions[bot]
fd14830a19
chore: release v3.1.3 (#1159)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-18 07:43:05 +09:00
Alex Hancock
0282172f33
docs: make ROADMAP status version-agnostic (#1181)
The roadmap pinned a specific patch release as "the current stable
release", so it drifted out of date as soon as the next release shipped.
Reference the releases page instead of a hardcoded version, and drop the
dated status line.

Historical entries (v3.0.0 ship date, the v1.4.0 fix reference) keep
their versions since they record facts that do not change.
2026-08-17 15:01:43 -04:00
stevenlee-oai
248cbca688
fix(auth): ignore query parameters when matching resources (#1177) 2026-08-17 14:05:21 -04:00
dependabot[bot]
f713ebd1a6
chore(deps): bump taiki-e/install-action from 2.85.10 to 2.85.11 (#1169)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.85.10 to 2.85.11.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.85.10...v2.85.11)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.85.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-13 21:33:23 +09:00
stevenlee-oai
a50a73fda2
fix(auth): retain state until issuer validation (#1167) 2026-08-12 19:27:43 +09:00
nightcityblade
d81bfa8854
fix(model): preserve elicitation property order metadata (#1150)
Co-authored-by: nightcityblade <nightcityblade@gmail.com>
2026-08-11 19:56:17 +09:00
dependabot[bot]
3f5a3f6a28
chore(deps): bump taiki-e/install-action from 2.85.8 to 2.85.10 (#1163)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.85.8 to 2.85.10.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.85.8...v2.85.10)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.85.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-11 19:21:19 +09:00
Dale Seo
a0e103c970
fix: time out auto discovery probe (#1149) 2026-08-11 19:20:13 +09:00
Recoordinate
b5cf34eee7
Fix typo in to_authorized_http_client doc comment (#1158) 2026-08-09 14:23:10 -04:00
ip2a
22be2cc5ad
fix(client): classify discover outcome at source, not at the error type (#1133)
`ClientLifecycleMode::Auto` only fell back from `server/discover` on
`-32601`, so legacy servers that reject the probe with other codes
(`-32600`, `-32602`, implementation-defined errors) failed to connect
even though `initialize` would have succeeded.

The previous attempt (indicates_legacy_server) classified the failure
after the fact by reverse-engineering the error type. This rewrite moves
the classification into `discover_startup` itself, where the full context
(request id, response correlation, transport state) is still available.

`discover_startup` now returns `DiscoverOutcome`: `Modern` on success,
`Legacy(error)` when the probe received a complete, correlated JSON-RPC
error whose code is not a modern-era rejection. Every other failure
becomes `Err`, so `Auto` simply matches the outcome — no methods on
`ClientInitializeError`, no downcast, no transport-specific types
leaking into the generic lifecycle layer.

Additional fixes that fall out naturally:

- Response correlation is now checked in `expect_response` for both
  success and error branches. Previously error responses skipped id
  correlation entirely. A new `UncorrelatedErrorResponse` variant
  surfaces responses that cannot be tied to the request.

- When both discover and the legacy fallback fail, a
  `LegacyFallbackFailed` compound error preserves both phases instead
  of discarding the discover error.

Fixes #1040.
2026-08-09 14:07:50 -04:00
github-actions[bot]
02c62aef2e
chore: release v3.1.2 (#1148)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-07 16:32:03 -04:00
Jeremy Schoemaker
c345078867
fix(auth): map 401/403 challenges on the SSE GET stream (#1152)
`post_message_with_max_sse_event_size` turns a 401 or 403 carrying a
`WWW-Authenticate` header into `StreamableHttpError::AuthRequired` /
`InsufficientScope`. `get_stream_with_max_sse_event_size`, in the same file,
handles only 405 and then falls through to `error_for_status()?`, so the same
challenge becomes an opaque `StreamableHttpError::Client`.

That matters because `AuthClient::get_stream` routes through
`call_reacting_to_challenges`, whose whole purpose is to catch `AuthRequired`,
run `try_refresh_or_reauth()`, and retry once. It never sees that variant from
this path, so an expired token on the standalone SSE stream is never refreshed —
the stream just fails, while the identical expiry on `post_message` recovers
silently.

Copies the two blocks verbatim from `post_message` in the same file.
`unix_socket.rs` already does the same thing in its own `get_stream`.

Adds four tests against an axum mock server: a 401 with a challenge maps to
`AuthRequired`, a 403 maps to `InsufficientScope` with the scope extracted, a
401 *without* a challenge is still not `AuthRequired`, and 405 keeps reporting
`ServerDoesNotSupportSse`. The first two fail before this change with
`Err(Client(reqwest::Error { kind: Status(401, None) }))`; the last two pass
either way and exist to catch an over-broad fix.
2026-08-07 15:39:55 -04:00
dependabot[bot]
f8e63824d3
chore(deps): bump taiki-e/install-action from 2.85.7 to 2.85.8 (#1153)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.85.7 to 2.85.8.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.85.7...v2.85.8)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.85.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 15:31:54 -04:00
dependabot[bot]
8fb3e046a1
chore(deps): bump github/codeql-action from 4.37.4 to 4.37.6 (#1154)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.37.4 to 4.37.6.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v4.37.4...v4.37.6)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.37.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 15:31:36 -04:00
Jeremy Schoemaker
3c8fb2a7e4
fix(sse): loop instead of recursing when skipping SSE events (#1146)
`SseAutoReconnectStream::poll_next` recursed into itself for every event it
skips — control frames, data-less frames, and frames whose data fails to
deserialize — plus once more on every state transition.

The inner stream returns `Poll::Ready` for each event it can parse out of
already-buffered bytes, so a burst of skipped events has no yield point
between them. Each one adds a stack frame, and `poll_next` is a large frame.
A client connected to a server that emits a run of non-JSON `message` frames
overflows the stack and aborts the process.

Wrap the body in a `loop` and replace the four `self.poll_next(cx)` tail calls
with `continue`. `this` is re-derived from `self.as_mut().project()` at the top
of each iteration, so the borrows end cleanly per iteration; no other logic
changes. The diff is mostly the resulting re-indent — review with `?w=1`.

Adds `skipped_events_do_not_grow_the_stack`, which feeds 50,000 undeserializable
frames through the stream. Before this change it aborts with
`fatal runtime error: stack overflow` (SIGABRT); after, it passes.
2026-08-07 10:34:18 -04:00
砂糖梨子
e150d4f3a6
fix(auth): preserve issuer trailing slash during discovery (#1145) 2026-08-07 10:31:24 -04:00
dependabot[bot]
9a3168af87
chore(deps): bump taiki-e/install-action from 2.85.6 to 2.85.7 (#1139)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.85.6 to 2.85.7.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.85.6...v2.85.7)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.85.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-07 09:53:43 -04:00
github-actions[bot]
baac607e52
chore: release v3.1.1 (#1115)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-05 10:52:23 -04:00
Dale Seo
f57d585168
chore: upgrade darling and syn (#1138) 2026-08-05 10:20:22 -04:00
Dale Seo
07bcda2973
fix: emit cache hints from handler macros (#1120) 2026-08-05 10:20:08 -04:00
Dale Seo
f8f9607d0b
chore: make async-trait optional (#1119) 2026-08-05 10:19:45 -04:00
Dale Seo
c4def55422
fix: expose MRTR state to tool handlers (#1104)
* fix: expose MRTR state to tool handlers

* docs: document tool call context fields
2026-08-05 09:55:09 -04:00
dependabot[bot]
7e14020cb8
chore(deps): bump taiki-e/install-action from 2.85.5 to 2.85.6 (#1129)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.85.5 to 2.85.6.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.85.5...v2.85.6)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.85.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 15:34:47 -04:00
dependabot[bot]
9cbb69d9e9
chore(deps): bump taiki-e/install-action from 2.85.3 to 2.85.5 (#1117)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.85.3 to 2.85.5.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.85.3...v2.85.5)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.85.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-03 14:59:42 -04:00
dependabot[bot]
5b216605f2
chore(deps): bump github/codeql-action from 4.37.3 to 4.37.4 (#1116)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.37.3 to 4.37.4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v4.37.3...v4.37.4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.37.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-03 14:59:28 -04:00
Dale Seo
684ff5a51e
fix: disambiguate input-required results (#1103) 2026-08-03 12:54:59 -04:00
dependabot[bot]
830e088d73
chore(deps): bump taiki-e/install-action from 2.85.2 to 2.85.3 (#1107)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.85.2 to 2.85.3.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.85.2...v2.85.3)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.85.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-31 14:48:08 -04:00
116 changed files with 12109 additions and 1592 deletions

View file

@ -16,11 +16,15 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
labels:
# Mark PRs as CI related change.
- T-CI
open-pull-requests-limit: 3
groups:
codeql-action:
patterns:
- "github/codeql-action*"
commit-message:
prefix: "chore"
include: "scope"
include: "scope"

View file

@ -3,6 +3,9 @@ on:
# Runs workflow when activity on a PR in the workflow's repository occurs.
pull_request_target:
permissions:
contents: read
jobs:
auto-label:
permissions:
@ -20,7 +23,7 @@ jobs:
PR_URL: ${{ github.event.pull_request.html_url }}
steps:
- uses: actions/labeler@v7
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7
with:
# Auto-include paths starting with dot (e.g. .github)
dot: true

View file

@ -12,34 +12,35 @@ env:
CARGO_TERM_COLOR: always
ARTIFACT_DIR: release-artifacts
permissions:
contents: read
jobs:
commit-lint:
name: Lint Commit Messages
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v7
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: '22'
- name: Install commitlint
run: |
npm install --save-dev @commitlint/cli @commitlint/config-conventional
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
run: npm install --global @commitlint/cli@20.4.3 @commitlint/config-conventional@20.4.3
- name: Lint commit messages
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
run: commitlint --extends @commitlint/config-conventional --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
fmt:
name: Code Formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust fmt
run: rustup toolchain install nightly --component rustfmt
@ -51,32 +52,40 @@ jobs:
name: Lint with Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Run clippy
- name: Run clippy (all features)
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Run clippy (all features except local)
run: |
FEATURES=$(cargo metadata --no-deps --format-version 1 \
| jq -r '[.packages[] | select(.name == "rmcp") | .features | keys[]
| select(startswith("__") | not)
| select(. != "local")] | join(",")')
cargo clippy --package rmcp --all-targets --no-default-features --features "$FEATURES" -- -D warnings
semver:
name: SemVer Check
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Install cargo-semver-checks
uses: taiki-e/install-action@v2.85.2
uses: taiki-e/install-action@3f74d7c16a4242f1c95561e98edc25d36adb4375 # v2.87.12
with:
tool: cargo-semver-checks
@ -117,7 +126,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
@ -125,12 +134,12 @@ jobs:
# to be installed (it does not need to be the default; the tool invokes it
# via `cargo +nightly`).
- name: Install Rust
uses: dtolnay/rust-toolchain@nightly
uses: dtolnay/rust-toolchain@7c8d7d138f5c09cef361f8214cf96882cd029cdb # nightly
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Install cargo-public-api
uses: taiki-e/install-action@v2.85.2
uses: taiki-e/install-action@3f74d7c16a4242f1c95561e98edc25d36adb4375 # v2.87.12
with:
tool: cargo-public-api
@ -201,9 +210,9 @@ jobs:
name: spell check with typos
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Spell Check Repo
uses: crate-ci/typos@master
uses: crate-ci/typos@d43b6c087ac471e2ea7b8af622ff15f05c0c365b # master
msrv:
name: Check MSRV
@ -211,7 +220,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Read MSRV from Cargo.toml
run: |
@ -220,11 +229,11 @@ jobs:
echo "MSRV=$MSRV" >> "$GITHUB_ENV"
- name: Install MSRV Rust
uses: dtolnay/rust-toolchain@master
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master
with:
toolchain: ${{ env.MSRV }}
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Check default workspace members with MSRV
run: cargo +${{ env.MSRV }} check --all-targets --all-features
@ -233,19 +242,19 @@ jobs:
name: Run Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# install nodejs
- name: Setup Node.js
uses: actions/setup-node@v7
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: '22'
- name: Install uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- name: Set up Python
run: uv python install
@ -253,7 +262,7 @@ jobs:
- name: Create venv for python
run: uv venv
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Run tests
run: cargo test --all-features
@ -262,19 +271,19 @@ jobs:
name: Run Tests (no local feature)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# install nodejs
- name: Setup Node.js
uses: actions/setup-node@v7
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: '22'
- name: Install uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- name: Set up Python
run: uv python install
@ -282,7 +291,7 @@ jobs:
- name: Create venv for python
run: uv venv
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Run tests without local feature
run: |
@ -296,21 +305,21 @@ jobs:
name: Code Coverage
runs-on: ubuntu-latest
permissions:
contents: write
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# install nodejs
- name: Setup Node.js
uses: actions/setup-node@v7
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: '22'
- name: Install uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- name: Set up Python
run: uv python install
@ -318,7 +327,7 @@ jobs:
- name: Create venv for python
run: uv venv
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Install cargo-llvm-cov
run: cargo install cargo-llvm-cov
@ -333,19 +342,19 @@ jobs:
name: Example test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# install nodejs
- name: Setup Node.js
uses: actions/setup-node@v7
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: '22'
- name: Install uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- name: Set up Python
run: uv python install
@ -353,7 +362,7 @@ jobs:
- name: Create venv for python
run: uv venv
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Add target WASI preview 2
run: |
@ -389,12 +398,12 @@ jobs:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Install cargo-audit
run: cargo install cargo-audit
@ -406,12 +415,12 @@ jobs:
name: Generate Documentation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust
uses: dtolnay/rust-toolchain@nightly
uses: dtolnay/rust-toolchain@7c8d7d138f5c09cef361f8214cf96882cd029cdb # nightly
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Generate documentation
run: |
@ -432,7 +441,7 @@ jobs:
# This happened recently in the attack on `tj-actions/changed-files`, but
# has happened many times before as well.
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Update Rust
run: |

View file

@ -8,6 +8,9 @@ on:
schedule:
- cron: '0 0 * * 1' # Weekly on Monday
permissions:
contents: read
jobs:
analyze:
name: Analyze
@ -21,16 +24,16 @@ jobs:
language: [rust, javascript-typescript, python, actions]
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Initialize CodeQL
uses: github/codeql-action/init@v4.37.3
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/codeql-config.yml
- name: Autobuild
uses: github/codeql-action/autobuild@v4.37.3
uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4.37.3
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0

View file

@ -14,6 +14,9 @@ concurrency:
env:
CONFORMANCE_VERSION: "0.2.0-alpha.10"
permissions:
contents: read
jobs:
server:
runs-on: ubuntu-latest
@ -21,12 +24,12 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Build conformance binaries
run: cargo build -p mcp-conformance
@ -91,7 +94,7 @@ jobs:
- name: Upload results
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: conformance-server-results
path: |
@ -104,12 +107,12 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Build conformance binaries
run: cargo build -p mcp-conformance
@ -140,7 +143,7 @@ jobs:
- name: Upload results
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: conformance-client-results
path: conformance-client-results

View file

@ -1,8 +1,6 @@
name: Release-plz
permissions:
pull-requests: write
contents: write
permissions: {}
on:
push:
@ -18,19 +16,20 @@ jobs:
if: ${{ github.repository_owner == 'modelcontextprotocol' }}
permissions:
contents: write
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
toolchain: "1.92"
# Using fork until semver_check_features support is released upstream.
# See: https://github.com/release-plz/release-plz/pull/2757
- name: Install release-plz from fork
run: cargo install --locked --git https://github.com/DaleSeo/release-plz --branch feat/semver-check-features release-plz
run: cargo install --locked --git https://github.com/DaleSeo/release-plz --rev c2ea743bacf9bbd56e344df68219e60b0ad35d04 release-plz
- name: Run release-plz release
run: release-plz release
env:
@ -51,17 +50,17 @@ jobs:
cancel-in-progress: false
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
toolchain: "1.92"
# Using fork until semver_check_features support is released upstream.
# See: https://github.com/release-plz/release-plz/pull/2757
- name: Install release-plz from fork
run: cargo install --locked --git https://github.com/DaleSeo/release-plz --branch feat/semver-check-features release-plz
run: cargo install --locked --git https://github.com/DaleSeo/release-plz --rev c2ea743bacf9bbd56e344df68219e60b0ad35d04 release-plz
- name: Run release-plz release-pr
run: release-plz release-pr
env:

48
.github/workflows/scorecard.yml vendored Normal file
View file

@ -0,0 +1,48 @@
# OpenSSF Scorecard supply-chain security analysis.
name: Scorecard supply-chain security
on:
branch_protection_rule:
schedule:
- cron: '5 23 * * 5'
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
security-events: write
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run analysis
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
# Publishing only succeeds on the default branch. PR and manual runs
# still validate the analysis and produce SARIF without publishing.
publish_results: ${{ github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- name: Upload SARIF artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: scorecard-results
path: results.sarif
retention-days: 5
- name: Upload to code scanning
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with:
sarif_file: results.sarif

View file

@ -4,6 +4,9 @@ on:
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
stale:
name: Clean Up
@ -12,7 +15,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v11
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11
with:
days-before-stale: 60
days-before-close: 14

View file

@ -39,7 +39,7 @@ jobs:
TRIAGE_MODEL: ${{ vars.TRIAGE_MODEL || 'gpt-4o-mini' }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install jq
run: sudo apt-get install -y jq

View file

@ -4,13 +4,13 @@ default-members = ["crates/rmcp", "crates/rmcp-macros"]
resolver = "2"
[workspace.dependencies]
rmcp = { version = "3.1.0", path = "./crates/rmcp" }
rmcp-macros = { version = "3.1.0", path = "./crates/rmcp-macros" }
rmcp = { version = "3.4.0", path = "./crates/rmcp" }
rmcp-macros = { version = "3.4.0", path = "./crates/rmcp-macros" }
[workspace.package]
edition = "2024"
rust-version = "1.88"
version = "3.1.0"
version = "3.4.0"
authors = ["4t145 <u4t145@163.com>"]
license = "Apache-2.0"
repository = "https://github.com/modelcontextprotocol/rust-sdk/"

View file

@ -99,10 +99,10 @@ Use [`ClientServiceExt::serve_with_lifecycle`](crates/rmcp/src/service/client.rs
select another lifecycle explicitly:
```rust, ignore
use rmcp::{ClientInfo, ClientLifecycleMode, ClientServiceExt, ProtocolVersion};
use rmcp::{ClientLifecycleMode, ClientServiceExt, ProtocolVersion, model::ClientConfig};
// Start directly with server/discover and include client metadata on every request.
let client = ClientInfo::default()
let client = ClientConfig::default()
.serve_with_lifecycle(
transport,
ClientLifecycleMode::Discover {
@ -112,8 +112,8 @@ let client = ClientInfo::default()
.await?;
// Or probe the discover lifecycle and fall back when a legacy server reports
// that server/discover is not implemented.
let client = ClientInfo::default()
// that server/discover is not implemented or does not respond within 10 seconds.
let client = ClientConfig::default()
.serve_with_lifecycle(
transport,
ClientLifecycleMode::Auto {
@ -369,8 +369,8 @@ use serde_json::json;
struct MyServer;
impl ServerHandler for MyServer {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(
fn get_info(&self) -> ServerConfig {
ServerConfig::new(
ServerCapabilities::builder()
.enable_resources()
.build(),
@ -582,8 +582,8 @@ impl MyServer {
#[prompt_handler]
impl ServerHandler for MyServer {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_prompts().build())
fn get_info(&self) -> ServerConfig {
ServerConfig::new(ServerCapabilities::builder().enable_prompts().build())
}
}
```
@ -934,8 +934,8 @@ Enable the logging capability, handle level changes from the client, and send lo
use rmcp::{ServerHandler, model::*, service::RequestContext};
impl ServerHandler for MyServer {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(
fn get_info(&self) -> ServerConfig {
ServerConfig::new(
ServerCapabilities::builder()
.enable_logging()
.build(),
@ -1007,8 +1007,8 @@ Enable the completions capability and implement the `complete()` handler. Use `r
use rmcp::{ErrorData as McpError, ServerHandler, model::*, service::RequestContext, RoleServer};
impl ServerHandler for MyServer {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(
fn get_info(&self) -> ServerConfig {
ServerConfig::new(
ServerCapabilities::builder()
.enable_completions()
.enable_prompts()
@ -1253,8 +1253,8 @@ use rmcp::{
};
impl ServerHandler for MyServer {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(
fn get_info(&self) -> ServerConfig {
ServerConfig::new(
ServerCapabilities::builder()
.enable_tools()
.enable_tool_list_changed()
@ -1363,11 +1363,21 @@ async fn call_tool(&self, request: CallToolRequestParams, _ctx: RequestContext<R
}
```
> **`requestState` is untrusted.** The client echoes it back verbatim, so a
> stateless server that stores meaningful data in it MUST verify integrity
> first. Enable the `request-state` feature and use `RequestStateCodec` to seal
> and open it (HMAC-tagged), or keep state server-side and use `requestState`
> only as an opaque handle.
> **`requestState` is untrusted.** [SEP-2322 requires servers to validate
> it](https://modelcontextprotocol.io/seps/2322-MRTR#protocol-requirements-for-ephemeral-workflow)
> because the client echoes it back verbatim. A stateless server that stores
> meaningful data in it MUST verify integrity first. Enable the `request-state`
> feature and use `RequestStateCodec` to seal and open it (HMAC-tagged), or keep
> state server-side and use `requestState` only as an opaque handle.
For multi-replica deployments, use `RequestStateCodec::new_with_keyring` to
rotate signing keys without invalidating in-flight requests:
1. Deploy the old and new keys everywhere, continuing to emit `rs1` with the
old key via `with_rs1_signing("old")`.
2. Start emitting `rs2` with the new key while retaining the old key via
`with_rs1_fallback("old")`.
3. After the maximum `requestState` lifetime has elapsed, remove the old key.
### Client-side
@ -1574,7 +1584,7 @@ use rmcp::transport::StreamableHttpClientTransport;
// Defaults are stateless-friendly.
let transport = StreamableHttpClientTransport::from_uri("http://localhost:8000/mcp");
let client = ClientInfo::default().serve(transport).await?;
let client = ClientConfig::default().serve(transport).await?;
```
**Example:** [`examples/servers/src/counter_streamhttp.rs`](examples/servers/src/counter_streamhttp.rs) (server), [`examples/clients/src/streamable_http.rs`](examples/clients/src/streamable_http.rs) (client)
@ -1628,9 +1638,27 @@ server example). The client transport connects with a single URI:
use rmcp::transport::StreamableHttpClientTransport;
let transport = StreamableHttpClientTransport::from_uri("http://localhost:8000/mcp");
let client = ClientInfo::default().serve(transport).await?;
let client = ClientConfig::default().serve(transport).await?;
```
The client allows up to 16 ordinary http POSTs at once. Configure this with
`StreamableHttpClientTransportConfig::with_uri(url).max_concurrent_requests(n)`;
`1` keeps ordinary POSTs serial, and `0` is treated as `1`. An open sse response
stream does not count against this limit. Cancellation and replies use a
separate queue with one extra POST slot. Configure their timeout with
`control_request_timeout` (default: five seconds). The timeout starts when the
POST starts, excluding time in the queue. Cancellation stops a queued or active
POST immediately.
For an open legacy response stream, the client stops reading but keeps the stream
alive until the cancellation send finishes or is dropped. This lets custom http
adapters handle cancellation before their stream state is removed.
Session recovery waits up to five seconds for old POSTs, then stops any that
remain. Those POSTs are not retried because the server may have processed them.
Configure this wait and the separate
reinitialization timeout with `session_recovery_timeout`. Callers still decide
which tools may run at the same time and which need approval.
#### Server-Sent Events (SSE)
Streamable HTTP responses arrive as either a single `application/json` body or a
@ -1714,10 +1742,10 @@ knows what the other supports. Declare yours with the `ServerCapabilities`
builder in `get_info()`:
```rust,ignore
use rmcp::model::{ServerCapabilities, ServerInfo};
use rmcp::model::{ServerCapabilities, ServerConfig};
fn get_info(&self) -> ServerInfo {
ServerInfo::new(
fn get_info(&self) -> ServerConfig {
ServerConfig::new(
ServerCapabilities::builder()
.enable_tools()
.enable_prompts()

View file

@ -2,8 +2,9 @@
This roadmap tracks the path to [SEP-1730](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1730) Tier 1 for the Rust MCP SDK.
**Status (2026-07-30): all SEP-1730 Tier 1 requirements are met.** Conformance is
100% across every date-versioned suite, the stable **v3.0.1** release has shipped,
**Status: all SEP-1730 Tier 1 requirements are met.** Conformance is
100% across every date-versioned suite, a stable ≥1.0.0 release has shipped
([Releases](https://github.com/modelcontextprotocol/rust-sdk/releases)),
issue triage and critical-bug (P0) resolution are within the Tier 1 SLAs, the
governance documents (`VERSIONING.md`, `DEPENDENCY_POLICY.md`, this `ROADMAP.md`) are
published, and all 48 non-experimental features are documented with examples. This
@ -15,7 +16,7 @@ document now serves as the ongoing tracker for spec conformance and SDK health.
| Client conformance 100% (date-versioned) | ✅ | 39/39 scored — see below |
| Issue triage ≥90% within 2 business days | ✅ | 95.2% (20/21) |
| All P0 bugs resolved within 7 days | ✅ | 0 open; last P0 (#741) resolved in 3 days |
| Stable release ≥1.0.0 (no pre-release suffix) | ✅ | `rmcp-v3.0.1` (see tooling note below) |
| Stable release ≥1.0.0 (no pre-release suffix) | ✅ | [latest release](https://github.com/modelcontextprotocol/rust-sdk/releases/latest) (see tooling note below) |
| Clear versioning + breaking-change policy | ✅ | [`VERSIONING.md`](VERSIONING.md) |
| All non-experimental features documented w/ examples| ✅ | 48/48 in [`README.md`](README.md) |
| Published dependency update policy | ✅ | [`DEPENDENCY_POLICY.md`](DEPENDENCY_POLICY.md) + [`.github/dependabot.yml`](.github/dependabot.yml) |
@ -31,8 +32,9 @@ and `extension` scenarios are informational and reported separately below.
> **Tooling note — `stable_release`:** the SEP-1730 `tier-check` CLI may report
> `stable_release` as failing because it does not parse the workspace tag prefix
> `rmcp-v` (as in `rmcp-v3.0.1`). `rmcp-v3.0.1` is a genuine stable, non-pre-release
> release ([Releases](https://github.com/modelcontextprotocol/rust-sdk/releases)); the
> `rmcp-v` (as in `rmcp-vX.Y.Z`). The published releases are genuine stable,
> non-pre-release versions ≥1.0.0
> ([Releases](https://github.com/modelcontextprotocol/rust-sdk/releases)); the
> flag is a tooling artifact, not an unmet requirement.
---
@ -75,8 +77,7 @@ the milestone:
### Tier 1 requirements
- [x] **v3.0.0 stable released** (2026-07-28) — MRTR, SEP-2549 cache hints, SEP-2243
standard headers, SEP-2575 stateless MCP, and SEP-2106 relaxations; **v3.0.1**
is the current stable release
standard headers, SEP-2575 stateless MCP, and SEP-2106 relaxations
- [x] 2025-11-25 server conformance 100% (30/30)
- [x] 2025-11-25 client conformance 100%
- [x] 2026-07-28 server conformance 100% (30/30 dated)

View file

@ -19,6 +19,7 @@ rmcp = { path = "../crates/rmcp", features = [
"elicitation",
"auth",
"auth-client-credentials-jwt",
"auth-enterprise-managed",
"request-state",
"transport-streamable-http-server",
"transport-streamable-http-client-reqwest",
@ -31,6 +32,7 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
axum = { version = "0.8", features = ["macros"] }
anyhow = "1"
oauth2 = { version = "5.0", default-features = false }
reqwest = { version = "0.13", features = ["json"] }
urlencoding = "2"
url = "2"

View file

@ -1,3 +1,10 @@
#![expect(
deprecated,
reason = "The conformance suite still exercises deprecated sampling scenarios"
)]
use anyhow::Context;
use oauth2::{ClientSecret, RefreshToken};
use rmcp::{
ClientHandler, ClientLifecycleMode, ClientServiceExt, ErrorData, RoleClient, ServiceExt,
model::*,
@ -6,7 +13,8 @@ use rmcp::{
AuthClient, AuthorizationManager, StreamableHttpClientTransport,
auth::{
AuthorizationCallback, AuthorizationRequest, ClientCredentialsConfig,
InMemoryCredentialStore, JwtSigningAlgorithm, OAuthState,
InMemoryCredentialStore, JwtSigningAlgorithm, OAuthState, default_oauth_http_client,
enterprise::{EmaAuthorizationServer, EmaClientAuthentication, EmaExchangeRequest},
},
streamable_http_client::StreamableHttpClientTransportConfig,
},
@ -36,6 +44,17 @@ struct ConformanceContext {
private_key_pem: Option<String>,
#[serde(default)]
signing_algorithm: Option<String>,
// enterprise-managed-authorization-refresh-token
#[serde(default)]
idp_client_id: Option<String>,
#[serde(default)]
idp_client_secret: Option<String>,
#[serde(default)]
idp_refresh_token: Option<String>,
#[serde(default)]
idp_issuer: Option<String>,
#[serde(default)]
idp_token_endpoint: Option<String>,
}
fn load_context() -> ConformanceContext {
@ -55,8 +74,8 @@ impl ClientHandler for BasicClientHandler {}
struct ElicitationDefaultsClientHandler;
impl ClientHandler for ElicitationDefaultsClientHandler {
fn get_info(&self) -> ClientInfo {
let mut info = ClientInfo::default();
fn get_info(&self) -> ClientConfig {
let mut info = ClientConfig::default();
info.capabilities.elicitation = Some(
ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
@ -144,8 +163,8 @@ impl ClientHandler for ElicitationDefaultsClientHandler {
struct FullClientHandler;
impl ClientHandler for FullClientHandler {
fn get_info(&self) -> ClientInfo {
let mut info = ClientInfo::default();
fn get_info(&self) -> ClientConfig {
let mut info = ClientConfig::default();
info.capabilities.elicitation = Some(
ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
@ -760,6 +779,66 @@ async fn run_client_credentials_jwt(
Ok(())
}
/// Exchange the fixture's IdP refresh token, then exercise authenticated MCP access.
async fn run_ema_refresh_token_client(
server_url: &str,
ctx: &ConformanceContext,
) -> anyhow::Result<()> {
let manager = AuthorizationManager::new(server_url).await?;
let metadata = manager.resolve_metadata().await?.metadata;
let idp = EmaAuthorizationServer::new(
ctx.idp_issuer.as_deref().context("Missing idp_issuer")?,
ctx.idp_token_endpoint
.as_deref()
.context("Missing idp_token_endpoint")?,
ctx.idp_client_id
.as_deref()
.context("Missing idp_client_id")?,
)
.with_client_authentication(EmaClientAuthentication::ClientSecretBasic(
ClientSecret::new(
ctx.idp_client_secret
.clone()
.context("Missing idp_client_secret")?,
),
));
let resource_as = EmaAuthorizationServer::new(
metadata
.issuer
.context("Missing authorization server issuer")?,
metadata.token_endpoint,
ctx.client_id.as_deref().context("Missing client_id")?,
)
.with_client_authentication(EmaClientAuthentication::ClientSecretBasic(
ClientSecret::new(ctx.client_secret.clone().context("Missing client_secret")?),
));
let refresh_token = RefreshToken::new(
ctx.idp_refresh_token
.clone()
.context("Missing idp_refresh_token")?,
);
let http = default_oauth_http_client()?;
let token = EmaExchangeRequest::new(idp, resource_as, server_url, &refresh_token)
.with_scopes(manager.select_scopes(None, &[]))
.exchange(&http, &http)
.await?;
let transport = StreamableHttpClientTransport::from_config(
StreamableHttpClientTransportConfig::with_uri(server_url)
.auth_header(token.access_token.secret()),
);
let client = BasicClientHandler
.serve_with_lifecycle(transport, conformance_lifecycle())
.await?;
let tools = client.list_tools(Default::default()).await?;
for tool in tools.tools {
let args = build_tool_arguments(&tool);
client.call_tool(call_tool_params(tool.name, args)).await?;
}
client.cancel().await?;
Ok(())
}
/// Cross-app access flow (SEP-1046 extension).
async fn run_cross_app_access_client(
server_url: &str,
@ -1110,6 +1189,11 @@ async fn run_scenario(
"auth/client-credentials-basic" => run_client_credentials_basic(server_url, ctx).await?,
"auth/client-credentials-jwt" => run_client_credentials_jwt(server_url, ctx).await?,
// Auth - enterprise-managed authorization with a refresh-token subject
"auth/enterprise-managed-authorization-refresh-token" => {
run_ema_refresh_token_client(server_url, ctx).await?
}
// Auth - cross-app access
"auth/cross-app-access-complete-flow" => {
run_cross_app_access_client(server_url, ctx).await?

View file

@ -52,6 +52,10 @@ fn custom_header_tool() -> Tool {
/// Signing key for SEP-2322 `requestState` sealing. A fixed key is fine for a
/// conformance harness; real servers must load a secret out of clients' reach.
const REQUEST_STATE_KEY: &[u8] = b"rust-sdk-conformance-request-state-key!!";
const _: () = assert!(
REQUEST_STATE_KEY.len() >= RequestStateCodec::MIN_KEY_LENGTH,
"REQUEST_STATE_KEY is shorter than RequestStateCodec::MIN_KEY_LENGTH",
);
#[derive(Clone)]
struct ConformanceServer {
@ -70,7 +74,7 @@ impl ConformanceServer {
subscriptions: Arc::new(Mutex::new(HashMap::new())),
next_subscription: Arc::new(AtomicU64::new(0)),
log_level: Arc::new(Mutex::new(LoggingLevel::Debug)),
request_state_codec: RequestStateCodec::new(REQUEST_STATE_KEY),
request_state_codec: RequestStateCodec::new_unchecked(REQUEST_STATE_KEY),
tasks: TaskManager::new(),
}
}
@ -700,8 +704,8 @@ impl ServerHandler for ConformanceServer {
(name == "test_custom_header").then(custom_header_tool)
}
fn get_info(&self) -> ServerInfo {
ServerInfo::new(
fn get_info(&self) -> ServerConfig {
ServerConfig::new(
ServerCapabilities::builder()
.enable_prompts()
.enable_prompts_list_changed()
@ -1740,7 +1744,9 @@ async fn main() -> anyhow::Result<()> {
tracing::info!("Starting conformance server on {}", bind_addr);
let server = ConformanceServer::new();
let config = StreamableHttpServerConfig::default();
let config = StreamableHttpServerConfig::default()
.with_allowed_origins([format!("http://{bind_addr}")])
.enforce_origin_validation();
let service = StreamableHttpService::new(
move || Ok(server.clone()),
LocalSessionManager::default().into(),

View file

@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [3.4.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v3.3.0...rmcp-macros-v3.4.0) - 2026-09-15
### Added
- *(model)* add ServerConfig and ClientConfig ([#1266](https://github.com/modelcontextprotocol/rust-sdk/pull/1266))
### Fixed
- *(model)* deprecate ServerInfo and ClientInfo aliases ([#1156](https://github.com/modelcontextprotocol/rust-sdk/pull/1156))
## [3.3.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v3.2.0...rmcp-macros-v3.3.0) - 2026-09-10
### Added
- *(macros)* reject empty tool_router ([#1233](https://github.com/modelcontextprotocol/rust-sdk/pull/1233))
## [3.2.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v3.1.4...rmcp-macros-v3.2.0) - 2026-08-31
### Added
- add request-state key rotation ([#1128](https://github.com/modelcontextprotocol/rust-sdk/pull/1128))
### Fixed
- allow concurrent streamable http requests ([#1186](https://github.com/modelcontextprotocol/rust-sdk/pull/1186))
## [3.1.3](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v3.1.2...rmcp-macros-v3.1.3) - 2026-08-17
### Fixed
- time out auto discovery probe ([#1149](https://github.com/modelcontextprotocol/rust-sdk/pull/1149))
## [3.1.1](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v3.1.0...rmcp-macros-v3.1.1) - 2026-08-05
### Fixed
- emit cache hints from handler macros ([#1120](https://github.com/modelcontextprotocol/rust-sdk/pull/1120))
### Other
- upgrade darling and syn ([#1138](https://github.com/modelcontextprotocol/rust-sdk/pull/1138))
## [3.1.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v3.0.1...rmcp-macros-v3.1.0) - 2026-07-31
### Added

View file

@ -16,11 +16,11 @@ documentation = "https://docs.rs/rmcp-macros"
proc-macro = true
[dependencies]
syn = {version = "2", features = ["full"]}
syn = {version = "3", features = ["full"]}
quote = "1"
proc-macro2 = "1"
serde_json = "1.0"
darling = { version = "0.23" }
darling = { version = "0.24" }
[features]
local = []

View file

@ -56,6 +56,7 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> TokenStream {
/// | `router` | `Ident` | The name of the router function to be generated. Defaults to `tool_router`. |
/// | `vis` | `Visibility` | The visibility of the generated router function. Defaults to empty. |
/// | `server_handler` | `flag` | When set, also emits `#[::rmcp::tool_handler]` on `impl ServerHandler for Self` so you can omit a separate `#[tool_handler]` block. |
/// | `allow_empty` | `flag` | When set, accepts an impl block with no `#[tool]` fn. Without it, an empty router is a compile error. |
///
/// ## Example
///
@ -122,6 +123,37 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> TokenStream {
/// }
/// }
/// ```
///
/// ### Empty routers
///
/// Collecting tools is this attribute's whole purpose, so an impl block with no `#[tool]` fn is a
/// compile error rather than a router that silently serves nothing. Pass `allow_empty` when that
/// is what you want:
///
/// ```rust,ignore
/// #[tool_router(allow_empty)]
/// impl MyToolHandler {}
/// ```
///
/// The usual way to hit this by accident is a `macro_rules!` helper *inside* the impl block. An
/// attribute macro receives the unexpanded item, so `#[tool]` fns produced by such a helper are
/// invisible to `#[tool_router]`. Let the `macro_rules!` emit the whole annotated impl instead:
///
/// ```rust,ignore
/// macro_rules! define_tools {
/// ($($name:ident => $description:literal),* $(,)?) => {
/// #[tool_router]
/// impl MyToolHandler {
/// $(
/// #[tool(description = $description)]
/// async fn $name(&self) -> String { stringify!($name).to_owned() }
/// )*
/// }
/// };
/// }
///
/// define_tools!(my_tool => "what my tool does");
/// ```
#[proc_macro_attribute]
pub fn tool_router(attr: TokenStream, input: TokenStream) -> TokenStream {
tool_router::tool_router(attr.into(), input.into())
@ -185,8 +217,8 @@ pub fn tool_router(attr: TokenStream, input: TokenStream) -> TokenStream {
/// ```rust,ignore
/// #[tool_handler]
/// impl ServerHandler for MyToolHandler {
/// fn get_info(&self) -> ServerInfo {
/// ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
/// fn get_info(&self) -> ServerConfig {
/// ServerConfig::new(ServerCapabilities::builder().enable_tools().build())
/// }
/// }
/// ```

View file

@ -133,7 +133,7 @@ pub fn prompt(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream>
let new_output = syn::parse2::<ReturnType>({
let mut lt = quote! { 'static };
if let Some(receiver) = fn_item.sig.receiver()
&& let Some((_, receiver_lt)) = receiver.reference.as_ref()
&& let syn::ReceiverKind::Reference(_, receiver_lt, _) = &receiver.kind
{
if let Some(receiver_lt) = receiver_lt {
lt = quote! { #receiver_lt };
@ -259,6 +259,20 @@ mod test {
Ok(())
}
#[test]
fn test_async_prompt_preserves_receiver_lifetime() -> syn::Result<()> {
let attr = quote! {};
let input = quote! {
async fn test_prompt_with_lifetime<'a>(&'a self) -> String {
"ok".to_string()
}
};
let result = prompt(attr, input)?;
assert!(result.to_string().contains("+ 'a"));
Ok(())
}
#[test]
fn test_async_prompt_local_omits_send() -> syn::Result<()> {
let attr = quote! { local };

View file

@ -57,16 +57,20 @@ pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> syn::Result<Toke
async fn list_prompts(
&self,
_request: Option<rmcp::model::PaginatedRequestParams>,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::ListPromptsResult, rmcp::ErrorData> {
let prompts = #router_expr.list_all();
let supports_cache_hints = context.protocol_version().is_some_and(|version| {
version >= rmcp::model::ProtocolVersion::V_2026_07_28
});
Ok(rmcp::model::ListPromptsResult {
result_type: Some(rmcp::model::ResultType::COMPLETE),
prompts,
meta: #meta,
next_cursor: None,
ttl_ms: None,
cache_scope: None,
ttl_ms: supports_cache_hints.then_some(0),
cache_scope: supports_cache_hints
.then_some(rmcp::model::CacheScope::Public),
})
}
};

Some files were not shown because too many files have changed in this diff Show more