Skip to content

Commit b5df93c

Browse files
authored
Merge branch 'main' into fix-www-auth-substring-match-3009
2 parents d4cfbc5 + 3a6f299 commit b5df93c

297 files changed

Lines changed: 12587 additions & 3451 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: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from typing import Any, cast
3939
from urllib.parse import parse_qs, urlparse
4040

41-
import httpx
41+
import httpx2
4242
import mcp_types as types
4343
from mcp_types.version import MODERN_PROTOCOL_VERSIONS
4444
from pydantic import AnyUrl
@@ -151,7 +151,7 @@ async def handle_redirect(self, authorization_url: str) -> None:
151151
"""Fetch the authorization URL and extract the auth code from the redirect."""
152152
logger.debug(f"Fetching authorization URL: {authorization_url}")
153153

154-
async with httpx.AsyncClient() as client:
154+
async with httpx2.AsyncClient() as client:
155155
response = await client.get(
156156
authorization_url,
157157
follow_redirects=False,
@@ -486,13 +486,13 @@ async def run_enterprise_managed_authorization(server_url: str) -> None:
486486
# learn it from the harness's PRM document (RFC 9728); production
487487
# deployments would supply it as static configuration instead.
488488
prm_url = build_protected_resource_metadata_discovery_urls(None, server_url)[0]
489-
async with httpx.AsyncClient(timeout=30.0) as http:
489+
async with httpx2.AsyncClient(timeout=30.0) as http:
490490
prm = (await http.get(prm_url)).raise_for_status().json()
491491
as_issuer = prm["authorization_servers"][0]
492492

493493
async def fetch_id_jag(audience: str, resource: str) -> str:
494494
"""Leg 1 - RFC 8693 token-exchange at the enterprise IdP."""
495-
async with httpx.AsyncClient(timeout=30.0) as http:
495+
async with httpx2.AsyncClient(timeout=30.0) as http:
496496
resp = await http.post(
497497
idp_token_endpoint,
498498
data={
@@ -563,9 +563,9 @@ async def run_auth_code_client(server_url: str) -> None:
563563
await _run_auth_session(server_url, oauth_auth)
564564

565565

566-
async def _run_auth_session(server_url: str, oauth_auth: httpx.Auth) -> None:
566+
async def _run_auth_session(server_url: str, oauth_auth: httpx2.Auth) -> None:
567567
"""Common session logic for all OAuth flows."""
568-
http_client = httpx.AsyncClient(auth=oauth_auth, timeout=30.0)
568+
http_client = httpx2.AsyncClient(auth=oauth_auth, timeout=30.0)
569569
transport = streamable_http_client(url=server_url, http_client=http_client)
570570
async with Client(transport, mode=client_mode(), elicitation_callback=default_elicitation_callback) as client:
571571
logger.debug("Initialized successfully")
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/bin/bash
2+
# Run a client conformance suite, re-verifying unexpected failures solo.
3+
# Concurrent suite runs on a 2-vCPU runner can push scenarios with real-time
4+
# waits past tolerance; solo, a real failure fails again while a contention
5+
# artifact passes. Failures that only reproduce under concurrency are excused.
6+
set -uo pipefail
7+
8+
: "${CONFORMANCE_PKG:?set CONFORMANCE_PKG (pinned in .github/workflows/conformance.yml)}"
9+
# One attempt: a solo failure on the quiet runner disproves the contention
10+
# hypothesis; a second try would be the blind retry this script avoids.
11+
SOLO_ATTEMPTS="${CONFORMANCE_SOLO_ATTEMPTS:-1}"
12+
13+
# Relative args resolve from the repo root; same contract as run-server.sh.
14+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15+
cd "$SCRIPT_DIR/../../.." || exit 1
16+
17+
log="$(mktemp)"
18+
trap 'rm -f "$log"' EXIT
19+
20+
npx --yes "$CONFORMANCE_PKG" client "$@" 2>&1 | tee "$log"
21+
rc=${PIPESTATUS[0]}
22+
if [ "$rc" -eq 0 ]; then
23+
exit 0
24+
fi
25+
26+
plain="$(sed 's/\x1b\[[0-9;]*m//g' "$log")"
27+
28+
# If the harness's summary wording changes, the list comes up empty and the
29+
# original exit code passes through - never a false green.
30+
mapfile -t scenarios < <(
31+
printf '%s\n' "$plain" |
32+
sed -n '/^Unexpected failures (not in baseline):$/,/^$/p' |
33+
sed -n 's/^ ✗ //p'
34+
)
35+
if [ "${#scenarios[@]}" -eq 0 ]; then
36+
exit "$rc"
37+
fi
38+
for scenario in "${scenarios[@]}"; do
39+
if ! [[ "$scenario" =~ ^[A-Za-z0-9/_-]+$ ]]; then
40+
echo "Extracted unexpected-failure name '${scenario}' does not look like a scenario name; passing the suite failure through." >&2
41+
exit "$rc"
42+
fi
43+
done
44+
45+
# A stale baseline entry is a configuration error a solo rerun cannot excuse.
46+
# Here-string, not a pipe: grep -q quitting early would SIGPIPE printf and,
47+
# under pipefail, skip this guard exactly when the pattern is present.
48+
if grep -q '^Stale baseline entries' <<<"$plain"; then
49+
echo "Suite also reported stale baseline entries; not retrying." >&2
50+
exit "$rc"
51+
fi
52+
53+
# Drop the suite-only flags: --scenario replaces --suite, and solo runs are
54+
# judged directly rather than against the baseline.
55+
rerun_args=()
56+
output_dir=""
57+
skip_next=0
58+
expect_output_dir=0
59+
for arg in "$@"; do
60+
if [ "$skip_next" -eq 1 ]; then
61+
if [ "$expect_output_dir" -eq 1 ]; then
62+
output_dir="$arg"
63+
fi
64+
skip_next=0
65+
expect_output_dir=0
66+
continue
67+
fi
68+
case "$arg" in
69+
--output-dir)
70+
skip_next=1
71+
expect_output_dir=1
72+
;;
73+
--suite | --expected-failures) skip_next=1 ;;
74+
--output-dir=*) output_dir="${arg#--output-dir=}" ;;
75+
--suite=* | --expected-failures=*) ;;
76+
*) rerun_args+=("$arg") ;;
77+
esac
78+
done
79+
if [ -n "$output_dir" ]; then
80+
rerun_args+=(--output-dir "${output_dir}-solo")
81+
fi
82+
83+
for scenario in "${scenarios[@]}"; do
84+
passed=0
85+
for attempt in $(seq 1 "$SOLO_ATTEMPTS"); do
86+
echo ""
87+
echo "Re-running '${scenario}' solo (attempt ${attempt}/${SOLO_ATTEMPTS})..."
88+
if npx --yes "$CONFORMANCE_PKG" client --scenario "$scenario" "${rerun_args[@]}"; then
89+
passed=1
90+
break
91+
fi
92+
done
93+
if [ "$passed" -ne 1 ]; then
94+
echo "'${scenario}' still fails when run alone: real failure, not suite contention." >&2
95+
exit 1
96+
fi
97+
done
98+
99+
if [ -n "$output_dir" ]; then
100+
mkdir -p "$output_dir"
101+
printf '%s\n' "${scenarios[@]}" > "$output_dir/FLAKE_RESCUED"
102+
fi
103+
echo "All ${#scenarios[@]} unexpected failure(s) passed when re-run solo; the suite failures were parallel-run contention."
104+
exit 0

.github/workflows/conformance.yml

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,20 @@ jobs:
6464
./.github/actions/conformance/run-server.sh
6565
--suite active
6666
--expected-failures ./.github/actions/conformance/expected-failures.yml
67+
--output-dir conformance-results/server-active
6768
- name: Run server conformance (draft suite)
6869
run: >-
6970
./.github/actions/conformance/run-server.sh
7071
--suite draft
7172
--expected-failures ./.github/actions/conformance/expected-failures.yml
73+
--output-dir conformance-results/server-draft
7274
- name: Run server conformance (2026-07-28 wire, all suite)
7375
run: >-
7476
./.github/actions/conformance/run-server.sh
7577
--suite all
7678
--spec-version 2026-07-28
7779
--expected-failures ./.github/actions/conformance/expected-failures.2026-07-28.yml
80+
--output-dir conformance-results/server-2026-07-28
7881
- name: Run server conformance (all suite, extension scenarios)
7982
# A bare `--suite all` (no --spec-version) selects every scenario
8083
# shipped with the pinned harness — including the extension-tagged
@@ -91,6 +94,15 @@ jobs:
9194
./.github/actions/conformance/run-server.sh
9295
--suite all
9396
--expected-failures ./.github/actions/conformance/expected-failures.yml
97+
--output-dir conformance-results/server-all
98+
- name: Upload conformance results
99+
# The log has only summary counts; per-check data is in checks.json.
100+
if: failure()
101+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
102+
with:
103+
name: server-conformance-results
104+
path: conformance-results/
105+
if-no-files-found: ignore
94106

95107
client-conformance:
96108
runs-on: ubuntu-latest
@@ -118,22 +130,39 @@ jobs:
118130
echo "CONFORMANCE_PKG=file:/tmp/conformance.tgz" >> "$GITHUB_ENV"
119131
;;
120132
esac
121-
- run: uv sync --frozen --all-extras --package mcp
133+
# --compile-bytecode: without it, ~40 concurrently spawned interpreters
134+
# race to byte-compile site-packages during the timing-sensitive window.
135+
- run: uv sync --frozen --all-extras --package mcp --compile-bytecode
136+
- name: Pre-compile bytecode (editable sources)
137+
run: uv run --frozen python -m compileall -q src .github/actions/conformance
122138
- name: Run client conformance (all suite)
123139
# The harness runs all scenarios via unbounded Promise.all; with 40
124140
# scenarios on a 2-core runner the slowest one (sse-retry, which has a
125141
# real-time SSE reconnect wait) needs more than the 30s default budget.
142+
# `.venv/bin/python` (not `uv run`) avoids lockfile re-checks in ~40
143+
# concurrent spawns; run-client.sh re-runs unexpected failures solo.
126144
run: >-
127-
npx --yes "$CONFORMANCE_PKG" client
128-
--command 'uv run --frozen python .github/actions/conformance/client.py'
145+
./.github/actions/conformance/run-client.sh
146+
--command '.venv/bin/python .github/actions/conformance/client.py'
129147
--suite all
130148
--timeout 60000
131149
--expected-failures ./.github/actions/conformance/expected-failures.yml
150+
--output-dir conformance-results/client-all
132151
- name: Run client conformance (2026-07-28 wire, all suite)
133152
run: >-
134-
npx --yes "$CONFORMANCE_PKG" client
135-
--command 'uv run --frozen python .github/actions/conformance/client.py'
153+
./.github/actions/conformance/run-client.sh
154+
--command '.venv/bin/python .github/actions/conformance/client.py'
136155
--suite all
137156
--timeout 60000
138157
--spec-version 2026-07-28
139158
--expected-failures ./.github/actions/conformance/expected-failures.2026-07-28.yml
159+
--output-dir conformance-results/client-2026-07-28
160+
- name: Upload conformance results
161+
# The log has only summary counts; per-check data is in checks.json.
162+
# Also on FLAKE_RESCUED: rescued-flake evidence is otherwise discarded.
163+
if: failure() || hashFiles('conformance-results/**/FLAKE_RESCUED') != ''
164+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
165+
with:
166+
name: client-conformance-results
167+
path: conformance-results/
168+
if-no-files-found: ignore

.github/workflows/deploy-docs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ on:
1414
- src/mcp/**
1515
- src/mcp-types/**
1616
- scripts/build-docs.sh
17+
- scripts/docs/**
1718
- pyproject.toml
1819
- uv.lock
1920
- .github/workflows/deploy-docs.yml

.github/workflows/docs-preview.yml

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
name: Docs Preview
22

3-
# Builds the mkdocs site for a PR and deploys it to Cloudflare Pages.
3+
# Builds the docs site for a PR and deploys it to Cloudflare Pages.
44
#
5-
# Security: mkdocs executes Python from the PR (mkdocstrings imports src/mcp,
6-
# `!!python/name:` directives). The build is gated by `authorize` (admin sender
7-
# for auto-preview, admin/maintainer commenter for /preview-docs) and isolated
8-
# from Cloudflare secrets — `build` runs PR code with no secrets and hands the
9-
# static site to `deploy` via an artifact, so PR code never shares a runner
10-
# with the Cloudflare token.
5+
# Security: the build executes Python from the PR (mkdocstrings imports
6+
# src/mcp, `!!python/name:` config directives run, and heads may ship their
7+
# own build scripts). The build is gated by `authorize` (admin sender for
8+
# auto-preview, admin/maintainer commenter for /preview-docs) and isolated
9+
# from Cloudflare secrets — `build` runs PR code with no secrets and hands
10+
# the static site to `deploy` via an artifact, so PR code never shares a
11+
# runner with the Cloudflare token.
1112
#
1213
# Required configuration:
1314
# - secrets.CLOUDFLARE_API_TOKEN (scope: Account → Cloudflare Pages → Edit)
@@ -21,6 +22,7 @@ on:
2122
- docs/**
2223
- docs_src/**
2324
- mkdocs.yml
25+
- scripts/docs/**
2426
- pyproject.toml
2527
issue_comment:
2628
types: [created]
@@ -128,14 +130,30 @@ jobs:
128130
enable-cache: false
129131
version: 0.9.5
130132

131-
- run: uv sync --frozen --group docs
132-
- run: uv run --frozen --no-sync mkdocs build
133+
# pull_request_target runs this workflow file from the base branch, so
134+
# the whole recipe — dependency sync included — must come from the
135+
# checkout itself: heads that ship scripts/docs/build.sh (the Zensical
136+
# toolchain) build with it; older heads, and v1.x heads previewed via
137+
# /preview-docs, still build with MkDocs. Both arms must write the site
138+
# to site/. Keep the detection in sync with build_site() in
139+
# scripts/build-docs.sh.
140+
- run: |
141+
if [ -f scripts/docs/build.sh ]; then
142+
bash scripts/docs/build.sh
143+
else
144+
uv sync --frozen --group docs
145+
# The env var silences mkdocs-material's MkDocs 2.0 warning banner.
146+
NO_MKDOCS_2_WARNING=1 uv run --frozen --no-sync mkdocs build
147+
fi
133148
134149
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
135150
with:
136151
name: site
137152
path: site/
138153
retention-days: 1
154+
# An empty site/ means the build arm broke its output contract; fail
155+
# here instead of surfacing as a confusing download error in deploy.
156+
if-no-files-found: error
139157

140158
deploy:
141159
needs: [authorize, build]

0 commit comments

Comments
 (0)