Compare commits

..
122 changed files with 1638 additions and 14808 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
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
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
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
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
uses: github/codeql-action/analyze@v4

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.0.1", path = "./crates/rmcp" }
rmcp-macros = { version = "3.0.1", path = "./crates/rmcp-macros" }
[workspace.package]
edition = "2024"
rust-version = "1.88"
version = "3.4.0"
version = "3.0.1"
authors = ["4t145 <u4t145@163.com>"]
license = "Apache-2.0"
repository = "https://github.com/modelcontextprotocol/rust-sdk/"

View file

@ -1,65 +0,0 @@
# Dependency Policy
This document describes how the Rust MCP SDK (`rmcp`, `rmcp-macros`) selects,
updates, and maintains its dependencies. It applies to all crates in the workspace.
## Goals
- Keep dependencies current and secure without churn that destabilizes the public API.
- Minimize the dependency footprint we impose on downstream users.
- Make dependency updates reviewable and traceable.
## Choosing dependencies
Before adding a new dependency we consider:
- **Necessity** — whether the functionality is worth an added dependency, or is small
enough to implement directly.
- **Maintenance & trust** — active maintenance, a healthy release history, and a
permissive, compatible license (the workspace is `Apache-2.0`).
- **Footprint** — transitive dependency count and compile-time cost.
- **Optionality** — anything not needed by every user should sit behind a Cargo
feature so downstream builds stay lean. The SDK is heavily feature-gated for this
reason (transports, `auth`, `schemars`, `reqwest`, etc.).
Version requirements are specified with caret (`^`, the Cargo default) constraints so
compatible upgrades are picked up automatically.
## Automated updates
Dependency updates are automated with [Dependabot](https://docs.github.com/en/code-security/dependabot),
configured in [`.github/dependabot.yml`](.github/dependabot.yml):
| Ecosystem | Cadence | PR label |
| -------------- | ------- | ----------------- |
| Cargo crates | Weekly | `T-dependencies` |
| GitHub Actions | Daily | `T-CI` |
Dependabot opens a limited number of PRs at a time (currently 3 per ecosystem) to
keep the review queue manageable. Update PRs run the full CI suite (build, format,
clippy, tests, and conformance) and must be green before merge.
## Review and merge
- Patch and minor updates that pass CI are reviewed and merged promptly by maintainers.
- Major (breaking) updates are evaluated individually; they may require code changes
and are scheduled deliberately rather than merged automatically.
- Any dependency change that affects the **public API** (for example a type re-exported
from a dependency) is treated as a potential breaking change and follows
[`VERSIONING.md`](VERSIONING.md).
## Security updates
- Security fixes take priority over routine updates and are expedited.
- Vulnerabilities in the SDK itself are handled per [`SECURITY.md`](SECURITY.md) via
GitHub's private Security Advisory process.
- Advisories affecting our dependencies (e.g. via the [RustSec Advisory Database](https://rustsec.org/))
are addressed as soon as a fixed version is available; if a released version of the
SDK is impacted, a patched release is published and, where warranted, the affected
versions are yanked from crates.io.
## Minimum Supported Rust Version
Dependency upgrades must not raise the SDK's declared MSRV (`rust-version` in
`Cargo.toml`) without an explicit, documented MSRV bump — which is itself treated as a
breaking change per [`VERSIONING.md`](VERSIONING.md).

614
README.md
View file

@ -28,7 +28,6 @@ below. For the full MCP specification, see
- [Resources](#resources)
- [Prompts](#prompts)
- [Sampling](#sampling)
- [Elicitation](#elicitation)
- [Roots](#roots)
- [Logging](#logging)
- [Completions](#completions)
@ -39,10 +38,6 @@ below. For the full MCP specification, see
- [Caching](#caching)
- [Standard HTTP Headers](#standard-http-headers)
- [Stateless Streamable HTTP](#stateless-streamable-http)
- [Transports](#transports)
- [Pagination](#pagination)
- [Capability & Protocol Version Negotiation](#capability--protocol-version-negotiation)
- [JSON Schema 2020-12](#json-schema-2020-12)
- [Examples](#examples)
- [OAuth Support](#oauth-support)
- [Related Resources](#related-resources)
@ -99,10 +94,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 +107,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 {
@ -259,77 +254,6 @@ impl ServerHandler for Calculator {}
See [`crates/rmcp-macros`](crates/rmcp-macros/README.md) for full macro documentation.
#### Tool result content types
Beyond a plain `String`, tools can return images, audio, embedded resources, and
mixed content. Build a `CallToolResult` from a `Vec<ContentBlock>`:
```rust,ignore
use rmcp::model::{CallToolResult, ContentBlock, ResourceContents};
#[tool(description = "Render a chart")]
async fn chart(&self) -> Result<CallToolResult, McpError> {
let png_base64 = render_png(); // base64-encoded image bytes
let wav_base64 = render_wav(); // base64-encoded audio bytes
Ok(CallToolResult::success(vec![
// Text
ContentBlock::text("Here is your chart:"),
// Image — base64 data + MIME type
ContentBlock::image(png_base64, "image/png"),
// Audio — base64 data + MIME type
ContentBlock::audio(wav_base64, "audio/wav"),
// Embedded resource — inline text (or ResourceContents::blob for binary)
ContentBlock::resource(ResourceContents::text(
"chart source data",
"chart://last/data.csv",
)),
]))
}
# fn render_png() -> String { String::new() }
# fn render_wav() -> String { String::new() }
```
Image and audio data are base64 strings with a MIME type. For embedded
resources, `ResourceContents::text(..)` inlines text and
`ResourceContents::blob(base64, uri)` inlines binary.
#### Error handling
Two failure modes, chosen by **whose problem it is**:
- **Tool-level error** — `Ok(CallToolResult::error(vec![...]))`. The tool ran but
failed in a way the caller should see (no rows matched, upstream 500). The
client renders your `content`, so the message reaches the user. Use this for
almost every "the tool ran and didn't work" case.
- **Protocol error** — `Err(McpError)` with a JSON-RPC code (e.g.
`McpError::invalid_params(..)`). Use this when the server can't route or process
the request at all; clients render these opaquely, so the caller does **not**
see your message.
```rust,ignore
use rmcp::model::{CallToolResult, ContentBlock};
use rmcp::ErrorData as McpError;
#[tool(description = "Look up a record")]
async fn lookup(&self, Parameters(args): Parameters<LookupArgs>) -> Result<CallToolResult, McpError> {
// Malformed request — the server can't run anything → protocol error.
if args.query.is_empty() {
return Err(McpError::invalid_params("query must be non-empty", None));
}
// Tool ran, no result → tool-level error the user should see.
let rows = self.run_query(&args.query).await;
if rows.is_empty() {
return Ok(CallToolResult::error(vec![ContentBlock::text(
format!("no rows matched '{}'", args.query),
)]));
}
Ok(CallToolResult::success(vec![ContentBlock::text(format_rows(&rows))]))
}
```
### Client-side
```rust,ignore
@ -369,8 +293,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(),
@ -404,28 +328,6 @@ impl ServerHandler for MyServer {
"memo://insights" => Ok(ReadResourceResult::new(vec![
ResourceContents::text("Analysis results...", &request.uri),
])),
// Binary resource — base64-encode the bytes and return a blob.
"file:///logo.png" => {
use base64::{Engine, prelude::BASE64_STANDARD};
let bytes = std::fs::read("logo.png").unwrap_or_default();
let blob = BASE64_STANDARD.encode(bytes);
Ok(ReadResourceResult::new(vec![
ResourceContents::blob(blob, &request.uri)
.with_mime_type("image/png"),
]))
}
// Template-expanded URI — the client fills in `{user_id}` from the
// `users://{user_id}/profile` template declared in
// `list_resource_templates`, and the server reads the concrete URI.
uri if uri.starts_with("users://") && uri.ends_with("/profile") => {
let user_id = uri
.trim_start_matches("users://")
.trim_end_matches("/profile");
Ok(ReadResourceResult::new(vec![ResourceContents::text(
format!(r#"{{"id": "{user_id}", "name": "User {user_id}"}}"#),
uri,
)]))
}
_ => Err(McpError::resource_not_found(
"resource_not_found",
Some(json!({ "uri": request.uri })),
@ -438,12 +340,8 @@ impl ServerHandler for MyServer {
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> Result<ListResourceTemplatesResult, McpError> {
// Declare a URI template with a `{user_id}` parameter. Clients expand it
// (e.g. `users://42/profile`) and pass the concrete URI to `read_resource`.
Ok(ListResourceTemplatesResult {
resource_templates: vec![
ResourceTemplate::new("users://{user_id}/profile", "user-profile"),
],
resource_templates: vec![],
next_cursor: None,
meta: None,
})
@ -464,12 +362,8 @@ let result = client.read_resource(
ReadResourceRequestParams::new("file:///config.json"),
).await?;
// List resource templates, then read a resource through one by expanding its
// parameters into a concrete URI (`users://{user_id}/profile` → `users://42/profile`).
// List resource templates
let templates = client.list_all_resource_templates().await?;
let profile = client.read_resource(
ReadResourceRequestParams::new("users://42/profile"),
).await?;
```
### Notifications
@ -582,8 +476,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())
}
}
```
@ -593,30 +487,6 @@ Prompt functions support several return types:
- `GetPromptResult` -- messages with an optional description
- `Result<T, McpError>` -- either of the above, with error handling
#### Image and embedded-resource content
A `PromptMessage` can also carry an image or embedded resource. Use the
dedicated constructors (image/audio require the `base64` feature):
```rust,ignore
use rmcp::model::{PromptMessage, Role};
// Image content — raw bytes are base64-encoded for you.
let screenshot: &[u8] = load_png();
let msg = PromptMessage::new_image(Role::User, screenshot, "image/png", None, None);
// Embedded resource — inline a text resource by URI. Pass `Some(text)` for a
// text resource, or `None` for a blob resource.
let msg = PromptMessage::new_resource(
Role::User,
"file:///spec.md".to_string(),
Some("text/markdown".to_string()),
Some("# Specification\n...".to_string()),
None, None, None,
);
# fn load_png() -> &'static [u8] { &[] }
```
### Client-side
```rust
@ -719,140 +589,6 @@ impl ClientHandler for MyClient {
---
## Elicitation
Elicitation lets a server pause mid-operation to ask the user for input, in one
of two modes: **form mode** (structured fields with a JSON Schema) or **URL
mode** (send the user to a web page and wait for completion).
**MCP Spec:** [Elicitation](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation)
### Server-side (form mode)
Define a struct deriving `JsonSchema`, mark it `elicit_safe!`, and call
`elicit::<T>()` on the peer. Schema validation, defaults, and enum choices all
come from the type.
```rust,ignore
use rmcp::{elicit_safe, model::*, service::{RequestContext, RoleServer}};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[schemars(description = "User information")]
pub struct UserInfo {
#[schemars(description = "User's name")]
pub name: String,
// Optional field; omitted if the user doesn't provide it.
#[serde(default)]
#[schemars(description = "Preferred greeting")]
pub greeting: Option<String>,
}
// Whitelist the type for elicitation (schema-validated on both ends).
elicit_safe!(UserInfo);
#[tool(description = "Greet the user")]
async fn greet(&self, ctx: RequestContext<RoleServer>) -> Result<CallToolResult, McpError> {
// Returns Ok(Some(UserInfo)) if the user accepts.
// Decline and cancel are returned as ElicitationError variants.
match ctx.peer.elicit::<UserInfo>("Please provide your name").await {
Ok(Some(info)) => Ok(CallToolResult::success(vec![ContentBlock::text(
format!("Hello, {}!", info.name),
)])),
Ok(None) => Ok(CallToolResult::success(vec![ContentBlock::text(
"No name provided.",
)])),
Err(e) => Ok(CallToolResult::error(vec![ContentBlock::text(
format!("Elicitation failed: {e}"),
)])),
}
}
```
#### Enum values
Enum fields become a choice list. `schemars` needs two hints to inline and type
the enum correctly:
```rust,ignore
#[derive(Debug, Serialize, Deserialize, JsonSchema, Default)]
#[schemars(inline)] // inline the enum into the parent schema
#[schemars(extend("type" = "string"))] // schemars omits `type` for enums; add it
enum Priority {
#[schemars(title = "Low priority")]
#[default]
Low,
#[schemars(title = "High priority")]
High,
}
```
See [`examples/servers/src/elicitation_enum_inference.rs`](examples/servers/src/elicitation_enum_inference.rs)
for single-select, multi-select, titled, and defaulted enum forms.
### Server-side (URL mode)
For flows a form can't capture (OAuth consent, a payment page), send the user to
a URL. `elicit_url` returns the user's `ElicitationAction` rather than typed data:
```rust,ignore
use rmcp::model::ElicitationAction;
use url::Url;
let action = ctx.peer.elicit_url(
"Please complete setup in your browser",
Url::parse("https://example.com/setup").unwrap(),
"setup-123", // a unique elicitation id
).await?;
match action {
ElicitationAction::Accept => { /* user consented */ }
ElicitationAction::Decline => { /* user declined */ }
ElicitationAction::Cancel => { /* user aborted */ }
}
```
### Client-side
Implement `ClientHandler::create_elicitation()`, matching on the request variant
to handle form vs. URL mode:
```rust,ignore
use rmcp::{ClientHandler, model::*, service::{RequestContext, RoleClient}};
impl ClientHandler for MyClient {
async fn create_elicitation(
&self,
request: ElicitRequestParams,
_context: RequestContext<RoleClient>,
) -> Result<ElicitResult, ErrorData> {
match request {
ElicitRequestParams::FormElicitationParams { message, .. } => {
// Show `message` + the requested schema, collect input, then:
Ok(ElicitResult {
action: ElicitationAction::Accept,
content: Some(rmcp::object!({ "name": "Ada" })),
meta: None,
})
}
ElicitRequestParams::UrlElicitationParams { url, .. } => {
// Open `url`, wait for the user, then report the action.
let _ = url;
Ok(ElicitResult { action: ElicitationAction::Accept, content: None, meta: None })
}
}
}
}
```
On completion the client sends a `notifications/elicitation/response`
notification to release the waiting server-side `elicit_url` call.
**Example:** [`examples/servers/src/elicitation_stdio.rs`](examples/servers/src/elicitation_stdio.rs) (form + URL), [`examples/servers/src/elicitation_enum_inference.rs`](examples/servers/src/elicitation_enum_inference.rs) (enum forms)
---
## Roots
> **Deprecated (SEP-2577):** Roots is deprecated and will be removed in a future release. It remains fully functional for now. See [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577).
@ -934,8 +670,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 +743,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()
@ -1022,7 +758,6 @@ impl ServerHandler for MyServer {
_context: RequestContext<RoleServer>,
) -> Result<CompleteResult, McpError> {
let values = match &request.r#ref {
// Completion for a prompt argument (`ref/prompt`).
Reference::Prompt(prompt_ref) if prompt_ref.name == "sql_query" => {
match request.argument.name.as_str() {
"operation" => vec!["SELECT", "INSERT", "UPDATE", "DELETE"],
@ -1043,17 +778,6 @@ impl ServerHandler for MyServer {
_ => vec![],
}
}
// Completion for a resource-template argument (`ref/resource`). The
// `uri` identifies the template (e.g. `users://{user_id}/profile`)
// and `argument.name` is the template variable being completed.
Reference::Resource(resource_ref)
if resource_ref.uri == "users://{user_id}/profile" =>
{
match request.argument.name.as_str() {
"user_id" => vec!["1", "2", "42"],
_ => vec![],
}
}
_ => vec![],
};
@ -1075,22 +799,12 @@ impl ServerHandler for MyServer {
```rust
use rmcp::model::*;
// Completion for a prompt argument.
let result = client.complete(CompleteRequestParams::new(
Reference::for_prompt("sql_query"),
ArgumentInfo::new("operation", "SEL"),
)).await?;
// result.completion.values contains suggestions like ["SELECT"]
// Completion for a resource-template argument: reference the template by URI
// and complete one of its variables (`user_id`).
let resource_completion = client.complete(CompleteRequestParams::new(
Reference::for_resource("users://{user_id}/profile"),
ArgumentInfo::new("user_id", "4"),
)).await?;
// resource_completion.completion.values contains suggestions like ["42"]
```
**Example:** [`examples/servers/src/completion_stdio.rs`](examples/servers/src/completion_stdio.rs)
@ -1151,55 +865,6 @@ impl ServerHandler for MyServer {
}
```
### Ping
Either side can send a `ping` request to check that its counterpart is still
responsive and the connection is alive. A ping carries no parameters and the
receiver replies with an empty result. Because pings can flow in both
directions, `rmcp` handles them symmetrically:
- **Sending a ping** — construct a `PingRequest` and send it over the peer.
A client pings the server with `ClientRequest::PingRequest`; a server pings
the client with `ServerRequest::PingRequest`. `send_request` resolves once the
empty response arrives, so a returned `Ok` confirms the peer is reachable:
```rust
use rmcp::model::{PingRequest, ServerRequest};
// From a server, ping the connected client to verify it is still alive.
context.peer
.send_request(ServerRequest::PingRequest(PingRequest::default()))
.await?;
```
```rust
use rmcp::model::{ClientRequest, PingRequest};
// From a client, ping the server. `running` is the value returned by serve().
running
.send_request(ClientRequest::PingRequest(PingRequest::default()))
.await?;
```
- **Responding to a ping** — `rmcp` answers incoming pings automatically. The
default `ping` method on `ServerHandler` and `ClientHandler` returns an empty
result, so no code is required. Override it only if you want to run custom
logic (for example, health checks) when a ping arrives:
```rust
impl ServerHandler for MyServer {
async fn ping(
&self,
_context: RequestContext<RoleServer>,
) -> Result<(), McpError> {
// Custom liveness logic here, if any.
Ok(())
}
}
```
**MCP Spec:** [Ping](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping)
### Initialized notification
Legacy clients send `initialized` after the `initialize` handshake completes.
@ -1253,8 +918,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 +1028,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
@ -1565,13 +1220,6 @@ let router = axum::Router::new().nest_service("/mcp", service);
> Because there is no per-session state, the `service_factory` runs per request.
> Keep shared state (DB pools, caches) in a `Clone` handle captured by the
> closure; don't rely on in-memory state surviving between requests.
>
> Modern-only servers can additionally call
> `with_stateless_protocol_metadata_required(true)` to reject the compatibility
> fallback for requests missing their per-request protocol signals. rmcp clients
> negotiated below `2026-07-28` do not attach that body metadata and will be
> rejected, so pair this option with a `supported_protocol_versions`
> implementation that advertises only `2026-07-28` and later.
### Client-side
@ -1584,231 +1232,13 @@ 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)
---
## Transports
A transport moves JSON-RPC messages between client and server. Any `Transport`
impl can be passed to `.serve(..)`; `rmcp` ships the common ones behind Cargo
features.
**MCP Spec:** [Transports](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports)
| Transport | Feature(s) | Notes |
| ---------------------------- | -------------------------------------------- | ----- |
| **stdio** | `transport-io` (client + server) | Communicate over `stdin`/`stdout`; the standard way to launch local MCP servers as child processes. |
| **Child process** (client) | `transport-child-process` | Spawn a server binary and talk to it over its stdio. |
| **Streamable HTTP** (server) | `transport-streamable-http-server` | The current HTTP transport. Exposes a Tower service you can mount on any router. |
| **Streamable HTTP** (client) | `transport-streamable-http-client-reqwest` | HTTP client transport built on `reqwest`. |
| **Worker / in-process** | `transport-worker` | For embedding or testing without real I/O. |
### stdio
```rust,ignore
use rmcp::{ServiceExt, transport::stdio};
// Server: serve over stdin/stdout.
let server = MyServer.serve(stdio()).await?;
server.waiting().await?;
```
```rust,ignore
use rmcp::{ServiceExt, transport::{TokioChildProcess, ConfigureCommandExt}};
use tokio::process::Command;
// Client: launch a server binary and talk to it over its stdio.
let transport = TokioChildProcess::new(Command::new("uvx").configure(|cmd| {
cmd.arg("mcp-server-git");
}))?;
let client = ().serve(transport).await?;
```
### Streamable HTTP
`StreamableHttpService` is a Tower service — mount it on any `axum`/`hyper`
router (see [Stateless Streamable HTTP](#stateless-streamable-http) for the full
server example). The client transport connects with a single URI:
```rust,ignore
use rmcp::transport::StreamableHttpClientTransport;
let transport = StreamableHttpClientTransport::from_uri("http://localhost:8000/mcp");
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
`text/event-stream` (Server-Sent Events) stream when the server pushes
notifications or requests before the result. `rmcp` handles both automatically
(SSE parsing lives behind the `client-side-sse` feature). There is no separate
"SSE transport" to configure — it's an implementation detail of Streamable HTTP.
#### Legacy HTTP+SSE transport (`2024-11-05`) — intentionally not provided
The standalone two-endpoint **HTTP+SSE transport** defined in protocol revision
`2024-11-05` (a separate `GET` SSE channel plus a `POST` message endpoint) is a
**deliberate non-goal** for `rmcp`. It was [replaced by Streamable HTTP in the
`2025-03-26` revision](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports),
and `rmcp` targets current spec revisions (`2025-11-25` and `2026-07-28`), so it
ships **no legacy HTTP+SSE client or server transport**.
What to use instead:
- **New client/server code** — use [Streamable HTTP](#streamable-http). It carries
the same SSE streaming semantics over a single endpoint and is the transport all
supported spec revisions expect.
- **Server-to-client streaming** (push notifications, resource updates) — this is
built into Streamable HTTP; see [Subscriptions](#subscriptions).
- **Talking to a legacy `2024-11-05`-only server** — front it with a proxy that
speaks Streamable HTTP, or pin a dependency to a release that predates the
transport's removal. `rmcp` will not add the legacy transport back.
This is a supported-surface decision, not a missing feature: every transport
`rmcp` implements is listed in the [Transports](#transports) table above.
---
## Pagination
List operations (`tools/list`, `prompts/list`, `resources/list`,
`resources/templates/list`) are paginated via a `next_cursor`. The `list_all_*`
helpers walk every page for you:
```rust,ignore
// Fetches all pages transparently.
let tools = client.list_all_tools().await?;
let prompts = client.list_all_prompts().await?;
let resources = client.list_all_resources().await?;
```
To page manually, call the single-page method and follow `next_cursor` until
it's `None`:
```rust,ignore
use rmcp::model::PaginatedRequestParams;
let mut cursor = None;
loop {
let page = client
.list_tools(Some(PaginatedRequestParams { meta: None, cursor }))
.await?;
for tool in &page.tools {
// handle each tool
}
cursor = page.next_cursor;
if cursor.is_none() {
break;
}
}
```
On the server, return a `next_cursor` from your `list_*` handler when more pages
remain (`None` when complete).
**MCP Spec:** [Pagination](https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/pagination)
---
## Capability & Protocol Version Negotiation
### Capabilities
At initialization, client and server exchange **capabilities** so each side
knows what the other supports. Declare yours with the `ServerCapabilities`
builder in `get_info()`:
```rust,ignore
use rmcp::model::{ServerCapabilities, ServerConfig};
fn get_info(&self) -> ServerConfig {
ServerConfig::new(
ServerCapabilities::builder()
.enable_tools()
.enable_prompts()
.enable_resources()
.enable_resources_subscribe()
.enable_tool_list_changed()
.enable_logging()
.build(),
)
}
```
Clients do the same via `ClientCapabilities::builder()`. Macros like
`#[tool_handler]` / `#[prompt_handler]` set the relevant flags automatically.
After connecting, read the peer's capabilities via `peer.peer_info()`.
### Protocol version
MCP is versioned by date. `rmcp` negotiates automatically on connect — the
client offers a preferred `ProtocolVersion` and falls back to one the server
supports:
```rust,ignore
use rmcp::model::ProtocolVersion;
ProtocolVersion::LATEST; // newest stable version this SDK defaults to
ProtocolVersion::V_2026_07_28; // a specific version constant
ProtocolVersion::KNOWN_VERSIONS; // every version this SDK understands
```
Version-specific behavior (SEP-2243 headers, SEP-2567 stateless serving, the
SEP-2575 subscription model) is gated on the negotiated version, so older clients
keep working while newer ones opt in.
**MCP Spec:** [Versioning and Compatibility](https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning)
---
## JSON Schema 2020-12
Deriving `schemars::JsonSchema` on your parameter and result types generates
[JSON Schema draft 2020-12](https://json-schema.org/) — the dialect the MCP spec
requires — for the tool's `inputSchema` and `outputSchema`.
```rust,ignore
use rmcp::schemars;
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct SearchParams {
/// Full-text query.
query: String,
/// Maximum number of results.
#[serde(default)]
limit: Option<u32>,
}
```
Field names, types, and doc comments flow into the schema — no manual authoring
needed. As of `2026-07-28` (SEP-2106), `outputSchema` may be any JSON Schema type
(not only `object`) and `structuredContent` may be any JSON value.
---
## Examples
See [examples](examples/README.md).

View file

@ -1,112 +1,116 @@
# RMCP Roadmap
This roadmap tracks the path to [SEP-1730](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1730) Tier 1 for the Rust MCP SDK.
This roadmap tracks the path to SEP-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)),
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
document now serves as the ongoing tracker for spec conformance and SDK health.
Spec 2025-11-25 (suite 0.1.16): Server 100% (30/30) · Client 100% (18/18)
Spec 2026-07-28 (suite 0.2.0-alpha.9): Server 97.5% (39/40) · Client 90.6% (29/32)
| SEP-1730 Tier 1 requirement | Status | Evidence |
| --------------------------------------------------- | ------ | -------- |
| Server conformance 100% (date-versioned) | ✅ | 30/30 — see below |
| 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) |
| 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) |
| Published roadmap tracking spec components | ✅ | this document |
| Suite (date-versioned) | Server | Client |
| ---------------------- | ------------- | ------------- |
| 2025-11-25 | 100% (30/30) | 100% |
| 2026-07-28 | 100% (30/30) | 100% |
Only date-versioned scenarios count toward SDK tiering. `draft` (2026-07-28 draft)
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
> flag is a tooling artifact, not an unmet requirement.
Extension scenarios are reported separately below because they are
informational and do not count toward SDK tiering.
---
## Conformance
## Target spec: 2026-07-28 (release 2026-07-28)
Per-scenario status for the current spec is tracked in the epic issue
[#977 — Tracking: 2026-07-28 spec conformance](https://github.com/modelcontextprotocol/rust-sdk/issues/977),
under the [`2026-07-28 spec` milestone](https://github.com/modelcontextprotocol/rust-sdk/milestone/3).
All 2026-07-28 work carries the `2026-07-28` label and the
[`2026-07-28 spec` milestone](https://github.com/modelcontextprotocol/rust-sdk/milestone/3).
Per-scenario conformance status is tracked in the epic issue:
[#977 — Tracking: 2026-07-28 spec conformance](https://github.com/modelcontextprotocol/rust-sdk/issues/977).
CI (`.github/workflows/conformance.yml`) runs the full `2025-11-25` and `2026-07-28`
server and client suites on every push and PR. Both suites are fully green.
### Versioned-spec conformance (baseline 2026-07-21, suite `0.2.0-alpha.9`)
### Informational (not scored for tiering)
- Server: 1 expected failure: `json-schema-2020-12`
- Client: 3 expected failures: `tools_call`, `auth/scope-step-up`, and `auth/authorization-server-migration`
- CI: runs the complete `2026-07-28` versioned-spec suites with a strict baseline; an unlisted failure or a listed scenario that starts passing fails the build
Extension-tagged scenarios are excluded by `--spec-version` filters, so CI runs them
in separate steps against `conformance/expected-failures-extensions.yaml`:
### Extension conformance (informational)
| Scenario | Tag | Status |
| --------------------------------------- | ---------- | ------ |
| `auth/client-credentials-basic` | extension | ✅ Pass |
| `auth/client-credentials-jwt` | extension | ✅ Pass |
| `auth/enterprise-managed-authorization` | extension | ❌ Expected failure (not implemented by the conformance client) |
| `auth/wif-jwt-bearer` | 2026-07-28 draft | ❌ Expected failure (WIF / SEP-1933, draft) |
| `tasks-*` (SEP-2663) | extension | ❌ 9 expected failures · ⏭️ 1 upstream-skipped |
Extension-tagged scenarios are excluded by `--spec-version` filters, so CI
runs them in separate server and client steps with
`conformance/expected-failures-extensions.yaml`.
- SEP-2663 Tasks server: 9 expected failures; `tasks-status-notifications` is currently skipped by the upstream harness; tracked in #868
- Client extensions: `auth/client-credentials-basic` and `auth/client-credentials-jwt` pass; `auth/enterprise-managed-authorization` is an expected failure
### Spec features without conformance scenarios
Conformance does not cover the entire spec surface. Remaining feature work tracked via
the milestone:
Conformance alone does not cover the full spec surface. Feature work tracked via the milestone:
- SEP-2567 sessionless MCP via explicit state handles (#870)
- SEP-2260 server requests must associate with a client request (#873)
- SEP-2549 follow-up: client-side TTL-honoring cache (#974)
(SEP-2575 discovery & negotiation is covered by the `server-stateless` conformance scenario;
implementation is in review — #869, PRs #973, #943.)
### Release
The 2026-07-28 implementation ships as **v3.0.0** (release PR #964): MRTR, SEP-2549 cache hints,
SEP-2243 standard headers, and the SEP-2106 relaxations are merged but unreleased — tiering and
relegation are evaluated against the latest stable release, so cutting v3.0.0 with the remaining
conformance fixes is on the critical path. Migration guide (draft, kept current until release):
[discussion #969](https://github.com/modelcontextprotocol/rust-sdk/discussions/969).
---
## Tier 1 (non-conformance requirements)
### Governance & Policy
- [ ] Create `VERSIONING.md` — document semver scheme, what constitutes a breaking change, and how breaking changes are communicated
- [ ] Publish a dependency update policy (Tier 1 requires a published policy)
- [ ] Cut v3.0.0 (#964) including all conformance fixes (tier relegation is evaluated against the latest stable release)
### Documentation (26/48 → 48/48 features with prose + examples)
#### Undocumented features (14)
- [ ] Tools — image results
- [ ] Tools — audio results
- [ ] Tools — embedded resources
- [ ] Prompts — embedded resources
- [ ] Prompts — image content
- [ ] Elicitation — URL mode
- [ ] Elicitation — default values
- [ ] Elicitation — complete notification
- [ ] Ping
- [ ] SSE transport — legacy (client)
- [ ] SSE transport — legacy (server)
- [ ] Pagination
- [ ] Protocol version negotiation
- [ ] JSON Schema 2020-12 support *(upgrade from partial)*
#### Partially documented features (7)
- [ ] Tools — error handling *(add dedicated prose + example)*
- [ ] Resources — reading binary *(add dedicated example)*
- [ ] Elicitation — form mode *(add prose docs, not just example README)*
- [ ] Elicitation — schema validation *(add prose docs)*
- [ ] Elicitation — enum values *(add prose docs)*
- [ ] Capability negotiation *(add dedicated prose explaining the builder API)*
- [ ] Protocol version negotiation *(document version negotiation behavior)*
---
## Completed
### 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
- [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)
- [x] 2026-07-28 client conformance 100% (dated)
- [x] Issue triage ≥90% within 2 business days (95.2%, 20/21) with the full SEP-1730
label taxonomy (bug, enhancement, question, needs confirmation, needs repro,
ready for work, good first issue, help wanted, P0–P3)
- [x] All P0 bugs resolved within 7 days (0 open; #741 resolved in 3 days). #815 was
reclassified from `P0` to `T-security` — it was a CVE/advisory-coordination task
for an already-shipped fix (PR #764, released in v1.4.0), not a critical-bug fix
- [x] `VERSIONING.md` — semver scheme, breaking-change definition, and communication policy
- [x] `DEPENDENCY_POLICY.md` — published dependency update policy (with `.github/dependabot.yml`)
- [x] `SECURITY.md` and Dependabot configuration
- [x] All 48 non-experimental features documented with examples in the README
(closed the last 6 gaps on 2026-07-30: audio results, resource-template reading,
resource-argument completion, ping, and the legacy HTTP+SSE non-goal writeup)
### Spec implementation
- [x] SEP-2322 MRTR (server scenarios + `sep-2322-client-request-state`)
- [x] SEP-2575 Make MCP Stateless (`server-stateless`)
- [x] 2025-11-25 server conformance 100% (30 scenarios + pending `json-schema-2020-12`, `server-sse-polling`)
- [x] 2025-11-25 client conformance 100% (18 scenarios + legacy `auth/2025-03-26-*`)
- [x] SEP-2322 MRTR (14 server scenarios + `sep-2322-client-request-state`)
- [x] SEP-2164 resource not found
- [x] SEP-2549 cache hints (`caching`)
- [x] SEP-2243 HTTP standardization (`http-header-validation`, standard headers)
- [x] DNS rebinding protection
- [x] Cache hints (`caching`)
- [x] `http-header-validation`
- [x] Issue triage labels (bug, enhancement, needs confirmation, needs repro, ready for work, P0–P3)
---
## Nice-to-have (scorecard hygiene, not required for Tier 1)
## Informational (not scored for tiering)
- [ ] Add a top-level `CHANGELOG.md` (release notes are currently managed by release-plz).
- [ ] Add a top-level `CONTRIBUTING.md` (contributor docs currently live at `docs/CONTRIBUTE.MD`).
These extension scenarios are tracked but do not count toward tier advancement:
| Scenario | Tag | Status |
|---|---|---|
| `auth/client-credentials-jwt` | extension | ✅ Passed |
| `auth/client-credentials-basic` | extension | ✅ Passed |
| `auth/enterprise-managed-authorization` | extension | ❌ Failed — scenario is not implemented by the conformance client |
| `tasks-*` | extension | ❌ 9 expected failures · ⏭️ 1 upstream-skipped scenario |

View file

@ -1,66 +0,0 @@
# Versioning Policy
`rmcp` and `rmcp-macros` follow [Semantic Versioning 2.0.0](https://semver.org/),
the standard versioning scheme for the Rust/Cargo ecosystem. Given a version
`MAJOR.MINOR.PATCH`:
- **MAJOR** — incremented for breaking changes to the public API.
- **MINOR** — incremented for new, backwards-compatible functionality.
- **PATCH** — incremented for backwards-compatible bug fixes.
The `rmcp` and `rmcp-macros` crates are versioned together and released with the
same version number.
## What counts as a breaking change
We treat a change as breaking if it would cause code that compiled against the
previous release to fail to compile, or to change behavior in a way callers could
not reasonably anticipate. This follows the Cargo SemVer guidelines
([The Cargo Book — SemVer Compatibility](https://doc.rust-lang.org/cargo/reference/semver.html)).
Examples include:
- Removing or renaming a public item (function, type, trait, module, variant, field).
- Changing a public function signature, trait method, or trait bound.
- Adding a required method to a public trait, or a field to a struct that callers
construct directly. (Public structs and enums are marked `#[non_exhaustive]` where
practical so that additive changes remain non-breaking.)
- Raising the Minimum Supported Rust Version (MSRV) — see below.
Additive changes — new functions, new types, new enum variants on
`#[non_exhaustive]` enums, new optional Cargo features — are **not** breaking and
ship in MINOR releases.
### Cargo features
The public API is feature-gated. SemVer compatibility is evaluated against the
crate's **default features** and all features except `local`.
## Pre-1.0 crates in the workspace
The core crates (`rmcp`, `rmcp-macros`) are `>= 1.0.0` and follow the rules above.
Any workspace crate still in `0.x` follows Cargo's pre-1.0 convention, where the
`MINOR` version acts as the breaking-change signal (`0.MINOR.PATCH`).
## Minimum Supported Rust Version (MSRV)
The MSRV is declared via `rust-version` in `Cargo.toml`. Raising the MSRV is
considered a breaking change and is only done in a MINOR or MAJOR release with a
note in the release notes.
## How breaking changes are communicated
- Release automation is handled by [release-plz](https://release-plz.dev/), which
derives version bumps from [Conventional Commits](https://www.conventionalcommits.org/)
(`feat!:` / `fix!:` and `BREAKING CHANGE:` footers trigger a MAJOR bump) and runs
`cargo-semver-checks` to catch unintended API breaks.
- Every release is published as a tagged [GitHub Release](https://github.com/modelcontextprotocol/rust-sdk/releases)
with generated notes.
- Significant migrations (for example the 2.x → 3.x upgrade) are accompanied by a
migration guide linked from the release notes and the README.
## Yanking
A published version that is later found to be broken or to contain a security issue
may be [yanked](https://doc.rust-lang.org/cargo/commands/cargo-yank.html) from
crates.io. Yanking prevents new dependents from selecting the version but does not
delete it; a fixed PATCH release is published alongside.

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,60 +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
- add strict stateless protocol metadata validation ([#1091](https://github.com/modelcontextprotocol/rust-sdk/pull/1091))
### Other
- document the ping utility with examples ([#1106](https://github.com/modelcontextprotocol/rust-sdk/pull/1106))
- complete Tier 1 feature docs and finalize roadmap ([#1101](https://github.com/modelcontextprotocol/rust-sdk/pull/1101))
- *(conformance)* meeting requirements for tier 1 ([#1087](https://github.com/modelcontextprotocol/rust-sdk/pull/1087))
## [3.0.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v3.0.0-beta.5...rmcp-macros-v3.0.0) - 2026-07-28
### Other

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())
/// }
/// }
/// ```

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