Compare commits

..
186 changed files with 8164 additions and 41907 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@v6
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@v6
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
@ -179,24 +170,10 @@ jobs:
- name: Check rmcp (all features except local)
run: |
BASE_SHA=${{ github.event.pull_request.base.sha }}
# `cargo public-api diff` builds both revisions with the same feature
# set, so a feature that exists on only one side (e.g. a feature added
# or removed by this PR) would fail to build the other revision. Diff
# only the features present in BOTH the base and the head; features
# unique to one side are necessarily pure additions/removals, which the
# release-type deny flags already govern.
list_features() {
cargo metadata --no-deps --format-version 1 --manifest-path "$1/Cargo.toml" \
| jq -r '.packages[] | select(.name == "rmcp") | .features | keys[]
| select(startswith("__") | not)
| select(. != "local")'
}
list_features "." | sort -u > "$RUNNER_TEMP/head_features"
git worktree add --detach "$RUNNER_TEMP/rmcp-base" "$BASE_SHA"
list_features "$RUNNER_TEMP/rmcp-base" | sort -u > "$RUNNER_TEMP/base_features"
git worktree remove --force "$RUNNER_TEMP/rmcp-base"
FEATURES=$(comm -12 "$RUNNER_TEMP/head_features" "$RUNNER_TEMP/base_features" | paste -sd, -)
FEATURES=$(cargo metadata --no-deps --format-version 1 \
| jq -r '[.packages[] | select(.name == "rmcp") | .features | keys[]
| select(startswith("__") | not)
| select(. != "local")] | join(",")')
cargo public-api \
--package rmcp \
--features "$FEATURES" \
@ -204,57 +181,33 @@ jobs:
diff \
$DENY \
--force \
"$BASE_SHA"..${{ github.sha }}
${{ github.event.pull_request.base.sha }}..${{ github.sha }}
spelling:
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
msrv:
name: Check MSRV
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Read MSRV from Cargo.toml
run: |
MSRV=$(cargo metadata --no-deps --format-version 1 \
| jq -r '.packages[] | select(.name == "rmcp") | .rust_version')
echo "MSRV=$MSRV" >> "$GITHUB_ENV"
- name: Install MSRV Rust
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master
with:
toolchain: ${{ env.MSRV }}
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Check default workspace members with MSRV
run: cargo +${{ env.MSRV }} check --all-targets --all-features
uses: crate-ci/typos@master
test:
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@v6
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 +215,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 +224,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@v6
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 +244,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 +258,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@v6
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 +280,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 +295,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@v6
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 +315,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 +351,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 +368,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 +394,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

@ -1,149 +0,0 @@
name: Conformance
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: conformance-${{ github.ref }}
cancel-in-progress: true
env:
CONFORMANCE_VERSION: "0.2.0-alpha.10"
permissions:
contents: read
jobs:
server:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Build conformance binaries
run: cargo build -p mcp-conformance
- name: Test conformance server
run: cargo test -p mcp-conformance --bin conformance-server
- name: Start conformance server
run: |
PORT=8001 ./target/debug/conformance-server &
echo $! > server.pid
for _ in $(seq 1 30); do
if curl -s -o /dev/null http://127.0.0.1:8001/mcp; then
exit 0
fi
sleep 1
done
echo "conformance server did not become ready" >&2
exit 1
- name: Run 2025-11-25 server suite
run: |
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" server \
--url http://127.0.0.1:8001/mcp \
--suite all \
--spec-version 2025-11-25 \
-o conformance-results/2025-11-25
- name: Run 2026-07-28 server suite
run: |
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" server \
--url http://127.0.0.1:8001/mcp \
--suite all \
--spec-version 2026-07-28 \
-o conformance-results/2026-07-28
- name: Run extension server scenarios (informational)
run: |
scenarios=(
tasks-lifecycle
tasks-capability-negotiation
tasks-wire-fields
tasks-request-state-removal
tasks-mrtr-input
tasks-request-headers
tasks-dispatch-and-envelope
tasks-status-notifications
tasks-required-task-error
tasks-mrtr-composition
)
for scenario in "${scenarios[@]}"; do
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" server \
--url http://127.0.0.1:8001/mcp \
--scenario "$scenario" \
--expected-failures conformance/expected-failures-extensions.yaml \
-o conformance-extension-results
done
- name: Stop conformance server
if: always()
run: kill "$(cat server.pid)" 2>/dev/null || true
- name: Upload results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: conformance-server-results
path: |
conformance-results
conformance-extension-results
client:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Build conformance binaries
run: cargo build -p mcp-conformance
- name: Run 2025-11-25 client suite
run: |
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" client \
--command "$(pwd)/target/debug/conformance-client" \
--suite all \
--spec-version 2025-11-25 \
-o conformance-client-results/2025-11-25
- name: Run 2026-07-28 client suite
run: |
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" client \
--command "$(pwd)/target/debug/conformance-client" \
--suite all \
--spec-version 2026-07-28 \
-o conformance-client-results/2026-07-28
- name: Run extension client suite (informational)
run: |
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" client \
--command "$(pwd)/target/debug/conformance-client" \
--suite extensions \
--expected-failures conformance/expected-failures-extensions.yaml \
-o conformance-client-results/extensions
- name: Upload results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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

@ -1,38 +0,0 @@
name: Clean Up Stale PRs and Issues
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
stale:
name: Clean Up
runs-on: ubuntu-24.04
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11
with:
days-before-stale: 60
days-before-close: 14
exempt-issue-labels: keep,security,pinned
exempt-pr-labels: keep,security,pinned
exempt-draft-pr: true
stale-issue-message: >
This issue has had no activity for 60 days and is now marked as stale.
It will be closed in 14 days if there is no further activity. Add the
`keep` label to keep it open.
close-issue-message: >
Closing this issue after 14 days of inactivity since it was marked stale.
Feel free to reopen if it is still relevant.
stale-pr-message: >
This pull request has had no activity for 60 days and is now marked as stale.
It will be closed in 14 days if there is no further activity. Add the
`keep` label to keep it open.
close-pr-message: >
Closing this pull request after 14 days of inactivity since it was marked stale.
Feel free to reopen if you plan to continue the work.

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,12 @@ 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 = "2.1.0", path = "./crates/rmcp" }
rmcp-macros = { version = "2.1.0", path = "./crates/rmcp-macros" }
[workspace.package]
edition = "2024"
rust-version = "1.88"
version = "3.4.0"
version = "2.1.0"
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).

1044
README.md

File diff suppressed because it is too large Load diff

View file

@ -1,112 +1,71 @@
# 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.
| 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.
Server conformance: 87.5% (28/32) · Client conformance: 80.0% (16/20)
---
## Conformance
## Tier 2 → Tier 1
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).
### Conformance
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 (87.5% → 100%)
### Informational (not scored for tiering)
- [ ] Fix `prompts-get-with-args` — prompt argument handling returns incorrect result (arg1/arg2 not substituted)
- [ ] Fix `prompts-get-embedded-resource` — embedded resource content in prompt responses (invalid content union)
- [ ] Fix `elicitation-sep1330-enums` — enum inference handling per SEP-1330 (missing enumNames for legacy titled enum)
- [ ] Fix `dns-rebinding-protection` — validate `Host` / `Origin` headers on Streamable HTTP transport (accepts invalid headers with 200)
Extension-tagged scenarios are excluded by `--spec-version` filters, so CI runs them
in separate steps against `conformance/expected-failures-extensions.yaml`:
#### Client (80.0% → 100%)
| 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 |
- [ ] Fix `auth/metadata-var3` — AS metadata discovery variant 3 (no authorization support detected)
- [ ] Fix `auth/scope-from-www-authenticate` — use scope parameter from WWW-Authenticate header on 403 insufficient_scope
- [ ] Fix `auth/scope-step-up` — handle 403 `insufficient_scope` and re-authorize with upgraded scopes
- [ ] Fix `auth/2025-03-26-oauth-endpoint-fallback` — legacy OAuth endpoint fallback for pre-2025-06-18 servers (no authorization support detected)
### Spec features without conformance scenarios
### Governance & Policy
Conformance does not cover the entire spec surface. Remaining feature work tracked via
the milestone:
- [ ] Create `VERSIONING.md` — document semver scheme, what constitutes a breaking change, and how breaking changes are communicated
- 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)
### 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
## Informational (not scored for tiering)
### Tier 1 requirements
These draft/extension scenarios are tracked but do not count toward tier advancement:
- [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] SEP-2549 cache hints (`caching`)
- [x] SEP-2243 HTTP standardization (`http-header-validation`, standard headers)
- [x] DNS rebinding protection
---
## Nice-to-have (scorecard hygiene, not required for Tier 1)
- [ ] 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`).
| Scenario | Tag | Status |
|---|---|---|
| `auth/resource-mismatch` | draft | ❌ Failed |
| `auth/client-credentials-jwt` | extension | ❌ Failed — JWT `aud` claim verification error |
| `auth/client-credentials-basic` | extension | ✅ Passed |
| `auth/cross-app-access-complete-flow` | extension | ❌ Failed — sends `authorization_code` grant instead of `jwt-bearer` |

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

@ -1,2 +1,3 @@
msrv = "1.85"
too-many-arguments-threshold = 10
check-private-items = false

View file

@ -18,9 +18,6 @@ rmcp = { path = "../crates/rmcp", features = [
"client",
"elicitation",
"auth",
"auth-client-credentials-jwt",
"auth-enterprise-managed",
"request-state",
"transport-streamable-http-server",
"transport-streamable-http-client-reqwest",
] }
@ -32,7 +29,8 @@ 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"
p256 = { version = "0.13", features = ["ecdsa"] }
base64 = "0.22"

View file

@ -1,26 +0,0 @@
# Known failures for informational extension scenarios in
# @modelcontextprotocol/conformance CONFORMANCE_VERSION.
#
# Extensions are not selected by a `--spec-version` run and do not count toward
# SDK tiering. CI runs them separately so a green versioned-spec suite does not
# hide extension gaps.
#
# This is a strict baseline:
# - an unlisted failure fails the build
# - a listed scenario that starts passing fails the build as a stale entry
#
# When bumping CONFORMANCE_VERSION, review the available extension and
# pending scenarios and update this file deliberately.
# The SEP-2663 Tasks Extension server scenarios (tracked in #868) all pass and
# were removed from this baseline. `tasks-status-notifications` remains
# upstream-skipped pending the subscriptions/listen rewrite; CI will fail if it
# becomes active but does not pass.
server: []
client:
# Informational OAuth extension scenarios.
- auth/enterprise-managed-authorization
- auth/dpop
- auth/dpop-nonce
- auth/wif-jwt-bearer

View file

@ -1,21 +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,
ClientHandler, ErrorData, RoleClient, ServiceExt,
model::*,
service::RequestContext,
transport::{
AuthClient, AuthorizationManager, StreamableHttpClientTransport,
auth::{
AuthorizationCallback, AuthorizationRequest, ClientCredentialsConfig,
InMemoryCredentialStore, JwtSigningAlgorithm, OAuthState, default_oauth_http_client,
enterprise::{EmaAuthorizationServer, EmaClientAuthentication, EmaExchangeRequest},
},
auth::{AuthorizationCallback, OAuthState},
streamable_http_client::StreamableHttpClientTransportConfig,
},
};
@ -24,17 +13,8 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
// ─── Context parsed from MCP_CONFORMANCE_CONTEXT ────────────────────────────
#[derive(Debug, Default, serde::Deserialize)]
struct ConformanceToolCall {
name: String,
#[serde(default)]
arguments: Option<serde_json::Map<String, Value>>,
}
#[derive(Debug, Default, serde::Deserialize)]
struct ConformanceContext {
#[serde(default, alias = "toolCalls")]
tool_calls: Vec<ConformanceToolCall>,
#[serde(default)]
client_id: Option<String>,
#[serde(default)]
@ -44,17 +24,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 +43,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 +132,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)),
@ -211,76 +180,27 @@ impl ClientHandler for FullClientHandler {
const CIMD_CLIENT_METADATA_URL: &str = "https://conformance-test.local/client-metadata.json";
const REDIRECT_URI: &str = "http://localhost:3000/callback";
const SCOPE_STEP_UP_INITIAL_SCOPES: &[&str] = &["mcp:basic"];
const SCOPE_STEP_UP_ESCALATED_SCOPES: &[&str] = &["mcp:basic", "mcp:write"];
/// Attempt the real connection unauthenticated and return the server's
/// `WWW-Authenticate` challenge from the 401 — the reactive discovery
/// trigger.
/// Perform the headless OAuth authorization-code flow.
///
/// `None` (server accepted the unauthenticated connection, which is then
/// closed cleanly) is a legitimate outcome, not an error: the scope-step-up
/// and scope-retry-limit mocks allow unauthenticated `initialize` and only
/// enforce authorization on tool calls.
async fn initialize_challenge(
server_url: &str,
lifecycle: ClientLifecycleMode,
) -> anyhow::Result<Option<String>> {
let transport = StreamableHttpClientTransport::from_uri(server_url);
match BasicClientHandler
.serve_with_lifecycle(transport, lifecycle)
.await
{
Ok(client) => {
client.cancel().await.ok();
Ok(None)
}
Err(error) => match error.auth_challenge() {
Some(challenge) => Ok(Some(challenge.to_string())),
None => Err(error.into()),
},
}
}
fn with_optional_challenge(
request: AuthorizationRequest,
challenge: Option<String>,
) -> AuthorizationRequest {
match challenge {
Some(challenge) => request.with_challenge(challenge),
None => request,
}
}
/// Perform the headless OAuth authorization-code flow, reactively:
///
/// 1. Attempt the real connection; take the 401's WWW-Authenticate challenge
/// 2. Discover from the challenge, register (or use CIMD), get auth URL
/// 3. Fetch the auth URL with redirect:manual → extract code from Location header
/// 4. Exchange code for token
/// 5. Return an `AuthClient` wrapping `reqwest::Client`
/// 1. Discover metadata, register (or use CIMD), get auth URL
/// 2. Fetch the auth URL with redirect:manual → extract code from Location header
/// 3. Exchange code for token
/// 4. Return an `AuthClient` wrapping `reqwest::Client`
async fn perform_oauth_flow(
server_url: &str,
_ctx: &ConformanceContext,
) -> anyhow::Result<AuthClient<reqwest::Client>> {
// Always the discover lifecycle here (not `conformance_lifecycle()`):
// this flow serves `run_auth_client`, whose 2026-07-28 auth mocks require
// the per-request MCP-Protocol-Version negotiation.
let challenge = initialize_challenge(
server_url,
ClientLifecycleMode::Discover {
preferred_versions: preferred_protocol_versions(),
},
)
.await?;
let mut oauth = OAuthState::new(server_url, None).await?;
// Discover + register + get auth URL
let request = AuthorizationRequest::new(REDIRECT_URI)
.with_client_name("conformance-client")
.with_client_metadata_url(CIMD_CLIENT_METADATA_URL);
oauth
.start_authorization(with_optional_challenge(request, challenge))
.start_authorization_with_metadata_url(
&[],
REDIRECT_URI,
Some("conformance-client"),
Some(CIMD_CLIENT_METADATA_URL),
)
.await?;
let auth_url = oauth.get_authorization_url().await?;
@ -315,38 +235,50 @@ async fn perform_oauth_flow(
Ok(AuthClient::new(reqwest::Client::default(), am))
}
/// Like `perform_oauth_flow` but uses pre-registered client credentials,
/// exercising the SDK's high-level `OAuthState` path (no DCR).
/// Like `perform_oauth_flow` but uses pre-registered client credentials.
async fn perform_oauth_flow_preregistered(
server_url: &str,
client_id: &str,
client_secret: &str,
) -> anyhow::Result<AuthClient<reqwest::Client>> {
let challenge = initialize_challenge(server_url, conformance_lifecycle()).await?;
let mut oauth = OAuthState::new(server_url, None).await?;
let mut manager = AuthorizationManager::new(server_url).await?;
let metadata = manager.discover_metadata().await?;
manager.set_metadata(metadata);
let request = AuthorizationRequest::new(REDIRECT_URI)
.with_preregistered_client(client_id)
// Configure with pre-registered credentials
let config = rmcp::transport::auth::OAuthClientConfig::new(client_id, REDIRECT_URI)
.with_client_secret(client_secret);
oauth
.start_authorization(with_optional_challenge(request, challenge))
.await?;
manager.configure_client(config)?;
let auth_url = oauth.get_authorization_url().await?;
let callback = headless_authorize(&auth_url).await?;
oauth
.handle_callback_with_issuer(
&callback.code,
&callback.csrf_token,
callback.issuer.as_deref(),
)
.await?;
let scopes = manager.select_scopes(None, &[]);
let scope_refs: Vec<&str> = scopes.iter().map(|s| s.as_str()).collect();
let auth_url = manager.get_authorization_url(&scope_refs).await?;
let am = oauth
.into_authorization_manager()
.ok_or_else(|| anyhow::anyhow!("Failed to get authorization manager"))?;
// Headless redirect
let http = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()?;
let resp = http.get(&auth_url).send().await?;
let location = resp
.headers()
.get("location")
.and_then(|v| v.to_str().ok())
.ok_or_else(|| anyhow::anyhow!("No Location header"))?;
let redirect_url = url::Url::parse(location)?;
let code = redirect_url
.query_pairs()
.find(|(k, _)| k == "code")
.map(|(_, v)| v.to_string())
.ok_or_else(|| anyhow::anyhow!("No code"))?;
let state = redirect_url
.query_pairs()
.find(|(k, _)| k == "state")
.map(|(_, v)| v.to_string())
.ok_or_else(|| anyhow::anyhow!("No state"))?;
Ok(AuthClient::new(reqwest::Client::default(), am))
manager.exchange_code_for_token(&code, &state).await?;
Ok(AuthClient::new(reqwest::Client::default(), manager))
}
/// Run the standard auth flow, then connect and exercise the server.
@ -358,17 +290,7 @@ async fn run_auth_client(server_url: &str, ctx: &ConformanceContext) -> anyhow::
StreamableHttpClientTransportConfig::with_uri(server_url),
);
// The 2026-07-28 auth mocks require the modern per-request lifecycle
// (MCP-Protocol-Version header on every request), so negotiate via the
// discover lifecycle rather than the legacy initialize handshake.
let client = BasicClientHandler
.serve_with_lifecycle(
transport,
ClientLifecycleMode::Discover {
preferred_versions: preferred_protocol_versions(),
},
)
.await?;
let client = BasicClientHandler.serve(transport).await?;
tracing::debug!("Connected (authenticated)");
let tools = client.list_tools(Default::default()).await?;
@ -392,14 +314,15 @@ async fn run_auth_scope_step_up_client(
server_url: &str,
_ctx: &ConformanceContext,
) -> anyhow::Result<()> {
let challenge = initialize_challenge(server_url, conformance_lifecycle()).await?;
// First auth
let mut oauth = OAuthState::new(server_url, None).await?;
let request = AuthorizationRequest::new(REDIRECT_URI)
.with_scopes(SCOPE_STEP_UP_INITIAL_SCOPES.iter().copied())
.with_client_name("conformance-client")
.with_client_metadata_url(CIMD_CLIENT_METADATA_URL);
oauth
.start_authorization(with_optional_challenge(request, challenge))
.start_authorization_with_metadata_url(
&[],
REDIRECT_URI,
Some("conformance-client"),
Some(CIMD_CLIENT_METADATA_URL),
)
.await?;
let auth_url = oauth.get_authorization_url().await?;
@ -422,9 +345,7 @@ async fn run_auth_scope_step_up_client(
StreamableHttpClientTransportConfig::with_uri(server_url),
);
let client = BasicClientHandler
.serve_with_lifecycle(transport, conformance_lifecycle())
.await?;
let client = BasicClientHandler.serve(transport).await?;
let tools = client.list_tools(Default::default()).await?;
tracing::debug!("Listed {} tools", tools.tools.len());
@ -444,13 +365,16 @@ async fn run_auth_scope_step_up_client(
// Drop old client, re-auth with upgraded scopes
client.cancel().await.ok();
// Re-do the full flow; the server will give us the right scopes
// on the second authorization request.
let mut oauth2 = OAuthState::new(server_url, None).await?;
// Pass the escalated scope hint
oauth2
.start_authorization(
AuthorizationRequest::new(REDIRECT_URI)
.with_scopes(SCOPE_STEP_UP_ESCALATED_SCOPES.iter().copied())
.with_client_name("conformance-client")
.with_client_metadata_url(CIMD_CLIENT_METADATA_URL),
.start_authorization_with_metadata_url(
&[],
REDIRECT_URI,
Some("conformance-client"),
Some(CIMD_CLIENT_METADATA_URL),
)
.await?;
let auth_url2 = oauth2.get_authorization_url().await?;
@ -463,20 +387,16 @@ async fn run_auth_scope_step_up_client(
)
.await?;
let am2 = oauth2.into_authorization_manager().ok_or_else(|| {
anyhow::anyhow!("Missing authorization manager after step-up")
})?;
let am2 = oauth2.into_authorization_manager().unwrap();
let auth_client2 = AuthClient::new(reqwest::Client::default(), am2);
let transport2 = StreamableHttpClientTransport::with_client(
auth_client2,
StreamableHttpClientTransportConfig::with_uri(server_url),
);
let client2 = BasicClientHandler
.serve_with_lifecycle(transport2, conformance_lifecycle())
.await?;
client2
let client2 = BasicClientHandler.serve(transport2).await?;
let _ = client2
.call_tool(call_tool_params(tool.name.clone(), args))
.await?;
.await;
client2.cancel().await.ok();
return Ok(());
}
@ -494,15 +414,16 @@ async fn run_auth_scope_retry_limit_client(
) -> anyhow::Result<()> {
let max_retries = 3u32;
let mut attempt = 0u32;
let challenge = initialize_challenge(server_url, conformance_lifecycle()).await?;
loop {
let mut oauth = OAuthState::new(server_url, None).await?;
let request = AuthorizationRequest::new(REDIRECT_URI)
.with_client_name("conformance-client")
.with_client_metadata_url(CIMD_CLIENT_METADATA_URL);
oauth
.start_authorization(with_optional_challenge(request, challenge.clone()))
.start_authorization_with_metadata_url(
&[],
REDIRECT_URI,
Some("conformance-client"),
Some(CIMD_CLIENT_METADATA_URL),
)
.await?;
let auth_url = oauth.get_authorization_url().await?;
let callback = headless_authorize(&auth_url).await?;
@ -514,9 +435,7 @@ async fn run_auth_scope_retry_limit_client(
)
.await?;
let am = oauth
.into_authorization_manager()
.ok_or_else(|| anyhow::anyhow!("Missing authorization manager"))?;
let am = oauth.into_authorization_manager().unwrap();
let auth_client = AuthClient::new(reqwest::Client::default(), am);
let transport = StreamableHttpClientTransport::with_client(
auth_client,
@ -524,18 +443,7 @@ async fn run_auth_scope_retry_limit_client(
);
let client = BasicClientHandler.serve(transport).await?;
let tools = match client.list_tools(Default::default()).await {
Ok(tools) => tools,
Err(err) => {
tracing::info!(
"Scope retry limit scenario stopped after authorization attempt {}: {}",
attempt + 1,
err
);
client.cancel().await.ok();
return Ok(());
}
};
let tools = client.list_tools(Default::default()).await?;
let mut got_403 = false;
for tool in &tools.tools {
@ -559,75 +467,12 @@ async fn run_auth_scope_retry_limit_client(
attempt += 1;
if attempt >= max_retries {
tracing::info!("Reached retry limit ({max_retries}), giving up");
return Ok(());
return Err(anyhow::anyhow!("Scope retry limit reached"));
}
}
Ok(())
}
async fn migration_token(
server_url: &str,
store: &InMemoryCredentialStore,
) -> anyhow::Result<String> {
let mut manager = AuthorizationManager::new(server_url).await?;
manager.set_credential_store(store.clone());
if manager.initialize_from_store().await? {
return Ok(manager.get_access_token().await?);
}
let challenge = initialize_challenge(server_url, conformance_lifecycle()).await?;
let resolution = manager
.resolve_metadata_from_challenge(challenge.as_deref())
.await?;
manager.set_metadata(resolution.metadata);
manager
.register_client("conformance-client", REDIRECT_URI, &[])
.await?;
let scopes = manager.select_scopes(None, &[]);
let scope_refs: Vec<&str> = scopes.iter().map(|s| s.as_str()).collect();
let auth_url = manager.get_authorization_url(&scope_refs).await?;
let callback = headless_authorize(&auth_url).await?;
manager
.exchange_code_for_token_with_issuer(
&callback.code,
&callback.csrf_token,
callback.issuer.as_deref(),
)
.await?;
Ok(manager.get_access_token().await?)
}
async fn run_auth_server_migration_client(
server_url: &str,
_ctx: &ConformanceContext,
) -> anyhow::Result<()> {
let store = InMemoryCredentialStore::new();
let http = reqwest::Client::new();
let body = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}});
let mut token = migration_token(server_url, &store).await?;
for _ in 0..3 {
let resp = http
.post(server_url)
.header(
"MCP-Protocol-Version",
conformance_protocol_version().as_str(),
)
.bearer_auth(&token)
.json(&body)
.send()
.await?;
if resp.status() == reqwest::StatusCode::UNAUTHORIZED {
token = migration_token(server_url, &store).await?;
}
}
Ok(())
}
/// Auth flow with pre-registered credentials (from context).
async fn run_auth_preregistered_client(
server_url: &str,
@ -679,12 +524,9 @@ async fn run_client_credentials_basic(
.unwrap_or("conformance-test-secret");
let mut manager = AuthorizationManager::new(server_url).await?;
let challenge = initialize_challenge(server_url, conformance_lifecycle()).await?;
let resolution = manager
.resolve_metadata_from_challenge(challenge.as_deref())
.await?;
let token_endpoint = resolution.metadata.token_endpoint.clone();
manager.set_metadata(resolution.metadata);
let metadata = manager.discover_metadata().await?;
let token_endpoint = metadata.token_endpoint.clone();
manager.set_metadata(metadata);
let http = reqwest::Client::new();
let resp = http
@ -727,43 +569,49 @@ async fn run_client_credentials_jwt(
) -> anyhow::Result<()> {
let client_id = ctx
.client_id
.clone()
.unwrap_or_else(|| "conformance-test-client".to_string());
let signing_key = ctx
.as_deref()
.unwrap_or("conformance-test-client");
let _pem = ctx
.private_key_pem
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Missing private_key_pem"))?
.as_bytes()
.to_vec();
let signing_algorithm = match ctx
.as_deref()
.ok_or_else(|| anyhow::anyhow!("Missing private_key_pem"))?;
let _alg = ctx
.signing_algorithm
.as_deref()
.ok_or_else(|| anyhow::anyhow!("Missing signing_algorithm"))?
{
"RS256" => JwtSigningAlgorithm::RS256,
"RS384" => JwtSigningAlgorithm::RS384,
"RS512" => JwtSigningAlgorithm::RS512,
"ES256" => JwtSigningAlgorithm::ES256,
"ES384" => JwtSigningAlgorithm::ES384,
algorithm => anyhow::bail!("Unsupported signing_algorithm: {algorithm}"),
};
let config = ClientCredentialsConfig::PrivateKeyJwt {
client_id,
signing_key,
signing_algorithm,
token_endpoint_audience: None,
scopes: vec![],
resource: Some(server_url.to_string()),
};
let mut oauth_state = OAuthState::new(server_url, None).await?;
oauth_state.authenticate_client_credentials(config).await?;
let manager = oauth_state
.into_authorization_manager()
.ok_or_else(|| anyhow::anyhow!("Client credentials flow did not authorize"))?;
.ok_or_else(|| anyhow::anyhow!("Missing signing_algorithm"))?;
// Discover metadata to get token endpoint
let mut manager = AuthorizationManager::new(server_url).await?;
let metadata = manager.discover_metadata().await?;
let token_endpoint = metadata.token_endpoint.clone();
manager.set_metadata(metadata);
// Build JWT assertion
// Parse the PEM private key
let key = openssl_free_ec_sign(_pem, client_id, &token_endpoint)?;
let http = reqwest::Client::new();
let form_body = format!(
"grant_type=client_credentials&client_assertion_type={}&client_assertion={}",
urlencoding::encode("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),
urlencoding::encode(&key),
);
let resp = http
.post(&token_endpoint)
.header("content-type", "application/x-www-form-urlencoded")
.body(form_body)
.send()
.await?;
let token_resp: serde_json::Value = resp.json().await?;
let access_token = token_resp["access_token"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("No access_token: {}", token_resp))?;
let transport = StreamableHttpClientTransport::with_client(
AuthClient::new(reqwest::Client::default(), manager),
StreamableHttpClientTransportConfig::with_uri(server_url),
reqwest::Client::default(),
StreamableHttpClientTransportConfig::with_uri(server_url)
.auth_header(access_token.to_string()),
);
let client = BasicClientHandler.serve(transport).await?;
@ -779,64 +627,71 @@ 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?;
/// Minimal ES256 JWT signing without heavy deps.
/// We use ring or pure-Rust approach. For simplicity, use the p256 + base64 crates
/// that are already transitive deps of oauth2.
fn openssl_free_ec_sign(pem: &str, client_id: &str, audience: &str) -> anyhow::Result<String> {
use std::time::{SystemTime, UNIX_EPOCH};
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?;
// Decode PEM → DER
let pem_body = pem
.lines()
.filter(|l| !l.starts_with("-----"))
.collect::<String>();
let der = base64_decode(&pem_body)?;
// Parse PKCS#8 DER to get the raw EC private key bytes
// PKCS#8 for EC P-256: the raw 32-byte key is at the end of the structure
let raw_key = extract_ec_private_key(&der)?;
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let header = base64url_encode(br#"{"alg":"ES256","typ":"JWT"}"#);
let payload_json = serde_json::json!({
"iss": client_id,
"sub": client_id,
"aud": audience,
"iat": now,
"exp": now + 300,
"jti": format!("jti-{}", now),
});
let payload = base64url_encode(payload_json.to_string().as_bytes());
let signing_input = format!("{}.{}", header, payload);
// Sign with p256
let secret_key = p256::ecdsa::SigningKey::from_bytes(raw_key.as_slice().into())
.map_err(|e| anyhow::anyhow!("Invalid EC key: {}", e))?;
use p256::ecdsa::signature::Signer;
let sig: p256::ecdsa::Signature = secret_key.sign(signing_input.as_bytes());
let sig_bytes = sig.to_bytes();
let sig_b64 = base64url_encode(&sig_bytes);
Ok(format!("{}.{}", signing_input, sig_b64))
}
fn base64url_encode(data: &[u8]) -> String {
use base64::Engine;
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data)
}
fn base64_decode(s: &str) -> anyhow::Result<Vec<u8>> {
use base64::Engine;
Ok(base64::engine::general_purpose::STANDARD.decode(s.trim())?)
}
/// Extract the raw 32-byte EC private key from a PKCS#8 DER blob.
fn extract_ec_private_key(der: &[u8]) -> anyhow::Result<Vec<u8>> {
// PKCS#8 wraps an ECPrivateKey. We look for the octet string containing
// the 32-byte private key. A simple heuristic: find 0x04 0x20 (OCTET STRING, len 32)
// followed by exactly 32 bytes that form the key.
// More robust: parse ASN.1. But for conformance testing this suffices.
for i in 0..der.len().saturating_sub(33) {
if der[i] == 0x04 && der[i + 1] == 0x20 && i + 34 <= der.len() {
return Ok(der[i + 2..i + 34].to_vec());
}
}
client.cancel().await?;
Ok(())
Err(anyhow::anyhow!(
"Could not extract 32-byte EC private key from PKCS#8 DER"
))
}
/// Cross-app access flow (SEP-1046 extension).
@ -925,53 +780,16 @@ async fn run_basic_client(server_url: &str) -> anyhow::Result<()> {
Ok(())
}
async fn run_tools_call_client(server_url: &str, ctx: &ConformanceContext) -> anyhow::Result<()> {
run_tools_call_client_with_lifecycle(server_url, ctx, conformance_lifecycle()).await
}
async fn run_discover_tools_call_client(
server_url: &str,
ctx: &ConformanceContext,
) -> anyhow::Result<()> {
run_tools_call_client_with_lifecycle(
server_url,
ctx,
ClientLifecycleMode::Discover {
preferred_versions: preferred_protocol_versions(),
},
)
.await
}
async fn run_tools_call_client_with_lifecycle(
server_url: &str,
ctx: &ConformanceContext,
lifecycle: ClientLifecycleMode,
) -> anyhow::Result<()> {
async fn run_tools_call_client(server_url: &str) -> anyhow::Result<()> {
let transport = StreamableHttpClientTransport::from_uri(server_url);
let client = FullClientHandler
.serve_with_lifecycle(transport, lifecycle)
.await?;
let client = FullClientHandler.serve(transport).await?;
let tools = client.list_tools(Default::default()).await?;
if ctx.tool_calls.is_empty() {
for tool in &tools.tools {
let args = build_tool_arguments(tool);
client
.call_tool(call_tool_params(tool.name.clone(), args))
.await?;
}
} else {
for tool_call in &ctx.tool_calls {
client
.call_tool(call_tool_params(
tool_call.name.clone().into(),
tool_call.arguments.clone(),
))
.await?;
}
for tool in &tools.tools {
let args = build_tool_arguments(tool);
let _ = client
.call_tool(call_tool_params(tool.name.clone(), args))
.await?;
}
client.cancel().await?;
Ok(())
}
@ -993,58 +811,6 @@ async fn run_elicitation_defaults_client(server_url: &str) -> anyhow::Result<()>
Ok(())
}
fn conformance_protocol_version() -> ProtocolVersion {
std::env::var("MCP_CONFORMANCE_PROTOCOL_VERSION")
.ok()
.and_then(|version| serde_json::from_value(Value::String(version)).ok())
.unwrap_or(ProtocolVersion::V_2025_11_25)
}
fn conformance_lifecycle() -> ClientLifecycleMode {
if conformance_protocol_version().as_str() >= ProtocolVersion::V_2026_07_28.as_str() {
ClientLifecycleMode::Discover {
preferred_versions: preferred_protocol_versions(),
}
} else {
ClientLifecycleMode::Initialize
}
}
/// Preferred protocol versions for discover-lifecycle negotiation: the
/// runner-provided version first, then all other known versions newest-first.
fn preferred_protocol_versions() -> Vec<ProtocolVersion> {
let mut preferred_versions = vec![conformance_protocol_version()];
for version in ProtocolVersion::KNOWN_VERSIONS.iter().rev() {
if !preferred_versions.contains(version) {
preferred_versions.push(version.clone());
}
}
preferred_versions
}
/// Runs scenarios through the discover lifecycle and Streamable HTTP transport.
async fn run_discover_client(server_url: &str) -> anyhow::Result<()> {
let preferred_versions = preferred_protocol_versions();
let transport = StreamableHttpClientTransport::from_uri(server_url);
let client = FullClientHandler
.serve_with_lifecycle(
transport,
ClientLifecycleMode::Discover { preferred_versions },
)
.await?;
let tools = client.list_tools(Default::default()).await?;
tracing::debug!("Listed {} tools", tools.tools.len());
for tool in &tools.tools {
let result = client
.call_tool(CallToolRequestParams::new(tool.name.clone()))
.await;
tracing::debug!("Called {}: {:?}", tool.name, result.is_ok());
}
client.cancel().await?;
Ok(())
}
async fn run_sse_retry_client(server_url: &str) -> anyhow::Result<()> {
let transport = StreamableHttpClientTransport::from_uri(server_url);
let client = BasicClientHandler.serve(transport).await?;
@ -1082,50 +848,14 @@ async fn main() -> anyhow::Result<()> {
tracing::info!("Running scenario '{}' against {}", scenario, server_url);
// Safety net: some harness servers intentionally misbehave (e.g. reply
// with an id-less error instead of answering a request), which would
// leave the client waiting forever. Exit on our own before the harness's
// 30s client timeout so it never has to kill us (which has been observed
// to wedge the harness process in CI).
let timeout_secs: u64 = std::env::var("MCP_CONFORMANCE_TIMEOUT_SECS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(25);
tokio::time::timeout(
std::time::Duration::from_secs(timeout_secs),
run_scenario(&scenario, &server_url, &ctx),
)
.await
.map_err(|_| anyhow::anyhow!("Scenario '{scenario}' timed out after {timeout_secs}s"))??;
Ok(())
}
async fn run_scenario(
scenario: &str,
server_url: &str,
ctx: &ConformanceContext,
) -> anyhow::Result<()> {
match scenario {
match scenario.as_str() {
// Non-auth scenarios
"initialize" => run_basic_client(server_url).await?,
// SEP-2106: the scenario serves a tool whose schema carries a network
// `$ref`; the check passes when the client lists tools without
// dereferencing (fetching) that URL. A plain connect → list_tools →
// close is sufficient; the scenario's mock server does not implement
// the discover lifecycle, so `run_discover_client` hangs against it.
"json-schema-ref-no-deref" => run_basic_client(server_url).await?,
"tools_call" => run_tools_call_client(server_url, ctx).await?,
"initialize" => run_basic_client(&server_url).await?,
"tools_call" => run_tools_call_client(&server_url).await?,
"elicitation-sep1034-client-defaults" => {
run_elicitation_defaults_client(server_url).await?
}
"sse-retry" => run_sse_retry_client(server_url).await?,
"request-metadata" | "sep-2322-client-request-state" => {
run_discover_client(server_url).await?
}
"http-standard-headers" | "http-custom-headers" | "http-invalid-tool-headers" => {
run_discover_tools_call_client(server_url, ctx).await?
run_elicitation_defaults_client(&server_url).await?
}
"sse-retry" => run_sse_retry_client(&server_url).await?,
// Auth scenarios - standard OAuth flow
"auth/metadata-default"
@ -1140,42 +870,21 @@ async fn run_scenario(
| "auth/token-endpoint-auth-post"
| "auth/token-endpoint-auth-none"
| "auth/2025-03-26-oauth-metadata-backcompat"
| "auth/2025-03-26-oauth-endpoint-fallback"
// Offline access scope handling: positive/negative variants both run
// the well-behaved flow; the referee inspects the requested scopes.
| "auth/offline-access-scope"
| "auth/offline-access-not-supported"
// SEP-2468 (RFC 9207 iss / RFC 8414 §3.3 issuer-echo). The client
// captures `iss` from the authorization redirect and passes it to the
// callback handler; the SDK validates internally. Positive scenarios
// proceed to the token endpoint; negative scenarios error out (the
// referee sets `allowClientError`).
| "auth/iss-supported"
| "auth/iss-not-advertised"
| "auth/iss-supported-missing"
| "auth/iss-wrong-issuer"
| "auth/iss-unexpected"
| "auth/iss-normalized"
| "auth/metadata-issuer-mismatch" => run_auth_client(server_url, ctx).await?,
| "auth/2025-03-26-oauth-endpoint-fallback" => run_auth_client(&server_url, &ctx).await?,
// Auth - scope step-up
"auth/scope-step-up" => run_auth_scope_step_up_client(server_url, ctx).await?,
"auth/scope-step-up" => run_auth_scope_step_up_client(&server_url, &ctx).await?,
// Auth - scope retry limit
"auth/scope-retry-limit" => run_auth_scope_retry_limit_client(server_url, ctx).await?,
// Auth - authorization server migration (SEP-2352)
"auth/authorization-server-migration" => {
run_auth_server_migration_client(server_url, ctx).await?
}
"auth/scope-retry-limit" => run_auth_scope_retry_limit_client(&server_url, &ctx).await?,
// Auth - pre-registration
"auth/pre-registration" => run_auth_preregistered_client(server_url, ctx).await?,
"auth/pre-registration" => run_auth_preregistered_client(&server_url, &ctx).await?,
// Auth - resource mismatch (should fail to auth → pass)
"auth/resource-mismatch" => {
// Try to auth; it should fail because PRM resource doesn't match
match run_auth_client(server_url, ctx).await {
match run_auth_client(&server_url, &ctx).await {
Ok(_) => {
tracing::warn!("Auth succeeded despite resource mismatch!");
}
@ -1186,48 +895,25 @@ async fn run_scenario(
}
// Auth - client credentials
"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/client-credentials-basic" => run_client_credentials_basic(&server_url, &ctx).await?,
"auth/client-credentials-jwt" => run_client_credentials_jwt(&server_url, &ctx).await?,
// Auth - cross-app access
"auth/cross-app-access-complete-flow" => {
run_cross_app_access_client(server_url, ctx).await?
run_cross_app_access_client(&server_url, &ctx).await?
}
unknown => anyhow::bail!("Unsupported conformance scenario: {unknown}"),
_ => {
tracing::warn!("Unknown scenario '{}', trying auth flow", scenario);
match run_auth_client(&server_url, &ctx).await {
Ok(_) => {}
Err(e) => {
tracing::debug!("Auth flow failed for unknown scenario: {e}");
run_basic_client(&server_url).await?
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn conformance_context_accepts_camel_case_tool_calls() {
let context: ConformanceContext = serde_json::from_value(json!({
"toolCalls": [{
"name": "test_custom_headers",
"arguments": { "region": "us-west1" }
}]
}))
.expect("valid conformance context");
assert_eq!(
context.tool_calls.first().map(|tool_call| (
tool_call.name.as_str(),
tool_call
.arguments
.as_ref()
.and_then(|arguments| arguments.get("region")),
)),
Some(("test_custom_headers", Some(&json!("us-west1"))))
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -7,99 +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
- release stable 3.0.0
## [3.0.0-beta.5](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v3.0.0-beta.4...rmcp-macros-v3.0.0-beta.5) - 2026-07-28
### Other
- prepare for stable 3.0 release ([#1073](https://github.com/modelcontextprotocol/rust-sdk/pull/1073))
## [3.0.0-beta.2](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v3.0.0-beta.1...rmcp-macros-v3.0.0-beta.2) - 2026-07-24
### Fixed
- [**breaking**] omit resultType for legacy protocol sessions ([#1038](https://github.com/modelcontextprotocol/rust-sdk/pull/1038))
### Other
- declare and check MSRV ([#1034](https://github.com/modelcontextprotocol/rust-sdk/pull/1034))
## [3.0.0-beta.1](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v2.2.0...rmcp-macros-v3.0.0-beta.1) - 2026-07-23
### Added
- add client-side TTL-honoring response cache (SEP-2549) ([#1025](https://github.com/modelcontextprotocol/rust-sdk/pull/1025))
- [**breaking**] Implement SEP-2663 Tasks Extension ([#1020](https://github.com/modelcontextprotocol/rust-sdk/pull/1020))
- add subscription listen streams (SEP-2575) ([#1000](https://github.com/modelcontextprotocol/rust-sdk/pull/1000))
- add modern client lifecycle modes (SEP-2575) ([#995](https://github.com/modelcontextprotocol/rust-sdk/pull/995))
- [**breaking**] implement SEP-2549 cache hints ([#889](https://github.com/modelcontextprotocol/rust-sdk/pull/889))
- [**breaking**] add MRTR behavior support (SEP-2322) ([#929](https://github.com/modelcontextprotocol/rust-sdk/pull/929))
- relax outputSchema to accept non-object JSON Schema types (SEP-2106) ([#895](https://github.com/modelcontextprotocol/rust-sdk/pull/895))
- [**breaking**] add MRTR model types (SEP-2322) ([#915](https://github.com/modelcontextprotocol/rust-sdk/pull/915))
### Other
- update for 2026-07-28 version ([#1032](https://github.com/modelcontextprotocol/rust-sdk/pull/1032))
## [2.0.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v1.8.0...rmcp-macros-v2.0.0) - 2026-06-27
### Added

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