Skip to content

Freeze conformance requirements per spec revision, run each at its wire - #447

Merged
felixweinberger merged 1 commit into
mainfrom
fweinberger/frozen-requirements
Aug 7, 2026
Merged

Freeze conformance requirements per spec revision, run each at its wire#447
felixweinberger merged 1 commit into
mainfrom
fweinberger/frozen-requirements

Conversation

@felixweinberger

@felixweinberger felixweinberger commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Adds requirements/<revision>.yaml, a frozen list of the scenarios a spec revision
requires, and runs each revision at that revision's wire version with the server
invocation the SDK's own config declares for that revision. Ships two sets,
2025-11-25 and 2026-07-28; tier-check runs both and Tier 1 means every listed
revision passes.

Fixes #446.

Motivation and Context

Two problems, one mechanism.

The suite accumulates scenarios continuously, so "which scenarios must my SDK
pass to conform to the spec released on 2026-07-28" has no answer today, and an SDK
can drop below 100% while standing still. json-schema-2020-12-preservation merged
three days after the 2026-07-28 spec shipped and four days after 0.2.0-alpha.10
was published; measured against main, typescript-sdk fails it on a commit that
passes 100% against the release it pins. SEP-1730 relegates Tier 1 on any
continuously failing test, so post-release additions start demotion clocks against
SDKs that changed nothing. --spec-version cannot express this: it filters by which
revision a scenario targets, not when it became a requirement.

A revision is also a wire. The dated revisions through 2025-11-25 use the
stateful initialize handshake; 2026-07-28 is stateless with per-request _meta,
and scenarios emit different checks under each. A scenario claimed by both revisions
must therefore run once per revision, on that revision's wire — and the server an
SDK needs may differ per revision. go-sdk only speaks >= 2026-07-28 when its
transport is constructed stateless (its CI starts the server twice); csharp-sdk
serves the stateless lifecycle at /stateless. A single --conformance-server-url
cannot describe either, which is why tier-check builds on src/sdk-runner's
existing per-revision specOverrides instead of inventing a parallel mechanism.

What this adds

# assess a known SDK against everything it claims — clone, build, per-revision
# server, both legs, one verdict
npx @modelcontextprotocol/conformance tier-check --sdk go-sdk

# what does conforming to 2026-07-28 require?
npx @modelcontextprotocol/conformance list --requirements 2026-07-28

# run one revision's set by hand
npx @modelcontextprotocol/conformance server --url http://localhost:3000/mcp --requirements 2026-07-28
  • requirements/<revision>.yaml — the frozen contract, covering the two roles the
    spec defines (MCP server as OAuth resource server, MCP client as OAuth client).
    not_scored entries run and are reported but never counted, each with a reason:
    extension (optional per SEP-1730), added-after-release (no implementation
    could have been passing it), or pending (the suite's own reference fixture
    cannot pass it yet — the implementation under test may, so it runs for
    visibility; typescript-sdk main already passes all three dated pending
    scenarios). Every scenario in the suite lands somewhere except the two
    2025-03-26-only legacy backcompat tests, which SEP-1730 excludes unless legacy
    support is claimed. An authorization section is rejected: the spec puts
    authorization-server implementation beyond its own scope.
  • --requirements <revision> on client, server and list; the revision pins
    the wire version and replaces --suite/--spec-version/--scenario (conflicts
    are errors, not precedence). sdk gains the same flag, resolved through
    specOverrides.
  • tier-check --sdk <name[@ref]> / --sdk-path <dir>: for each claimed revision,
    resolve that revision's build/server/client from known-sdks.ts, manage the
    server, run both legs with cwd = the SDK checkout (so CI-style relative client
    commands work), and merge. --repo/--branch derive from the config.
    --requirements defaults to every shipped revision. The single-URL mode remains
    for SDKs not in the config, with its constraint documented.
  • Exit code: nonzero when any scored conformance scenario fails or a leg could not
    be measured; governance findings never affect it. A child run that cannot happen
    (stale set naming a renamed scenario, unreachable server) is reported as
    not measured, never as 0% against the SDK.
  • --expected-failures under --requirements judges scored scenarios only, so a
    not-scored failure cannot flag as an unexpected regression.
  • Reports label every entry with its revision (tools-list [2026-07-28]); terminal,
    markdown and JSON agree on counts. JSON passed/failed/total describe the scored
    set and always agree with pass_rate; unscored runs are under not_scored.
  • 2026-07-28 is a faithful snapshot (anchored to 0.2.0-alpha.10, published the
    day before the revision shipped). 2025-11-25 is a reconstruction from the same
    anchor and its header says so: the release current at its ship date predates
    spec-version awareness entirely.
  • Fixes exposed by driving every tier-1 SDK through the new path:
    • csharp's server launches from its build-output directory: ASP.NET reads
      appsettings.json from the content root, and AllowedHosts — which
      dns-rebinding-protection tests — silently never loaded from the repo root.
    • rust's STATELESS=1 override is removed: its CI runs one default server for
      both revisions, and the forced mode failed its SEP-2575 scenarios.
    • withManagedServer waits for the child to actually exit before returning, so
      consecutive runs on one port cannot race a dying predecessor.
    • The tier-check legs spawn their child asynchronously. The previous
      synchronous exec blocked the event loop for a whole leg, so the parent
      stopped draining the managed server's log pipes; a chatty server (rust's
      per-request tracing) filled the 64KB pipe, its writes blocked, and the
      frozen server was scored as 26 conformance failures. The child's stdout is
      dropped entirely (results come from -o), removing the maxBuffer kill for
      verbose SDKs along the way.

How Has This Been Tested?

Driven end to end against local main checkouts of all five tier-1 SDKs:

SDK Mode Server Client
typescript-sdk URL 67/67 50/50
python-sdk URL and --sdk-path 67/67 50/50
go-sdk --sdk-path 67/67 50/50
csharp-sdk --sdk-path 67/67 50/50
rust-sdk --sdk-path 67/67 50/50

Each --sdk run demonstrably switches wire per revision (go: stateful then
stateless server process; csharp: / then /stateless). A follow-up adversarial
review (six fresh lenses, refute-by-default verification) drove out and fixed:
skipped legs merging as failures (and the inverse silently passing an unmeasured
revision), the legacy exit-code regression, requirement-file validation (unknown
keys, duplicates, scored∩not_scored), branch attribution in --sdk mode, baseline
auto-attachment under --requirements on the sdk command, duplicate-revision
double counting, and list --requirements omitting pending entries. Negative controls: a
deliberately broken server scores 1/37 with server_conformance blocking and exit
1; a set naming a scenario the build lacks reports not measured naming the
scenario; --requirements with --suite, --scenario, --spec-version, unknown
revisions, comma lists on single-revision commands, and empty values each exit 1
with a message naming the problem. Unit tests pin both requirement sets against the
build (a rename cannot silently shrink a frozen set) and validate the not_scored
taxonomy.

Breaking Changes

None to scenario execution; all flags are opt-in, and tier-check without
--requirements keeps its report-only exit 0. Under --requirements the exit code
gates on scored conformance failures (or an unmeasured leg), which is the CI
contract the frozen sets make meaningful.

Two behaviour changes to disclose for existing consumers:

  • tier-check's JSON now reports passed/failed/total over the scored set (so
    they always agree with pass_rate; previously they counted every scenario and
    the object was self-contradictory), and details[].scenario carries canonical
    scenario names rather than timestamped result-directory names.
  • Three KNOWN_SDKS entries changed, so conformance sdk results are not
    comparable across this PR for them: rust-sdk's STATELESS=1 override for
    2026-07-28 is removed (its own CI runs one default server for both revisions,
    and the forced mode fails its SEP-2575 scenarios), csharp-sdk's server launches
    from its build-output directory (ASP.NET loads appsettings.json from the
    content root; from the repo root the AllowedHosts filter that
    dns-rebinding-protection tests silently never loaded), and python-sdk's build
    mirrors its CI's two targeted syncs (--all-packages is unbuildable on main).

Types of changes

  • New feature (non-breaking change which adds functionality)

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Deliberately out of scope: a CI freeze check pinning shipped requirement files
(currently a header rule plus review), and promoting 2026-07-28 out of
DRAFT_PROTOCOL_VERSION (shifts every SDK's active suite; separate decision). A
requirement set is this repo's contract — the opposite of an SDK's own
expected-failures baseline, which lives in the SDK repo and records what it knows it
fails; a baselined failure is still a failure against a requirement set, so the two
compose rather than substitute.

@felixweinberger
felixweinberger force-pushed the fweinberger/frozen-requirements branch 2 times, most recently from 89c806c to f0120f1 Compare August 7, 2026 11:21
@pkg-pr-new

pkg-pr-new Bot commented Aug 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

npx https://pkg.pr.new/@modelcontextprotocol/conformance@447

commit: b73c385

@pkg-pr-new

pkg-pr-new Bot commented Aug 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

npx https://pkg.pr.new/@modelcontextprotocol/conformance@447

commit: f0120f1

There is no way to answer "which scenarios must my implementation pass
to conform to spec revision X". The suite accumulates scenarios
continuously, so what an implementation is measured against keeps
growing after the revision shipped, and it can drop below 100% while
standing still. --spec-version does not answer it: it filters on which
revision a scenario targets, not on when the scenario became a
requirement.

Add requirements/<revision>.yaml naming the scenarios a revision
requires, and --requirements <revision> on the client, server, list and
sdk commands to run exactly that set. The revision also fixes the wire
version its scenarios speak: the dated revisions through 2025-11-25 use
the stateful initialize handshake and 2026-07-28 is stateless with
per-request _meta, so a scenario belonging to two revisions runs once
under each and one run does not cover the other. Passing no flag keeps
today's behaviour of running everything.

tier-check gains --sdk <name[@ref]> / --sdk-path <dir>: for each claimed
revision it resolves that revision's build, server invocation and client
command from the sdk-runner's KNOWN_SDKS specOverrides, manages the
server, runs both legs with cwd = the SDK checkout, and merges. Every
claimed revision must pass for Tier 1. This is what makes multi-revision
tiering correct for SDKs whose wire era is fixed per server process
(go-sdk only speaks >= 2026-07-28 when constructed stateless) or per
endpoint (csharp-sdk serves it at /stateless): one
--conformance-server-url cannot describe them, and their config already
does. The single-URL mode remains for SDKs not in the config.

Ship two sets. 2026-07-28 is a snapshot of 0.2.0-alpha.10, published the
day before that revision shipped. 2025-11-25 is a reconstruction from
the same anchor and its header says so: the release current on its ship
date predates spec-version awareness entirely. not_scored entries run
and are reported with a reason (extension per SEP-1730, or
added-after-release) but never affect a pass rate or the exit code, and
--expected-failures judges scored scenarios only. Reports label every
entry with its revision, and JSON passed/failed/total describe the
scored set so they always agree with pass_rate.

tier-check's exit code now gates on the machine-checkable half: nonzero
when a scored scenario fails or a leg could not be measured. A child run
that cannot happen at all (a set naming a renamed scenario, an
unreachable server) reports as not measured rather than as 0% against
the implementation. Conformance runs to completion before the GitHub
checks, so an API failure can no longer interrupt a leg and orphan a
managed server, and the legs spawn their child asynchronously: the
previous synchronous exec blocked the event loop, the parent stopped
draining the managed server's log pipes, and a chatty server (rust-sdk's
per-request tracing) froze on a full pipe and was scored as failing.

Driving every tier-1 SDK through the new path also fixed three configs:
csharp-sdk's server now launches from its build-output directory so
ASP.NET finds appsettings.json (its AllowedHosts filter, which
dns-rebinding-protection tests, silently never loaded from the repo
root); rust-sdk's STATELESS=1 override is removed because its CI runs
one default server for both revisions and the forced mode fails its
SEP-2575 scenarios; python-sdk's build mirrors its CI's two targeted
syncs because --all-packages is unbuildable on main. withManagedServer
now waits for its child to exit before returning, so consecutive runs on
one port cannot race a dying predecessor.
@felixweinberger
felixweinberger force-pushed the fweinberger/frozen-requirements branch from f0120f1 to b73c385 Compare August 7, 2026 12:36
@felixweinberger
felixweinberger merged commit 232a901 into main Aug 7, 2026
8 checks passed
@felixweinberger
felixweinberger deleted the fweinberger/frozen-requirements branch August 7, 2026 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Skill README's Go SDK example produces wrong tier verdict — contradicts known-sdks.ts

1 participant