Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
697c497
fix: allow concurrent streamable http requests (#1186)
nickcoai Aug 24, 2026
02decfb
fix(transport): fall back after sessionless HTTP discover rejections …
jstar0 Aug 25, 2026
78cf9f1
ci: pin GitHub Actions to commit SHAs (#1216)
jamadeo Aug 25, 2026
103bf23
ci: scope release token permissions to jobs (#1220)
jamadeo Aug 27, 2026
3501f3e
ci: default workflow tokens to read-only contents (#1218)
jamadeo Aug 27, 2026
c8bb1c6
ci: remove coverage job write permission (#1219)
jamadeo Aug 27, 2026
12db028
ci: pin release-plz fork revision (#1221)
jamadeo Aug 27, 2026
3ef34e6
feat: add request-state key rotation (#1128)
gocamille Aug 28, 2026
ec041b7
ci: reduce dependabot update noise (#1184)
DaleSeo Aug 28, 2026
1afde5b
docs: use auto lifecycle in HTTP example (#1187)
DaleSeo Aug 28, 2026
3a2ebbc
chore(deps): bump taiki-e/install-action from 2.85.13 to 2.86.7 (#1230)
dependabot[bot] Aug 29, 2026
4e74499
feat(auth): coordinate OAuth refreshes through credential stores (#1232)
mzeng-openai Aug 31, 2026
ad9832e
fix: keep initialize on legacy protocol versions (#1228)
DaleSeo Aug 31, 2026
51ccb42
chore: release v3.2.0 (#1227)
github-actions[bot] Aug 31, 2026
42cf8d9
chore(deps): update process-wrap requirement from 9.0 to 10.0 (#1229)
dependabot[bot] Sep 1, 2026
3b5ca4d
chore(deps): bump astral-sh/setup-uv from 7.6.0 to 10.0.1 (#1238)
dependabot[bot] Sep 1, 2026
3023198
feat(auth): add enterprise refresh-token and ID-JAG exchanges (#1234)
nicksteele-oai Sep 4, 2026
add9cbe
ci: bump codeql-action to 4.37.9 and group updates (#1240)
DaleSeo Sep 8, 2026
04c2f3f
fix(auth): unify refresh checks and error handling (#1236)
DaleSeo Sep 8, 2026
7d14f3a
feat(macros): reject empty tool_router (#1233)
DaleSeo Sep 8, 2026
f9a68bf
feat: add ServerHandler::negotiate_initialize (#1247)
DaleSeo Sep 8, 2026
6fa4053
fix: resolve clippy warnings across workspace (#1195)
DaleSeo Sep 8, 2026
e8a0d70
chore(deps): bump taiki-e/install-action from 2.86.7 to 2.87.6 (#1250)
dependabot[bot] Sep 9, 2026
744b9f9
chore(deps): bump crate-ci/typos (#1249)
dependabot[bot] Sep 9, 2026
46db531
fix(sse): saturate exponential reconnect backoff to avoid overflow pa…
ump45nose Sep 10, 2026
b2c90b1
ci: pin commitlint dependencies (#1217)
jamadeo Sep 10, 2026
3e636ca
chore: release v3.3.0 (#1252)
github-actions[bot] Sep 10, 2026
0e1184b
feat: preserve typed custom responses on rmcp 3.3 and enforce Origin …
jmagar Sep 13, 2026
68e6f4a
fix(rmcp): harden typed response compatibility
jmagar Sep 13, 2026
2faf762
chore: merge fork main into rmcp 3.3 branch
jmagar Sep 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ updates:
# 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"
32 changes: 19 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,10 @@ jobs:
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
Expand All @@ -61,9 +59,17 @@ jobs:

- 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
Expand All @@ -79,7 +85,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2

- name: Install cargo-semver-checks
uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc # v2.86.7
uses: taiki-e/install-action@7b8d4719ee4aaa279bdf55df38dacb9ebfe12a6c # v2.87.6
with:
tool: cargo-semver-checks

Expand Down Expand Up @@ -133,7 +139,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2

- name: Install cargo-public-api
uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc # v2.86.7
uses: taiki-e/install-action@7b8d4719ee4aaa279bdf55df38dacb9ebfe12a6c # v2.87.6
with:
tool: cargo-public-api

Expand Down Expand Up @@ -206,7 +212,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Spell Check Repo
uses: crate-ci/typos@1a51d4b5a03bb97576af186c813af67e9137ba7c # master
uses: crate-ci/typos@d43b6c087ac471e2ea7b8af622ff15f05c0c365b # master

msrv:
name: Check MSRV
Expand Down Expand Up @@ -245,7 +251,7 @@ jobs:
node-version: '22'

- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1

- name: Install Rust
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
Expand Down Expand Up @@ -274,7 +280,7 @@ jobs:
node-version: '22'

- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1

- name: Install Rust
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
Expand Down Expand Up @@ -310,7 +316,7 @@ jobs:
node-version: '22'

- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1

- name: Install Rust
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
Expand Down Expand Up @@ -345,7 +351,7 @@ jobs:
node-version: '22'

- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1

- name: Install Rust
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Initialize CodeQL
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/codeql-config.yml

- name: Autobuild
uses: github/codeql-action/autobuild@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
uses: github/codeql-action/autobuild@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ default-members = ["crates/rmcp", "crates/rmcp-macros"]
resolver = "2"

[workspace.dependencies]
rmcp = { version = "3.1.4", path = "./crates/rmcp" }
rmcp-macros = { version = "3.1.4", path = "./crates/rmcp-macros" }
rmcp = { version = "3.3.0", path = "./crates/rmcp" }
rmcp-macros = { version = "3.3.0", path = "./crates/rmcp-macros" }

[workspace.package]
edition = "2024"
rust-version = "1.88"
version = "3.1.4"
version = "3.3.0"
authors = ["4t145 <u4t145@163.com>"]
license = "Apache-2.0"
repository = "https://github.com/modelcontextprotocol/rust-sdk/"
Expand Down
2 changes: 2 additions & 0 deletions conformance/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
86 changes: 85 additions & 1 deletion conformance/src/bin/client.rs
Original file line number Diff line number Diff line change
@@ -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::*,
Expand All @@ -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,
},
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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?
Expand Down
16 changes: 16 additions & 0 deletions crates/rmcp-macros/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [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
Expand Down
32 changes: 32 additions & 0 deletions crates/rmcp-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -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())
Expand Down
Loading
Loading