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 |
||
|
|
4e361b715f |
||
|
|
9334c97f0d |
||
|
|
29c068d22b |
||
|
|
9a41811a66 |
||
|
|
e54c2220af |
||
|
|
b429fc1240 |
||
|
|
82a6c48e92 |
||
|
|
ce28e6d323 |
||
|
|
25213a271a |
||
|
|
0dea39d101 |
||
|
|
e5403cf956 |
||
|
|
fff05d40fd |
||
|
|
506e692074 |
||
|
|
1e9e4700f7 |
||
|
|
1f63e43479 |
||
|
|
2047ae42ec |
||
|
|
9528801f6e |
||
|
|
125dbfd5af |
||
|
|
c284607794 |
||
|
|
92c0793247 |
||
|
|
14298b72e0 |
||
|
|
60a7a75703 |
||
|
|
6839cfd613 |
||
|
|
76988023b2 |
||
|
|
e660b805ee |
||
|
|
190ea44218 |
||
|
|
e903341136 |
||
|
|
5554a41d67 |
||
|
|
397d416fdc |
||
|
|
aad4d4e4c6 |
||
|
|
50dd8e2040 |
||
|
|
151970775c |
||
|
|
07abfdfd99 |
||
|
|
ac9c637d04 |
||
|
|
8803d39034 |
||
|
|
99ee024499 |
||
|
|
4e36fd9531 |
||
|
|
a8308dc943 |
||
|
|
9bf459962e |
||
|
|
7044ccfe7b |
||
|
|
ff1a715b6e |
||
|
|
8ee9ee68f1 |
||
|
|
dd11e5be99 |
||
|
|
c87b6422f7 |
||
|
|
5b053416b8 |
||
|
|
a5bb51c9d2 |
||
|
|
7abbf29e93 |
||
|
|
61b40afdca |
||
|
|
8efc142e51 |
||
|
|
7d811c49d1 |
||
|
|
bc2c5f3c4a |
||
|
|
9df629e631 |
||
|
|
2e2c79173c |
||
|
|
81745f1375 |
||
|
|
77eb607ccb |
||
|
|
7824bfdada |
||
|
|
3549e86d28 |
||
|
|
b93220d4a7 |
||
|
|
839922d8fd |
||
|
|
92581bee53 |
||
|
|
24ba5265e2 |
||
|
|
1543a1afee |
||
|
|
f3459fa777 |
||
|
|
2fe90a245b |
||
|
|
98bb2635f4 |
||
|
|
93ea09a51b |
||
|
|
3662d20ac2 |
||
|
|
d8331d9442 |
||
|
|
2d4c29fb02 |
||
|
|
dd30a70f84 |
||
|
|
a7194590ff |
||
|
|
11f525c503 |
||
|
|
60d3e77f52 |
||
|
|
9e3de344f4 |
||
|
|
ba00b15097 |
||
|
|
f4ff56b81d |
186 changed files with 40955 additions and 8436 deletions
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
|
|
@ -16,11 +16,15 @@ updates:
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "weekly"
|
||||||
labels:
|
labels:
|
||||||
# Mark PRs as CI related change.
|
# Mark PRs as CI related change.
|
||||||
- T-CI
|
- T-CI
|
||||||
open-pull-requests-limit: 3
|
open-pull-requests-limit: 3
|
||||||
|
groups:
|
||||||
|
codeql-action:
|
||||||
|
patterns:
|
||||||
|
- "github/codeql-action*"
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "chore"
|
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.
|
# Runs workflow when activity on a PR in the workflow's repository occurs.
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
auto-label:
|
auto-label:
|
||||||
permissions:
|
permissions:
|
||||||
|
|
@ -20,7 +23,7 @@ jobs:
|
||||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v6
|
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7
|
||||||
with:
|
with:
|
||||||
# Auto-include paths starting with dot (e.g. .github)
|
# Auto-include paths starting with dot (e.g. .github)
|
||||||
dot: true
|
dot: true
|
||||||
|
|
|
||||||
155
.github/workflows/ci.yml
vendored
155
.github/workflows/ci.yml
vendored
|
|
@ -12,34 +12,35 @@ env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
ARTIFACT_DIR: release-artifacts
|
ARTIFACT_DIR: release-artifacts
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
commit-lint:
|
commit-lint:
|
||||||
name: Lint Commit Messages
|
name: Lint Commit Messages
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Install commitlint
|
- name: Install commitlint
|
||||||
run: |
|
run: npm install --global @commitlint/cli@20.4.3 @commitlint/config-conventional@20.4.3
|
||||||
npm install --save-dev @commitlint/cli @commitlint/config-conventional
|
|
||||||
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
|
|
||||||
|
|
||||||
- name: Lint commit messages
|
- 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:
|
fmt:
|
||||||
name: Code Formatting
|
name: Code Formatting
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Install Rust fmt
|
- name: Install Rust fmt
|
||||||
run: rustup toolchain install nightly --component rustfmt
|
run: rustup toolchain install nightly --component rustfmt
|
||||||
|
|
@ -51,32 +52,40 @@ jobs:
|
||||||
name: Lint with Clippy
|
name: Lint with Clippy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Install Rust
|
- 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
|
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:
|
semver:
|
||||||
name: SemVer Check
|
name: SemVer Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install Rust
|
- 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
|
- name: Install cargo-semver-checks
|
||||||
uses: taiki-e/install-action@v2
|
uses: taiki-e/install-action@3f74d7c16a4242f1c95561e98edc25d36adb4375 # v2.87.12
|
||||||
with:
|
with:
|
||||||
tool: cargo-semver-checks
|
tool: cargo-semver-checks
|
||||||
|
|
||||||
|
|
@ -117,7 +126,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|
@ -125,12 +134,12 @@ jobs:
|
||||||
# to be installed (it does not need to be the default; the tool invokes it
|
# to be installed (it does not need to be the default; the tool invokes it
|
||||||
# via `cargo +nightly`).
|
# via `cargo +nightly`).
|
||||||
- name: Install Rust
|
- 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
|
- name: Install cargo-public-api
|
||||||
uses: taiki-e/install-action@v2
|
uses: taiki-e/install-action@3f74d7c16a4242f1c95561e98edc25d36adb4375 # v2.87.12
|
||||||
with:
|
with:
|
||||||
tool: cargo-public-api
|
tool: cargo-public-api
|
||||||
|
|
||||||
|
|
@ -170,10 +179,24 @@ jobs:
|
||||||
|
|
||||||
- name: Check rmcp (all features except local)
|
- name: Check rmcp (all features except local)
|
||||||
run: |
|
run: |
|
||||||
FEATURES=$(cargo metadata --no-deps --format-version 1 \
|
BASE_SHA=${{ github.event.pull_request.base.sha }}
|
||||||
| jq -r '[.packages[] | select(.name == "rmcp") | .features | keys[]
|
# `cargo public-api diff` builds both revisions with the same feature
|
||||||
| select(startswith("__") | not)
|
# set, so a feature that exists on only one side (e.g. a feature added
|
||||||
| select(. != "local")] | join(",")')
|
# 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, -)
|
||||||
cargo public-api \
|
cargo public-api \
|
||||||
--package rmcp \
|
--package rmcp \
|
||||||
--features "$FEATURES" \
|
--features "$FEATURES" \
|
||||||
|
|
@ -181,33 +204,57 @@ jobs:
|
||||||
diff \
|
diff \
|
||||||
$DENY \
|
$DENY \
|
||||||
--force \
|
--force \
|
||||||
${{ github.event.pull_request.base.sha }}..${{ github.sha }}
|
"$BASE_SHA"..${{ github.sha }}
|
||||||
|
|
||||||
spelling:
|
spelling:
|
||||||
name: spell check with typos
|
name: spell check with typos
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
- name: Spell Check Repo
|
- name: Spell Check Repo
|
||||||
uses: crate-ci/typos@master
|
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
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: Run Tests
|
name: Run Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
# install nodejs
|
# install nodejs
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
run: uv python install
|
run: uv python install
|
||||||
|
|
@ -215,7 +262,7 @@ jobs:
|
||||||
- name: Create venv for python
|
- name: Create venv for python
|
||||||
run: uv venv
|
run: uv venv
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --all-features
|
run: cargo test --all-features
|
||||||
|
|
@ -224,19 +271,19 @@ jobs:
|
||||||
name: Run Tests (no local feature)
|
name: Run Tests (no local feature)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
# install nodejs
|
# install nodejs
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
run: uv python install
|
run: uv python install
|
||||||
|
|
@ -244,7 +291,7 @@ jobs:
|
||||||
- name: Create venv for python
|
- name: Create venv for python
|
||||||
run: uv venv
|
run: uv venv
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||||
|
|
||||||
- name: Run tests without local feature
|
- name: Run tests without local feature
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -258,21 +305,21 @@ jobs:
|
||||||
name: Code Coverage
|
name: Code Coverage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
# install nodejs
|
# install nodejs
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
run: uv python install
|
run: uv python install
|
||||||
|
|
@ -280,7 +327,7 @@ jobs:
|
||||||
- name: Create venv for python
|
- name: Create venv for python
|
||||||
run: uv venv
|
run: uv venv
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||||
|
|
||||||
- name: Install cargo-llvm-cov
|
- name: Install cargo-llvm-cov
|
||||||
run: cargo install cargo-llvm-cov
|
run: cargo install cargo-llvm-cov
|
||||||
|
|
@ -295,19 +342,19 @@ jobs:
|
||||||
name: Example test
|
name: Example test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
# install nodejs
|
# install nodejs
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
run: uv python install
|
run: uv python install
|
||||||
|
|
@ -315,7 +362,7 @@ jobs:
|
||||||
- name: Create venv for python
|
- name: Create venv for python
|
||||||
run: uv venv
|
run: uv venv
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||||
|
|
||||||
- name: Add target WASI preview 2
|
- name: Add target WASI preview 2
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -351,12 +398,12 @@ jobs:
|
||||||
name: Security Audit
|
name: Security Audit
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Install Rust
|
- 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
|
- name: Install cargo-audit
|
||||||
run: cargo install cargo-audit
|
run: cargo install cargo-audit
|
||||||
|
|
@ -368,12 +415,12 @@ jobs:
|
||||||
name: Generate Documentation
|
name: Generate Documentation
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Install Rust
|
- 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
|
- name: Generate documentation
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -394,7 +441,7 @@ jobs:
|
||||||
# This happened recently in the attack on `tj-actions/changed-files`, but
|
# This happened recently in the attack on `tj-actions/changed-files`, but
|
||||||
# has happened many times before as well.
|
# has happened many times before as well.
|
||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Update Rust
|
- name: Update Rust
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
11
.github/workflows/codeql.yml
vendored
11
.github/workflows/codeql.yml
vendored
|
|
@ -8,6 +8,9 @@ on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * 1' # Weekly on Monday
|
- cron: '0 0 * * 1' # Weekly on Monday
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
|
|
@ -21,16 +24,16 @@ jobs:
|
||||||
language: [rust, javascript-typescript, python, actions]
|
language: [rust, javascript-typescript, python, actions]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v4
|
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
config-file: ./.github/codeql/codeql-config.yml
|
config-file: ./.github/codeql/codeql-config.yml
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v4
|
uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v4
|
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||||
|
|
|
||||||
87
.github/workflows/conformance.yml
vendored
87
.github/workflows/conformance.yml
vendored
|
|
@ -12,27 +12,31 @@ concurrency:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Pinned for reproducible runs; bump deliberately when the suite updates.
|
CONFORMANCE_VERSION: "0.2.0-alpha.10"
|
||||||
CONFORMANCE_VERSION: "0.1.16"
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
server:
|
server:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- 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
|
||||||
|
|
||||||
# Build the whole package (server + client bins): the conformance crate is
|
|
||||||
# excluded from the workspace default-members.
|
|
||||||
- name: Build conformance binaries
|
- name: Build conformance binaries
|
||||||
run: cargo build -p mcp-conformance
|
run: cargo build -p mcp-conformance
|
||||||
|
|
||||||
|
- name: Test conformance server
|
||||||
|
run: cargo test -p mcp-conformance --bin conformance-server
|
||||||
|
|
||||||
- name: Start conformance server
|
- name: Start conformance server
|
||||||
run: |
|
run: |
|
||||||
PORT=8001 ./target/debug/conformance-server &
|
PORT=8001 ./target/debug/conformance-server &
|
||||||
|
|
@ -46,22 +50,42 @@ jobs:
|
||||||
echo "conformance server did not become ready" >&2
|
echo "conformance server did not become ready" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Run server conformance suite
|
- name: Run 2025-11-25 server suite
|
||||||
run: |
|
run: |
|
||||||
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" server \
|
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" server \
|
||||||
--url http://127.0.0.1:8001/mcp \
|
--url http://127.0.0.1:8001/mcp \
|
||||||
|
--suite all \
|
||||||
--spec-version 2025-11-25 \
|
--spec-version 2025-11-25 \
|
||||||
-o conformance-results
|
-o conformance-results/2025-11-25
|
||||||
|
|
||||||
# These pass today but are excluded from the default "active" suite;
|
- name: Run 2026-07-28 server suite
|
||||||
# run them explicitly so regressions are still caught.
|
|
||||||
- name: Run pending scenarios
|
|
||||||
run: |
|
run: |
|
||||||
for scenario in json-schema-2020-12 server-sse-polling; do
|
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 \
|
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" server \
|
||||||
--url http://127.0.0.1:8001/mcp \
|
--url http://127.0.0.1:8001/mcp \
|
||||||
--scenario "$scenario" \
|
--scenario "$scenario" \
|
||||||
-o conformance-results
|
--expected-failures conformance/expected-failures-extensions.yaml \
|
||||||
|
-o conformance-extension-results
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Stop conformance server
|
- name: Stop conformance server
|
||||||
|
|
@ -70,37 +94,56 @@ jobs:
|
||||||
|
|
||||||
- name: Upload results
|
- name: Upload results
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: conformance-server-results
|
name: conformance-server-results
|
||||||
path: conformance-results
|
path: |
|
||||||
|
conformance-results
|
||||||
|
conformance-extension-results
|
||||||
|
|
||||||
client:
|
client:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- 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
|
- name: Build conformance binaries
|
||||||
run: cargo build -p mcp-conformance
|
run: cargo build -p mcp-conformance
|
||||||
|
|
||||||
- name: Run full client conformance suite
|
- name: Run 2025-11-25 client suite
|
||||||
run: |
|
run: |
|
||||||
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" client \
|
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" client \
|
||||||
--command "$(pwd)/target/debug/conformance-client" \
|
--command "$(pwd)/target/debug/conformance-client" \
|
||||||
--suite all \
|
--suite all \
|
||||||
--spec-version 2025-11-25 \
|
--spec-version 2025-11-25 \
|
||||||
-o conformance-client-results/full
|
-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
|
- name: Upload results
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: conformance-client-results
|
name: conformance-client-results
|
||||||
path: 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
|
name: Release-plz
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
pull-requests: write
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
@ -18,19 +16,20 @@ jobs:
|
||||||
if: ${{ github.repository_owner == 'modelcontextprotocol' }}
|
if: ${{ github.repository_owner == 'modelcontextprotocol' }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
pull-requests: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||||
with:
|
with:
|
||||||
toolchain: "1.92"
|
toolchain: "1.92"
|
||||||
# Using fork until semver_check_features support is released upstream.
|
# Using fork until semver_check_features support is released upstream.
|
||||||
# See: https://github.com/release-plz/release-plz/pull/2757
|
# See: https://github.com/release-plz/release-plz/pull/2757
|
||||||
- name: Install release-plz from fork
|
- 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
|
- name: Run release-plz release
|
||||||
run: release-plz release
|
run: release-plz release
|
||||||
env:
|
env:
|
||||||
|
|
@ -51,17 +50,17 @@ jobs:
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||||
with:
|
with:
|
||||||
toolchain: "1.92"
|
toolchain: "1.92"
|
||||||
# Using fork until semver_check_features support is released upstream.
|
# Using fork until semver_check_features support is released upstream.
|
||||||
# See: https://github.com/release-plz/release-plz/pull/2757
|
# See: https://github.com/release-plz/release-plz/pull/2757
|
||||||
- name: Install release-plz from fork
|
- 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
|
- name: Run release-plz release-pr
|
||||||
run: release-plz release-pr
|
run: release-plz release-pr
|
||||||
env:
|
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
|
||||||
38
.github/workflows/stale.yml
vendored
Normal file
38
.github/workflows/stale.yml
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
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.
|
||||||
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' }}
|
TRIAGE_MODEL: ${{ vars.TRIAGE_MODEL || 'gpt-4o-mini' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Install jq
|
- name: Install jq
|
||||||
run: sudo apt-get install -y jq
|
run: sudo apt-get install -y jq
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@ default-members = ["crates/rmcp", "crates/rmcp-macros"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
rmcp = { version = "2.2.0", path = "./crates/rmcp" }
|
rmcp = { version = "3.4.0", path = "./crates/rmcp" }
|
||||||
rmcp-macros = { version = "2.2.0", path = "./crates/rmcp-macros" }
|
rmcp-macros = { version = "3.4.0", path = "./crates/rmcp-macros" }
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
version = "2.2.0"
|
rust-version = "1.88"
|
||||||
|
version = "3.4.0"
|
||||||
authors = ["4t145 <u4t145@163.com>"]
|
authors = ["4t145 <u4t145@163.com>"]
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
repository = "https://github.com/modelcontextprotocol/rust-sdk/"
|
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).
|
||||||
145
ROADMAP.md
145
ROADMAP.md
|
|
@ -1,71 +1,112 @@
|
||||||
# RMCP Roadmap
|
# 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.
|
||||||
|
|
||||||
Server conformance: 87.5% (28/32) · Client conformance: 80.0% (16/20)
|
**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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Tier 2 → Tier 1
|
## Conformance
|
||||||
|
|
||||||
### Conformance
|
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).
|
||||||
|
|
||||||
#### Server (87.5% → 100%)
|
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.
|
||||||
|
|
||||||
- [ ] Fix `prompts-get-with-args` — prompt argument handling returns incorrect result (arg1/arg2 not substituted)
|
### Informational (not scored for tiering)
|
||||||
- [ ] 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)
|
|
||||||
|
|
||||||
#### Client (80.0% → 100%)
|
Extension-tagged scenarios are excluded by `--spec-version` filters, so CI runs them
|
||||||
|
in separate steps against `conformance/expected-failures-extensions.yaml`:
|
||||||
|
|
||||||
- [ ] Fix `auth/metadata-var3` — AS metadata discovery variant 3 (no authorization support detected)
|
| Scenario | Tag | Status |
|
||||||
- [ ] 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
|
| `auth/client-credentials-basic` | extension | ✅ Pass |
|
||||||
- [ ] Fix `auth/2025-03-26-oauth-endpoint-fallback` — legacy OAuth endpoint fallback for pre-2025-06-18 servers (no authorization support detected)
|
| `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 |
|
||||||
|
|
||||||
### Governance & Policy
|
### Spec features without conformance scenarios
|
||||||
|
|
||||||
- [ ] Create `VERSIONING.md` — document semver scheme, what constitutes a breaking change, and how breaking changes are communicated
|
Conformance does not cover the entire spec surface. Remaining feature work tracked via
|
||||||
|
the milestone:
|
||||||
|
|
||||||
### Documentation (26/48 → 48/48 features with prose + examples)
|
- SEP-2567 sessionless MCP via explicit state handles (#870)
|
||||||
|
- SEP-2260 server requests must associate with a client request (#873)
|
||||||
#### Undocumented features (14)
|
- SEP-2549 follow-up: client-side TTL-honoring cache (#974)
|
||||||
|
|
||||||
- [ ] 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)*
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Informational (not scored for tiering)
|
## Completed
|
||||||
|
|
||||||
These draft/extension scenarios are tracked but do not count toward tier advancement:
|
### Tier 1 requirements
|
||||||
|
|
||||||
| Scenario | Tag | Status |
|
- [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
|
||||||
| `auth/resource-mismatch` | draft | ❌ Failed |
|
- [x] 2025-11-25 server conformance 100% (30/30)
|
||||||
| `auth/client-credentials-jwt` | extension | ❌ Failed — JWT `aud` claim verification error |
|
- [x] 2025-11-25 client conformance 100%
|
||||||
| `auth/client-credentials-basic` | extension | ✅ Passed |
|
- [x] 2026-07-28 server conformance 100% (30/30 dated)
|
||||||
| `auth/cross-app-access-complete-flow` | extension | ❌ Failed — sends `authorization_code` grant instead of `jwt-bearer` |
|
- [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`).
|
||||||
|
|
|
||||||
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.
|
||||||
|
|
@ -1,3 +1,2 @@
|
||||||
msrv = "1.85"
|
|
||||||
too-many-arguments-threshold = 10
|
too-many-arguments-threshold = 10
|
||||||
check-private-items = false
|
check-private-items = false
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ rmcp = { path = "../crates/rmcp", features = [
|
||||||
"client",
|
"client",
|
||||||
"elicitation",
|
"elicitation",
|
||||||
"auth",
|
"auth",
|
||||||
|
"auth-client-credentials-jwt",
|
||||||
|
"auth-enterprise-managed",
|
||||||
|
"request-state",
|
||||||
"transport-streamable-http-server",
|
"transport-streamable-http-server",
|
||||||
"transport-streamable-http-client-reqwest",
|
"transport-streamable-http-client-reqwest",
|
||||||
] }
|
] }
|
||||||
|
|
@ -29,8 +32,7 @@ tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
axum = { version = "0.8", features = ["macros"] }
|
axum = { version = "0.8", features = ["macros"] }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
oauth2 = { version = "5.0", default-features = false }
|
||||||
reqwest = { version = "0.13", features = ["json"] }
|
reqwest = { version = "0.13", features = ["json"] }
|
||||||
urlencoding = "2"
|
urlencoding = "2"
|
||||||
url = "2"
|
url = "2"
|
||||||
p256 = { version = "0.14", features = ["ecdsa"] }
|
|
||||||
base64 = "0.22"
|
|
||||||
|
|
|
||||||
26
conformance/expected-failures-extensions.yaml
Normal file
26
conformance/expected-failures-extensions.yaml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# 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
|
||||||
|
|
@ -1,10 +1,21 @@
|
||||||
|
#![expect(
|
||||||
|
deprecated,
|
||||||
|
reason = "The conformance suite still exercises deprecated sampling scenarios"
|
||||||
|
)]
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use oauth2::{ClientSecret, RefreshToken};
|
||||||
use rmcp::{
|
use rmcp::{
|
||||||
ClientHandler, ErrorData, RoleClient, ServiceExt,
|
ClientHandler, ClientLifecycleMode, ClientServiceExt, ErrorData, RoleClient, ServiceExt,
|
||||||
model::*,
|
model::*,
|
||||||
service::RequestContext,
|
service::RequestContext,
|
||||||
transport::{
|
transport::{
|
||||||
AuthClient, AuthorizationManager, StreamableHttpClientTransport,
|
AuthClient, AuthorizationManager, StreamableHttpClientTransport,
|
||||||
auth::{AuthorizationCallback, OAuthState},
|
auth::{
|
||||||
|
AuthorizationCallback, AuthorizationRequest, ClientCredentialsConfig,
|
||||||
|
InMemoryCredentialStore, JwtSigningAlgorithm, OAuthState, default_oauth_http_client,
|
||||||
|
enterprise::{EmaAuthorizationServer, EmaClientAuthentication, EmaExchangeRequest},
|
||||||
|
},
|
||||||
streamable_http_client::StreamableHttpClientTransportConfig,
|
streamable_http_client::StreamableHttpClientTransportConfig,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -13,8 +24,17 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
// ─── Context parsed from MCP_CONFORMANCE_CONTEXT ────────────────────────────
|
// ─── 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)]
|
#[derive(Debug, Default, serde::Deserialize)]
|
||||||
struct ConformanceContext {
|
struct ConformanceContext {
|
||||||
|
#[serde(default, alias = "toolCalls")]
|
||||||
|
tool_calls: Vec<ConformanceToolCall>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
client_id: Option<String>,
|
client_id: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
@ -24,6 +44,17 @@ struct ConformanceContext {
|
||||||
private_key_pem: Option<String>,
|
private_key_pem: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
signing_algorithm: Option<String>,
|
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 {
|
fn load_context() -> ConformanceContext {
|
||||||
|
|
@ -43,8 +74,8 @@ impl ClientHandler for BasicClientHandler {}
|
||||||
struct ElicitationDefaultsClientHandler;
|
struct ElicitationDefaultsClientHandler;
|
||||||
|
|
||||||
impl ClientHandler for ElicitationDefaultsClientHandler {
|
impl ClientHandler for ElicitationDefaultsClientHandler {
|
||||||
fn get_info(&self) -> ClientInfo {
|
fn get_info(&self) -> ClientConfig {
|
||||||
let mut info = ClientInfo::default();
|
let mut info = ClientConfig::default();
|
||||||
info.capabilities.elicitation = Some(
|
info.capabilities.elicitation = Some(
|
||||||
ElicitationCapability::new()
|
ElicitationCapability::new()
|
||||||
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
|
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
|
||||||
|
|
@ -132,8 +163,8 @@ impl ClientHandler for ElicitationDefaultsClientHandler {
|
||||||
struct FullClientHandler;
|
struct FullClientHandler;
|
||||||
|
|
||||||
impl ClientHandler for FullClientHandler {
|
impl ClientHandler for FullClientHandler {
|
||||||
fn get_info(&self) -> ClientInfo {
|
fn get_info(&self) -> ClientConfig {
|
||||||
let mut info = ClientInfo::default();
|
let mut info = ClientConfig::default();
|
||||||
info.capabilities.elicitation = Some(
|
info.capabilities.elicitation = Some(
|
||||||
ElicitationCapability::new()
|
ElicitationCapability::new()
|
||||||
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
|
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
|
||||||
|
|
@ -180,28 +211,76 @@ impl ClientHandler for FullClientHandler {
|
||||||
|
|
||||||
const CIMD_CLIENT_METADATA_URL: &str = "https://conformance-test.local/client-metadata.json";
|
const CIMD_CLIENT_METADATA_URL: &str = "https://conformance-test.local/client-metadata.json";
|
||||||
const REDIRECT_URI: &str = "http://localhost:3000/callback";
|
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"];
|
const SCOPE_STEP_UP_ESCALATED_SCOPES: &[&str] = &["mcp:basic", "mcp:write"];
|
||||||
|
|
||||||
/// Perform the headless OAuth authorization-code flow.
|
/// Attempt the real connection unauthenticated and return the server's
|
||||||
|
/// `WWW-Authenticate` challenge from the 401 — the reactive discovery
|
||||||
|
/// trigger.
|
||||||
///
|
///
|
||||||
/// 1. Discover metadata, register (or use CIMD), get auth URL
|
/// `None` (server accepted the unauthenticated connection, which is then
|
||||||
/// 2. Fetch the auth URL with redirect:manual → extract code from Location header
|
/// closed cleanly) is a legitimate outcome, not an error: the scope-step-up
|
||||||
/// 3. Exchange code for token
|
/// and scope-retry-limit mocks allow unauthenticated `initialize` and only
|
||||||
/// 4. Return an `AuthClient` wrapping `reqwest::Client`
|
/// 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`
|
||||||
async fn perform_oauth_flow(
|
async fn perform_oauth_flow(
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
_ctx: &ConformanceContext,
|
_ctx: &ConformanceContext,
|
||||||
) -> anyhow::Result<AuthClient<reqwest::Client>> {
|
) -> 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?;
|
let mut oauth = OAuthState::new(server_url, None).await?;
|
||||||
|
|
||||||
// Discover + register + get auth URL
|
// 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
|
oauth
|
||||||
.start_authorization_with_metadata_url(
|
.start_authorization(with_optional_challenge(request, challenge))
|
||||||
&[],
|
|
||||||
REDIRECT_URI,
|
|
||||||
Some("conformance-client"),
|
|
||||||
Some(CIMD_CLIENT_METADATA_URL),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let auth_url = oauth.get_authorization_url().await?;
|
let auth_url = oauth.get_authorization_url().await?;
|
||||||
|
|
@ -236,50 +315,38 @@ async fn perform_oauth_flow(
|
||||||
Ok(AuthClient::new(reqwest::Client::default(), am))
|
Ok(AuthClient::new(reqwest::Client::default(), am))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like `perform_oauth_flow` but uses pre-registered client credentials.
|
/// Like `perform_oauth_flow` but uses pre-registered client credentials,
|
||||||
|
/// exercising the SDK's high-level `OAuthState` path (no DCR).
|
||||||
async fn perform_oauth_flow_preregistered(
|
async fn perform_oauth_flow_preregistered(
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
client_id: &str,
|
client_id: &str,
|
||||||
client_secret: &str,
|
client_secret: &str,
|
||||||
) -> anyhow::Result<AuthClient<reqwest::Client>> {
|
) -> anyhow::Result<AuthClient<reqwest::Client>> {
|
||||||
let mut manager = AuthorizationManager::new(server_url).await?;
|
let challenge = initialize_challenge(server_url, conformance_lifecycle()).await?;
|
||||||
let metadata = manager.discover_metadata().await?;
|
let mut oauth = OAuthState::new(server_url, None).await?;
|
||||||
manager.set_metadata(metadata);
|
|
||||||
|
|
||||||
// Configure with pre-registered credentials
|
let request = AuthorizationRequest::new(REDIRECT_URI)
|
||||||
let config = rmcp::transport::auth::OAuthClientConfig::new(client_id, REDIRECT_URI)
|
.with_preregistered_client(client_id)
|
||||||
.with_client_secret(client_secret);
|
.with_client_secret(client_secret);
|
||||||
manager.configure_client(config)?;
|
oauth
|
||||||
|
.start_authorization(with_optional_challenge(request, challenge))
|
||||||
|
.await?;
|
||||||
|
|
||||||
let scopes = manager.select_scopes(None, &[]);
|
let auth_url = oauth.get_authorization_url().await?;
|
||||||
let scope_refs: Vec<&str> = scopes.iter().map(|s| s.as_str()).collect();
|
let callback = headless_authorize(&auth_url).await?;
|
||||||
let auth_url = manager.get_authorization_url(&scope_refs).await?;
|
oauth
|
||||||
|
.handle_callback_with_issuer(
|
||||||
|
&callback.code,
|
||||||
|
&callback.csrf_token,
|
||||||
|
callback.issuer.as_deref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Headless redirect
|
let am = oauth
|
||||||
let http = reqwest::Client::builder()
|
.into_authorization_manager()
|
||||||
.redirect(reqwest::redirect::Policy::none())
|
.ok_or_else(|| anyhow::anyhow!("Failed to get authorization manager"))?;
|
||||||
.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"))?;
|
|
||||||
|
|
||||||
manager.exchange_code_for_token(&code, &state).await?;
|
Ok(AuthClient::new(reqwest::Client::default(), am))
|
||||||
|
|
||||||
Ok(AuthClient::new(reqwest::Client::default(), manager))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the standard auth flow, then connect and exercise the server.
|
/// Run the standard auth flow, then connect and exercise the server.
|
||||||
|
|
@ -291,7 +358,17 @@ async fn run_auth_client(server_url: &str, ctx: &ConformanceContext) -> anyhow::
|
||||||
StreamableHttpClientTransportConfig::with_uri(server_url),
|
StreamableHttpClientTransportConfig::with_uri(server_url),
|
||||||
);
|
);
|
||||||
|
|
||||||
let client = BasicClientHandler.serve(transport).await?;
|
// 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?;
|
||||||
tracing::debug!("Connected (authenticated)");
|
tracing::debug!("Connected (authenticated)");
|
||||||
|
|
||||||
let tools = client.list_tools(Default::default()).await?;
|
let tools = client.list_tools(Default::default()).await?;
|
||||||
|
|
@ -315,15 +392,14 @@ async fn run_auth_scope_step_up_client(
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
_ctx: &ConformanceContext,
|
_ctx: &ConformanceContext,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// First auth
|
let challenge = initialize_challenge(server_url, conformance_lifecycle()).await?;
|
||||||
let mut oauth = OAuthState::new(server_url, None).await?;
|
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
|
oauth
|
||||||
.start_authorization_with_metadata_url(
|
.start_authorization(with_optional_challenge(request, challenge))
|
||||||
&[],
|
|
||||||
REDIRECT_URI,
|
|
||||||
Some("conformance-client"),
|
|
||||||
Some(CIMD_CLIENT_METADATA_URL),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let auth_url = oauth.get_authorization_url().await?;
|
let auth_url = oauth.get_authorization_url().await?;
|
||||||
|
|
@ -346,7 +422,9 @@ async fn run_auth_scope_step_up_client(
|
||||||
StreamableHttpClientTransportConfig::with_uri(server_url),
|
StreamableHttpClientTransportConfig::with_uri(server_url),
|
||||||
);
|
);
|
||||||
|
|
||||||
let client = BasicClientHandler.serve(transport).await?;
|
let client = BasicClientHandler
|
||||||
|
.serve_with_lifecycle(transport, conformance_lifecycle())
|
||||||
|
.await?;
|
||||||
|
|
||||||
let tools = client.list_tools(Default::default()).await?;
|
let tools = client.list_tools(Default::default()).await?;
|
||||||
tracing::debug!("Listed {} tools", tools.tools.len());
|
tracing::debug!("Listed {} tools", tools.tools.len());
|
||||||
|
|
@ -368,11 +446,11 @@ async fn run_auth_scope_step_up_client(
|
||||||
|
|
||||||
let mut oauth2 = OAuthState::new(server_url, None).await?;
|
let mut oauth2 = OAuthState::new(server_url, None).await?;
|
||||||
oauth2
|
oauth2
|
||||||
.start_authorization_with_metadata_url(
|
.start_authorization(
|
||||||
SCOPE_STEP_UP_ESCALATED_SCOPES,
|
AuthorizationRequest::new(REDIRECT_URI)
|
||||||
REDIRECT_URI,
|
.with_scopes(SCOPE_STEP_UP_ESCALATED_SCOPES.iter().copied())
|
||||||
Some("conformance-client"),
|
.with_client_name("conformance-client")
|
||||||
Some(CIMD_CLIENT_METADATA_URL),
|
.with_client_metadata_url(CIMD_CLIENT_METADATA_URL),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let auth_url2 = oauth2.get_authorization_url().await?;
|
let auth_url2 = oauth2.get_authorization_url().await?;
|
||||||
|
|
@ -393,10 +471,12 @@ async fn run_auth_scope_step_up_client(
|
||||||
auth_client2,
|
auth_client2,
|
||||||
StreamableHttpClientTransportConfig::with_uri(server_url),
|
StreamableHttpClientTransportConfig::with_uri(server_url),
|
||||||
);
|
);
|
||||||
let client2 = BasicClientHandler.serve(transport2).await?;
|
let client2 = BasicClientHandler
|
||||||
let _ = client2
|
.serve_with_lifecycle(transport2, conformance_lifecycle())
|
||||||
|
.await?;
|
||||||
|
client2
|
||||||
.call_tool(call_tool_params(tool.name.clone(), args))
|
.call_tool(call_tool_params(tool.name.clone(), args))
|
||||||
.await;
|
.await?;
|
||||||
client2.cancel().await.ok();
|
client2.cancel().await.ok();
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -414,16 +494,15 @@ async fn run_auth_scope_retry_limit_client(
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let max_retries = 3u32;
|
let max_retries = 3u32;
|
||||||
let mut attempt = 0u32;
|
let mut attempt = 0u32;
|
||||||
|
let challenge = initialize_challenge(server_url, conformance_lifecycle()).await?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let mut oauth = OAuthState::new(server_url, None).await?;
|
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
|
oauth
|
||||||
.start_authorization_with_metadata_url(
|
.start_authorization(with_optional_challenge(request, challenge.clone()))
|
||||||
&[],
|
|
||||||
REDIRECT_URI,
|
|
||||||
Some("conformance-client"),
|
|
||||||
Some(CIMD_CLIENT_METADATA_URL),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
let auth_url = oauth.get_authorization_url().await?;
|
let auth_url = oauth.get_authorization_url().await?;
|
||||||
let callback = headless_authorize(&auth_url).await?;
|
let callback = headless_authorize(&auth_url).await?;
|
||||||
|
|
@ -486,6 +565,69 @@ async fn run_auth_scope_retry_limit_client(
|
||||||
Ok(())
|
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).
|
/// Auth flow with pre-registered credentials (from context).
|
||||||
async fn run_auth_preregistered_client(
|
async fn run_auth_preregistered_client(
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
|
|
@ -537,9 +679,12 @@ async fn run_client_credentials_basic(
|
||||||
.unwrap_or("conformance-test-secret");
|
.unwrap_or("conformance-test-secret");
|
||||||
|
|
||||||
let mut manager = AuthorizationManager::new(server_url).await?;
|
let mut manager = AuthorizationManager::new(server_url).await?;
|
||||||
let metadata = manager.discover_metadata().await?;
|
let challenge = initialize_challenge(server_url, conformance_lifecycle()).await?;
|
||||||
let token_endpoint = metadata.token_endpoint.clone();
|
let resolution = manager
|
||||||
manager.set_metadata(metadata);
|
.resolve_metadata_from_challenge(challenge.as_deref())
|
||||||
|
.await?;
|
||||||
|
let token_endpoint = resolution.metadata.token_endpoint.clone();
|
||||||
|
manager.set_metadata(resolution.metadata);
|
||||||
|
|
||||||
let http = reqwest::Client::new();
|
let http = reqwest::Client::new();
|
||||||
let resp = http
|
let resp = http
|
||||||
|
|
@ -582,49 +727,43 @@ async fn run_client_credentials_jwt(
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let client_id = ctx
|
let client_id = ctx
|
||||||
.client_id
|
.client_id
|
||||||
.as_deref()
|
.clone()
|
||||||
.unwrap_or("conformance-test-client");
|
.unwrap_or_else(|| "conformance-test-client".to_string());
|
||||||
let _pem = ctx
|
let signing_key = ctx
|
||||||
.private_key_pem
|
.private_key_pem
|
||||||
.as_deref()
|
.as_ref()
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing private_key_pem"))?;
|
.ok_or_else(|| anyhow::anyhow!("Missing private_key_pem"))?
|
||||||
let _alg = ctx
|
.as_bytes()
|
||||||
|
.to_vec();
|
||||||
|
let signing_algorithm = match ctx
|
||||||
.signing_algorithm
|
.signing_algorithm
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing signing_algorithm"))?;
|
.ok_or_else(|| anyhow::anyhow!("Missing signing_algorithm"))?
|
||||||
|
{
|
||||||
// Discover metadata to get token endpoint
|
"RS256" => JwtSigningAlgorithm::RS256,
|
||||||
let mut manager = AuthorizationManager::new(server_url).await?;
|
"RS384" => JwtSigningAlgorithm::RS384,
|
||||||
let metadata = manager.discover_metadata().await?;
|
"RS512" => JwtSigningAlgorithm::RS512,
|
||||||
let token_endpoint = metadata.token_endpoint.clone();
|
"ES256" => JwtSigningAlgorithm::ES256,
|
||||||
manager.set_metadata(metadata);
|
"ES384" => JwtSigningAlgorithm::ES384,
|
||||||
|
algorithm => anyhow::bail!("Unsupported signing_algorithm: {algorithm}"),
|
||||||
// Build JWT assertion
|
};
|
||||||
// Parse the PEM private key
|
let config = ClientCredentialsConfig::PrivateKeyJwt {
|
||||||
let key = openssl_free_ec_sign(_pem, client_id, &token_endpoint)?;
|
client_id,
|
||||||
|
signing_key,
|
||||||
let http = reqwest::Client::new();
|
signing_algorithm,
|
||||||
let form_body = format!(
|
token_endpoint_audience: None,
|
||||||
"grant_type=client_credentials&client_assertion_type={}&client_assertion={}",
|
scopes: vec![],
|
||||||
urlencoding::encode("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),
|
resource: Some(server_url.to_string()),
|
||||||
urlencoding::encode(&key),
|
};
|
||||||
);
|
let mut oauth_state = OAuthState::new(server_url, None).await?;
|
||||||
let resp = http
|
oauth_state.authenticate_client_credentials(config).await?;
|
||||||
.post(&token_endpoint)
|
let manager = oauth_state
|
||||||
.header("content-type", "application/x-www-form-urlencoded")
|
.into_authorization_manager()
|
||||||
.body(form_body)
|
.ok_or_else(|| anyhow::anyhow!("Client credentials flow did not authorize"))?;
|
||||||
.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(
|
let transport = StreamableHttpClientTransport::with_client(
|
||||||
reqwest::Client::default(),
|
AuthClient::new(reqwest::Client::default(), manager),
|
||||||
StreamableHttpClientTransportConfig::with_uri(server_url)
|
StreamableHttpClientTransportConfig::with_uri(server_url),
|
||||||
.auth_header(access_token.to_string()),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let client = BasicClientHandler.serve(transport).await?;
|
let client = BasicClientHandler.serve(transport).await?;
|
||||||
|
|
@ -640,71 +779,64 @@ async fn run_client_credentials_jwt(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Minimal ES256 JWT signing without heavy deps.
|
/// Exchange the fixture's IdP refresh token, then exercise authenticated MCP access.
|
||||||
/// We use ring or pure-Rust approach. For simplicity, use the p256 + base64 crates
|
async fn run_ema_refresh_token_client(
|
||||||
/// that are already transitive deps of oauth2.
|
server_url: &str,
|
||||||
fn openssl_free_ec_sign(pem: &str, client_id: &str, audience: &str) -> anyhow::Result<String> {
|
ctx: &ConformanceContext,
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
) -> 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?;
|
||||||
|
|
||||||
// Decode PEM → DER
|
let transport = StreamableHttpClientTransport::from_config(
|
||||||
let pem_body = pem
|
StreamableHttpClientTransportConfig::with_uri(server_url)
|
||||||
.lines()
|
.auth_header(token.access_token.secret()),
|
||||||
.filter(|l| !l.starts_with("-----"))
|
);
|
||||||
.collect::<String>();
|
let client = BasicClientHandler
|
||||||
let der = base64_decode(&pem_body)?;
|
.serve_with_lifecycle(transport, conformance_lifecycle())
|
||||||
|
.await?;
|
||||||
// Parse PKCS#8 DER to get the raw EC private key bytes
|
let tools = client.list_tools(Default::default()).await?;
|
||||||
// PKCS#8 for EC P-256: the raw 32-byte key is at the end of the structure
|
for tool in tools.tools {
|
||||||
let raw_key = extract_ec_private_key(&der)?;
|
let args = build_tool_arguments(&tool);
|
||||||
|
client.call_tool(call_tool_params(tool.name, args)).await?;
|
||||||
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_slice(raw_key.as_slice())
|
|
||||||
.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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(anyhow::anyhow!(
|
client.cancel().await?;
|
||||||
"Could not extract 32-byte EC private key from PKCS#8 DER"
|
Ok(())
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cross-app access flow (SEP-1046 extension).
|
/// Cross-app access flow (SEP-1046 extension).
|
||||||
|
|
@ -793,16 +925,53 @@ async fn run_basic_client(server_url: &str) -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_tools_call_client(server_url: &str) -> anyhow::Result<()> {
|
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<()> {
|
||||||
let transport = StreamableHttpClientTransport::from_uri(server_url);
|
let transport = StreamableHttpClientTransport::from_uri(server_url);
|
||||||
let client = FullClientHandler.serve(transport).await?;
|
let client = FullClientHandler
|
||||||
|
.serve_with_lifecycle(transport, lifecycle)
|
||||||
|
.await?;
|
||||||
let tools = client.list_tools(Default::default()).await?;
|
let tools = client.list_tools(Default::default()).await?;
|
||||||
for tool in &tools.tools {
|
|
||||||
let args = build_tool_arguments(tool);
|
if ctx.tool_calls.is_empty() {
|
||||||
let _ = client
|
for tool in &tools.tools {
|
||||||
.call_tool(call_tool_params(tool.name.clone(), args))
|
let args = build_tool_arguments(tool);
|
||||||
.await?;
|
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?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.cancel().await?;
|
client.cancel().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -824,6 +993,58 @@ async fn run_elicitation_defaults_client(server_url: &str) -> anyhow::Result<()>
|
||||||
Ok(())
|
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<()> {
|
async fn run_sse_retry_client(server_url: &str) -> anyhow::Result<()> {
|
||||||
let transport = StreamableHttpClientTransport::from_uri(server_url);
|
let transport = StreamableHttpClientTransport::from_uri(server_url);
|
||||||
let client = BasicClientHandler.serve(transport).await?;
|
let client = BasicClientHandler.serve(transport).await?;
|
||||||
|
|
@ -861,14 +1082,50 @@ async fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
tracing::info!("Running scenario '{}' against {}", scenario, server_url);
|
tracing::info!("Running scenario '{}' against {}", scenario, server_url);
|
||||||
|
|
||||||
match scenario.as_str() {
|
// 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 {
|
||||||
// Non-auth scenarios
|
// Non-auth scenarios
|
||||||
"initialize" => run_basic_client(&server_url).await?,
|
"initialize" => run_basic_client(server_url).await?,
|
||||||
"tools_call" => run_tools_call_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?,
|
||||||
"elicitation-sep1034-client-defaults" => {
|
"elicitation-sep1034-client-defaults" => {
|
||||||
run_elicitation_defaults_client(&server_url).await?
|
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?
|
||||||
}
|
}
|
||||||
"sse-retry" => run_sse_retry_client(&server_url).await?,
|
|
||||||
|
|
||||||
// Auth scenarios - standard OAuth flow
|
// Auth scenarios - standard OAuth flow
|
||||||
"auth/metadata-default"
|
"auth/metadata-default"
|
||||||
|
|
@ -883,21 +1140,42 @@ async fn main() -> anyhow::Result<()> {
|
||||||
| "auth/token-endpoint-auth-post"
|
| "auth/token-endpoint-auth-post"
|
||||||
| "auth/token-endpoint-auth-none"
|
| "auth/token-endpoint-auth-none"
|
||||||
| "auth/2025-03-26-oauth-metadata-backcompat"
|
| "auth/2025-03-26-oauth-metadata-backcompat"
|
||||||
| "auth/2025-03-26-oauth-endpoint-fallback" => run_auth_client(&server_url, &ctx).await?,
|
| "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 - scope step-up
|
// 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
|
||||||
"auth/scope-retry-limit" => run_auth_scope_retry_limit_client(&server_url, &ctx).await?,
|
"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 - pre-registration
|
// 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 (should fail to auth → pass)
|
||||||
"auth/resource-mismatch" => {
|
"auth/resource-mismatch" => {
|
||||||
// Try to auth; it should fail because PRM resource doesn't match
|
// 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(_) => {
|
Ok(_) => {
|
||||||
tracing::warn!("Auth succeeded despite resource mismatch!");
|
tracing::warn!("Auth succeeded despite resource mismatch!");
|
||||||
}
|
}
|
||||||
|
|
@ -908,25 +1186,48 @@ async fn main() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth - client credentials
|
// Auth - client credentials
|
||||||
"auth/client-credentials-basic" => run_client_credentials_basic(&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/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
|
||||||
"auth/cross-app-access-complete-flow" => {
|
"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(())
|
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
|
|
@ -7,6 +7,99 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [2.0.0](https://github.com/modelcontextprotocol/rust-sdk/compare/rmcp-macros-v1.8.0...rmcp-macros-v2.0.0) - 2026-06-27
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue