Compare commits

..
116 changed files with 1583 additions and 12100 deletions

View file

@ -16,15 +16,11 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
interval: "daily"
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,9 +3,6 @@ on:
# Runs workflow when activity on a PR in the workflow's repository occurs.
pull_request_target:
permissions:
contents: read
jobs:
auto-label:
permissions:
@ -23,7 +20,7 @@ jobs:
PR_URL: ${{ github.event.pull_request.html_url }}
steps:
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7
- uses: actions/labeler@v7
with:
# Auto-include paths starting with dot (e.g. .github)
dot: true

View file

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

View file

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

View file

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

View file

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

View file

@ -1,48 +0,0 @@
# 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,9 +4,6 @@ on:
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
stale:
name: Clean Up
@ -15,7 +12,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11
- uses: actions/stale@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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
- 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.4.0", path = "./crates/rmcp" }
rmcp-macros = { version = "3.4.0", path = "./crates/rmcp-macros" }
rmcp = { version = "3.1.0", path = "./crates/rmcp" }
rmcp-macros = { version = "3.1.0", path = "./crates/rmcp-macros" }
[workspace.package]
edition = "2024"
rust-version = "1.88"
version = "3.4.0"
version = "3.1.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::{ClientLifecycleMode, ClientServiceExt, ProtocolVersion, model::ClientConfig};
use rmcp::{ClientInfo, ClientLifecycleMode, ClientServiceExt, ProtocolVersion};
// Start directly with server/discover and include client metadata on every request.
let client = ClientConfig::default()
let client = ClientInfo::default()
.serve_with_lifecycle(
transport,
ClientLifecycleMode::Discover {
@ -112,8 +112,8 @@ let client = ClientConfig::default()
.await?;
// Or probe the discover lifecycle and fall back when a legacy server reports
// that server/discover is not implemented or does not respond within 10 seconds.
let client = ClientConfig::default()
// that server/discover is not implemented.
let client = ClientInfo::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) -> ServerConfig {
ServerConfig::new(
fn get_info(&self) -> ServerInfo {
ServerInfo::new(
ServerCapabilities::builder()
.enable_resources()
.build(),
@ -582,8 +582,8 @@ impl MyServer {
#[prompt_handler]
impl ServerHandler for MyServer {
fn get_info(&self) -> ServerConfig {
ServerConfig::new(ServerCapabilities::builder().enable_prompts().build())
fn get_info(&self) -> ServerInfo {
ServerInfo::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) -> ServerConfig {
ServerConfig::new(
fn get_info(&self) -> ServerInfo {
ServerInfo::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) -> ServerConfig {
ServerConfig::new(
fn get_info(&self) -> ServerInfo {
ServerInfo::new(
ServerCapabilities::builder()
.enable_completions()
.enable_prompts()
@ -1253,8 +1253,8 @@ use rmcp::{
};
impl ServerHandler for MyServer {
fn get_info(&self) -> ServerConfig {
ServerConfig::new(
fn get_info(&self) -> ServerInfo {
ServerInfo::new(
ServerCapabilities::builder()
.enable_tools()
.enable_tool_list_changed()
@ -1363,21 +1363,11 @@ async fn call_tool(&self, request: CallToolRequestParams, _ctx: RequestContext<R
}
```
> **`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.
> **`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.
### Client-side
@ -1584,7 +1574,7 @@ use rmcp::transport::StreamableHttpClientTransport;
// Defaults are stateless-friendly.
let transport = StreamableHttpClientTransport::from_uri("http://localhost:8000/mcp");
let client = ClientConfig::default().serve(transport).await?;
let client = ClientInfo::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)
@ -1638,27 +1628,9 @@ 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 = ClientConfig::default().serve(transport).await?;
let client = ClientInfo::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
@ -1742,10 +1714,10 @@ knows what the other supports. Declare yours with the `ServerCapabilities`
builder in `get_info()`:
```rust,ignore
use rmcp::model::{ServerCapabilities, ServerConfig};
use rmcp::model::{ServerCapabilities, ServerInfo};
fn get_info(&self) -> ServerConfig {
ServerConfig::new(
fn get_info(&self) -> ServerInfo {
ServerInfo::new(
ServerCapabilities::builder()
.enable_tools()
.enable_prompts()

View file

@ -2,9 +2,8 @@
This roadmap tracks the path to [SEP-1730](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1730) Tier 1 for the Rust MCP SDK.
**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)),
**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,
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
@ -16,7 +15,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) | ✅ | [latest release](https://github.com/modelcontextprotocol/rust-sdk/releases/latest) (see tooling note below) |
| Stable release ≥1.0.0 (no pre-release suffix) | ✅ | `rmcp-v3.0.1` (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) |
@ -32,9 +31,8 @@ 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-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
> `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
> flag is a tooling artifact, not an unmet requirement.
---
@ -77,7 +75,8 @@ 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
standard headers, SEP-2575 stateless MCP, and SEP-2106 relaxations; **v3.0.1**
is the current stable release
- [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,7 +19,6 @@ 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",
@ -32,7 +31,6 @@ 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,10 +1,3 @@
#![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::*,
@ -13,8 +6,7 @@ use rmcp::{
AuthClient, AuthorizationManager, StreamableHttpClientTransport,
auth::{
AuthorizationCallback, AuthorizationRequest, ClientCredentialsConfig,
InMemoryCredentialStore, JwtSigningAlgorithm, OAuthState, default_oauth_http_client,
enterprise::{EmaAuthorizationServer, EmaClientAuthentication, EmaExchangeRequest},
InMemoryCredentialStore, JwtSigningAlgorithm, OAuthState,
},
streamable_http_client::StreamableHttpClientTransportConfig,
},
@ -44,17 +36,6 @@ 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 {
@ -74,8 +55,8 @@ impl ClientHandler for BasicClientHandler {}
struct ElicitationDefaultsClientHandler;
impl ClientHandler for ElicitationDefaultsClientHandler {
fn get_info(&self) -> ClientConfig {
let mut info = ClientConfig::default();
fn get_info(&self) -> ClientInfo {
let mut info = ClientInfo::default();
info.capabilities.elicitation = Some(
ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
@ -163,8 +144,8 @@ impl ClientHandler for ElicitationDefaultsClientHandler {
struct FullClientHandler;
impl ClientHandler for FullClientHandler {
fn get_info(&self) -> ClientConfig {
let mut info = ClientConfig::default();
fn get_info(&self) -> ClientInfo {
let mut info = ClientInfo::default();
info.capabilities.elicitation = Some(
ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
@ -779,66 +760,6 @@ 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,
@ -1189,11 +1110,6 @@ 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,10 +52,6 @@ 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 {
@ -74,7 +70,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_unchecked(REQUEST_STATE_KEY),
request_state_codec: RequestStateCodec::new(REQUEST_STATE_KEY),
tasks: TaskManager::new(),
}
}
@ -704,8 +700,8 @@ impl ServerHandler for ConformanceServer {
(name == "test_custom_header").then(custom_header_tool)
}
fn get_info(&self) -> ServerConfig {
ServerConfig::new(
fn get_info(&self) -> ServerInfo {
ServerInfo::new(
ServerCapabilities::builder()
.enable_prompts()
.enable_prompts_list_changed()
@ -1744,9 +1740,7 @@ async fn main() -> anyhow::Result<()> {
tracing::info!("Starting conformance server on {}", bind_addr);
let server = ConformanceServer::new();
let config = StreamableHttpServerConfig::default()
.with_allowed_origins([format!("http://{bind_addr}")])
.enforce_origin_validation();
let config = StreamableHttpServerConfig::default();
let service = StreamableHttpService::new(
move || Ok(server.clone()),
LocalSessionManager::default().into(),

View file

@ -7,48 +7,6 @@ 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 = "3", features = ["full"]}
syn = {version = "2", features = ["full"]}
quote = "1"
proc-macro2 = "1"
serde_json = "1.0"
darling = { version = "0.24" }
darling = { version = "0.23" }
[features]
local = []

View file

@ -56,7 +56,6 @@ 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
///
@ -123,37 +122,6 @@ 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())
@ -217,8 +185,8 @@ pub fn tool_router(attr: TokenStream, input: TokenStream) -> TokenStream {
/// ```rust,ignore
/// #[tool_handler]
/// impl ServerHandler for MyToolHandler {
/// fn get_info(&self) -> ServerConfig {
/// ServerConfig::new(ServerCapabilities::builder().enable_tools().build())
/// fn get_info(&self) -> ServerInfo {
/// ServerInfo::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 syn::ReceiverKind::Reference(_, receiver_lt, _) = &receiver.kind
&& let Some((_, receiver_lt)) = receiver.reference.as_ref()
{
if let Some(receiver_lt) = receiver_lt {
lt = quote! { #receiver_lt };
@ -259,20 +259,6 @@ 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,20 +57,16 @@ 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: supports_cache_hints.then_some(0),
cache_scope: supports_cache_hints
.then_some(rmcp::model::CacheScope::Public),
ttl_ms: None,
cache_scope: None,
})
}
};

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