Skip to content

Commit 047223c

Browse files
committed
Merge remote-tracking branch 'origin/main' into HEAD
2 parents 087f0e7 + 734746a commit 047223c

136 files changed

Lines changed: 29059 additions & 3685 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.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 @modelcontextprotocol/conformance pinned in
5+
# .github/workflows/conformance.yml (CONFORMANCE_VERSION = 0.2.0-alpha.3).
6+
# New conformance releases are adopted by deliberately bumping that pin and
7+
# reconciling this file 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-2106 (JSON Schema $ref handling): client still dereferences network $refs.
25+
- json-schema-ref-no-deref
26+
# SEP-2468 (authorization response iss parameter): not implemented in the client.
27+
- auth/iss-supported
28+
- auth/iss-not-advertised
29+
- auth/iss-supported-missing
30+
- auth/iss-wrong-issuer
31+
- auth/iss-unexpected
32+
- auth/iss-normalized
33+
- auth/metadata-issuer-mismatch
34+
# SEP-2352 (authorization server migration): client does not re-register when
35+
# PRM authorization_servers changes.
36+
- auth/authorization-server-migration
37+
# SEP-837 (application_type during DCR): the check only fires on draft-version
38+
# runs; this draft scenario is the one place the client still hits it.
39+
- auth/offline-access-not-supported
40+
41+
# --- Pre-existing scenarios that fail on checks added after conformance 0.1.15 ---
42+
# SEP-2350 (scope step-up): WARNING-only; the expected-failures evaluator
43+
# counts WARNINGs as failures.
44+
- auth/scope-step-up
45+
# SEP-990 (enterprise-managed authorization extension): no fixture handler /
46+
# client support for the token-exchange + JWT bearer flow.
47+
- auth/enterprise-managed-authorization
48+
49+
server:
50+
# --- Draft-spec scenarios (in `--suite draft`; the `active` suite is green) ---
51+
# SEP-2575 (stateless HTTP / _meta envelope): server has no stateless mode,
52+
# _meta-derived capabilities, error-code mappings, or server/discover yet.
53+
- server-stateless
54+
# SEP-2322 (multi-round-trip requests / IncompleteResult): not implemented;
55+
# most scenarios currently fail early with "Missing session ID" because
56+
# mcp-everything-server only runs in stateful mode.
57+
- input-required-result-basic-elicitation
58+
- input-required-result-basic-sampling
59+
- input-required-result-basic-list-roots
60+
- input-required-result-request-state
61+
- input-required-result-multiple-input-requests
62+
- input-required-result-multi-round
63+
- input-required-result-non-tool-request
64+
- input-required-result-result-type
65+
- input-required-result-tampered-state
66+
- input-required-result-capability-check
67+
# SEP-2549 (caching): no ttlMs/cacheScope support; scenario also hits the
68+
# stateful-mode "Missing session ID" error.
69+
- caching
70+
# SEP-2243 (HTTP header standardization): -32001 HeaderMismatch handling and
71+
# case-insensitive/whitespace-trimmed header validation not implemented.
72+
- http-header-validation
73+
- http-custom-header-server-validation
74+
# WARNING-only entries: these scenarios emit no FAILURE checks, only SHOULD-level
75+
# WARNINGs, but the expected-failures evaluator counts WARNINGs as failures.
76+
# SEP-2164: server returns -32600 (not -32602) and omits error.data.uri.
77+
- sep-2164-resource-not-found
78+
# SEP-2322 SHOULD-level behaviours (re-request missing inputResponses, ignore
79+
# unrecognized inputResponses keys).
80+
- input-required-result-missing-input-response
81+
- input-required-result-ignore-extra-params
82+
# Intentionally NOT baselined (2 of 19 draft scenarios): the SEP-2322
83+
# negative-case scenarios input-required-result-unsupported-methods and
84+
# input-required-result-validate-input pass today only because the stateful
85+
# server's -32600 "Missing session ID" satisfies their assertions. They will
86+
# start failing for real once stateless mode lands; add them then.

.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 @modelcontextprotocol/conformance@"${CONFORMANCE_VERSION:?set CONFORMANCE_VERSION (pinned in .github/workflows/conformance.yml)}" \
51+
server --url "$SERVER_URL" "$@"

.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: 99 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,50 +13,99 @@ 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:
2727
script: |
2828
const currentTag = process.env.CURRENT_TAG;
2929
30-
// Get all releases
31-
const { data: releases } = await github.rest.repos.listReleases({
30+
// Paginate: with two release lines publishing interleaved, the
31+
// previous release on this line can sit far down the list.
32+
const releases = await github.paginate(github.rest.repos.listReleases, {
3233
owner: context.repo.owner,
3334
repo: context.repo.repo,
3435
per_page: 100
3536
});
3637
37-
// Find current release index
38-
const currentIndex = releases.findIndex(r => r.tag_name === currentTag);
39-
40-
if (currentIndex === -1) {
38+
if (!releases.some(r => r.tag_name === currentTag)) {
4139
console.log('Current release not found in list');
4240
return null;
4341
}
4442
45-
// Get previous release (next in the list since they're sorted by date desc)
46-
const previousRelease = releases[currentIndex + 1];
43+
const major = tag => (tag.match(/^v?(\d+)/) || [])[1];
4744
48-
if (!previousRelease) {
49-
console.log('No previous release found, this might be the first release');
45+
if (major(currentTag) === undefined) {
46+
console.log(`Cannot parse a major version from ${currentTag}; skipping comments`);
5047
return null;
5148
}
5249
53-
console.log(`Found previous release: ${previousRelease.tag_name}`);
50+
// The list is ordered by release creation date, which does not
51+
// reliably reflect tag topology (for example, a release published
52+
// from a long-lived draft keeps its draft creation date). Instead
53+
// of trusting list order, compare every same-major release and
54+
// pick the nearest ancestor of the current tag: the one the
55+
// smallest number of commits behind it. The major check runs
56+
// first so cross-line candidates cost no API calls; per_page=1
57+
// because only status/ahead_by are needed here (the commits are
58+
// fetched in the next step). For the first release of a new major
59+
// line there is no same-line predecessor, and we skip commenting
60+
// rather than compare across the entire new line's history.
61+
let best = null;
62+
for (const candidate of releases) {
63+
if (candidate.tag_name === currentTag || candidate.draft) continue;
64+
if (major(candidate.tag_name) !== major(currentTag)) continue;
65+
66+
let comparison;
67+
try {
68+
({ data: comparison } = await github.rest.repos.compareCommits({
69+
owner: context.repo.owner,
70+
repo: context.repo.repo,
71+
base: candidate.tag_name,
72+
head: currentTag,
73+
per_page: 1
74+
}));
75+
} catch (error) {
76+
// Tolerate only candidates whose tag no longer resolves;
77+
// anything else (rate limits, server errors) must fail the
78+
// job rather than silently produce a wrong comparison base.
79+
if (error.status === 404) {
80+
console.log(`Skipping ${candidate.tag_name}: tag does not resolve`);
81+
continue;
82+
}
83+
throw error;
84+
}
85+
86+
// 'identical' covers a release re-cut on the same commit; it
87+
// yields an empty commit range downstream, hence no comments.
88+
if (comparison.status !== 'ahead' && comparison.status !== 'identical') {
89+
console.log(`Skipping ${candidate.tag_name}: not an ancestor of ${currentTag} (status: ${comparison.status})`);
90+
continue;
91+
}
92+
93+
if (best === null || comparison.ahead_by < best.aheadBy) {
94+
best = { tagName: candidate.tag_name, aheadBy: comparison.ahead_by };
95+
}
96+
}
97+
98+
if (best === null) {
99+
console.log(`No previous release found for ${currentTag} on its major line (it may be the first); skipping comments`);
100+
return null;
101+
}
54102
55-
return previousRelease.tag_name;
103+
console.log(`Found previous release: ${best.tagName} (${best.aheadBy} commits behind ${currentTag})`);
104+
return best.tagName;
56105
57106
- name: Get merged PRs between releases
58107
id: get_prs
59-
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
108+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
60109
env:
61110
CURRENT_TAG: ${{ github.event.release.tag_name }}
62111
PREVIOUS_TAG_JSON: ${{ steps.previous_release.outputs.result }}
@@ -72,15 +121,28 @@ jobs:
72121
73122
console.log(`Finding PRs between ${previousTag} and ${currentTag}`);
74123
75-
// Get commits between previous and current release
76-
const comparison = await github.rest.repos.compareCommits({
77-
owner: context.repo.owner,
78-
repo: context.repo.repo,
79-
base: previousTag,
80-
head: currentTag
81-
});
82-
83-
const commits = comparison.data.commits;
124+
// Get commits between previous and current release. A single
125+
// compare response caps the commit list, so paginate — but bound
126+
// the total: a range this large means a mis-selected base, and
127+
// commenting on hundreds of PRs is worse than commenting on none.
128+
const MAX_COMMITS = 250;
129+
const commits = [];
130+
for (let page = 1; ; page++) {
131+
const { data: comparison } = await github.rest.repos.compareCommits({
132+
owner: context.repo.owner,
133+
repo: context.repo.repo,
134+
base: previousTag,
135+
head: currentTag,
136+
per_page: 100,
137+
page
138+
});
139+
commits.push(...comparison.commits);
140+
if (commits.length > MAX_COMMITS) {
141+
console.log(`Range ${previousTag}...${currentTag} exceeds ${MAX_COMMITS} commits; skipping comments`);
142+
return [];
143+
}
144+
if (comparison.commits.length < 100) break;
145+
}
84146
console.log(`Found ${commits.length} commits`);
85147
86148
// Get PRs associated with each commit using GitHub API
@@ -109,33 +171,41 @@ jobs:
109171
return Array.from(prNumbers);
110172
111173
- name: Comment on PRs
112-
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
174+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
113175
env:
114176
PR_NUMBERS_JSON: ${{ steps.get_prs.outputs.result }}
115177
RELEASE_TAG: ${{ github.event.release.tag_name }}
116178
RELEASE_URL: ${{ github.event.release.html_url }}
179+
RELEASE_IS_PRERELEASE: ${{ github.event.release.prerelease }}
117180
with:
118181
script: |
119182
const prNumbers = JSON.parse(process.env.PR_NUMBERS_JSON);
120183
const releaseTag = process.env.RELEASE_TAG;
121184
const releaseUrl = process.env.RELEASE_URL;
185+
// Trust the tag as well as the flag, in case the release manager
186+
// forgets to tick the pre-release checkbox.
187+
const isPrerelease = process.env.RELEASE_IS_PRERELEASE === 'true' || /\d(a|b|rc)\d/.test(releaseTag);
188+
const releaseKind = isPrerelease ? 'pre-release' : 'release';
122189
123-
const comment = `This pull request is included in [${releaseTag}](${releaseUrl})`;
190+
const comment = `This pull request is included in ${releaseKind} [${releaseTag}](${releaseUrl})`;
124191
125192
let commentedCount = 0;
126193
127194
for (const prNumber of prNumbers) {
128195
try {
129-
// Check if we've already commented on this PR for this release
130-
const { data: comments } = await github.rest.issues.listComments({
196+
// Check if we've already commented on this PR for this
197+
// release. Paginate: comments are returned oldest-first, so
198+
// on a busy PR an earlier bot comment is exactly what would
199+
// fall off a single page.
200+
const comments = await github.paginate(github.rest.issues.listComments, {
131201
owner: context.repo.owner,
132202
repo: context.repo.repo,
133203
issue_number: prNumber,
134204
per_page: 100
135205
});
136206
137207
const alreadyCommented = comments.some(c =>
138-
c.user.type === 'Bot' && c.body.includes(releaseTag)
208+
c.user.type === 'Bot' && c.body.includes(`[${releaseTag}]`)
139209
);
140210
141211
if (alreadyCommented) {

0 commit comments

Comments
 (0)