Skip to content

Commit b4a5af8

Browse files
author
Gautham Prabhu
committed
Merge remote-tracking branch 'origin/main' into feature/gauthamprabhu/correlate-invalid-envelope-request-id
# Conflicts: # docs/migration.md # tests/interaction/transports/test_hosting_http.py # tests/shared/test_streamable_http.py
2 parents e3f1cfd + a527142 commit b4a5af8

162 files changed

Lines changed: 29193 additions & 3512 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/conformance/client.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
Contract:
77
- MCP_CONFORMANCE_SCENARIO env var -> scenario name
88
- MCP_CONFORMANCE_CONTEXT env var -> optional JSON (for client-credentials scenarios)
9+
- MCP_CONFORMANCE_PROTOCOL_VERSION env var -> spec version the harness mock
10+
server is speaking (e.g. "2025-11-25", "2026-07-28"). Always set; when
11+
--spec-version is omitted the harness picks per-scenario (LATEST_SPEC_VERSION
12+
for active scenarios, DRAFT_PROTOCOL_VERSION for draft-only ones).
913
- Server URL as last CLI argument (sys.argv[1])
1014
- Must exit 0 within 30 seconds
1115
@@ -40,7 +44,7 @@
4044
)
4145
from mcp.client.context import ClientRequestContext
4246
from mcp.client.streamable_http import streamable_http_client
43-
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
47+
from mcp.shared.auth import AuthorizationCodeResult, OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
4448

4549
# Set up logging to stderr (stdout is for conformance test output)
4650
logging.basicConfig(
@@ -50,6 +54,14 @@
5054
)
5155
logger = logging.getLogger(__name__)
5256

57+
#: Spec version the harness is running this scenario at (e.g. "2025-11-25",
58+
#: "2026-07-28"). The harness always sets this (when --spec-version is omitted
59+
#: it picks per-scenario: LATEST_SPEC_VERSION for active scenarios,
60+
#: DRAFT_PROTOCOL_VERSION for draft-only ones), so None means we were invoked
61+
#: outside the harness. Handlers that need to take the stateless 2026 path will
62+
#: branch on this once the SDK has one; today it is logged only.
63+
PROTOCOL_VERSION: str | None = os.environ.get("MCP_CONFORMANCE_PROTOCOL_VERSION")
64+
5365
# Type for async scenario handler functions
5466
ScenarioHandler = Callable[[str], Coroutine[Any, None, None]]
5567

@@ -109,6 +121,7 @@ class ConformanceOAuthCallbackHandler:
109121
def __init__(self) -> None:
110122
self._auth_code: str | None = None
111123
self._state: str | None = None
124+
self._iss: str | None = None
112125

113126
async def handle_redirect(self, authorization_url: str) -> None:
114127
"""Fetch the authorization URL and extract the auth code from the redirect."""
@@ -130,6 +143,8 @@ async def handle_redirect(self, authorization_url: str) -> None:
130143
self._auth_code = query_params["code"][0]
131144
state_values = query_params.get("state")
132145
self._state = state_values[0] if state_values else None
146+
iss_values = query_params.get("iss")
147+
self._iss = iss_values[0] if iss_values else None
133148
logger.debug(f"Got auth code from redirect: {self._auth_code[:10]}...")
134149
return
135150
else:
@@ -139,15 +154,15 @@ async def handle_redirect(self, authorization_url: str) -> None:
139154
else:
140155
raise RuntimeError(f"Expected redirect response, got {response.status_code} from {authorization_url}")
141156

142-
async def handle_callback(self) -> tuple[str, str | None]:
143-
"""Return the captured auth code and state."""
157+
async def handle_callback(self) -> AuthorizationCodeResult:
158+
"""Return the captured auth code, state, and iss."""
144159
if self._auth_code is None:
145160
raise RuntimeError("No authorization code available - was handle_redirect called?")
146-
auth_code = self._auth_code
147-
state = self._state
161+
result = AuthorizationCodeResult(code=self._auth_code, state=self._state, iss=self._iss)
148162
self._auth_code = None
149163
self._state = None
150-
return auth_code, state
164+
self._iss = None
165+
return result
151166

152167

153168
# --- Scenario Handlers ---
@@ -164,6 +179,18 @@ async def run_initialize(server_url: str) -> None:
164179
logger.debug("Listed tools successfully")
165180

166181

182+
@register("json-schema-ref-no-deref")
183+
async def run_json_schema_ref_no_deref(server_url: str) -> None:
184+
"""Initialize and list tools; the scenario fails only if the client fetches a network $ref.
185+
186+
ClientSession never walks inputSchema or resolves $refs, so listing is enough (SEP-2106).
187+
"""
188+
async with streamable_http_client(url=server_url) as (read_stream, write_stream):
189+
async with ClientSession(read_stream, write_stream) as session:
190+
await session.initialize()
191+
await session.list_tools()
192+
193+
167194
@register("tools_call")
168195
async def run_tools_call(server_url: str) -> None:
169196
"""Connect, initialize, list tools, call add_numbers(a=5, b=3), close."""
@@ -347,6 +374,7 @@ def main() -> None:
347374

348375
server_url = sys.argv[1]
349376
scenario = os.environ.get("MCP_CONFORMANCE_SCENARIO")
377+
logger.debug(f"Conformance protocol version: {PROTOCOL_VERSION!r}")
350378

351379
if scenario:
352380
logger.debug(f"Running explicit scenario '{scenario}' against {server_url}")
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Expected failures for the carried-forward 2026-07-28 legs
2+
# (`--suite all --spec-version 2026-07-28` for both server and client).
3+
#
4+
# This baseline is separate from expected-failures.yml because entries are
5+
# keyed by scenario name only: a scenario that passes at its default version
6+
# in the 2025 legs but fails when forced to 2026-07-28 (or vice versa) cannot
7+
# be expressed in a shared file (the passing leg would flag the entry as
8+
# stale). Like expected-failures.yml, this single file covers both
9+
# directions: the client 2026 leg reads the `client:` section and the server
10+
# 2026 leg reads the `server:` section. Both burn down independently of the
11+
# 2025 legs.
12+
#
13+
# Baseline established against the harness pinned via CONFORMANCE_PKG in
14+
# .github/workflows/conformance.yml. New conformance releases are adopted by
15+
# deliberately bumping that pin and reconciling both this file and
16+
# expected-failures.yml in the same change.
17+
#
18+
# Entries are grouped by what unblocks them. As each gap closes the
19+
# corresponding scenarios start passing and MUST be removed from this list
20+
# (the runner fails on stale entries), so the baseline burns down per
21+
# milestone.
22+
23+
client:
24+
# --- No stateless client path on main yet ---
25+
# client.py drives the 2025 stateful lifecycle (initialize handshake +
26+
# session). The 2026-mode mock server is stateless, so the call sequence
27+
# never reaches the assertion. Unblocks when client.py's is_modern_protocol()
28+
# branch takes the per-request _meta path.
29+
- tools_call
30+
31+
# --- Auth scenarios cut short by the 2026 connection lifecycle ---
32+
# The auth fixture flow drives the 2025 stateful lifecycle; the 2026-mode
33+
# mock rejects the MCP POST before the scope-escalation behaviour these
34+
# scenarios measure, so no authorization requests are observed. Unblocks
35+
# when client.py's auth flow speaks the 2026 per-request lifecycle.
36+
- auth/scope-step-up
37+
- auth/scope-retry-limit
38+
39+
# --- Same gaps as the 2025 baseline (fail identically when forced to 2026-07-28) ---
40+
# SEP-2575 (request metadata / _meta envelope): client does not populate the
41+
# _meta envelope or the MCP-Protocol-Version header semantics yet.
42+
- request-metadata
43+
# SEP-2322 (multi-round-trip requests): client does not echo requestState /
44+
# handle IncompleteResult yet.
45+
- sep-2322-client-request-state
46+
# SEP-2243 (HTTP standardization): no fixture handler / client header support yet.
47+
- http-custom-headers
48+
- http-invalid-tool-headers
49+
# SEP-2352 (authorization server migration): the client re-registers and does not reuse the old
50+
# AS credentials, but the 2026-mode mock rejects the MCP POST before the migration 401 fires
51+
# (client.py drives the 2025 stateful lifecycle), so the re-register check is never reached.
52+
# Unblocks with the 2026 stateless client lifecycle.
53+
- auth/authorization-server-migration
54+
# auth/enterprise-managed-authorization (SEP-990) is in the 2025 baseline but
55+
# NOT here: the harness skips it as inapplicable at --spec-version 2026-07-28
56+
# (it is an extension scenario not carried into the 2026 wire), so it is
57+
# neither run nor evaluated on this leg.
58+
59+
server:
60+
# --- Carried-forward 2025-era scenarios still failing on the 2026 wire ---
61+
# The stateless 2026 path now reaches handlers for plain request/response
62+
# scenarios; tools-call-with-progress still fails because the stateless
63+
# server has no channel for server→client progress notifications.
64+
- tools-call-with-progress
65+
# SEP-2106 (JSON Schema 2020-12 in tool inputSchema): the fixture tool's
66+
# schema has none of the 2020-12 keywords the scenario checks. The scenario
67+
# is in `--suite all` but not `--suite active`, so this is the only leg that
68+
# runs it; it fails identically at 2025-11-25 (not a 2026-path regression).
69+
- json-schema-2020-12
70+
71+
# --- Draft scenarios (same failures and reasons as the `--suite draft` leg) ---
72+
# SEP-2322 (multi-round-trip requests / IncompleteResult): not implemented.
73+
- input-required-result-basic-elicitation
74+
- input-required-result-basic-sampling
75+
- input-required-result-basic-list-roots
76+
- input-required-result-request-state
77+
- input-required-result-multiple-input-requests
78+
- input-required-result-multi-round
79+
- input-required-result-non-tool-request
80+
- input-required-result-result-type
81+
- input-required-result-tampered-state
82+
- input-required-result-capability-check
83+
# SEP-2243 (HTTP header standardization): Mcp-Method / Mcp-Name cross-check
84+
# against the request body is not implemented.
85+
- http-header-validation
86+
# WARNING-only entries: these scenarios emit no FAILURE checks but the
87+
# expected-failures evaluator counts WARNINGs as failures (the summary line
88+
# only shows passed/failed, not warnings, so a local re-probe can mis-read
89+
# these as stale).
90+
- input-required-result-missing-input-response
91+
- input-required-result-validate-input
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Conformance scenarios not yet passing against the Python SDK on main.
2+
# CI exits 0 if only these fail, exits 1 on unexpected failures or stale entries.
3+
#
4+
# Baseline established against the harness pinned via CONFORMANCE_PKG in
5+
# .github/workflows/conformance.yml. New conformance releases are adopted by
6+
# deliberately bumping that pin and reconciling both this file and
7+
# expected-failures.2026-07-28.yml in the same change.
8+
#
9+
# Entries are grouped by SEP. As each SEP lands in the SDK the corresponding
10+
# scenarios start passing and MUST be removed from this list (the runner fails
11+
# on stale entries), so the baseline burns down per milestone.
12+
13+
client:
14+
# --- Draft-spec scenarios (in `--suite draft`, also part of `--suite all`) ---
15+
# SEP-2575 (request metadata / _meta envelope): client does not populate the
16+
# _meta envelope or the MCP-Protocol-Version header semantics yet.
17+
- request-metadata
18+
# SEP-2322 (multi-round-trip requests): client does not echo requestState /
19+
# handle IncompleteResult yet.
20+
- sep-2322-client-request-state
21+
# SEP-2243 (HTTP standardization): no fixture handler / client header support yet.
22+
- http-custom-headers
23+
- http-invalid-tool-headers
24+
# SEP-2352 (authorization server migration): the client re-registers and does not reuse the old
25+
# AS credentials, but this 2026-introduced scenario runs at 2026-07-28, where client.py's 2025
26+
# stateful lifecycle is rejected (400 on initialize) before the migration 401 fires, so the
27+
# re-register check is never reached. Unblocks with the 2026 stateless client lifecycle.
28+
- auth/authorization-server-migration
29+
30+
# --- Pre-existing scenarios that fail on checks added after conformance 0.1.15 ---
31+
# SEP-990 (enterprise-managed authorization extension): no fixture handler /
32+
# client support for the token-exchange + JWT bearer flow.
33+
- auth/enterprise-managed-authorization
34+
35+
server:
36+
# --- Draft-spec scenarios (in `--suite draft`; the `active` suite is green) ---
37+
# SEP-2322 (multi-round-trip requests / IncompleteResult): not implemented.
38+
- input-required-result-basic-elicitation
39+
- input-required-result-basic-sampling
40+
- input-required-result-basic-list-roots
41+
- input-required-result-request-state
42+
- input-required-result-multiple-input-requests
43+
- input-required-result-multi-round
44+
- input-required-result-non-tool-request
45+
- input-required-result-result-type
46+
- input-required-result-tampered-state
47+
- input-required-result-capability-check
48+
# SEP-2243 (HTTP header standardization): Mcp-Method / Mcp-Name cross-check
49+
# against the request body is not implemented.
50+
- http-header-validation
51+
# WARNING-only entries: these scenarios emit no FAILURE checks but the
52+
# expected-failures evaluator counts WARNINGs as failures (the summary line
53+
# only shows passed/failed, not warnings, so a local re-probe can mis-read
54+
# these as stale).
55+
- input-required-result-missing-input-response
56+
- input-required-result-validate-input

.github/actions/conformance/run-server.sh

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,36 @@ SERVER_URL="http://localhost:${PORT}/mcp"
77
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
88
cd "$SCRIPT_DIR/../../.."
99

10-
# Start everything-server
10+
# Refuse to start if something is already listening on the port. The readiness
11+
# check below cannot tell our server apart from a stale one, so a leftover
12+
# listener would mean silently running conformance against old code.
13+
if (: > "/dev/tcp/localhost/${PORT}") 2>/dev/null; then
14+
echo "Error: port ${PORT} is already in use." >&2
15+
echo "Stop the stale process first (lsof -ti:${PORT} -sTCP:LISTEN | xargs kill) or set PORT to a free port." >&2
16+
exit 1
17+
fi
18+
19+
echo "Starting mcp-everything-server on port ${PORT}..."
1120
uv run --frozen mcp-everything-server --port "$PORT" &
1221
SERVER_PID=$!
13-
trap "kill $SERVER_PID 2>/dev/null || true; wait $SERVER_PID 2>/dev/null || true" EXIT
1422

15-
# Wait for server to be ready
23+
cleanup() {
24+
echo "Stopping server (PID: ${SERVER_PID})..."
25+
kill $SERVER_PID 2>/dev/null || true
26+
wait $SERVER_PID 2>/dev/null || true
27+
}
28+
trap cleanup EXIT
29+
30+
# Wait for server to be ready. --max-time keeps a hung listener from wedging
31+
# the loop, and a dead server process fails fast instead of retrying.
32+
echo "Waiting for server to be ready..."
1633
MAX_RETRIES=30
1734
RETRY_COUNT=0
18-
while ! curl -s "$SERVER_URL" > /dev/null 2>&1; do
35+
while ! curl -s --max-time 2 "$SERVER_URL" > /dev/null 2>&1; do
36+
if ! kill -0 $SERVER_PID 2>/dev/null; then
37+
echo "Server process exited unexpectedly" >&2
38+
exit 1
39+
fi
1940
RETRY_COUNT=$((RETRY_COUNT + 1))
2041
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
2142
echo "Server failed to start after ${MAX_RETRIES} retries" >&2
@@ -26,5 +47,5 @@ done
2647

2748
echo "Server ready at $SERVER_URL"
2849

29-
# Run conformance tests
30-
npx @modelcontextprotocol/conformance@0.1.10 server --url "$SERVER_URL" "$@"
50+
npx --yes "${CONFORMANCE_PKG:?set CONFORMANCE_PKG (pinned in .github/workflows/conformance.yml)}" \
51+
server --url "$SERVER_URL" "$@"

.github/dependabot.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
version: 2
22
updates:
3+
- package-ecosystem: "uv"
4+
directory: "/"
5+
schedule:
6+
interval: monthly
7+
cooldown:
8+
default-days: 14
9+
groups:
10+
python-packages:
11+
patterns:
12+
- "*"
313
- package-ecosystem: "github-actions"
414
directory: "/"
515
schedule:
616
interval: monthly
17+
cooldown:
18+
default-days: 14
719
groups:
820
github-actions:
921
patterns:

.github/workflows/claude.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ jobs:
2727
actions: read # Required for Claude to read CI results on PRs
2828
steps:
2929
- name: Checkout repository
30-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
30+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
3131
with:
3232
fetch-depth: 1
3333
persist-credentials: false
3434

3535
- name: Run Claude Code
3636
id: claude
37-
uses: anthropics/claude-code-action@2f8ba26a219c06cfb0f468eef8d97055fa814f97 # v1.0.53
37+
uses: anthropics/claude-code-action@d5726de019ec4498aa667642bc3a80fca83aa102 # v1.0.148
3838
with:
3939
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} # zizmor: ignore[secrets-outside-env]
4040
use_commit_signing: true

.github/workflows/comment-on-release.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ jobs:
1313
runs-on: ubuntu-latest
1414
steps:
1515
- name: Checkout
16-
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
16+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
1717
with:
1818
fetch-depth: 0
1919
persist-credentials: false
2020

2121
- name: Get previous release
2222
id: previous_release
23-
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
23+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
2424
env:
2525
CURRENT_TAG: ${{ github.event.release.tag_name }}
2626
with:
@@ -105,7 +105,7 @@ jobs:
105105
106106
- name: Get merged PRs between releases
107107
id: get_prs
108-
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
108+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
109109
env:
110110
CURRENT_TAG: ${{ github.event.release.tag_name }}
111111
PREVIOUS_TAG_JSON: ${{ steps.previous_release.outputs.result }}
@@ -171,7 +171,7 @@ jobs:
171171
return Array.from(prNumbers);
172172
173173
- name: Comment on PRs
174-
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
174+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
175175
env:
176176
PR_NUMBERS_JSON: ${{ steps.get_prs.outputs.result }}
177177
RELEASE_TAG: ${{ github.event.release.tag_name }}

0 commit comments

Comments
 (0)