Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbd2382755 |
||
|
|
9f7ace357a |
||
|
|
011f281ced |
||
|
|
0cfbe7f453 |
||
|
|
fd7811fdaa |
||
|
|
0550e13c1b |
||
|
|
b037c0fa88 |
||
|
|
d481cb0eec |
||
|
|
47fcdf4b57 |
||
|
|
165ff92df1 |
||
|
|
fbfc5cc96e |
||
|
|
3075dc9152 |
||
|
|
a5d1169e20 |
||
|
|
4fa84a5130 |
||
|
|
b86995aebd |
||
|
|
e27c5d1c24 |
||
|
|
3e636cab26 |
||
|
|
b2c90b1edc |
||
|
|
46db531df9 |
||
|
|
744b9f904c |
||
|
|
e8a0d7099a |
||
|
|
6fa40536de |
||
|
|
f9a68bfa5c |
||
|
|
7d14f3a8a2 |
||
|
|
04c2f3fac9 |
||
|
|
add9cbeab5 |
||
|
|
302319861a |
||
|
|
3b5ca4dd3f |
||
|
|
42cf8d9fa3 |
||
|
|
51ccb42993 |
||
|
|
ad9832ec21 |
||
|
|
4e74499783 |
||
|
|
3a2ebbc920 |
||
|
|
1afde5b4fc |
||
|
|
ec041b7430 |
||
|
|
3ef34e64e7 |
||
|
|
12db02833f |
||
|
|
c8bb1c6f7d |
||
|
|
3501f3e646 |
||
|
|
103bf239b3 |
||
|
|
78cf9f1cb6 |
||
|
|
02decfb682 |
||
|
|
697c497724 |
||
|
|
4a738b9dd9 |
||
|
|
6f8dcde04f |
||
|
|
d5c46b3fe0 |
||
|
|
38428f66bc |
||
|
|
018ab24fac |
||
|
|
f9b39f3c13 |
||
|
|
0f164cd67e |
||
|
|
fd14830a19 |
||
|
|
0282172f33 |
||
|
|
248cbca688 |
||
|
|
f713ebd1a6 |
||
|
|
a50a73fda2 |
||
|
|
d81bfa8854 |
||
|
|
3f5a3f6a28 |
||
|
|
a0e103c970 |
||
|
|
b5cf34eee7 |
||
|
|
22be2cc5ad |
||
|
|
02c62aef2e |
||
|
|
c345078867 |
||
|
|
f8e63824d3 |
||
|
|
8fb3e046a1 |
||
|
|
3c8fb2a7e4 |
||
|
|
e150d4f3a6 |
||
|
|
9a3168af87 |
||
|
|
baac607e52 |
||
|
|
f57d585168 |
||
|
|
07bcda2973 |
||
|
|
f8f9607d0b |
||
|
|
c4def55422 |
||
|
|
7e14020cb8 |
||
|
|
9cbb69d9e9 |
||
|
|
5b216605f2 |
||
|
|
684ff5a51e |
||
|
|
830e088d73 |
||
|
|
1f9358eddc |
||
|
|
1cf6deb419 |
||
|
|
00bcf13800 |
||
|
|
65f05e9924 |
||
|
|
3240b6e782 |
||
|
|
def31f0f51 |
||
|
|
570c478962 |
||
|
|
983a1374ff |
||
|
|
58b136f44f |
||
|
|
d272389fd4 |
||
|
|
51c8ee60e3 |
||
|
|
1d0473ba75 |
||
|
|
3bb3c8dbce |
||
|
|
d82c94aa8e |
||
|
|
f3c786706a |
||
|
|
596a7e1e0d |
||
|
|
cb50ae7890 |
124 changed files with 15138 additions and 1647 deletions
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
|
|
@ -16,11 +16,15 @@ updates:
|
|||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
labels:
|
||||
# Mark PRs as CI related change.
|
||||
- T-CI
|
||||
open-pull-requests-limit: 3
|
||||
groups:
|
||||
codeql-action:
|
||||
patterns:
|
||||
- "github/codeql-action*"
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
include: "scope"
|
||||
|
|
|
|||
5
.github/workflows/auto-label-pr.yml
vendored
5
.github/workflows/auto-label-pr.yml
vendored
|
|
@ -3,6 +3,9 @@ on:
|
|||
# Runs workflow when activity on a PR in the workflow's repository occurs.
|
||||
pull_request_target:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
auto-label:
|
||||
permissions:
|
||||
|
|
@ -20,7 +23,7 @@ jobs:
|
|||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
|
||||
steps:
|
||||
- uses: actions/labeler@v7
|
||||
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7
|
||||
with:
|
||||
# Auto-include paths starting with dot (e.g. .github)
|
||||
dot: true
|
||||
|
|
|
|||
115
.github/workflows/ci.yml
vendored
115
.github/workflows/ci.yml
vendored
|
|
@ -12,34 +12,35 @@ env:
|
|||
CARGO_TERM_COLOR: always
|
||||
ARTIFACT_DIR: release-artifacts
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
commit-lint:
|
||||
name: Lint Commit Messages
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install commitlint
|
||||
run: |
|
||||
npm install --save-dev @commitlint/cli @commitlint/config-conventional
|
||||
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
|
||||
|
||||
run: npm install --global @commitlint/cli@20.4.3 @commitlint/config-conventional@20.4.3
|
||||
|
||||
- name: Lint commit messages
|
||||
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
|
||||
run: commitlint --extends @commitlint/config-conventional --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
|
||||
|
||||
fmt:
|
||||
name: Code Formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Install Rust fmt
|
||||
run: rustup toolchain install nightly --component rustfmt
|
||||
|
|
@ -51,32 +52,40 @@ jobs:
|
|||
name: Lint with Clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
|
||||
- name: Run clippy
|
||||
- name: Run clippy (all features)
|
||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
- name: Run clippy (all features except local)
|
||||
run: |
|
||||
FEATURES=$(cargo metadata --no-deps --format-version 1 \
|
||||
| jq -r '[.packages[] | select(.name == "rmcp") | .features | keys[]
|
||||
| select(startswith("__") | not)
|
||||
| select(. != "local")] | join(",")')
|
||||
cargo clippy --package rmcp --all-targets --no-default-features --features "$FEATURES" -- -D warnings
|
||||
|
||||
semver:
|
||||
name: SemVer Check
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
|
||||
- name: Install cargo-semver-checks
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@3f74d7c16a4242f1c95561e98edc25d36adb4375 # v2.87.12
|
||||
with:
|
||||
tool: cargo-semver-checks
|
||||
|
||||
|
|
@ -117,7 +126,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
|
@ -125,12 +134,12 @@ jobs:
|
|||
# to be installed (it does not need to be the default; the tool invokes it
|
||||
# via `cargo +nightly`).
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
uses: dtolnay/rust-toolchain@7c8d7d138f5c09cef361f8214cf96882cd029cdb # nightly
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
|
||||
- name: Install cargo-public-api
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@3f74d7c16a4242f1c95561e98edc25d36adb4375 # v2.87.12
|
||||
with:
|
||||
tool: cargo-public-api
|
||||
|
||||
|
|
@ -201,9 +210,9 @@ jobs:
|
|||
name: spell check with typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Spell Check Repo
|
||||
uses: crate-ci/typos@master
|
||||
uses: crate-ci/typos@d43b6c087ac471e2ea7b8af622ff15f05c0c365b # master
|
||||
|
||||
msrv:
|
||||
name: Check MSRV
|
||||
|
|
@ -211,7 +220,7 @@ jobs:
|
|||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Read MSRV from Cargo.toml
|
||||
run: |
|
||||
|
|
@ -220,11 +229,11 @@ jobs:
|
|||
echo "MSRV=$MSRV" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install MSRV Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master
|
||||
with:
|
||||
toolchain: ${{ env.MSRV }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
|
||||
- name: Check default workspace members with MSRV
|
||||
run: cargo +${{ env.MSRV }} check --all-targets --all-features
|
||||
|
|
@ -233,19 +242,19 @@ jobs:
|
|||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
# install nodejs
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install
|
||||
|
|
@ -253,7 +262,7 @@ jobs:
|
|||
- name: Create venv for python
|
||||
run: uv venv
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --all-features
|
||||
|
|
@ -262,19 +271,19 @@ jobs:
|
|||
name: Run Tests (no local feature)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
# install nodejs
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install
|
||||
|
|
@ -282,7 +291,7 @@ jobs:
|
|||
- name: Create venv for python
|
||||
run: uv venv
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
|
||||
- name: Run tests without local feature
|
||||
run: |
|
||||
|
|
@ -296,21 +305,21 @@ jobs:
|
|||
name: Code Coverage
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
# install nodejs
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install
|
||||
|
|
@ -318,7 +327,7 @@ jobs:
|
|||
- name: Create venv for python
|
||||
run: uv venv
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
|
||||
- name: Install cargo-llvm-cov
|
||||
run: cargo install cargo-llvm-cov
|
||||
|
|
@ -333,19 +342,19 @@ jobs:
|
|||
name: Example test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
# install nodejs
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install
|
||||
|
|
@ -353,7 +362,7 @@ jobs:
|
|||
- name: Create venv for python
|
||||
run: uv venv
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
|
||||
- name: Add target WASI preview 2
|
||||
run: |
|
||||
|
|
@ -389,12 +398,12 @@ jobs:
|
|||
name: Security Audit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
|
||||
- name: Install cargo-audit
|
||||
run: cargo install cargo-audit
|
||||
|
|
@ -406,12 +415,12 @@ jobs:
|
|||
name: Generate Documentation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
uses: dtolnay/rust-toolchain@7c8d7d138f5c09cef361f8214cf96882cd029cdb # nightly
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
|
||||
- name: Generate documentation
|
||||
run: |
|
||||
|
|
@ -432,7 +441,7 @@ jobs:
|
|||
# This happened recently in the attack on `tj-actions/changed-files`, but
|
||||
# has happened many times before as well.
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Update Rust
|
||||
run: |
|
||||
|
|
|
|||
11
.github/workflows/codeql.yml
vendored
11
.github/workflows/codeql.yml
vendored
|
|
@ -8,6 +8,9 @@ on:
|
|||
schedule:
|
||||
- cron: '0 0 * * 1' # Weekly on Monday
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
|
|
@ -21,16 +24,16 @@ jobs:
|
|||
language: [rust, javascript-typescript, python, actions]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
|
|
|
|||
19
.github/workflows/conformance.yml
vendored
19
.github/workflows/conformance.yml
vendored
|
|
@ -14,6 +14,9 @@ concurrency:
|
|||
env:
|
||||
CONFORMANCE_VERSION: "0.2.0-alpha.10"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
server:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -21,12 +24,12 @@ jobs:
|
|||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
|
||||
- name: Build conformance binaries
|
||||
run: cargo build -p mcp-conformance
|
||||
|
|
@ -91,7 +94,7 @@ jobs:
|
|||
|
||||
- name: Upload results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: conformance-server-results
|
||||
path: |
|
||||
|
|
@ -104,12 +107,12 @@ jobs:
|
|||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
|
||||
- name: Build conformance binaries
|
||||
run: cargo build -p mcp-conformance
|
||||
|
|
@ -140,7 +143,7 @@ jobs:
|
|||
|
||||
- name: Upload results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: conformance-client-results
|
||||
path: conformance-client-results
|
||||
|
|
|
|||
17
.github/workflows/release-plz.yml
vendored
17
.github/workflows/release-plz.yml
vendored
|
|
@ -1,8 +1,6 @@
|
|||
name: Release-plz
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
@ -18,19 +16,20 @@ jobs:
|
|||
if: ${{ github.repository_owner == 'modelcontextprotocol' }}
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
with:
|
||||
toolchain: "1.92"
|
||||
# Using fork until semver_check_features support is released upstream.
|
||||
# See: https://github.com/release-plz/release-plz/pull/2757
|
||||
- name: Install release-plz from fork
|
||||
run: cargo install --locked --git https://github.com/DaleSeo/release-plz --branch feat/semver-check-features release-plz
|
||||
run: cargo install --locked --git https://github.com/DaleSeo/release-plz --rev c2ea743bacf9bbd56e344df68219e60b0ad35d04 release-plz
|
||||
- name: Run release-plz release
|
||||
run: release-plz release
|
||||
env:
|
||||
|
|
@ -51,17 +50,17 @@ jobs:
|
|||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
with:
|
||||
toolchain: "1.92"
|
||||
# Using fork until semver_check_features support is released upstream.
|
||||
# See: https://github.com/release-plz/release-plz/pull/2757
|
||||
- name: Install release-plz from fork
|
||||
run: cargo install --locked --git https://github.com/DaleSeo/release-plz --branch feat/semver-check-features release-plz
|
||||
run: cargo install --locked --git https://github.com/DaleSeo/release-plz --rev c2ea743bacf9bbd56e344df68219e60b0ad35d04 release-plz
|
||||
- name: Run release-plz release-pr
|
||||
run: release-plz release-pr
|
||||
env:
|
||||
|
|
|
|||
48
.github/workflows/scorecard.yml
vendored
Normal file
48
.github/workflows/scorecard.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# OpenSSF Scorecard supply-chain security analysis.
|
||||
name: Scorecard supply-chain security
|
||||
|
||||
on:
|
||||
branch_protection_rule:
|
||||
schedule:
|
||||
- cron: '5 23 * * 5'
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecard analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run analysis
|
||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
# Publishing only succeeds on the default branch. PR and manual runs
|
||||
# still validate the analysis and produce SARIF without publishing.
|
||||
publish_results: ${{ github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||
|
||||
- name: Upload SARIF artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: scorecard-results
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload to code scanning
|
||||
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
5
.github/workflows/stale.yml
vendored
5
.github/workflows/stale.yml
vendored
|
|
@ -4,6 +4,9 @@ on:
|
|||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
name: Clean Up
|
||||
|
|
@ -12,7 +15,7 @@ jobs:
|
|||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v11
|
||||
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11
|
||||
with:
|
||||
days-before-stale: 60
|
||||
days-before-close: 14
|
||||
|
|
|
|||
2
.github/workflows/triage.yml
vendored
2
.github/workflows/triage.yml
vendored
|
|
@ -39,7 +39,7 @@ jobs:
|
|||
TRIAGE_MODEL: ${{ vars.TRIAGE_MODEL || 'gpt-4o-mini' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Install jq
|
||||
run: sudo apt-get install -y jq
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ default-members = ["crates/rmcp", "crates/rmcp-macros"]
|
|||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
rmcp = { version = "3.0.0", path = "./crates/rmcp" }
|
||||
rmcp-macros = { version = "3.0.0", path = "./crates/rmcp-macros" }
|
||||
rmcp = { version = "3.4.0", path = "./crates/rmcp" }
|
||||
rmcp-macros = { version = "3.4.0", path = "./crates/rmcp-macros" }
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
version = "3.0.0"
|
||||
version = "3.4.0"
|
||||
authors = ["4t145 <u4t145@163.com>"]
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/modelcontextprotocol/rust-sdk/"
|
||||
|
|
|
|||
65
DEPENDENCY_POLICY.md
Normal file
65
DEPENDENCY_POLICY.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# 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
614
README.md
|
|
@ -28,6 +28,7 @@ below. For the full MCP specification, see
|
|||
- [Resources](#resources)
|
||||
- [Prompts](#prompts)
|
||||
- [Sampling](#sampling)
|
||||
- [Elicitation](#elicitation)
|
||||
- [Roots](#roots)
|
||||
- [Logging](#logging)
|
||||
- [Completions](#completions)
|
||||
|
|
@ -38,6 +39,10 @@ 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)
|
||||
|
|
@ -94,10 +99,10 @@ Use [`ClientServiceExt::serve_with_lifecycle`](crates/rmcp/src/service/client.rs
|
|||
select another lifecycle explicitly:
|
||||
|
||||
```rust, ignore
|
||||
use rmcp::{ClientInfo, ClientLifecycleMode, ClientServiceExt, ProtocolVersion};
|
||||
use rmcp::{ClientLifecycleMode, ClientServiceExt, ProtocolVersion, model::ClientConfig};
|
||||
|
||||
// Start directly with server/discover and include client metadata on every request.
|
||||
let client = ClientInfo::default()
|
||||
let client = ClientConfig::default()
|
||||
.serve_with_lifecycle(
|
||||
transport,
|
||||
ClientLifecycleMode::Discover {
|
||||
|
|
@ -107,8 +112,8 @@ let client = ClientInfo::default()
|
|||
.await?;
|
||||
|
||||
// Or probe the discover lifecycle and fall back when a legacy server reports
|
||||
// that server/discover is not implemented.
|
||||
let client = ClientInfo::default()
|
||||
// that server/discover is not implemented or does not respond within 10 seconds.
|
||||
let client = ClientConfig::default()
|
||||
.serve_with_lifecycle(
|
||||
transport,
|
||||
ClientLifecycleMode::Auto {
|
||||
|
|
@ -254,6 +259,77 @@ 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
|
||||
|
|
@ -293,8 +369,8 @@ use serde_json::json;
|
|||
struct MyServer;
|
||||
|
||||
impl ServerHandler for MyServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(
|
||||
fn get_info(&self) -> ServerConfig {
|
||||
ServerConfig::new(
|
||||
ServerCapabilities::builder()
|
||||
.enable_resources()
|
||||
.build(),
|
||||
|
|
@ -328,6 +404,28 @@ 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 })),
|
||||
|
|
@ -340,8 +438,12 @@ 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![],
|
||||
resource_templates: vec![
|
||||
ResourceTemplate::new("users://{user_id}/profile", "user-profile"),
|
||||
],
|
||||
next_cursor: None,
|
||||
meta: None,
|
||||
})
|
||||
|
|
@ -362,8 +464,12 @@ let result = client.read_resource(
|
|||
ReadResourceRequestParams::new("file:///config.json"),
|
||||
).await?;
|
||||
|
||||
// List resource templates
|
||||
// List resource templates, then read a resource through one by expanding its
|
||||
// parameters into a concrete URI (`users://{user_id}/profile` → `users://42/profile`).
|
||||
let templates = client.list_all_resource_templates().await?;
|
||||
let profile = client.read_resource(
|
||||
ReadResourceRequestParams::new("users://42/profile"),
|
||||
).await?;
|
||||
```
|
||||
|
||||
### Notifications
|
||||
|
|
@ -476,8 +582,8 @@ impl MyServer {
|
|||
|
||||
#[prompt_handler]
|
||||
impl ServerHandler for MyServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_prompts().build())
|
||||
fn get_info(&self) -> ServerConfig {
|
||||
ServerConfig::new(ServerCapabilities::builder().enable_prompts().build())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -487,6 +593,30 @@ 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
|
||||
|
|
@ -589,6 +719,140 @@ 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).
|
||||
|
|
@ -670,8 +934,8 @@ Enable the logging capability, handle level changes from the client, and send lo
|
|||
use rmcp::{ServerHandler, model::*, service::RequestContext};
|
||||
|
||||
impl ServerHandler for MyServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(
|
||||
fn get_info(&self) -> ServerConfig {
|
||||
ServerConfig::new(
|
||||
ServerCapabilities::builder()
|
||||
.enable_logging()
|
||||
.build(),
|
||||
|
|
@ -743,8 +1007,8 @@ Enable the completions capability and implement the `complete()` handler. Use `r
|
|||
use rmcp::{ErrorData as McpError, ServerHandler, model::*, service::RequestContext, RoleServer};
|
||||
|
||||
impl ServerHandler for MyServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(
|
||||
fn get_info(&self) -> ServerConfig {
|
||||
ServerConfig::new(
|
||||
ServerCapabilities::builder()
|
||||
.enable_completions()
|
||||
.enable_prompts()
|
||||
|
|
@ -758,6 +1022,7 @@ 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"],
|
||||
|
|
@ -778,6 +1043,17 @@ 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![],
|
||||
};
|
||||
|
||||
|
|
@ -799,12 +1075,22 @@ 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)
|
||||
|
|
@ -865,6 +1151,55 @@ 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.
|
||||
|
|
@ -918,8 +1253,8 @@ use rmcp::{
|
|||
};
|
||||
|
||||
impl ServerHandler for MyServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(
|
||||
fn get_info(&self) -> ServerConfig {
|
||||
ServerConfig::new(
|
||||
ServerCapabilities::builder()
|
||||
.enable_tools()
|
||||
.enable_tool_list_changed()
|
||||
|
|
@ -1028,11 +1363,21 @@ async fn call_tool(&self, request: CallToolRequestParams, _ctx: RequestContext<R
|
|||
}
|
||||
```
|
||||
|
||||
> **`requestState` is untrusted.** The client echoes it back verbatim, so a
|
||||
> stateless server that stores meaningful data in it MUST verify integrity
|
||||
> first. Enable the `request-state` feature and use `RequestStateCodec` to seal
|
||||
> and open it (HMAC-tagged), or keep state server-side and use `requestState`
|
||||
> only as an opaque handle.
|
||||
> **`requestState` is untrusted.** [SEP-2322 requires servers to validate
|
||||
> it](https://modelcontextprotocol.io/seps/2322-MRTR#protocol-requirements-for-ephemeral-workflow)
|
||||
> because the client echoes it back verbatim. A stateless server that stores
|
||||
> meaningful data in it MUST verify integrity first. Enable the `request-state`
|
||||
> feature and use `RequestStateCodec` to seal and open it (HMAC-tagged), or keep
|
||||
> state server-side and use `requestState` only as an opaque handle.
|
||||
|
||||
For multi-replica deployments, use `RequestStateCodec::new_with_keyring` to
|
||||
rotate signing keys without invalidating in-flight requests:
|
||||
|
||||
1. Deploy the old and new keys everywhere, continuing to emit `rs1` with the
|
||||
old key via `with_rs1_signing("old")`.
|
||||
2. Start emitting `rs2` with the new key while retaining the old key via
|
||||
`with_rs1_fallback("old")`.
|
||||
3. After the maximum `requestState` lifetime has elapsed, remove the old key.
|
||||
|
||||
### Client-side
|
||||
|
||||
|
|
@ -1220,6 +1565,13 @@ 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
|
||||
|
||||
|
|
@ -1232,13 +1584,231 @@ use rmcp::transport::StreamableHttpClientTransport;
|
|||
|
||||
// Defaults are stateless-friendly.
|
||||
let transport = StreamableHttpClientTransport::from_uri("http://localhost:8000/mcp");
|
||||
let client = ClientInfo::default().serve(transport).await?;
|
||||
let client = ClientConfig::default().serve(transport).await?;
|
||||
```
|
||||
|
||||
**Example:** [`examples/servers/src/counter_streamhttp.rs`](examples/servers/src/counter_streamhttp.rs) (server), [`examples/clients/src/streamable_http.rs`](examples/clients/src/streamable_http.rs) (client)
|
||||
|
||||
---
|
||||
|
||||
## 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).
|
||||
|
|
|
|||
170
ROADMAP.md
170
ROADMAP.md
|
|
@ -1,116 +1,112 @@
|
|||
# RMCP Roadmap
|
||||
|
||||
This roadmap tracks the path to SEP-1730 Tier 1 for the Rust MCP SDK.
|
||||
This roadmap tracks the path to [SEP-1730](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1730) Tier 1 for the Rust MCP SDK.
|
||||
|
||||
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)
|
||||
**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.
|
||||
|
||||
Extension scenarios are reported separately below because they are
|
||||
informational and do not count toward SDK tiering.
|
||||
| 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.
|
||||
|
||||
---
|
||||
|
||||
## Target spec: 2026-07-28 (release 2026-07-28)
|
||||
## Conformance
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
### Versioned-spec conformance (baseline 2026-07-21, suite `0.2.0-alpha.9`)
|
||||
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.
|
||||
|
||||
- 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
|
||||
### Informational (not scored for tiering)
|
||||
|
||||
### Extension conformance (informational)
|
||||
Extension-tagged scenarios are excluded by `--spec-version` filters, so CI runs them
|
||||
in separate steps against `conformance/expected-failures-extensions.yaml`:
|
||||
|
||||
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
|
||||
| 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 |
|
||||
|
||||
### Spec features without conformance scenarios
|
||||
|
||||
Conformance alone does not cover the full spec surface. Feature work tracked via the milestone:
|
||||
Conformance does not cover the entire spec surface. Remaining 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
|
||||
|
||||
- [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`)
|
||||
### 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] SEP-2164 resource not found
|
||||
- [x] Cache hints (`caching`)
|
||||
- [x] `http-header-validation`
|
||||
- [x] Issue triage labels (bug, enhancement, needs confirmation, needs repro, ready for work, P0–P3)
|
||||
- [x] SEP-2549 cache hints (`caching`)
|
||||
- [x] SEP-2243 HTTP standardization (`http-header-validation`, standard headers)
|
||||
- [x] DNS rebinding protection
|
||||
|
||||
---
|
||||
|
||||
## Informational (not scored for tiering)
|
||||
## Nice-to-have (scorecard hygiene, not required for Tier 1)
|
||||
|
||||
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 |
|
||||
- [ ] 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`).
|
||||
|
|
|
|||
66
VERSIONING.md
Normal file
66
VERSIONING.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# 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.
|
||||
|
|
@ -19,6 +19,7 @@ rmcp = { path = "../crates/rmcp", features = [
|
|||
"elicitation",
|
||||
"auth",
|
||||
"auth-client-credentials-jwt",
|
||||
"auth-enterprise-managed",
|
||||
"request-state",
|
||||
"transport-streamable-http-server",
|
||||
"transport-streamable-http-client-reqwest",
|
||||
|
|
@ -31,6 +32,7 @@ tracing = "0.1"
|
|||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
axum = { version = "0.8", features = ["macros"] }
|
||||
anyhow = "1"
|
||||
oauth2 = { version = "5.0", default-features = false }
|
||||
reqwest = { version = "0.13", features = ["json"] }
|
||||
urlencoding = "2"
|
||||
url = "2"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
#![expect(
|
||||
deprecated,
|
||||
reason = "The conformance suite still exercises deprecated sampling scenarios"
|
||||
)]
|
||||
|
||||
use anyhow::Context;
|
||||
use oauth2::{ClientSecret, RefreshToken};
|
||||
use rmcp::{
|
||||
ClientHandler, ClientLifecycleMode, ClientServiceExt, ErrorData, RoleClient, ServiceExt,
|
||||
model::*,
|
||||
|
|
@ -6,7 +13,8 @@ use rmcp::{
|
|||
AuthClient, AuthorizationManager, StreamableHttpClientTransport,
|
||||
auth::{
|
||||
AuthorizationCallback, AuthorizationRequest, ClientCredentialsConfig,
|
||||
InMemoryCredentialStore, JwtSigningAlgorithm, OAuthState,
|
||||
InMemoryCredentialStore, JwtSigningAlgorithm, OAuthState, default_oauth_http_client,
|
||||
enterprise::{EmaAuthorizationServer, EmaClientAuthentication, EmaExchangeRequest},
|
||||
},
|
||||
streamable_http_client::StreamableHttpClientTransportConfig,
|
||||
},
|
||||
|
|
@ -36,6 +44,17 @@ struct ConformanceContext {
|
|||
private_key_pem: Option<String>,
|
||||
#[serde(default)]
|
||||
signing_algorithm: Option<String>,
|
||||
// enterprise-managed-authorization-refresh-token
|
||||
#[serde(default)]
|
||||
idp_client_id: Option<String>,
|
||||
#[serde(default)]
|
||||
idp_client_secret: Option<String>,
|
||||
#[serde(default)]
|
||||
idp_refresh_token: Option<String>,
|
||||
#[serde(default)]
|
||||
idp_issuer: Option<String>,
|
||||
#[serde(default)]
|
||||
idp_token_endpoint: Option<String>,
|
||||
}
|
||||
|
||||
fn load_context() -> ConformanceContext {
|
||||
|
|
@ -55,8 +74,8 @@ impl ClientHandler for BasicClientHandler {}
|
|||
struct ElicitationDefaultsClientHandler;
|
||||
|
||||
impl ClientHandler for ElicitationDefaultsClientHandler {
|
||||
fn get_info(&self) -> ClientInfo {
|
||||
let mut info = ClientInfo::default();
|
||||
fn get_info(&self) -> ClientConfig {
|
||||
let mut info = ClientConfig::default();
|
||||
info.capabilities.elicitation = Some(
|
||||
ElicitationCapability::new()
|
||||
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
|
||||
|
|
@ -144,8 +163,8 @@ impl ClientHandler for ElicitationDefaultsClientHandler {
|
|||
struct FullClientHandler;
|
||||
|
||||
impl ClientHandler for FullClientHandler {
|
||||
fn get_info(&self) -> ClientInfo {
|
||||
let mut info = ClientInfo::default();
|
||||
fn get_info(&self) -> ClientConfig {
|
||||
let mut info = ClientConfig::default();
|
||||
info.capabilities.elicitation = Some(
|
||||
ElicitationCapability::new()
|
||||
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
|
||||
|
|
@ -760,6 +779,66 @@ async fn run_client_credentials_jwt(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Exchange the fixture's IdP refresh token, then exercise authenticated MCP access.
|
||||
async fn run_ema_refresh_token_client(
|
||||
server_url: &str,
|
||||
ctx: &ConformanceContext,
|
||||
) -> anyhow::Result<()> {
|
||||
let manager = AuthorizationManager::new(server_url).await?;
|
||||
let metadata = manager.resolve_metadata().await?.metadata;
|
||||
let idp = EmaAuthorizationServer::new(
|
||||
ctx.idp_issuer.as_deref().context("Missing idp_issuer")?,
|
||||
ctx.idp_token_endpoint
|
||||
.as_deref()
|
||||
.context("Missing idp_token_endpoint")?,
|
||||
ctx.idp_client_id
|
||||
.as_deref()
|
||||
.context("Missing idp_client_id")?,
|
||||
)
|
||||
.with_client_authentication(EmaClientAuthentication::ClientSecretBasic(
|
||||
ClientSecret::new(
|
||||
ctx.idp_client_secret
|
||||
.clone()
|
||||
.context("Missing idp_client_secret")?,
|
||||
),
|
||||
));
|
||||
let resource_as = EmaAuthorizationServer::new(
|
||||
metadata
|
||||
.issuer
|
||||
.context("Missing authorization server issuer")?,
|
||||
metadata.token_endpoint,
|
||||
ctx.client_id.as_deref().context("Missing client_id")?,
|
||||
)
|
||||
.with_client_authentication(EmaClientAuthentication::ClientSecretBasic(
|
||||
ClientSecret::new(ctx.client_secret.clone().context("Missing client_secret")?),
|
||||
));
|
||||
let refresh_token = RefreshToken::new(
|
||||
ctx.idp_refresh_token
|
||||
.clone()
|
||||
.context("Missing idp_refresh_token")?,
|
||||
);
|
||||
let http = default_oauth_http_client()?;
|
||||
let token = EmaExchangeRequest::new(idp, resource_as, server_url, &refresh_token)
|
||||
.with_scopes(manager.select_scopes(None, &[]))
|
||||
.exchange(&http, &http)
|
||||
.await?;
|
||||
|
||||
let transport = StreamableHttpClientTransport::from_config(
|
||||
StreamableHttpClientTransportConfig::with_uri(server_url)
|
||||
.auth_header(token.access_token.secret()),
|
||||
);
|
||||
let client = BasicClientHandler
|
||||
.serve_with_lifecycle(transport, conformance_lifecycle())
|
||||
.await?;
|
||||
let tools = client.list_tools(Default::default()).await?;
|
||||
for tool in tools.tools {
|
||||
let args = build_tool_arguments(&tool);
|
||||
client.call_tool(call_tool_params(tool.name, args)).await?;
|
||||
}
|
||||
client.cancel().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cross-app access flow (SEP-1046 extension).
|
||||
async fn run_cross_app_access_client(
|
||||
server_url: &str,
|
||||
|
|
@ -1110,6 +1189,11 @@ async fn run_scenario(
|
|||
"auth/client-credentials-basic" => run_client_credentials_basic(server_url, ctx).await?,
|
||||
"auth/client-credentials-jwt" => run_client_credentials_jwt(server_url, ctx).await?,
|
||||
|
||||
// Auth - enterprise-managed authorization with a refresh-token subject
|
||||
"auth/enterprise-managed-authorization-refresh-token" => {
|
||||
run_ema_refresh_token_client(server_url, ctx).await?
|
||||
}
|
||||
|
||||
// Auth - cross-app access
|
||||
"auth/cross-app-access-complete-flow" => {
|
||||
run_cross_app_access_client(server_url, ctx).await?
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ fn custom_header_tool() -> Tool {
|
|||
/// Signing key for SEP-2322 `requestState` sealing. A fixed key is fine for a
|
||||
/// conformance harness; real servers must load a secret out of clients' reach.
|
||||
const REQUEST_STATE_KEY: &[u8] = b"rust-sdk-conformance-request-state-key!!";
|
||||
const _: () = assert!(
|
||||
REQUEST_STATE_KEY.len() >= RequestStateCodec::MIN_KEY_LENGTH,
|
||||
"REQUEST_STATE_KEY is shorter than RequestStateCodec::MIN_KEY_LENGTH",
|
||||
);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ConformanceServer {
|
||||
|
|
@ -70,7 +74,7 @@ impl ConformanceServer {
|
|||
subscriptions: Arc::new(Mutex::new(HashMap::new())),
|
||||
next_subscription: Arc::new(AtomicU64::new(0)),
|
||||
log_level: Arc::new(Mutex::new(LoggingLevel::Debug)),
|
||||
request_state_codec: RequestStateCodec::new(REQUEST_STATE_KEY),
|
||||
request_state_codec: RequestStateCodec::new_unchecked(REQUEST_STATE_KEY),
|
||||
tasks: TaskManager::new(),
|
||||
}
|
||||
}
|
||||
|
|
@ -700,8 +704,8 @@ impl ServerHandler for ConformanceServer {
|
|||
(name == "test_custom_header").then(custom_header_tool)
|
||||
}
|
||||
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(
|
||||
fn get_info(&self) -> ServerConfig {
|
||||
ServerConfig::new(
|
||||
ServerCapabilities::builder()
|
||||
.enable_prompts()
|
||||
.enable_prompts_list_changed()
|
||||
|
|
@ -1740,7 +1744,9 @@ async fn main() -> anyhow::Result<()> {
|
|||
tracing::info!("Starting conformance server on {}", bind_addr);
|
||||
|
||||
let server = ConformanceServer::new();
|
||||
let config = StreamableHttpServerConfig::default();
|
||||
let config = StreamableHttpServerConfig::default()
|
||||
.with_allowed_origins([format!("http://{bind_addr}")])
|
||||
.enforce_origin_validation();
|
||||
let service = StreamableHttpService::new(
|
||||
move || Ok(server.clone()),
|
||||
LocalSessionManager::default().into(),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,60 @@ 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
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ documentation = "https://docs.rs/rmcp-macros"
|
|||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = {version = "2", features = ["full"]}
|
||||
syn = {version = "3", features = ["full"]}
|
||||
quote = "1"
|
||||
proc-macro2 = "1"
|
||||
serde_json = "1.0"
|
||||
darling = { version = "0.23" }
|
||||
darling = { version = "0.24" }
|
||||
|
||||
[features]
|
||||
local = []
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> TokenStream {
|
|||
/// | `router` | `Ident` | The name of the router function to be generated. Defaults to `tool_router`. |
|
||||
/// | `vis` | `Visibility` | The visibility of the generated router function. Defaults to empty. |
|
||||
/// | `server_handler` | `flag` | When set, also emits `#[::rmcp::tool_handler]` on `impl ServerHandler for Self` so you can omit a separate `#[tool_handler]` block. |
|
||||
/// | `allow_empty` | `flag` | When set, accepts an impl block with no `#[tool]` fn. Without it, an empty router is a compile error. |
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
|
|
@ -122,6 +123,37 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> TokenStream {
|
|||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ### Empty routers
|
||||
///
|
||||
/// Collecting tools is this attribute's whole purpose, so an impl block with no `#[tool]` fn is a
|
||||
/// compile error rather than a router that silently serves nothing. Pass `allow_empty` when that
|
||||
/// is what you want:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[tool_router(allow_empty)]
|
||||
/// impl MyToolHandler {}
|
||||
/// ```
|
||||
///
|
||||
/// The usual way to hit this by accident is a `macro_rules!` helper *inside* the impl block. An
|
||||
/// attribute macro receives the unexpanded item, so `#[tool]` fns produced by such a helper are
|
||||
/// invisible to `#[tool_router]`. Let the `macro_rules!` emit the whole annotated impl instead:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// macro_rules! define_tools {
|
||||
/// ($($name:ident => $description:literal),* $(,)?) => {
|
||||
/// #[tool_router]
|
||||
/// impl MyToolHandler {
|
||||
/// $(
|
||||
/// #[tool(description = $description)]
|
||||
/// async fn $name(&self) -> String { stringify!($name).to_owned() }
|
||||
/// )*
|
||||
/// }
|
||||
/// };
|
||||
/// }
|
||||
///
|
||||
/// define_tools!(my_tool => "what my tool does");
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn tool_router(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
tool_router::tool_router(attr.into(), input.into())
|
||||
|
|
@ -185,8 +217,8 @@ pub fn tool_router(attr: TokenStream, input: TokenStream) -> TokenStream {
|
|||
/// ```rust,ignore
|
||||
/// #[tool_handler]
|
||||
/// impl ServerHandler for MyToolHandler {
|
||||
/// fn get_info(&self) -> ServerInfo {
|
||||
/// ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||
/// fn get_info(&self) -> ServerConfig {
|
||||
/// ServerConfig::new(ServerCapabilities::builder().enable_tools().build())
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue