* Add DPoP client conformance scenario + shared DPoP helpers (SEP-1932, #368)
Client-anchored packaging: carries the shared DPoP foundation (dpopProof /
dpopToken helpers, the createAuthServer DPoP core, and sep-1932.yaml) together
with the client scenario, so it can merge first; the server (#369) and
authorization-server (#370) scenarios are follow-ups that depend on it.
DPoP is treated as a draft/not-yet-official feature (mirroring the WIF SEP-1933
scenario): the scenario's source is `introducedIn: DRAFT_PROTOCOL_VERSION` and
it is registered in draftScenariosList (informational, not scored). It is NOT
registered in EXTENSION_IDS, since the ext-auth DPoP extension is still on a
branch rather than merged.
Client checks (the two extension MUSTs, RFC 9449 §7.1 / §4.2-4.3):
- sep-1932-client-dpop-auth-scheme — token presented with the DPoP scheme.
- sep-1932-client-fresh-proof — a fresh, well-formed DPoP proof per request.
The test MCP server judges the client via an opt-in DPoP middleware
(dpopResourceAuth) passed through createServer's authMiddleware hook; a compliant
example client (covered by the Client Draft Scenarios loop) plus deliberately-
broken bearer/replay variants drive the pass/fail acceptance tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* client/dpop: split into nonce-less baseline + nonce-required scenario
Server-provided nonces are optional in RFC 9449 (AS §8 "MAY", RS §9 "can
also choose"), and the nonce-less flow is the common case. The single
auth/dpop scenario hard-wired nonce enforcement on both the test AS and
MCP server, so a plain nonce-less proof was never accepted end-to-end and
a client that only does plain proofs was never exercised on its happy path.
Parameterize DPoPClientScenario with `requireNonce` and register it twice:
- auth/dpop (requireNonce=false) — nonce-less baseline; emits the
three baseline checks (token-request-proof, dpop-auth-scheme, fresh-proof).
- auth/dpop-nonce (requireNonce=true) — prior behavior; adds as-nonce and
rs-nonce (five checks).
Check IDs are reused (no new IDs, no sep-1932.yaml change); the two nonce
checks are emitted only by auth/dpop-nonce, so traceability stays intact.
Add auth-test-dpop-no-nonce.ts (nonce-incapable but otherwise compliant)
asserting SUCCESS against auth/dpop — proof that a client with no nonce
handling still completes DPoP when the server does not require a nonce.
Baseline negatives stay on auth/dpop (nonce-independent); the two nonce
negatives are re-pointed to auth/dpop-nonce.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* client/dpop: round-4 nonce-surface correctness fixes
Address review findings on the auth/dpop-nonce posture:
- Record the token-request proof observation BEFORE the §8 nonce challenge,
so a client that is challenged but does not retry is no longer mis-reported
by token-request-proof as "never completed a token request" (its proof was
just validated). (#2)
- Record the resource-request jti/replay on ANY valid proof (before the §9
nonce gate), so fresh-proof is never asserted on zero evidence and a client
that never honours the RS nonce fails ONLY rs-nonce. (#5)
- Require challengeIssued && honored for as-nonce/rs-nonce SUCCESS, closing a
vacuous-pass path (a client that pre-sends the nonce is never challenged). (#3a)
- Gate the AS nonce observation on the authorization_code exchange, matching
recordTokenRequestProof (a refresh exchange must not satisfy §8). (#3c)
- Collapse duplicate shared token-flow check IDs (token-request, pkce-*) that
the §8 round-trip re-POST would otherwise emit twice. (#4)
- Example client: retry the token request only on a use_dpop_nonce error code,
not any 400 carrying a DPoP-Nonce header (consistent with the RS path). (#6)
- Fix the DpopTokenRequestObservation docstring ("last write wins" →
sticky-failure). (#7)
Adds an expectedSuccessSlugs option to the client test helper and tightens the
two nonce negative tests to pin the now-clean behavior (token-request-proof
SUCCESS for no-as-nonce; only rs-nonce fails for no-rs-nonce).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* client/dpop: round-5 fixes — dedupe masking, wording, replay decision
- dedupeSharedChecks was masking failures: it kept the last occurrence of a
duplicate check ID regardless of status and ran in BOTH postures, so a
FAILURE on a challenged POST (or an earlier attempt in a restarted flow) was
silently dropped. Replace with an exported, unit-tested collapseDuplicateChecks
that prefers the MOST-SEVERE occurrence, and only apply it in the nonce
posture (the baseline keeps genuinely distinct repeated attempts). (R5 #1)
- Reword the token-request-proof description: it can be SUCCESS for a client
that presented a valid proof but never obtained a token (challenged, no
retry), so it no longer claims "obtaining a DPoP-bound access token". (R5 #2)
- Reword the as-nonce/rs-nonce failure messages to cover a client that DID
retry but whose retry proof was rejected (not only "did not retry"). (R5 #3)
- Document the deliberate replay decision: recording jti on challenged requests
means re-sending the identical proof across the challenge/retry boundary trips
replay detection — a genuine RFC 9449 §4.2 jti-uniqueness violation. (R5 #5)
Adds src/scenarios/client/auth/dpop.test.ts unit-testing collapseDuplicateChecks
(prefers failures, keeps INFO, preserves order) — the dedupe fix is now pinned.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* client/dpop: round-6 polish — pin collapse wiring; docstring
- Add an integration assertion that the §8/§9 double-POST is collapsed to one
token-request/pkce-* entry each in auth/dpop-nonce, so the requireNonce gating
of collapseDuplicateChecks can't be silently deleted or inverted (previously
mutation-survivable). runClientAgainstScenario now returns the emitted checks
so callers can make occurrence-level assertions (backward-compatible).
- Docstring: note the severity ladder ranks SKIPPED below SUCCESS, and that
equal-severity ties keep the last occurrence.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(auth): add JWT-bearer helper for WIF client conformance (SEP-1933)
Adds createWorkloadJwt and generateWorkloadKeypair to provide reusable,
tested JWT signing infrastructure for the upcoming wif-jwt-bearer scenario
(PR #2). Also extracts JWT_BEARER_GRANT_TYPE constant and migrates
cross-app-access.ts to use it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(auth): add JWT-bearer helper for WIF client conformance (SEP-1933)
Adds createWorkloadJwt and generateWorkloadKeypair to provide reusable,
tested JWT signing infrastructure for the upcoming wif-jwt-bearer scenario
(PR #2). Also extracts JWT_BEARER_GRANT_TYPE constant and migrates
cross-app-access.ts to use it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(auth): add WIF JWT-bearer scenario and negative tests (SEP-1933)
Adds the auth/wif-jwt-bearer client conformance scenario using the RFC 7523
JWT-bearer grant (urn:ietf:params:oauth:grant-type:jwt-bearer).
The scenario pre-signs valid, wrong-audience, and expired JWTs on start()
to simulate cloud workload identity tokens. The conformance AS verifies the
assertion and emits per-class checks (wif-assertion-verified,
wif-assertion-missing, wif-assertion-audience, wif-assertion-expired,
wif-assertion-malformed). Broken example clients exercise the missing-assertion
and wrong-audience failure paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: apply prettier formatting to wif-jwt-bearer.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(auth): add expired-assertion negative test; clarify wif-jwt-bearer comments
- Add runWifJwtBearerExpiredAssertion + auth-test-wif-expired-assertion.ts
to exercise the wif-assertion-expired check path (was dead code)
- Clarify WifAssertionVerified description to note iss is not validated
- Add comment explaining clockTolerance: 5 is intentional (same-run keypair)
- Add comment explaining numeric expiresIn is absolute epoch seconds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(wif-jwt-bearer): disable DCR, detect retries, improve getChecks description
- Add WIF_CLIENT_ID constant; pre-seed provider's clientInformation() so
the SDK skips Dynamic Client Registration entirely (disableDynamicRegistration
on the auth server + pre-seeded client_id on the provider side)
- Add failedOnce/tokenRequestReceived tracking on the scenario; reject and
record wif-no-retry FAILURE if the client attempts a second token request
after the first fails
- Add hasAttempted guard in WifJwtBearerProvider.prepareTokenRequest() to
throw on retry from the client side
- Fix getChecks() sentinel description: distinguish "no request made" from
"request made but verification failed"
- Add client_id to context and Zod schema
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: prettier formatting on everything-client.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(wif-jwt-bearer): let errors propagate from broken example clients
The try/catch blocks were silently swallowing auth failures, so the
negative tests passed purely because expectedFailureSlugs found the
AS-emitted check — not because client error-surfacing was verified.
allowClientError: true on the test cases handles the non-zero exit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(wif-jwt-bearer): tag as draft, add scope-rejected check, add SEP traceability
- Change specVersions from ['extension'] to [DRAFT_PROTOCOL_VERSION] and add
source = { introducedIn: DRAFT_PROTOCOL_VERSION } so the scenario is
reachable via --spec-version draft (extension tag excluded it from all
spec-version runs)
- Move registration from extensionScenariosList to draftScenariosList in
index.ts to match the tag/list convention enforced by spec-version.test.ts
- Add wif-assertion-scope-rejected check: AS returns invalid_scope for a
valid JWT when the client requests the reserved 'wif.rejected' scope;
verify client surfaces the error and does not retry
- Add runWifJwtBearerScopeRejected to everything-client.ts; add optional
scope param to WifJwtBearerProvider; add CLI entry point and vitest case
- Add src/seps/sep-1933.yaml with requirements mapped to each check ID,
covering the three deferred checks (iss, sub, jti) with exclusion rationale
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: prettier formatting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(wif-jwt-bearer): add wif-grant-fallback check for unauthorized_client fallback
Client bug class: WIF client receives unauthorized_client from JWT-bearer grant
and silently switches to authorization_code instead of surfacing the error.
The MCP SDK retries after UnauthorizedClientError (auth.js:152-154), calling
prepareTokenRequest() a second time. WifGrantFallbackProvider exploits this by
returning authorization_code params on the second call.
Spec anchor: RFC 7523 §2.1 — clients MUST use the JWT-bearer grant type;
silent grant-type switching hides authentication failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: rename unused scope param in WifGrantFallbackProvider
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(wif-jwt-bearer): use realistic SPIFFE and K8s PSAT token formats
valid_jwt uses a SPIFFE JWT-SVID subject (spiffe://conformance-test.local/...)
issued by a SPIRE-style issuer. wrong_audience_jwt uses a Kubernetes PSAT
subject and kubernetes.io claims, mirroring how K8s projected service-account
tokens look in production. Token verification and client behaviour are
unchanged; the formats ground the scenario in real workload identity platforms.
k8s_issuer and k8s_subject added to context and schema for external clients
that want to construct their own K8s-style assertions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: prettier formatting on wif-jwt-bearer.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(wif-jwt-bearer): drop informational k8s_issuer/k8s_subject from context
These fields were not used by any client handler and are redundant since
the K8s claims are already baked into wrong_audience_jwt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(wif-jwt-bearer): address pre-merge review feedback
- Downgrade wif-no-retry, wif-grant-fallback, and wif-assertion-scope-rejected
to WARNING; RFC 7523 is silent on client retry and grant-type switching, so
FAILURE status was not traceable to spec text
- Add WifRetryProvider broken client that re-sends JWT-bearer after
unauthorized_client, making wif-no-retry actually exercisable as a WARNING
- Drop getChecks() sentinel (non-standard pattern that overloaded the
wif-assertion-verified check ID); test harness detects empty check sets
- Add comment on DRAFT_PROTOCOL_VERSION workaround for runner limitation
- Add comment explaining why both slash/no-slash audience forms are accepted
- Tighten schema comments for audience URL constraint and ES256 literal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(wif-jwt-bearer): drop decorative fields and neutralise JWT fixtures
- Remove issuer and subject from context schema and scenario return value;
neither is validated by the AS or used by the client, making them
misleading. The iss exclusion rationale is updated to explain why.
- Replace SPIFFE/K8s-PSAT constants and the kubernetes.io additionalClaims
with neutral values across all three JWT fixtures; no check inspects
token format, so format flavour was decorative and invited false
assumptions about fixture semantics.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(wif-jwt-bearer): remove workaround comment, drop signing_algorithm, annotate retry provider
- Remove self-admitted workaround comment on DRAFT_PROTOCOL_VERSION; the
classification stands on its own and the comment was inviting challenge
- Drop signing_algorithm from context schema and scenario return; the field
was redundant once the value was fixed to ES256, and the assertion is opaque
to the client regardless
- Add comment to WifRetryProvider explaining it deliberately omits the
hasAttempted guard so the SDK retry reaches the AS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(wif-jwt-bearer): export scope constants, remove unused AS field, annotate state machine
- Export WIF_TRIGGER_UNAUTHORIZED_SCOPE and WIF_REJECTED_SCOPE from
createWorkloadJwt.ts and import them in everything-client.ts; removes
duplicate bare-string declarations that could silently diverge
- Remove tokenEndpointAuthSigningAlgValuesSupported from mock AS config;
the SDK does not consume this field in the JWT-bearer flow
- Add comment to onTokenRequest clarifying wif-no-retry and wif-grant-fallback
fire on any second request after any failure, not only post-unauthorized_client
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(wif-jwt-bearer): address pcarleton review comments
- Move all broken WIF provider classes and runners to wif-broken-clients.ts;
everything-client.ts now contains only the conformant WifJwtBearerProvider
- Add sentinel in getChecks() for !tokenRequestReceived to prevent a silent
pass when the client never sends a request
- Drop wif-assertion-scope-rejected check; the no-retry behaviour after
invalid_scope is already covered by wif-no-retry, and sep-1933.yaml updated
accordingly
- Remove audience from context schema and scenario return; the field was
unused by any client implementation
- Expand scenario description to tell a client implementer what to implement
- Apply suggested description fix to wif-grant-fallback (does not assume
unauthorized_client was the specific error that preceded the fallback)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(wif-jwt-bearer): drop number type from expiresIn, use jose duration strings
Jose supports negative duration strings ('-60s', '60 seconds ago') that produce
already-expired tokens without needing absolute epoch arithmetic. Drop the
number overload, update the expired_jwt call site to '-60s', and update the
unit test accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Updates SEP-2549 caching traceability text to match
modelcontextprotocol/modelcontextprotocol#2890:
- Scope the five *-caching-hints requirements to results with
resultType: "complete" (interim input_required results carry no
hints).
- Add two excluded client MUST NOT rows from the new Cache Key
section (cache-key mismatch, multi round-trip retry results).
Neither is wire-observable since a conformant client may simply
never cache.
No check IDs change; traceability.json will be refreshed by the
scheduled traceability workflow.
Claude-Session: https://claude.ai/code/session_01Gbqx5LodtAghT5fRYFxeja
The tasks extension scenarios hardcoded -32003 (Missing Required Client
Capability) and -32001 (Header Mismatch). The draft spec types renumbered
these to -32021 and -32020 per modelcontextprotocol#2907, so a server
built against current draft.ts now returns the new codes and these checks
would produce false failures.
Import MISSING_REQUIRED_CLIENT_CAPABILITY and HEADER_MISMATCH from
spec-types/draft instead of hardcoding, matching the existing
http-standard-headers scenario. The checks now assert whatever the schema
currently defines, so a future renumber propagates automatically.
Also drop the now-stale code number from the affected check-ID slugs
(for example sep-2663-server-returns-32003-when-required becomes
sep-2663-server-returns-missing-capability-when-required) and update the
sep-2663.yaml requirement text to -32021, keeping it consistent with the
already-updated sep-2575 and sep-2243 manifests.
Scenarios are still in the pending list so default CI is unaffected. The
traceability.json manifest already predates the upstream renumber and is
refreshed separately from a full suite run.
* feat(tasks): SEP-2663 lifecycle scenario (8 checks)
Adds the first scenario for the SEP-2663 io.modelcontextprotocol/tasks
extension — a single TasksLifecycleScenario covering sync vs async
dispatch, DetailedTask shape on tasks/get, tool errors vs protocol
errors, and cancellation semantics. 8 ConformanceCheck records, all
passing against a SEP-2663-conformant Go fixture.
Why "tasks" (not "tasks-v2"): SEP-2663 IS the tasks surface once it
lands; the v2 suffix is only meaningful in implementations that
maintain a v1 surface alongside, which the conformance suite does not.
Layout:
- src/scenarios/server/tasks/lifecycle.ts — scenario class
- src/scenarios/server/tasks/helpers.ts — raw-fetch escape hatch
(the SDK's typed schemas strip resultType/inputRequests/...)
- src/scenarios/server/tasks/lifecycle.test.ts — fork-local vitest
runner. Two modes: spawn a fixture binary via MCPKIT_TASKS_BINARY,
or point at an already-running server via MCPKIT_TASKS_SERVER_URL.
Skips when neither is set so it doesn't break upstream CI runs that
go through everything-server (which doesn't yet implement
io.modelcontextprotocol/tasks).
Scenario is registered in pendingClientScenariosList so
all-scenarios.test.ts skips it; promote to active once the upstream
fixture grows extension support.
Tagged ['extension', DRAFT_PROTOCOL_VERSION] — selectable via
--suite extensions and --spec-version draft.
* style(tasks): apply prettier formatting
* feat(tasks,mrtr): port full SEP-2663 + SEP-2322 scenario suite
Builds out the rest of the tasks scenarios (atop the lifecycle canary)
and adds the SEP-2322 ephemeral MRTR scenario in a sibling folder.
Both target their own fixtures; both runners are brand-neutral and
language-agnostic (TASKS_SERVER_URL / TASKS_SERVER_CMD,
MRTR_SERVER_URL / MRTR_SERVER_CMD; readiness via TCP polling).
Tasks ClientScenario classes:
- TasksLifecycleScenario (8 checks; v2-01..v2-08)
- TasksCapabilityNegotiationScenario (4 checks; v2-11/22/23/25, SEP-2575)
- TasksWireFieldsScenario (3 checks; v2-12/13/21)
- TasksRequestStateScenario (3 checks; v2-14/15/28)
- TasksMRTRInputScenario (3 checks; v2-16/17/29 partial fulfillment)
- TasksRequestHeadersScenario (3 checks; SEP-2243 request-header tolerance)
- TasksDispatchScenario (8 checks; v2-09/10/19/20/26/27/30/31)
- TasksStatusNotificationsScenario (1 check; SEP-2663 §notifications, optional)
MRTR ClientScenario class:
- MrtrEphemeralFlowScenario (7 checks + 1 SKIPPED; mrtr-01..07,
mrtr-08 deferred for spec terminology +
reference-impl reasons)
Both runners spawn the fixture via a shell command and detect readiness
by TCP-polling the URL's host/port — no log-line scanning, no
language-specific assumptions. The same env vars work for any server
implementation.
Scenarios are tagged ['extension', DRAFT_PROTOCOL_VERSION] and registered
in pendingClientScenariosList so all-scenarios.test.ts (which targets
the upstream everything-server) skips them until the fixture grows
SEP-2322 / SEP-2663 support.
* docs(tasks,mrtr): scenario READMEs for upstream porting
Restructured around ClientScenario classes (one row per class with
check-list under it) rather than per-numbered-test slugs. Documents
fixture requirements, env vars, open spec questions, and the
wire-format diff for each suite.
Per AGENTS.md, severity follows spec keyword (MUST/MUST NOT → FAILURE,
SHOULD/SHOULD NOT → WARNING). The READMEs explain why some checks emit
INFO rather than FAILURE (optional emission paths per SEP-2322).
* tasks: assert createdAt + lastUpdatedAt; factor _shared/ helpers
Two reviewer-driven additions:
1. SEP-2663 createdAt / lastUpdatedAt ISO-8601 assertion in
`tasks-server-task-creation` (per Luca's PR #262 review feedback).
The check now flags servers that emit non-ISO timestamps (epoch
seconds, RFC-2822, etc.) on TaskInfoV2 envelopes.
2. Factor cross-cutting test-harness helpers into _shared/:
- `_shared/test-runner.ts` — `waitForServerReady` (renamed from
`waitForTcpReady`; the call site cares about server readiness,
not the TCP-poll mechanism). Imported by tasks/ and mrtr/
all-scenarios.test.ts; replaces ~30 LOC of inline duplication
in each.
- `_shared/wire-format.ts` — `ISO_8601_PATTERN` constant +
`isIso8601(s)` predicate. Documented rationale for choosing a
regex over `Date.parse` (too permissive),
`new Date(s).toISOString()` (too strict), or
`Temporal.Instant.from` (Node 24+ experimental). Future
wire-shape predicates (data URI, percent-encoded filename,
etc.) can land here.
Cherry-pick footprint when graduating to upstream PR is the SEP
folder + the imported `_shared/` files. First PR through carries
them upstream; subsequent feat branches inherit via standard
upstream-sync flow.
All 9 scenario tests still pass against the Go reference fixtures.
* refactor(tasks,mrtr): use SDK Client + AnyResult instead of raw-fetch helpers
Drops initRawSession/rawRequest/rawRequestFull from tasks/helpers.ts in
favor of the SDK's Client + StreamableHTTPClientTransport, paired with
a Zod passthrough schema (AnyResult) that preserves SEP-2663 / SEP-2322
draft fields the SDK's typed schemas would strip.
headers.ts and notifications.ts keep a small inline fetch where the SDK
can't reach: per-request HTTP headers (SEP-2243) and SSE notification
observation. Both reuse the SDK session via transport.sessionId.
All SEP-2663 + MRTR ephemeral-flow scenarios pass against the Go fixture.
* style: prettier formatting on tasks/mrtr scenarios
* tasks: align ttlMs / pollIntervalMs assertions per 2026-05-07 SEP
PR 2663 commit 62758914 standardised every duration field on the Ms
suffix, integer milliseconds. wire-fields.ts now asserts ttlMs and
pollIntervalMs are present on CreateTaskResult, the legacy v1 ttl
and pollInterval keys are absent (already covered), and the interim
ttlSeconds / pollIntervalMilliseconds keys are also absent on a
post-2026-05-07 server. lifecycle.ts and the scenario README pick up
matching prose updates.
Verified by make testconf-tasks-v2 (8/8) against a renamed mcpkit
fixture, and make testconf-mrtr (7/7 + 1 SKIPPED) against the paired
MRTR surface.
* style: prettier column alignment on tasks README
* mrtr: flip MRTR_INPUT_REQUIRED_RESULT_TYPE per merged SEP-2322
SEP-2322 merged on 2026-05-06 with the variant renamed from
IncompleteResult to InputRequiredResult and the resultType discriminator
from "incomplete" to "input_required" (commit de6d76fb, per dsp-ant
request). The MRTR_INCOMPLETE_RESULT_TYPE constant was specifically
designed as a one-line flip point for this scenario.
Renames
- MRTR_INCOMPLETE_RESULT_TYPE = "incomplete" ->
MRTR_INPUT_REQUIRED_RESULT_TYPE = "input_required"
- isIncompleteResult -> isInputRequiredResult
- All "IncompleteResult" -> "InputRequiredResult" in scenario prose
and check descriptions (ephemeral-flow.ts, README.md)
SEP-2663 had not yet flipped its discriminator literal as of PR head
82fb2c4d (5/7 21:52 UTC). Caitie's 5/15 RC commitment (issue comment
4384052694 on PR 2322) tracks the alignment to "input_required" both
sides. The constant remains the one-line flip point in case the 2663
follow-up surprises us.
Tested via mcpkit's make testconf-mrtr (7/7 + 1 SKIPPED green against
a renamed mcpkit fixture) and make testconf-tasks-v2 (8/8 still
green, no regressions on the paired surface).
* mrtr: prettier formatting + fix README rename typo
Lefthook prettier reformatted column alignment on first push attempt;
README also had a stale "renamed from InputRequiredResult" — should
read "renamed from IncompleteResult". Fix both.
* mrtr-08: refresh SKIPPED prose post SEP-2322 merge
Two stale references in the mrtr-tasks-composition SKIPPED check:
- Comment block + errorMessage framed blocker (a) as "spec authors
disagree" / "input_required vs incomplete". SEP-2322 merged
2026-05-06 with "input_required" (commit de6d76fb). The blocker
now reads as "SEP-2663 has not yet aligned to the merged 2322
literal" — Caitie's 5/15 RC commitment (PR 2322 issue comment
4384052694) tracks the alignment.
- errorMessage referenced "IsIncomplete signal" — that field was
renamed to IsInputRequired on the mcpkit side in lockstep with
the SEP-2322 wire-variant rename. Updated to match.
Status stays SKIPPED because blocker (b) — the mcpkit middleware
refactor (issue 347) — is still open.
* mrtr-08: drop the resolved-discriminator framing from the SKIPPED message
After SEP-2322 merged with "input_required", the only blocker that
actually keeps mrtr-08 SKIPPED is the eager-task-creation pattern in
reference-server middleware (panyam/mcpkit issue 347). The earlier
two-blocker framing read as if the test were waiting on both, but
blocker (a) is effectively resolved for any server that emits the
merged-2322 literal — leaving (b) as the sole gate.
Tighten the comment block + description + errorMessage to lead with
the middleware refactor and demote the discriminator history to a
parenthetical aside.
* style: prettier line-break on mrtr-08 description string
* tasks-wire-fields: address PR 262 review feedback
Two requested changes from the SEP-2663 author's review pass on
modelcontextprotocol/conformance PR 262
(pullrequestreview-4254601106).
(1) Drop the interim-key absence checks. The transition window between
"ttlSeconds / pollIntervalMilliseconds" and the final
"ttlMs / pollIntervalMs" wording is over now that the merged
spec settled on the Ms suffix. Useful while the spec was in
flight, noise once it stabilized. Removes the absence checks
plus the surrounding comment + description + details fields.
(2) Add Number.isInteger() to ttlMs and pollIntervalMs validation.
Spec says integer milliseconds; the previous typeof + range check
would have allowed fractional values. Now both fields fail if
they're not integers.
README scenario table tightened: "ttlMs + pollIntervalMs present and
integer-valued; legacy ttl / pollInterval keys absent".
* style: prettier column alignment on wire-fields README row
* feat(tasks): align scenarios with the Final-merged SEP-2663 spec
SEP-2663 merged Final on 2026-05-15. Four normative wire changes baked in at merge time. This commit updates the tasks server-conformance scenarios so the suite validates each one as a MUST rather than tolerating divergence as INFO/MAY.
1. notifications/tasks/status renamed to notifications/tasks. The status-notifications scenario now FAILs on observing the legacy method on the v2 surface, and validates the new name's payload shape when emitted.
2. notifications/progress and notifications/message MUST NOT be sent for tasks. Two new absence-asserts on the status-notifications scenario, reusing the existing SSE-observation harness.
3. requestState removed from the tasks-v2 wire. The previous request-state scenario tested the deleted "Request State Management" section; replaced with absence-asserts on CreateTaskResult, DetailedTask, and notifications/tasks payloads. SEP-2322's InputRequiredResult on the MRTR surface still carries requestState and is unchanged.
4. -32003 (Missing Required Client Capability) for required-task tools when the client did not negotiate the extension. New scenario TasksRequiredTaskErrorScenario initializes without declaring the extension, calls a TaskSupport=required tool, and asserts the rejection code plus the structured requiredCapabilities payload. Documents `failing_job` as a fixture requirement.
Verified locally against panyam/mcpkit examples/tasks-v2: 8/9 scenarios pass; the request-state-removal scenario intentionally FAILs against the current mcpkit main because the implementation still emits requestState on DetailedTask. That gap is the next item on the mcpkit catch-up plan.
* chore(tasks): address PR 262 review feedback
- notifications.ts: skip the entire scenario; SEP-2663 routes
notifications/tasks over the SEP-2575 subscriptions/listen stream,
not the tools/call POST SSE the prior harness observed. Body
preserved in git blame for the rewrite follow-up.
- request-state.ts: keep the absence-asserts (per the asymmetric
recommendation in review) and reframe the doc block around the
SEP-2322 lexical-adjacency motivation rather than pre-merge SEP
history.
- required-task-error.ts: cite SEP-2575 §"Missing Required
Capabilities" as the canonical home of -32003; add SEP_2575_REF to
specReferences on the -32003 checks.
- Sweep decontextualized "merged spec dropped X" / "pre-merge SEP-2663
said Y" framing from tasks/* doc blocks and inline check
descriptions.
* chore(tasks): prettier — strip trailing blank line in notifications.ts
* chore(tasks): drop `requestState` from `tasks/update` params
`taskId` is the session-continuation handle on the tasks-v2 MRTR-resume
path; `requestState` lives only on the SEP-2322 ephemeral
InputRequiredResult surface, not on the tasks-v2 wire.
* chore(tasks): loosen strict ack equality on tasks/cancel + tasks/update
The cancel-empty-ack and tasks/update-ack checks were comparing the
whole response against the literal `{resultType:"complete"}` via
`JSON.stringify` equality. That rejected acks carrying `_meta` or any
other future result-shape metadata even though the spec permits them.
Now structural: assert `resultType === "complete"` plus absence of the
task-envelope fields (`taskId`, `status`, `result`, `error`,
`inputRequests`). `_meta` and other metadata pass through.
* chore(tasks): poll for terminal status after tasks/cancel
The cancel-empty-ack check observed status via an immediate tasks/get
after the cancel ack. Cancellation is eventually-consistent, so the
task could still be `working` (or transitioning) at the moment of
observation, making the assertion flaky against any reasonable server.
Now polls via `waitForTerminal()` and asserts the terminal status is
`cancelled`. README's `slow_compute` fixture row documents the
matching tool contract ("MUST settle to `cancelled` when
`tasks/cancel` arrives while running") so the test has a deterministic
anchor.
* chore(tasks): tasks/cancel on a terminal task is idempotent
The previous check asserted `-32602` when cancelling an already-terminal
task. That forces every client to handle the race between observing a
task's terminal status and racing in a cancel request — the task can
terminate between the two and the cancel then errors instead of acking.
Check 8 renamed to `tasks-cancel-terminal-idempotent-ack` and now
asserts the same empty-ack shape (`resultType:"complete"` + no task-
envelope fields) as on an active task. README's lifecycle table row
updated to match.
* docs(tasks): drop `requestState` from SEP-2663 wire-field listings
`requestState` belongs to the SEP-2322 ephemeral MRTR result shape
(`InputRequiredResult`), not to the tasks-v2 wire. The README listed
it under SEP-2663 wire fields in four spots — top blurb, the SEP-2663
row of the Specs table, the v1↔v2 wire-format diff, and the raw-fetch
design note. All four now describe only the actual SEP-2663 shapes;
the raw-fetch note still calls out the SEP-2322 ephemeral
`requestState` separately so the rationale for stripping it via raw
fetch stays intact. The SEP-2322 row of the Specs table is unchanged
since `requestState` is correctly listed there as an MRTR base type.
* docs(tasks): rewrite stale request-state table to match absence-asserts
The README's `tasks-request-state` table still described the prior
scenario shape (shape / echo / stale-tolerance checks), which had
assumed `requestState` was on the tasks-v2 wire. The scenario itself
was rewritten earlier into the two-check absence assertion
(`tasks-create-result-no-request-state` /
`tasks-get-detailed-no-request-state`); the README now matches.
A motivational paragraph above the table explains why a negative
test exists for a field the spec never defines (the SEP-2322
lexical-adjacency confusion vector).
* chore(tasks): assert -32003 (not -32601) for gated tasks methods
When a client calls tasks/get / tasks/update / tasks/cancel without
having negotiated the io.modelcontextprotocol/tasks extension, the
suite now asserts -32003 (Missing Required Client Capability,
SEP-2575 §"Missing Required Capabilities") instead of -32601.
The spec doesn't currently mandate this code for the gated-method
path, so the assertion is forward-looking: it follows the SEP-2575
pattern that already governs `required` tools (and that
subscriptions/listen for tasks is expected to use). README's
open-questions list calls this out so readers know it's a
spec-clarification ahead-of-the-curve.
Drops the now-obsolete "Invalid requestState — silent ack vs -32602"
open-questions item: the previous request-state scenario covered
that, but the rewritten absence-asserts scenario no longer exercises
that path.
* chore(tasks): prettier — realign README table pipes after row edits
* feat(tasks/mrtr): initRawSession helper for draft-version handshake
The SDK's Client.connect() pins protocolVersion to LATEST_PROTOCOL_VERSION
in the initialize body. Scenarios tagged DRAFT_PROTOCOL_VERSION would
still negotiate the previous stable on the wire, which means a strict
draft-only server rejects the handshake with unsupported_protocol_version.
initRawSession sidesteps the SDK: a raw fetch initialize that carries the
draft version, captures the session ID, sends notifications/initialized,
and exposes the small surface scenarios actually use - request,
requestFull, notification, close. Errors are thrown as McpError so
existing instanceof / .code checks keep working. SSE response parsing
folds in the per-scenario raw helper from headers.ts.
All 8 SEP-2663 tasks scenarios + the MRTR ephemeral-flow scenario migrate
to the raw session. tsc clean.
* feat(tasks/mrtr): wire-mode matrix; SEP-2575 _meta + SEP-2243 routing
headers
initRawSession grows a `stateless` option that picks between the
legacy session wire (initialize handshake + Mcp-Session-Id) and the
SEP-2575 stateless wire (no initialize; server/discover up front;
per-request `_meta.io.modelcontextprotocol/{protocolVersion,
clientInfo, clientCapabilities}` envelope; `MCP-Protocol-Version`
header on every call).
Tasks behavior is wire-independent in spec, so the harness can run
each scenario against both wires. To keep the legacy run green
through this PR's refactor (and to surface stateless-mode gaps
deliberately rather than as a regression), the default is legacy-
only — set TASKS_WIRE_MODES=legacy,stateless (or just stateless) to
opt the matrix on. Same toggle for MRTR via MRTR_WIRE_MODES.
Two follow-on cleanups in the same commit so the legacy run actually
passes against an SEP-2243-enforcing server:
- All request paths now auto-emit Mcp-Method (and Mcp-Name for
tools/call.params.name / resources/read.params.uri) on protocol
versions in `SEP_2243_ENFORCED_VERSIONS` (today DRAFT-2026-v1).
Scenarios that probe the mismatch path can still override via
`extraHeaders`. Empty record on older versions so the helper stays
silent when the spec doesn't require it.
- `tasks-headers-body-method-authoritative` was written under an
older SEP-2243 reading where Mcp-Method was tolerant. The current
spec (and mcpkit's PR 477 implementation, plus the upstream
http-header-validation scenario) says the server MUST reject
mismatches with -32001 HeaderMismatch. Renamed to
`tasks-headers-reject-mismatched-method` and the assertion flipped
to expect the rejection.
Verification: testconf-tasks-v2 + testconf-mrtr both green on
legacy wire against mcpkit's tasks-v2 fixture (9/9 + 1/1).
* rename TASKS_WIRE_MODES / MRTR_WIRE_MODES → MCP_WIRE_MODES
Wire-mode choice is a cross-cutting conformance concern, not a
per-suite knob. Operators wouldn't realistically toggle tasks and
mrtr to different wires (both fixtures speak the same MCP server,
either it supports both wires or one). Single env var read by both
harnesses; default unchanged at ['legacy'].
* default MCP_WIRE_MODES to legacy+stateless
The conformance suite is brand-neutral — every SDK consuming it
should validate both MCP wires for SEP-2663 / SEP-2322 behavior by
default. Pinning the default to legacy-only let mcpkit's CI stay
green while gaps in its stateless dispatcher pended, but quietly
under-tested any other SDK that adopts the suite.
Default now runs every tasks-v2 scenario × 2 wires and the mrtr
ephemeral-flow scenario × 2 wires. SDKs with only one wire
implemented pin via MCP_WIRE_MODES=legacy or =stateless. mcpkit's
own audit will need that pin (or to close panyam/mcpkit issue 480)
until the stateless dispatcher consumes per-request
_meta.clientCapabilities for the tasks extension declaration.
* helpers: send MCP-Protocol-Version on every post-initialize POST
Per 2025-11-25 transport spec §Protocol-Version Header, clients MUST
include MCP-Protocol-Version on every subsequent HTTP request after
initialize. Universal post-initialize requirement; applies to both
the legacy session wire and the SEP-2575 stateless wire.
The helper was dropping the header on legacy follow-up calls as a
workaround for our server's detectWireKind treating the header as a
stateless-wire signal. That workaround was non-spec-compliant; the
right fix is for our server to drop the header from its wire-detection
precedence list and rely on body-level _meta.protocolVersion as the
SEP-2575 signal. Filed separately.
Comment thread: PR 5 review (Luca, 2026-05-27).
* types/helpers: parameterize wire mode via ScenarioRunOptions
Drop the module-level defaultStateless flag + setDefaultWireStateless
shim that the harness was toggling in beforeEach. Wire mode now flows
as an explicit parameter through ClientScenario.run, so tests can be
parallelized safely (no shared mutable state between concurrent runs).
ClientScenario.run gains an optional ScenarioRunOptions second arg.
Existing scenarios that ignore the arg satisfy the interface as-is
(TypeScript permits an implementation to declare fewer parameters than
the interface). The 10 task/mrtr scenarios that consume wire mode add
one line each to thread `opts?.stateless` into initRawSession.
Harness loops call `scenario.run(SERVER_URL, { stateless: wire === 'stateless' })`
directly; no more beforeEach setter.
Comment thread: PR 5 review (Luca, 2026-05-27, comments 2 and 7 on parallelism).
* harness: use describe.each + it.each for the (wire x scenario) matrix
Replaces the nested for-loops in both the tasks and mrtr harnesses
with vitest's table-driven describe.each + it.each idioms. Same N x M
cardinality of tests, but vitest sees the parameter table directly:
- Reporter labels render the wire row inline:
`... > stateless wire > 'tasks-lifecycle' - all checks ...`
- Each (wire, scenario) becomes a leaf test vitest can schedule
independently, opening the door to test.concurrent / pool config
later without restructuring.
WIRE_TABLE pairs each WireMode with the boolean flag the harness
needs to pass via ScenarioRunOptions, so the `it.each` callback
receives the prepared `stateless` directly rather than deriving it
inside every test body.
No on-wire change. Behavior identical against the reference fixture
(9 task scenarios + 1 mrtr scenario, each per wire).
Comment thread: PR 5 review (Luca, 2026-05-27, comments 5, 6, 9, 10).
* helpers: extend Mcp-Name to prompts/get + tasks/* (SEP-2243 + SEP-2663)
routingHeaders now auto-emits Mcp-Name for the full standard-header
surface the spec defines:
- prompts/get -> params.name (SEP-2243 §Standard Headers)
- tasks/get -> params.taskId (SEP-2663 §Streamable HTTP routing headers)
- tasks/update -> params.taskId (SEP-2663)
- tasks/cancel -> params.taskId (SEP-2663)
tools/call (params.name) and resources/read (params.uri) were already
covered. Switch from if/else chain to a method-name switch since the
surface is now wider; cleaner to extend further.
Helper doc updated with the full surface list and a TODO for SEP-2243
§Custom Headers from Tool Parameters (Mcp-Param-* mirroring from
x-mcp-header annotations), which requires caching the tools/list
schema. Tracked as a separate follow-up; no current tasks/mrtr
scenario exercises a tool with x-mcp-header annotated inputs.
Comment thread: PR 5 review (Luca, 2026-05-27, comment 4).
* _shared/wire-mode: hoist parseWireModes out of the per-suite harnesses
Tasks and mrtr harnesses had byte-identical copies of WireMode type,
VALID_MODES, DEFAULT_WIRE_MODES, parseWireModes, and WIRE_MODES.
Hoisted to src/scenarios/server/_shared/wire-mode.ts so the single
parser drives both suites. Each harness now does:
import { parseWireModes, type WireMode } from '../_shared/wire-mode';
const WIRE_MODES: WireMode[] = parseWireModes();
No behavior change. MCP_WIRE_MODES env var semantics identical;
default still legacy+stateless.
Companion TODO landed in tasks/helpers.ts: most of that module
(initRawSession, RawSession, routingHeaders, SEP_2243_ENFORCED_VERSIONS,
readJsonRpcResponse, AnyResult, the failureCheck/skipCheck/errMsg test
helpers) isn't actually tasks-specific and belongs in _shared too.
Holding that file reshuffle for a separate follow-up PR so the move
gets its own focused review thread without crowding the SEP work.
Comment thread: PR 5 review (Luca, 2026-05-27, comment 8).
* refactor: hoist raw-session + sep-refs + checks helpers into _shared/
Picks up the TODO left in tasks/helpers.ts on PR 5: the raw fetch
session machinery, the SEP-2243 routing-header builder, the SEP_*
reference constants, and the failureCheck / skipCheck / errMsg /
AnyResult test scaffolding were never tasks-specific. They lived in
tasks/helpers.ts only because tasks/ was the first server suite to
need them. mrtr/helpers.ts re-declared SEP_2322_REF + errMsg +
failureCheck for the same reason.
Move them to src/scenarios/server/_shared/:
- sep-refs.ts: SEP_2243_REF, SEP_2322_REF, SEP_2575_REF, SEP_2663_REF
- checks.ts: errMsg, failureCheck, skipCheck, AnyResult
- raw-session.ts: JsonRpcResponse, RawSession, InitRawOptions,
initRawSession (legacy + stateless paths),
routingHeaders, SEP_2243_ENFORCED_VERSIONS,
readJsonRpcResponse, nextRawId
tasks/helpers.ts shrinks to the tasks-shaped pieces it actually owns:
TASKS_EXTENSION_ID and the waitForTerminal / waitForStatus polling
loops. mrtr/helpers.ts drops the duplicate SEP ref + the duplicate
test-scaffolding wrappers and re-imports from _shared/.
skipCheck and failureCheck in _shared/checks.ts make specReferences
required (a shared scaffold can't pick a sensible default), so all
call sites now pass the SEP they're grading against explicitly.
Pure file move + import-path churn. No behavioral change. Typecheck
+ lint clean.
* feat: emit SEP-2243 Mcp-Param-* headers from tool inputSchema annotations
Closes the TODO in routingHeaders. SEP-2243 §"Custom Headers from
Tool Parameters" requires the client to mirror primitive-typed
tools/call arguments as `Mcp-Param-<Suffix>` HTTP headers when the
tool's inputSchema annotates the property with
`x-mcp-header: "<Suffix>"`. Servers behind L7 routers / WAFs use
these headers to make routing or policy decisions without parsing
the JSON body.
Implementation:
- RawSession gains a `toolSchemas: Map<string, unknown>` cache.
- `initRawSession` issues a best-effort `tools/list` after the
handshake (after `notifications/initialized` for legacy; after
`server/discover` for SEP-2575 stateless) and populates the
cache from the response. Failures are swallowed — empty cache is
the safe default.
- `routingHeaders` takes an optional `toolSchemas` and walks the
cached schema for tools/call: for each primitive-typed property
(string / number / integer / boolean) with `x-mcp-header`, it
encodes the argument value per SEP-2243 §value-encoding (plain
printable ASCII verbatim; everything else wrapped as
`=?base64?{b64-utf8}?=`) and emits `Mcp-Param-<Suffix>`.
- Both wire flavours (legacy + SEP-2575) call routingHeaders the
same way, so the same Mcp-Param-* headers ship regardless of
transport mode.
Behavior wash for tools without `x-mcp-header` annotations — they
emit only Mcp-Method + Mcp-Name as before. Behavior wash for
non-tools/call methods. No new dependencies (uses
`globalThis.btoa` + `TextEncoder`, both standard in Node 20+).
Typecheck + lint + the existing vitest suite stay green.
* chore(_shared/raw-session): drop reference-impl naming from doc comments
Tightens three doc-comment passages that named or hinted at a
specific server-side implementation as the canonical reference. The
conformance suite is positioned brand-neutral; the doc comments now
describe the wire behavior in spec-anchored language only.
No behavioral change.
* sep-refs: point at rendered SEP pages on modelcontextprotocol.io
PR URLs freeze at merge commit; the rendered SEP pages on the spec
website absorb post-merge amendments. Switch all four refs:
SEP-2243 → /seps/2243-http-standardization
SEP-2322 → /seps/2322-MRTR
SEP-2575 → /seps/2575-stateless-mcp
SEP-2663 → /seps/2663-tasks-extension
* raw-session: use eventsource-parser for SSE parsing
Delegate the SSE branch of readJsonRpcResponse to eventsource-parser
(the same parser the MCP TS SDK uses). The hand-rolled splitter
worked for the single-frame JSON-RPC response we need but didn't
cover the full WHATWG SSE semantics — multi-line `data:`
continuation, CR/CRLF line endings, comment lines, the
`event:`/`id:`/`retry:` fields. The library covers all of it.
Already a direct dependency (3.x), so no new transitive footprint.
* test: pin readJsonRpcResponse contract for SSE + JSON branches
12 vitest cases against `_shared/raw-session.ts`'s content-type
dispatcher: application/json (id-match, mismatch, error frames) and
text/event-stream (single-frame extraction, id-filter across multi-
event streams, multi-line `data:` continuation, CRLF tolerance,
comment/retry/id field handling, error-frame return, empty body,
non-JSON data interleaving).
Builds synthetic `Response` objects locally; no fixture server
required, runs in every CI. Tests pin the contract the
eventsource-parser swap inherits — guards against future parser-
version regressions and against any subsequent rewrite that subtly
changes the SSE branch.
* tasks: isolate negative-path dispatch checks (conformance#262)
The removed-method (-32601) and unknown-taskId (-32602) checks
in tasks-dispatch-and-envelope previously sent requests missing
Mcp-Name, so strict servers validating routing headers per
SEP-2243 §Server Behavior could reject before reaching method
dispatch — making the asserted error code unreachable.
- routingHeaders(): emit Mcp-Name on tasks/result so the
removed-method check fails specifically on -32601, not
-32001 HeaderMismatch.
- validTasksParams() helper: baseline params per
tasks-namespace method; negative checks override one field
to express isolation intent declaratively.
- ISOLATION_HINT on wrong-code failures points at the most
likely cause (routing headers / _meta / params shape
short-circuit before dispatch).
Forward-safe under either reading of the open SEP-2663
client-MUST → server-MUST-reject elevation question: the added
Mcp-Name matches the body, so SEP-2243's universal mismatch
rule can't fire either way.
Verified: 18/18 tasks scenarios pass.
* tasks: rename scenario check IDs to sep-2663-* slugs
Aligns 13 tasks scenario check IDs with the SEP-2484 traceability
convention used by sep-2322.yaml and others: the slug emitted at
runtime matches the check: row in the SEP's requirement-traceability
yaml, so plan.modelcontextprotocol.io can link the SEP requirement
to its implementing scenario.
Rename map:
tasks-tools-call-without-extension-sync
→ sep-2663-server-rejects-undeclared-client
tasks-methods-gated-without-extension
→ sep-2663-tasks-methods-non-declaring-32003
tasks-server-task-creation
→ sep-2663-result-type-task-on-create
tasks-get-during-working
→ sep-2663-tasks-get-status-working
tasks-get-terminal-inlined-result
→ sep-2663-tasks-get-status-completed
tasks-tool-error-completed-iserror
→ sep-2663-tool-error-uses-completed-status
tasks-protocol-error-failed-shape
→ sep-2663-tasks-get-status-failed
tasks-cancel-empty-ack
→ sep-2663-cancel-ack-empty-result
tasks-removed-tasks-result
→ sep-2663-tasks-result-removed-method-not-found
tasks-legacy-task-param-ignored
→ sep-2663-legacy-task-param-ignored
tasks-strong-consistency-immediate-get
→ sep-2663-durable-create-strong-consistency
tasks-get-unknown-task-id-rejected
→ sep-2663-tasks-get-invalid-task-id-32602
tasks-mrtr-input-requests-on-tasks-get
→ sep-2663-tasks-get-status-input-required
Wire behavior unchanged — only the reported slugs change. Verified
18/18 tasks scenarios still pass.
Untouched (intentionally): scenario-flow gates like
tasks-extension-advertised, tasks-sync-tool-call, tasks-wire-field-renames,
tasks-immediate-result-shortcut etc., which test end-to-end flows
rather than a specific RFC-2119 sentence and surface in the
traceability manifest as untracked (same pattern as sep-2322.yaml).
* seps: add sep-2663.yaml requirement-traceability declaration
Declares the 37 conformance checks (and 12 excluded requirements)
the SEP-2663 Tasks Extension surface should produce, mapping each
normative MUST/SHOULD sentence on the SEP page to either a check ID
emitted by an existing scenario or a documented reason for exclusion.
Exclusions are all client-internal, host-internal, or architectural
requirements that aren't observable at the protocol level — confirmed
during the new-sep skill's exclusion round.
The 13 scenario checks renamed in the previous commit now match the
check: slugs declared here; the remaining ~12 yaml rows describe
requirements without a scenario emitter yet — those are gaps to fill
in follow-up work (notifications via subscriptions/listen, key
uniqueness across MRTR rounds, legacy-capability migration probes).
spec_url points at the SEP page itself (status: Final) — SEP-2663's
normative content lives on /seps/2663-tasks-extension and is mirrored
in modelcontextprotocol/experimental-ext-tasks for source-control
purposes. Every quoted text: field was verified copy-paste findable
against the rendered SEP page after stripping markdown formatting.
* tasks: enforce SEP-2663 routing-header validation on tasks surface
Two related fixes:
1. tasks/headers.ts description previously said servers "MUST NOT
require" the Mcp-Method / Mcp-Name routing headers on the tasks
surface and "MUST NOT change dispatch behavior based on them —
including when the headers disagree with the body." Both claims
are spec-incorrect. SEP-2663 author confirmed in the conformance
review thread that the client-MUST in §"Streamable HTTP: Routing
Headers" elevates Mcp-Name to "required standard header" status
for tasks/get / tasks/update / tasks/cancel, so SEP-2243's
§"Server Behavior" mismatch + missing-header rejection applies.
Description rewritten to match the spec: routing headers are
REQUIRED, server MUST reject missing or mismatched with -32001.
2. Add a negative-path check covering the most common failure mode
on the tasks surface: tasks/get with mismatched Mcp-Name MUST
return -32001 HeaderMismatch. Probes tasks/get specifically;
tasks/update / tasks/cancel follow by extension. Missing-header
case is deferred (requires raw-session enhancement to suppress
a header entirely; current extraHeaders mechanism only overrides).
3. New yaml row pinning the rule to the SEP-2243 §"Server Behavior"
sentence via row-level url override (the rule itself is SEP-2243;
the scope of application on the tasks surface is what's
distinctively SEP-2663).
Known impact: surfaces a real gap in SDKs that derived their
routing-header validation from the SEP-2243 base table only (which
lists tasks/* as NOT requiring Mcp-Name). Those SDKs will see this
check go FAILURE until they extend their validation to cover
tasks-namespace methods per SEP-2663 §540.
CI against everything-server is unaffected (skips the tasks suite
entirely; everything-server doesn't implement the extension).
* mrtr: activate mrtr-08 composition check + fold into sep-2663.yaml
Folds PR 7 (panyam/mcpconformance) into PR 262:
- Cherry-picks the mrtr-tasks-composition activation from
feat/sep-2663-mrtr-08-composition (ad4f267 upstream): real end-to-end
check driving the SEP-2663 commit 451f5e1 promotion flow
(InputRequiredResult rounds gather user_name via elicitation/create,
then CreateTaskResult on the final round escalates to async; the
final tasks/get result reflects the answer gathered during MRTR).
- Addresses LucaButBoring's PR 7 nit: replaces the inline SEP-2663
reference literal with the shared SEP_2663_REF constant (now also
pointing at the rendered SEP page).
- Renames the scenario's emitted check ID from `mrtr-tasks-composition`
to `sep-2663-mrtr-synchronous-before-task-creation` to match the
corresponding row in sep-2663.yaml — closes the gap between
declared requirement and implementing check.
- Updates mrtr/README.md: refreshes the Specs-covered table and the
per-check description table for the new slug, and drops the "Open
issues" section (both blockers are resolved — discriminator value
aligned upstream; mcpkit middleware refactored per panyam/mcpkit#347).
- Updates the sep-2663.yaml flow-gate comment block to note that
the ephemeral mrtr-* flow gates also fall into the untracked
category for the same reason as the tasks-* flow gates.
One fewer skipped check on PR 262. Verified 123/123 server-suite
tests pass against mcpkit's tasks-v2 + mrtr fixtures (post
panyam/mcpkit#499 + #501).
Closespanyam/mcpconformance#7.
* tasks/mrtr: drop legacy wire when targeting DRAFT-2026-v1
SEP-2575 (Accepted) removes the initialize handshake on DRAFT-2026-v1
and replaces it with self-contained per-request _meta. Driving the
legacy initialize+Mcp-Session-Id wire against a draft fixture is
therefore a logical contradiction: the harness asserts
`protocolVersion: "DRAFT-2026-v1"` while doing the handshake the
version removed. Servers that strictly enforce SEP-2575 correctly
reject these requests with -32602 (see Randgalt's report on
conformance PR 262, 2026-05-31); lenient servers silently accept
them and mask the spec gap.
Both effects are wrong for the harness — we should only emit
spec-permitted traffic.
Adds `effectiveWireModes(protocolVersion)` in _shared/wire-mode.ts.
It wraps `parseWireModes()` and filters `legacy` out when the target
version is in POST_SEP_2575_VERSIONS (currently just DRAFT-2026-v1;
widen when a future dated release picks SEP-2575 up). If the user
explicitly pinned MCP_WIRE_MODES=legacy against a post-SEP-2575
version, the helper warns and falls back to ['stateless'] so the
suite still runs.
Both draft-only suites (tasks, mrtr) switch to
`effectiveWireModes(DRAFT_PROTOCOL_VERSION)`. The MCP_WIRE_MODES
override remains honored within the spec-permitted set.
Verified end-to-end with a strict mcpkit fixture (panyam/mcpkit
fix/sep-2575-enforce-meta-on-draft):
- pre-fix harness → 7/18 fail with the spec-required error
- post-fix harness → 9/9 pass (stateless wire only)
* tasks/mrtr: derive wire from spec version, drop RunContext.wire
`RunContext.wire` was a parallel knob to `specVersion` that the tasks
and MRTR harnesses used to loop scenarios across both the legacy
session wire and the SEP-2575 stateless wire. After SEP-2575 collapsed
the legacy wire on DRAFT-2026-v1 (a6f7c27), the matrix shrank to a
single column on draft — and an explicit `wire` field was both
redundant and unsafe: it let callers go out of sync with the
`specVersion` they declared on the wire.
The CLI's `runServerConformanceTest` never set the field, so every
`npm start -- server --scenario tasks-* --url ...` invocation silently
emitted the legacy `initialize` handshake against a draft server,
producing requests with no `_meta.io.modelcontextprotocol/*` envelope.
Strict SEP-2575 servers correctly rejected the traffic; lenient ones
accepted it and masked the gap.
Changes:
- Drop `wire?` from `RunContext`.
- Add `isStateless(ctx)` in `_shared/wire-mode.ts`; scenarios derive
`stateless` from `POST_SEP_2575_VERSIONS.has(ctx.specVersion)`.
- Extend the CLI's spec-version inference to default extension
scenarios to draft (today every extension scenario in the repo lives
there). Without this, the inference stayed on `LATEST_SPEC_VERSION`
for tasks scenarios and kept emitting legacy traffic.
- Flatten the now-1-element wire loop in the tasks + mrtr harnesses;
remove the parseWireModes/effectiveWireModes/MCP_WIRE_MODES infra
that drove it (no remaining consumers).
Adds a vitest regression in `runner/server.test.ts` that spins up an
in-process recording HTTP server, drives `runServerConformanceTest`
at `tasks-lifecycle`, and asserts the first outgoing call is
`server/discover` carrying `_meta.io.modelcontextprotocol/*` — the
SEP-2575 fingerprint — so the CLI wire selection can't silently
regress.
* scenarios + connection: address conformance#262 layout review
Folds the parallel mini-framework the tasks/MRTR suites had grown into
the existing harness shape pcarleton called out in conformance PR 262.
No new spec coverage; existing scenarios still pass 241/241 unit tests
and the regression added for the wire-derivation fix still pins the CLI
fingerprint.
Layout
scenarios/server/_shared/ is gone. The helper bundle moves to
scenarios/server/tasks-mrtr-helpers.ts (matching the existing
input-required-result-helpers.ts precedent); raw-session.ts and its
test fold into connection/stateless.ts.
Per-suite runners + READMEs (tasks/all-scenarios.test.ts,
mrtr/all-scenarios.test.ts, _shared/test-runner.ts, both READMEs) are
removed. The scenarios are already registered in
scenarios/index.ts pendingClientScenariosList, so the CLI runner
(`npm start -- server --scenario tasks-lifecycle --url ...` or
`--suite pending --url ...`) is the supported entry point. Per
AGENTS.md: don't reimplement the runner.
Connection extension
Connection grows the three things tasks/MRTR scenarios actually need
beyond the existing surface:
- `request(method, params, extraHeaders?)` for the SEP-2243
header-mismatch tests.
- `discover(): Promise<Record<string, unknown>>` for
capability-negotiation scenarios. Resolved eagerly from the SDK
accessors on the stateful wire; issued lazily as `server/discover`
on the stateless wire and memoized.
- `ConnectOptions { capabilities?, clientInfo? }` on
RunContext.connect() so scenarios can negotiate extensions
(`extensions: { 'io.modelcontextprotocol/tasks': {} }`) at
bootstrap on either wire.
Crucially the stateless wire does NOT eagerly emit `server/discover`
when capabilities are declared. SEP-2575 says per-request
`_meta.io.modelcontextprotocol/clientCapabilities` is authoritative;
a server that gates extensions on a prior `server/discover` is
non-conformant, and the conformance suite's job is to surface that,
not paper over it.
Scenario migration
Every tasks/MRTR scenario switches from `initRawSession(...)` to
`await ctx.connect({ capabilities })`, and the capability scenario
switches from `session.initializeResult.capabilities` to
`await withExt.discover()`. The wire choice now flows entirely from
`specVersion` — `isStateless(ctx)` (moved to connection/select.ts
next to `connectFor`) is the single source of truth.
Description sweep
Each scenario's `description` string now spells out the required
server fixtures (tool names + behavior) directly. Previously that
content lived only in the file-header comment, so an implementer
reading the runner's emitted description was missing it.
Reviewer notes
- 23 files changed, +455 / -291 (net + because the Connection
abstraction now carries surface that used to live in raw-session;
the raw-session deletion was 692 lines on its own).
- Unit tests: 241 pass (the 12 deleted tests were redundant with
connection-level coverage; the genuinely-unique SSE multi-line
`data:` test pinned behavior the connection parser doesn't have).
- Reference fixture runs: tasks scenarios surface a fixture-side
spec gap (per-request `_meta` not honored without a prior
`server/discover`). That's the suite working; the gap will be
filed upstream against the fixture, not papered over here.
* tasks: dedupe MRTR ephemeral-flow against #188, fold composition into tasks/
Of the 8 checks in mrtr/ephemeral-flow.ts, 7 substantially overlapped
the SEP-2322 InputRequiredResult* scenarios that #188 already shipped
(basic-elicitation, sampling, list-roots, request-state, multiple-input-
requests, multi-round, wrong-input-key). Only sep-2663-mrtr-synchronous-
before-task-creation — the SEP-2663 commit 451f5e1 composition path
where the MRTR loop's final round returns a CreateTaskResult — is
genuinely new versus #188's coverage, and it's SEP-2663-specific, not
SEP-2322-specific.
Moves that one check to its own tasks scenario in tasks/composition.ts
(extensionId = io.modelcontextprotocol/tasks, registered alongside the
other tasks-* scenarios in scenarios/index.ts) and deletes the entire
mrtr/ directory.
Reuses the existing #188 helpers (isInputRequiredResult, mockElicitResponse
from input-required-result-helpers.ts) rather than reintroducing the
parallel mrtr/helpers.ts ones.
* address review on conformance#262 follow-up
- connectStateless.discover() now reuses the request() pipeline (send +
drainEvents + unwrap) instead of inlining a duplicate copy. The local
memoization stays via discoverPromise ??=.
- runner/server.test.ts mock server no longer silently swallows
unparseable bodies. They land in a parseFailures sidecar that the
test asserts is empty — a non-JSON body to this mock is exactly the
kind of regression the test exists to catch.
- Drop the tautological connectFor identity tests. They asserted
`typeof === 'function'` and `not.toBe(connectStateful)`, both of which
hold trivially once connectFor wraps in a closure. Wire-shape behavior
is already covered by the connectStateless mock-fetch suite and by
runner/server.test.ts.
* tasks/mrtr: address PR 262 audit before review
Pre-emptive cleanup against the upstream patterns established by recent
merges (#318 Connection abstraction, #319 draft-spec consistency, #303
SEP-2243 check IDs, #321 MockServer surface). No behavior change for
scenarios — every check is still emitted; only the shape of how they're
emitted moves toward upstream's house style.
Surface
- Revert withRequestMeta / sendStatelessRequest signatures to match
upstream byte-for-byte. The need that pushed me to widen those
(per-request _meta.clientCapabilities for extension declaration on
the SEP-2575 stateless wire) is now handled inside connectStateless:
ConnectOptions.capabilities / clientInfo are folded into params._meta
via the trailing _meta spread that withRequestMeta already supports.
This keeps the new "extension declaration" concept on the Connection
layer only — the lower-level helpers stay shared with every other
scenario in the suite.
- Document that Connection.request's extraHeaders throws on the
stateful wire instead of "ignored with a warning" — the SDK transport
manages headers internally, so silent drop in a conformance harness
would mask test correctness.
Layout
- src/scenarios/server/tasks-mrtr-helpers.ts moves under tasks/ as
tasks/mrtr-helpers.ts. All 10 importers live in tasks/; the file's
prior placement next to input-required-result-helpers.ts no longer
applies once those scenarios moved into tasks/.
- Drop the unused AnyResult export + zod import. The Connection wrapper
doesn't take a zod schema, so the helper was dead on arrival.
- SEP-2322 spec reference now re-exports MRTR_SPEC_REFERENCES[0] from
input-required-result-helpers so the codebase holds one URL per SEP
instead of two for SEP-2322.
Naming
- All 10 tasks/*.ts switch to `readonly source = { ... } as const` to
match the convention used by caching.ts, resources.ts,
http-standard-headers.ts. Drops the ScenarioSource import where now
unused.
- Scenario-scope each scenario's connect-failure check ID
(tasks-lifecycle-bootstrap, tasks-headers-bootstrap, …) instead of
sharing tasks-session-bootstrap. Matches the upstream caching.ts
pattern (sep-2549-caching-connection) where each scenario owns its
own bootstrap-failure ID.
Traceability
- sep-2663.yaml gains the positive-path counterpart to the existing
rejection requirement under Streamable HTTP: Routing Headers, so
every sep-2663-* check ID emitted by tasks/*.ts is declared in the
manifest.
Verification
- Conformance vitest: 270/270.
- Build: clean.
- mcpkit testconf-tasks-v2 (47 fork scenarios + 1 sentinel): pass.
- mcpkit testconf-mrtr (3 fork scenarios + 1 sentinel): pass.
* tasks/required-task-error: anchor -32003 checks to sep-2663.yaml rows
The two checks in required-task-error.ts each verified a distinct
normative requirement that sep-2663.yaml already declared, but the
check IDs (tasks-required-task-error-code, tasks-required-task-error-data-shape)
used the tasks-* flow-gate prefix and went untracked by the traceability
manifest.
Promote both check IDs to sep-2663-*:
- tasks-required-task-error-code → sep-2663-server-returns-32003-when-required
- tasks-required-task-error-data-shape → sep-2663-server-returns-32003-data-shape
Split the existing YAML entry to mirror the split. The spec sentence
glues two observable requirements with a trailing colon (the JSON-RPC
code, and the data-shape payload that names the missing extension);
each gets its own row so each is verifiable independently.
Remove the now-stale tasks-required-task-error-code reference from the
sep-2663.yaml flow-gate comment block — that check is no longer a
flow-gate.
No behavior change. 270/270 vitest still pass.
* runner: forward ConnectOptions from RunContext.connect
server.ts built the RunContext with `connect: () => connectFor(...)(serverUrl)`,
dropping the opts argument scenarios pass to ctx.connect(). TypeScript
doesn't catch this because `() => T` is assignable to `(opts?) => T`.
Result: scenarios declaring capabilities sent DEFAULT_CLIENT_CAPABILITIES
on the wire in CLI runs while testContext() forwarded opts correctly,
so unit tests passed but `npm start -- server --scenario tasks-*` against
a strict-draft server failed with missing-capability errors.
Tightens the regression test to assert scenario-passed capabilities reach
_meta.clientCapabilities.extensions, not just that defaults are present.
* tasks: align scenario descriptions and cancel-ack check with SEP-2663
capability.ts: description said non-declaring tasks/* requests get
-32601 MethodNotFound; the assertion checks MissingRequiredClientCapability
per SEP-2663 §Error Handling. Reworded to the named error type and
per-request _meta/server-discover framing (SEP-2575).
lifecycle.ts: description claimed cancel-on-terminal MUST return
-32602; Check 8 in the same file asserts an idempotent ack and the
SEP has no -32602 carve-out for terminal tasks. Removed the bullet.
lifecycle.ts Check 7: SEP-2663 §Task Cancellation says transition to
`cancelled` is not guaranteed; stop failing the ack-shape check on
the settled status and record it in details instead.
* connection: fix stale docstring and drop redundant eta-expansion
stateless.ts docstring referenced `connection.initializeResult`; the
implementation exposes `discover()`.
select.ts wrapped connectStateful in an identity arrow; pass it
directly now that signatures match.
---------
Co-authored-by: Paul Carleton <paulc@anthropic.com>
The error-code allocation policy in modelcontextprotocol#2907 renumbers
the draft-only codes: HeaderMismatch -32001 -> -32020,
MissingRequiredClientCapability -32003 -> -32021,
UnsupportedProtocolVersion -32004 -> -32022. This updates every
reference in the suite (mocks, scenarios, fixtures, tests, traceability
yaml) and re-syncs the vendored draft schema to that commit.
src/seps/traceability.json is generated and intentionally left for the
scheduled refresh workflow.
* chore: refresh SEP traceability manifest after #303 and #304
Regenerated from a full client+server suite run against
typescript-sdk@258f1a04a5d3 on current main:
- sep-2468: the two new client checks are emitted (tested) and the two
authorization-server rows move to excluded (#304)
- sep-2243: 12 rows flip to tested now that the custom-header check IDs
match their requirements (#303)
* chore: regenerate the manifest on top of SEP-2106 (#295)
Re-ran both suites after rebasing onto main so the manifest also picks
up the new sep-2106 block (1 tested, 4 excluded, 3 untracked check IDs
emitted by the server scenario with no yaml row yet).
* chore: add SEP-2106 requirement-traceability YAML (tools JSON Schema 2020-12)
* Drop structured-content text-fallback row from SEP-2106
That SHOULD predates SEP-2106 (pre-existing context line in tools.mdx); 2106
only relaxes structuredContent to allow non-object values (permissive). Point
spec_url at the SEP doc security section where the remaining obligations live.
* feat: SEP-2106 JSON Schema 2020-12 vocabulary preservation checks
Extend the json-schema-2020-12 server scenario (previously SEP-1613 only) with
three checks that verify the broader JSON Schema 2020-12 vocabulary SEP-2106
permits in a tool inputSchema survives tools/list rather than being stripped:
composition (allOf/anyOf), conditional (if/then/else), and reference ($anchor)
keywords.
- everything-server: add these keywords to json_schema_2020_12_tool's raw schema
- add a negative-test server (sep-2106-stripped-schema.ts) plus a negative test
asserting the checks emit FAILURE when the vocabulary is stripped
* feat: SEP-2106 client scenario for network $ref dereferencing
Add json-schema-ref-no-deref, a client-suite scenario that advertises a tool
whose inputSchema contains a $ref pointing at a canary URL hosted by the
scenario. The sep-2106-no-network-ref-deref check fails if the client fetches
the canary while processing the schema, covering the SEP-2106 requirement that
implementations MUST NOT automatically dereference network-URI $ref values.
- register the scenario in the client scenarios list
- everything-client: handle the scenario with the basic connect+listTools flow
- tests: compliant client passes, a deliberately dereferencing client is
caught, and a client that never lists tools cannot pass
* Address review findings in SEP-2106 scenarios
- json-schema-ref-deref: make getChecks() idempotent (fresh array per call,
matching the http-standard-headers convention) and guard the async /mcp
handler like the example servers do
- json-schema-2020-12: include the SEP-2106 keywords ($anchor, allOf/anyOf,
if/then/else) in the description's example inputSchema so a server that
copies it verbatim passes the new checks
- sep-2106-stripped-schema: default to port 3007 to match negative.test.ts and
avoid sharing a default with sep-2549-no-caching-hints
* Soft-gate SEP-2106 keyword checks on the negotiated protocol version
The json-schema-2020-12 scenario is introducedIn 2025-11-25 (SEP-1613) but the
three SEP-2106 keyword-preservation checks test draft-era behavior. Rather than
failing servers that only negotiated 2025-11-25, gate the failure on the wire
protocolVersion from the initialize handshake: preserved keywords are SUCCESS at
any version, stripped keywords are FAILURE only when the server negotiated the
draft version and SKIPPED otherwise.
The published SDK does not advertise the draft version yet, so the negative test
asserts SKIPPED for the stripped-schema server; the FAILURE branch is covered by
unit tests on sep2106KeywordCheckStatus.
Add two client scenarios for the SEP-2468 requirements that had no
conformance coverage:
- auth/metadata-issuer-mismatch: the AS metadata document declares an
issuer that differs from the issuer identifier used to construct the
well-known URL; the client must not use the metadata
(sep-2468-client-validate-metadata-issuer).
- auth/iss-normalized: the authorization redirect carries an iss value
that only matches the recorded issuer after URL normalization; the
client must compare with simple string comparison and reject
(sep-2468-client-no-normalization).
The two authorization-server-side rows (AS SHOULD include iss, AS MUST
advertise authorization_response_iss_parameter_supported) are excluded:
observing iss in an authorization response requires driving an
Authorization Code Grant against the AS under test, which the
authorization-server suite does not implement yet (#208).
'All scopes required for the current operation' has no harness-observable
ground truth: the WWW-Authenticate challenge is the only place the server
declares what an operation requires, so there is nothing independent to
compare it against. Detecting the negative (incremental challenging) would
need server-side auth scenarios that do not exist yet and would exercise the
example app's scope configuration rather than SDK behavior, and the spec
explicitly permits dynamic per-request scope determination, so a second
challenge is not conclusively non-conformant.
The client-side half of SEP-2350 (scope union on re-authorization) remains
tested by auth/scope-step-up.
* chore: refresh SEP traceability manifest (typescript-sdk@main)
Regenerated from a client+server suite run against typescript-sdk@5fc42e9be115
following the recipe in .github/workflows/traceability.yml.
New entries since the last refresh (typescript-sdk@22595b96):
- SEP-2322 (MRTR, #188): 17 tested, 0 untested, 16 excluded, 3 untracked
- SEP-2549 (TTL for list results, #275): 7 tested, 0 untested, 13 excluded
- SEP-2260: 12 excluded rows, no checks
- SEP-2207: yaml rows added since the last refresh now appear
(1 tested, 1 untested: sep-2207-server-no-offline-access)
No previously-tested requirement regressed.
* Exclude sep-2207 server offline_access guidance until RS auth scenarios exist
sep-2207-server-no-offline-access was declared in the yaml but no scenario
emits it, so it surfaced as the only untested requirement in the refreshed
manifest. The check needs to probe the SDK server's Protected Resource
Metadata scopes_supported and WWW-Authenticate challenge scope, and the
server suite does not yet exercise the SDK server as an OAuth protected
resource at all.
Mark the requirement excluded with a pointer to #116 (server-side
authorization baseline) rather than leaving it as a permanently-untested
row; revisit when server-side authorization scenarios land.
* chore: additional server conformance tests for SEP-2575
* fix(sep-2575): use the spec's notifications filter object in subscriptions/listen
The scenario sent params.subscriptions: [{type: 'tools/list-changed'}], a
shape that does not exist in the SEP. SubscriptionsListenRequestParams
requires params.notifications: {toolsListChanged?, promptsListChanged?, ...}
(see schema.ts SubscriptionFilter and the canonical
SubscriptionsListenRequest example). A compliant server would either reject
the old request as invalid params or treat it as subscribing to nothing,
failing the ack/list-changed checks.
Also makes the everything-server's acknowledgment echo the agreed
notifications filter, as the SubscriptionsAcknowledgedNotification schema
requires.
* fix(sep-2575): match the spec's notifications/*/list_changed method names
The list-changed checks and the everything-server used
notifications/tools/list-changed (hyphen); the spec's method is
notifications/tools/list_changed (underscore) per ToolListChangedNotification
/ PromptListChangedNotification in schema.ts. With the hyphen form the
notification-filter check cannot catch a real leak, and the list-changed
checks fail a server that emits the correct name.
Also drops the f.params.toolsListChanged === true fallback match: that field
is the subscriptions/listen request filter, not a notification parameter, so
it never appears on a compliant server's notification.
* fix(sep-2575): keep collected frames when the listen stream read times out
listenToStream aborts the fetch after timeoutMs. If the abort fired while
blocked in reader.read(), the rejection propagated to the outer catch and the
helper returned [] — discarding every frame already received. A compliant
server holds a subscriptions/listen stream open indefinitely, so the timeout
is the *normal* way these reads end; previously every subscription check
would report a false 'no frames received' FAILURE against such a server.
The current everything-server only avoided this by res.end()ing immediately
after the ack.
* fix(sep-2575): open the listen stream before triggering list mutations
The notification-filter and list-changed checks fired the mutation trigger
*before* opening the subscriptions/listen stream. A compliant server only
delivers list-changed notifications to streams that are open at the time of
the change, so the SHOULD checks would fail any server that does not replay
past changes to new subscribers, and the filter check could never observe a
real leak.
listenToStream now takes an onFirstFrame hook; the three trigger-based
checks open the stream, wait for the acknowledgment, then fire the mutation
on a separate connection and keep reading.
The everything-server now keeps subscriptions/listen streams open, registers
them with their filters, and fans the list_changed notification out to open
streams when a trigger tool runs - instead of unconditionally emitting a
list-changed frame on every stream open (which itself violated the
notification-filter requirement in spirit: it announced a change when
nothing had changed).
* fix(sep-2575): check the subscription id on every listen-stream notification
server-tags-subscription-id only inspected the acknowledgment frame, so a
server that tags the ack but omits io.modelcontextprotocol/subscriptionId
from subsequent notifications passed. The check now triggers a tool-list
change once the stream is acknowledged and asserts every notification frame
on the stream carries the id in params._meta.
Also drops the f.body.method / f.params.method fallbacks when resolving a
frame's method - those shapes don't exist in JSON-RPC and only masked
malformed frames.
* fix(sep-2575): report WARNING, not FAILURE, for the SHOULD-level list-changed checks
The spec text behind server-sends-{tools,prompts}-list-changed-on-subscription
is a SHOULD ('servers that declared the listChanged capability SHOULD send a
notification...'). Severity follows the spec keyword: MUST -> FAILURE,
SHOULD -> WARNING. runCheck now accepts a warning flag and the two
list-changed checks use it on their failure paths.
* fix(sep-2575): skip stream checks when the diagnostic tool is not exposed
http-server-no-independent-requests-on-stream and
server-no-log-without-loglevel reported SUCCESS when the server rejected the
tools/call outright (e.g. test_streaming_elicitation / test_logging_tool do
not exist): a single error frame contains no independent request and no log
notification, so the check passed without exercising anything. The sibling
list-changed checks already SKIP when their trigger tool is missing; these
now do the same.
* chore(sep-2575): exclude the four unobservable server requirements from traceability
Marks the section-C rows of #296 as excluded rather than untested:
no-prior-context, no-connection-reuse-required, disconnect-is-cancel and
stops-on-cancel describe internal server state that a black-box harness
cannot observe on the wire. With these excluded and the part-B checks landed,
every testable server-side SEP-2575 requirement is covered; the remaining
untested rows are all client-side and blocked on a SEP-2575-aware reference
client.
* fix(sep-2575): emit the request-metadata checks even when the client never connects
The reference SDK conformance client has no handler registered for the
request-metadata scenario, so it exits without sending a request, the
scenario emits zero checks, the suite reports "0 passed, 0 failed", and the
traceability manifest records all seven client-side requirements as
untested.
Declare the scenario check IDs up front and backfill any that were never
emitted as FAILURE from getChecks(): the emitted ID set is now the same for
every client, and a client that never connects fails loudly. A check that
is legitimately not applicable opts out by being emitted as SKIPPED
explicitly (version-header-matches-meta now does this when either version
field is absent). The positive-path test pins the declared list against the
reference client in both directions.
Also refresh the traceability manifest from a run against
typescript-sdk@22595b96: the seven client rows flip to tested and the
manifest catches up with the server-side scenarios from #297.
---------
Co-authored-by: Yuan Teoh <yuanteoh@google.com>
* chore: additional server conformance tests for SEP-2575
* fix(sep-2575): use the spec's notifications filter object in subscriptions/listen
The scenario sent params.subscriptions: [{type: 'tools/list-changed'}], a
shape that does not exist in the SEP. SubscriptionsListenRequestParams
requires params.notifications: {toolsListChanged?, promptsListChanged?, ...}
(see schema.ts SubscriptionFilter and the canonical
SubscriptionsListenRequest example). A compliant server would either reject
the old request as invalid params or treat it as subscribing to nothing,
failing the ack/list-changed checks.
Also makes the everything-server's acknowledgment echo the agreed
notifications filter, as the SubscriptionsAcknowledgedNotification schema
requires.
* fix(sep-2575): match the spec's notifications/*/list_changed method names
The list-changed checks and the everything-server used
notifications/tools/list-changed (hyphen); the spec's method is
notifications/tools/list_changed (underscore) per ToolListChangedNotification
/ PromptListChangedNotification in schema.ts. With the hyphen form the
notification-filter check cannot catch a real leak, and the list-changed
checks fail a server that emits the correct name.
Also drops the f.params.toolsListChanged === true fallback match: that field
is the subscriptions/listen request filter, not a notification parameter, so
it never appears on a compliant server's notification.
* fix(sep-2575): keep collected frames when the listen stream read times out
listenToStream aborts the fetch after timeoutMs. If the abort fired while
blocked in reader.read(), the rejection propagated to the outer catch and the
helper returned [] — discarding every frame already received. A compliant
server holds a subscriptions/listen stream open indefinitely, so the timeout
is the *normal* way these reads end; previously every subscription check
would report a false 'no frames received' FAILURE against such a server.
The current everything-server only avoided this by res.end()ing immediately
after the ack.
* fix(sep-2575): open the listen stream before triggering list mutations
The notification-filter and list-changed checks fired the mutation trigger
*before* opening the subscriptions/listen stream. A compliant server only
delivers list-changed notifications to streams that are open at the time of
the change, so the SHOULD checks would fail any server that does not replay
past changes to new subscribers, and the filter check could never observe a
real leak.
listenToStream now takes an onFirstFrame hook; the three trigger-based
checks open the stream, wait for the acknowledgment, then fire the mutation
on a separate connection and keep reading.
The everything-server now keeps subscriptions/listen streams open, registers
them with their filters, and fans the list_changed notification out to open
streams when a trigger tool runs - instead of unconditionally emitting a
list-changed frame on every stream open (which itself violated the
notification-filter requirement in spirit: it announced a change when
nothing had changed).
* fix(sep-2575): check the subscription id on every listen-stream notification
server-tags-subscription-id only inspected the acknowledgment frame, so a
server that tags the ack but omits io.modelcontextprotocol/subscriptionId
from subsequent notifications passed. The check now triggers a tool-list
change once the stream is acknowledged and asserts every notification frame
on the stream carries the id in params._meta.
Also drops the f.body.method / f.params.method fallbacks when resolving a
frame's method - those shapes don't exist in JSON-RPC and only masked
malformed frames.
* fix(sep-2575): report WARNING, not FAILURE, for the SHOULD-level list-changed checks
The spec text behind server-sends-{tools,prompts}-list-changed-on-subscription
is a SHOULD ('servers that declared the listChanged capability SHOULD send a
notification...'). Severity follows the spec keyword: MUST -> FAILURE,
SHOULD -> WARNING. runCheck now accepts a warning flag and the two
list-changed checks use it on their failure paths.
* fix(sep-2575): skip stream checks when the diagnostic tool is not exposed
http-server-no-independent-requests-on-stream and
server-no-log-without-loglevel reported SUCCESS when the server rejected the
tools/call outright (e.g. test_streaming_elicitation / test_logging_tool do
not exist): a single error frame contains no independent request and no log
notification, so the check passed without exercising anything. The sibling
list-changed checks already SKIP when their trigger tool is missing; these
now do the same.
* chore(sep-2575): exclude the four unobservable server requirements from traceability
Marks the section-C rows of #296 as excluded rather than untested:
no-prior-context, no-connection-reuse-required, disconnect-is-cancel and
stops-on-cancel describe internal server state that a black-box harness
cannot observe on the wire. With these excluded and the part-B checks landed,
every testable server-side SEP-2575 requirement is covered; the remaining
untested rows are all client-side and blocked on a SEP-2575-aware reference
client.
---------
Co-authored-by: Paul Carleton <paulc@anthropic.com>
* Add SEP-2350 scope-accumulation check to auth/scope-step-up
SEP-2350 clarifies that on step-up re-authorization, clients SHOULD
compute the union of previously-granted and newly-challenged scopes so
they don't lose permissions for other operations.
- Traceability yaml: 1 client check (two spec sentences merged), 1 server
check tracked for a future server-side scenario, 1 excluded reword
- ScopeStepUpAuthScenario now challenges with only the missing scope so
union accumulation is observable on the second authorization request
- New check sep-2350-scope-union-on-reauth (WARNING when the previously
granted scope is dropped)
- everything-client withOAuthRetry now accumulates prior token scope into
the re-auth scope (passing example)
- New auth-test-echo-scope negative client + vitest case
* feat: add client conformance tests for SEP-2575 (#270)
* feat: merge latest request-metadata scenario stacked branch
* test: implement optional capability conditional checks and simulated negotiation retry
* fix(sep-2243): collapse check IDs to one-per-requirement (#287)
The sep-2243 scenario check IDs drifted from the requirement-traceability
yaml in #259 — the code emitted one ID per test case while the yaml
declares one per normative requirement, so the IDs no longer matched.
Rename the emitted IDs so a check ID maps to a single MUST/SHOULD and is
emitted once per case (the per-case detail moves to name/description),
matching the repo's existing 'same id, vary status/message' convention:
- mcp-method-header-* / mcp-name-header-* -> client-includes-standard-headers
- reject-invalid-tool-* / keep-valid-tool -> client-reject-invalid-tool
- server-accepts-{lower,upper}case-name -> header-name-case-insensitive
- server reject status checks (mismatch/missing/case x method/name)
-> server-reject-invalid-headers
- their error-code variants -> server-reject-error-code
Merge the yaml's server-reject-mismatch + server-reject-status into one
server-reject-invalid-headers MUST (HTTP 400 on a header-validation
failure); keep server-reject-error-code as the SHOULD. Base64/custom-header
rejection checks keep their own param-validation IDs (out of scope here).
Gates and the whitespace-acceptance check keep their scenario-matching
names. No behavior change — only check IDs, descriptions, and the yaml.
* feat: `sdk` subcommand to run local conformance against any SDK ref (#277)
* feat: add sdk subcommand to run conformance against any SDK ref (#250)
* Revise sdk runner: explicit --mode, KNOWN_SDKS-only config, v1/v2 entries
Addresses review feedback on the sdk subcommand:
- Require --mode (client|server) and remove "both". Each invocation now
tests exactly one side with its own exit code; the old default ran
client then server but combined exit codes with ||=, which skipped the
server side entirely whenever the client run failed.
- Resolve build/run config from KNOWN_SDKS + CLI flags only; drop the
conformance.config.yaml loader (no SDK ships one yet). The Zod schema
stays as the type for the built-in entries.
- Split the typescript entry by major version: typescript-sdk (v2/main,
pnpm install + build:all, expected-failures.yaml) and typescript-sdk-v1
(v1.x, npm ci + build, conformance-baseline.yml). An entry may set
`repo` (real clone target for an alias) and `defaultRef` (branch used
when no @ref is given). parseSdkSpec now leaves ref undefined when
omitted so defaultRef can apply; a trailing @ is treated as no ref.
- Key the clone cache by ref (<repo>/<ref>) so different refs of the same
repo no longer share one checkout.
- Bound the server readiness probe with a per-request AbortSignal timeout
so a server that accepts the socket but never responds can't hang past
the overall deadline.
* fix(sdk-runner): resolve -o to absolute path; add --expected-failures override; replaceAll for safeName
* feat: add SEP conformance traceability manifest (#288)
squashed; see PR #288 body
* feat: add server conformance tests for SEP-2575 (#271)
* feat: add server conformance tests for SEP-2575
* refactor and add new checks
* Fix stateless scenario source field and report SKIPPED for inapplicable capability checks
- Add the required `source` field (DRAFT) so the scenario typechecks and is
selectable via --spec-version. Without it the class did not satisfy the
ClientScenario interface and s.source was dereferenced by the spec-version
filter at list time.
- Teach runCheck about a SKIPPED status and use it for the two client-capability
checks when the server does not return -32003, instead of reporting a green
PASS for a requirement that was never exercised.
* Remove unimplemented placeholder checks from stateless server scenario
The subscriptions/listen, statelessness-invariant, list-changed and
disconnect-is-cancel checks emitted SUCCESS without probing the server,
which inflated coverage in the traceability report. Drop them until they
have real assertions; the corresponding rows remain declared in
src/seps/sep-2575.yaml so traceability shows them as not-yet-covered.
Also fix the checkErrorId helper to push under the
sep-2575-http-server-error-jsonrpc-id slug so its failure path actually
short-circuits the aggregate SUCCESS check at the end of the scenario.
---------
Co-authored-by: Paul Carleton <paulc@anthropic.com>
* SEP-2352: authorization-server migration scenario (#286)
* Add SEP-2352 authorization-server migration scenario
SEP-2352 requires that client credentials are bound to the issuing
authorization server: when PRM authorization_servers changes to a new
issuer, clients MUST re-register and MUST NOT reuse the previous AS's
client credentials.
- Traceability yaml: 3 checks, 3 excluded (internal state / UI)
- New auth/authorization-server-migration scenario (draft suite): two
auth servers; PRM flips from AS1 to AS2 after the first authenticated
request; AS2 asserts it received a fresh /register and never saw AS1's
client_id at /authorize or /token
- ConformanceOAuthProvider gains invalidateCredentials and bindIssuer so
the everything-client can key credentials by issuer (passing example)
- everything-client adds an issuer-aware handler for this scenario that
re-reads PRM on each 401 and rebinds before re-authorizing
- auth-test-reuse-credentials negative client + vitest case
* Drop application_type from DCR metadata (not in SDK OAuthClientMetadata type)
---------
Co-authored-by: Paul Carleton <paulc@anthropic.com>
* ci(traceability): enable corepack so the SDK build can use pnpm (#289)
The refresh run failed with 'pnpm: not found' — the reference SDK's
build command (typescript-sdk: pnpm install && pnpm run build:all) needs
pnpm on PATH. Add 'corepack enable' to the run job.
* ci(traceability): tolerate SDK conformance failures in the run step (#290)
The `sdk` command exits non-zero when the SDK has conformance failures
not in its baseline. The traceability manifest only needs the emitted
check IDs (written regardless of pass/fail), so a failing SDK must not
fail the refresh. Add `|| true`; the existing 'no results produced'
guard catches a genuinely broken run.
* chore: refresh SEP traceability manifest (typescript-sdk@main) (#291)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Make scopesSupported disjoint from challenge scopes so the SEP-2350 union check can't be satisfied by scopes_supported ∪ challenge
A client that unions scopes_supported with the challenge — instead of its
prior grant with the challenge — would have requested mcp:basic mcp:write
and falsely passed sep-2350-scope-union-on-reauth, since scopesSupported
happened to coincide with the previously-granted scope. Setting it to a
disjoint value makes the check actually require the prior token's scope.
Spec backing: clients MUST NOT assume any set relationship between the
challenged scope set and scopes_supported, so a disjoint advertisement is
realistic.
* SEP-837: application_type check in DCR registration (#284)
* Add SEP-837 application_type check to DCR registration
SEP-837 requires MCP clients to specify an appropriate application_type
during Dynamic Client Registration so OIDC authorization servers can
apply the correct redirect-URI constraints.
- Traceability yaml: 1 check (presence + valid value), 4 excluded
(class-specific SHOULDs unobservable; UI/robustness)
- Check added in the shared createAuthServer DCR handler so it fires in
every auth scenario that performs DCR (no new scenario)
- withOAuthRetry now sets application_type: native (passing example;
the conformance example clients are CLI tools)
- New auth-test-no-application-type negative client + vitest case
* Widen ConformanceOAuthProvider metadata type for application_type
Fixes CI typecheck (TS2353 at withOAuthRetry.ts:76): the SDK's
OAuthClientMetadataSchema doesn't include application_type yet.
registerClient() spreads clientMetadata verbatim into the /register
POST body, so a local type intersection is sufficient to get the
field on the wire without an SDK release.
* Set application_type in runAuthMigrationClient
The SEP-2352 authorization-server-migration handler constructs its own
ConformanceOAuthProvider (not via withOAuthRetry), so it was missing
application_type after rebasing onto main. The scenario does DCR twice
(old AS, new AS) and the SEP-837 check fires on each.
---------
Co-authored-by: Paul Carleton <paulc@anthropic.com>
* feat: add conformance tests for iss parameter (SEP-2468) (#220)
* feat: add conformance tests for iss parameter (SEP-2468)
Adds 5 draft conformance scenarios testing RFC 9207 issuer parameter
validation in OAuth authorization responses:
- auth/iss-supported: server advertises support and sends correct iss
- auth/iss-not-advertised: server omits iss parameter entirely
- auth/iss-supported-missing: client must reject missing iss when required
- auth/iss-wrong-issuer: client must reject mismatched iss value
- auth/iss-unexpected: client must reject iss when not advertised
Also adds auth-test-iss-validation.ts, a reference client that correctly
validates iss per RFC 9207, and negative tests confirming the standard
client fails all three rejection scenarios.
TODO: Update RFC_9207_ISS_PARAMETER spec reference once SEP-2468
(modelcontextprotocol/modelcontextprotocol#2468) is merged.
* update scenarios
* fix: createAuthServer iss option type/guard and NotAdvertised scenario duplication
The doc comments said 'Default: not included' but the destructure defaulted
to true/'correct', and the `!== undefined` guard at L155 was unreachable —
so there was no way to omit the metadata field, and IssParameterNotAdvertised
silently advertised support (a duplicate of IssParameterSupported).
Kept the on-by-default behavior (mock AS models a well-behaved server) but
made issParameterSupported `boolean | null` so callers pass null to omit,
matching the codeChallengeMethodsSupported pattern. Doc comments now match.
Scenarios that need omission pass null/'omit' explicitly.
* fix: rejection scenarios silently pass when client never reaches auth endpoint
correctlyRejected = !tokenRequestMade reports SUCCESS if the client errors
out before hitting /authorize. Gate on authReached so a setup failure shows
as FAILURE with authReached:false in details.
* fix: iss-unexpected scenario contradicts SEP-2468 spec table row 3
The spec table says: supported=false/absent + iss present -> *Compare* to
the recorded issuer (not reject). The scenario sent a *correct* iss and
FAILed compliant clients for proceeding after a successful comparison.
Now sends a mismatched iss so the comparison fails and rejection is the
spec-required outcome. Reference client updated to compare-when-present
instead of throw-on-presence.
* refactor: replace harness-config checks with client-proceeded checks
iss-advertised-in-metadata / iss-sent-in-redirect (and the not-* variants)
fired in onAuthorizationRequest before the redirect happened, asserting only
that the harness was configured correctly — a client that ignores iss passes
identically. Replaced with one check per scenario keyed on tokenRequestMade,
which observes that the client actually proceeded through the iss path.
* refactor: rename check IDs to sep-2468-* and align with spec table rows
One ID per spec table row; auth/iss-supported and auth/iss-wrong-issuer
both emit sep-2468-client-compare-iss-supported (same comparison, opposite
input) per the same-slug-for-SUCCESS-and-FAIL convention.
* feat: add sep-2468.yaml requirement traceability
8 check rows (4 client table-row checks, 1 metadata-issuer, 2 AS-side,
1 no-normalization), 1 excluded (error-display is UI-facing). The
record-issuer MUST is merged into the compare-iss-supported row text
since it has no independent wire observation.
* fix: migrate iss scenarios specVersions->source (post-#265)
Replaces `specVersions: ['draft']` with `source: { introducedIn: DRAFT_PROTOCOL_VERSION }`
in the 5 iss-parameter scenarios.
This commit typechecks once the stack is rebased onto main >= #265 (the
ScenarioSource migration). Adding it now so the rebase is mechanical.
* fix: include application_type in iss-validation example DCR (post-#284)
The SEP-837 application_type check now runs in every auth scenario; the
hand-rolled DCR in auth-test-iss-validation.ts was omitting the field.
---------
Co-authored-by: Paul Carleton <paulc@anthropic.com>
---------
Co-authored-by: Anubhav Dhawan <anubhavdhawan@google.com>
Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>
Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
Co-authored-by: Paul Carleton <paulc@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Max Gerber <89937743+max-stytch@users.noreply.github.com>
* feat: add conformance tests for iss parameter (SEP-2468)
Adds 5 draft conformance scenarios testing RFC 9207 issuer parameter
validation in OAuth authorization responses:
- auth/iss-supported: server advertises support and sends correct iss
- auth/iss-not-advertised: server omits iss parameter entirely
- auth/iss-supported-missing: client must reject missing iss when required
- auth/iss-wrong-issuer: client must reject mismatched iss value
- auth/iss-unexpected: client must reject iss when not advertised
Also adds auth-test-iss-validation.ts, a reference client that correctly
validates iss per RFC 9207, and negative tests confirming the standard
client fails all three rejection scenarios.
TODO: Update RFC_9207_ISS_PARAMETER spec reference once SEP-2468
(modelcontextprotocol/modelcontextprotocol#2468) is merged.
* update scenarios
* fix: createAuthServer iss option type/guard and NotAdvertised scenario duplication
The doc comments said 'Default: not included' but the destructure defaulted
to true/'correct', and the `!== undefined` guard at L155 was unreachable —
so there was no way to omit the metadata field, and IssParameterNotAdvertised
silently advertised support (a duplicate of IssParameterSupported).
Kept the on-by-default behavior (mock AS models a well-behaved server) but
made issParameterSupported `boolean | null` so callers pass null to omit,
matching the codeChallengeMethodsSupported pattern. Doc comments now match.
Scenarios that need omission pass null/'omit' explicitly.
* fix: rejection scenarios silently pass when client never reaches auth endpoint
correctlyRejected = !tokenRequestMade reports SUCCESS if the client errors
out before hitting /authorize. Gate on authReached so a setup failure shows
as FAILURE with authReached:false in details.
* fix: iss-unexpected scenario contradicts SEP-2468 spec table row 3
The spec table says: supported=false/absent + iss present -> *Compare* to
the recorded issuer (not reject). The scenario sent a *correct* iss and
FAILed compliant clients for proceeding after a successful comparison.
Now sends a mismatched iss so the comparison fails and rejection is the
spec-required outcome. Reference client updated to compare-when-present
instead of throw-on-presence.
* refactor: replace harness-config checks with client-proceeded checks
iss-advertised-in-metadata / iss-sent-in-redirect (and the not-* variants)
fired in onAuthorizationRequest before the redirect happened, asserting only
that the harness was configured correctly — a client that ignores iss passes
identically. Replaced with one check per scenario keyed on tokenRequestMade,
which observes that the client actually proceeded through the iss path.
* refactor: rename check IDs to sep-2468-* and align with spec table rows
One ID per spec table row; auth/iss-supported and auth/iss-wrong-issuer
both emit sep-2468-client-compare-iss-supported (same comparison, opposite
input) per the same-slug-for-SUCCESS-and-FAIL convention.
* feat: add sep-2468.yaml requirement traceability
8 check rows (4 client table-row checks, 1 metadata-issuer, 2 AS-side,
1 no-normalization), 1 excluded (error-display is UI-facing). The
record-issuer MUST is merged into the compare-iss-supported row text
since it has no independent wire observation.
* fix: migrate iss scenarios specVersions->source (post-#265)
Replaces `specVersions: ['draft']` with `source: { introducedIn: DRAFT_PROTOCOL_VERSION }`
in the 5 iss-parameter scenarios.
This commit typechecks once the stack is rebased onto main >= #265 (the
ScenarioSource migration). Adding it now so the rebase is mechanical.
* fix: include application_type in iss-validation example DCR (post-#284)
The SEP-837 application_type check now runs in every auth scenario; the
hand-rolled DCR in auth-test-iss-validation.ts was omitting the field.
---------
Co-authored-by: Paul Carleton <paulc@anthropic.com>
* Add SEP-837 application_type check to DCR registration
SEP-837 requires MCP clients to specify an appropriate application_type
during Dynamic Client Registration so OIDC authorization servers can
apply the correct redirect-URI constraints.
- Traceability yaml: 1 check (presence + valid value), 4 excluded
(class-specific SHOULDs unobservable; UI/robustness)
- Check added in the shared createAuthServer DCR handler so it fires in
every auth scenario that performs DCR (no new scenario)
- withOAuthRetry now sets application_type: native (passing example;
the conformance example clients are CLI tools)
- New auth-test-no-application-type negative client + vitest case
* Widen ConformanceOAuthProvider metadata type for application_type
Fixes CI typecheck (TS2353 at withOAuthRetry.ts:76): the SDK's
OAuthClientMetadataSchema doesn't include application_type yet.
registerClient() spreads clientMetadata verbatim into the /register
POST body, so a local type intersection is sufficient to get the
field on the wire without an SDK release.
* Set application_type in runAuthMigrationClient
The SEP-2352 authorization-server-migration handler constructs its own
ConformanceOAuthProvider (not via withOAuthRetry), so it was missing
application_type after rebasing onto main. The scenario does DCR twice
(old AS, new AS) and the SEP-837 check fires on each.
---------
Co-authored-by: Paul Carleton <paulc@anthropic.com>
* Add SEP-2352 authorization-server migration scenario
SEP-2352 requires that client credentials are bound to the issuing
authorization server: when PRM authorization_servers changes to a new
issuer, clients MUST re-register and MUST NOT reuse the previous AS's
client credentials.
- Traceability yaml: 3 checks, 3 excluded (internal state / UI)
- New auth/authorization-server-migration scenario (draft suite): two
auth servers; PRM flips from AS1 to AS2 after the first authenticated
request; AS2 asserts it received a fresh /register and never saw AS1's
client_id at /authorize or /token
- ConformanceOAuthProvider gains invalidateCredentials and bindIssuer so
the everything-client can key credentials by issuer (passing example)
- everything-client adds an issuer-aware handler for this scenario that
re-reads PRM on each 401 and rebinds before re-authorizing
- auth-test-reuse-credentials negative client + vitest case
* Drop application_type from DCR metadata (not in SDK OAuthClientMetadata type)
---------
Co-authored-by: Paul Carleton <paulc@anthropic.com>
The sep-2243 scenario check IDs drifted from the requirement-traceability
yaml in #259 — the code emitted one ID per test case while the yaml
declares one per normative requirement, so the IDs no longer matched.
Rename the emitted IDs so a check ID maps to a single MUST/SHOULD and is
emitted once per case (the per-case detail moves to name/description),
matching the repo's existing 'same id, vary status/message' convention:
- mcp-method-header-* / mcp-name-header-* -> client-includes-standard-headers
- reject-invalid-tool-* / keep-valid-tool -> client-reject-invalid-tool
- server-accepts-{lower,upper}case-name -> header-name-case-insensitive
- server reject status checks (mismatch/missing/case x method/name)
-> server-reject-invalid-headers
- their error-code variants -> server-reject-error-code
Merge the yaml's server-reject-mismatch + server-reject-status into one
server-reject-invalid-headers MUST (HTTP 400 on a header-validation
failure); keep server-reject-error-code as the SHOULD. Base64/custom-header
rejection checks keep their own param-validation IDs (out of scope here).
Gates and the whitespace-acceptance check keep their scenario-matching
names. No behavior change — only check IDs, descriptions, and the yaml.
* Add prepare script for git dependency installs
* Add conformance tests for SEP-2243 HTTP Standardization
* Add traceability file for SEP-2243
* Fix prettier formatting in http-standard-headers.ts
* Improve SEP-2243 conformance test coverage
Close gaps in HTTP header conformance scenarios:
Client standard headers (http-standard-headers.ts):
- Enforce all expected methods in checks, failing any that were not observed
- Track Mcp-Name header per-method separately from Mcp-Method
- Advertise resources and prompts capabilities so clients exercise those endpoints
- Add a prompt entry for prompts/get testing
Client custom headers (http-custom-headers.ts):
- Add Base64 encoding checks for non-ASCII, whitespace, and control-char values
- Add null/omitted parameter test (second tool call with null value)
- Add client-keeps-valid-tool check verifying clients still call valid tools
after filtering out invalid ones
Server header validation (server/http-standard-headers.ts):
- Replace fetch-based sendRawRequest with http.request to preserve exact
header casing on the wire (fetch/Headers lowercases names)
- Compute defaultArgs and defaultHeaders from tool schema so test requests
satisfy all required parameters while varying only the param under test
Traceability (sep-2243.yaml):
- Add 7 new spec-to-check mappings (nonascii, whitespace, controlchar,
keeps-valid-tool, literal-missing-base64-prefix/suffix, no-mirror-unannotated)
- Fix 2 incorrect existing mappings
* Fill SEP-2243 conformance test coverage gaps
Add missing test cases identified by comparing the branch against the
SEP-2243 spec's Conformance Test Cases section:
- Mcp-Method on notifications: add notifications/initialized to
expectedMethods so clients are checked for header on notifications
- Boolean true: add debug=true param to complement verbose=false
- Leading-space-only and trailing-space-only: separate checks for
Base64 encoding when only one edge has whitespace
- Internal spaces only: verify plain ASCII (no Base64) for values
like 'us west 1' that have spaces only in the middle
- CRLF string: add line1\r\nline2 test (complements existing \n test)
- Leading tab: add \tindented test for tab-triggered Base64 encoding
- Server rejects invalid Mcp-Param chars: mark as excluded in YAML
since HTTP itself prevents these characters in headers
Register all new checks in sep-2243.yaml.
* Remove case-insensitive =?base64? prefix test
The spec states header values are case-sensitive (RFC 9110). The
=?base64? prefix is part of the header value, not the header name,
so it should be matched case-sensitively. This was identified as a
bug in the conformance tests per feedback on SEP-2243.
Changes:
- Remove server-accepts-case-insensitive-base64 check from server
validation scenario
- Change base64 prefix regex from case-insensitive (/i) to
case-sensitive in client header validation
- Remove corresponding entry from sep-2243.yaml
* Remove nested x-mcp-header rejection test
The spec constrains x-mcp-header to primitive types but does not
restrict it to top-level properties only. A nested string property
with x-mcp-header is valid per the spec. This was confirmed by
mikekistler in SEP-2243 PR discussion.
Removes invalid_nested_header from the invalid tool definitions
list and its corresponding sep-2243.yaml entry.
* Use numeric comparison for number header values
Compare number header values numerically instead of as strings to
accommodate cross-SDK floating point representation differences
(e.g., '42' vs '42.0'). For integers, exact numeric match is
required. For decimals, a tolerance of 1e-9 is allowed.
See SEP-2243 discussion on number precision.
* client/http-standard-headers: SKIPPED (not FAILURE) for unexercised methods; make getChecks() idempotent
A client that never calls prompts/list isn't violating SEP-2243 — the spec
sentence is 'The client MUST include the standard MCP request headers on each
POST request', and a request that was never sent can't fail it. The Mcp-Method
requirement is already proven by initialize/tools/list etc., so there's nothing
unique to prompts/list. SKIPPED keeps the gap visible without a false red.
Separately: getChecks() was pushing into this.checks on every call. The runner
may call it more than once (e.g. progress + final report); that produced
duplicate rows. Build a fresh array per call instead.
* client/http-standard-headers: de-dup guard in checkMcpNameHeader
checkMcpMethodHeader already has 'if (this.methodHeaderChecks.has(method)) return'.
checkMcpNameHeader was missing the equivalent. The test server advertises two
tools and two resources; a client that calls both produces two
'client-mcp-name-header-tools-call' / '...-resources-read' rows.
* client/http-standard-headers: replace -> /\//g for slug generation
String.replace with a string arg only replaces the first occurrence.
'notifications/initialized' is fine (one slash) but a method like
'notifications/resources/updated' would yield 'notifications-resources/updated'
in the check id. (replaceAll isn't in this project's TS lib target.)
* client/http-custom-headers: base64 regex (.+) -> (.*) so empty string round-trips
The spec doesn't forbid base64-encoding values that don't strictly need it.
A client that always wraps would send '=?base64??=' for empty_val: ''. The
'.+' wouldn't match, so the harness would compare '=?base64??=' === ''
literally and FAIL a compliant client. '(.*)' lets the empty payload decode
to '' and match.
* client/http-custom-headers: emit SUCCESS for no-mirror-unannotated
This check only pushed on FAILURE; a correct client produced no row at all,
so the pass was invisible in reports and couldn't be counted toward coverage.
Same-slug SUCCESS/FAILURE pair is the repo convention.
* client/http-custom-headers: drop redundant optional-present check
checkParamHeader('Verbose', ...) already pushes a FAILURE when the header is
missing. The explicit 'optional-present' block re-tests the same condition
under a second check id, doubling the row.
* server/http-standard-headers: defaultArgs must use real number/boolean, not strings
defaultArgs feeds the JSON-RPC body. With Record<string, string> + '0'/'false',
a server that validates inputSchema rejects on type mismatch — which is also a
400. Every header-rejection check then false-PASSes (server rejected for the
wrong reason), and server-accepts-valid-base64 false-FAILs a compliant server
because it never gets past schema validation.
Also handle 'integer' (JSON Schema distinguishes it from 'number').
* server/http-standard-headers: createAcceptanceCheck must also assert no JSON-RPC error in body
A server can return HTTP 200 with {error: {code: -32001, ...}} in the body.
Status-only acceptance lets that through as a pass.
* use DRAFT_PROTOCOL_VERSION constant instead of 'DRAFT-2026-v1' literal
The literal will rot when the draft cycle rolls over. types.ts already exports
DRAFT_PROTOCOL_VERSION for this.
* sep-2243.yaml: spec-first rewrite + move to src/seps/
Regenerated from the SEP-2243 spec diff (transports.mdx + tools.mdx) rather
than from the scenario implementations: 21 check rows + 4 excluded vs the
previous 46 + 2. Differences:
- one check id per spec sentence (test variants go in 'details', not new ids)
- 'text:' quotes the spec sentence verbatim instead of paraphrasing
- '-reject-status' (MUST 400) split from '-reject-error-code' (SHOULD -32001
for standard headers, MUST for custom)
- rows with no spec backing dropped (whitespace, base64-padding/chars, prefix/
suffix-literal, control-char-name)
- two more SHOULD excludes (log-warning, no-sensitive-params)
- check ids use sep-2243-<slug> convention
- check: key first, excludes grouped at bottom (matches sep-2164)
Moved to src/seps/ to match #272 layout (this branch predates it; will
reconcile cleanly on rebase).
* add negative test for HttpStandardHeadersScenario
Self-contained vitest that POSTs initialize with and without Mcp-Method,
asserts FAILURE/SUCCESS on the pinned check id, and proves getChecks() is
idempotent. No example client/server file needed for this one because the
scenario *is* the server — the test crafts the bad request directly.
This is the negative-test half of AGENTS.md §Testing your scenario; the
everything-client passing-example half is still TODO.
* server/http-standard-headers: severity fixes for whitespace + malformed-base64 tests
These five tests assert behavior the SEP-2243 text doesn't pin down. Kept
because they're useful consistency signals across SDKs, but adjusted so they
don't FAIL spec-compliant servers:
- server-accepts-whitespace-header-value: this IS a MUST, but per RFC 9110
§5.5 (field parsing MUST exclude OWS), not SEP-2243. Kept as FAILURE,
specReference now cites RFC 9110.
- server-rejects-invalid-base64-padding / -chars: downgraded to INFO.
SEP-2243 says only 'MUST decode them accordingly' without picking RFC 4648
strict vs lenient. Node's Buffer.from() accepts unpadded/dirty input and
decodes to a matching value (server accepts); .NET's Convert.FromBase64String
throws (server rejects). Either is currently compliant. WARNING would force
Tier-1 SDKs into non-default strict decoding (#245); INFO records the
behavior without prescribing it, so cross-SDK divergence is visible
without tier impact.
- server-literal-missing-base64-prefix / -suffix: unchanged. The wrapper
syntax is '=?base64?{x}?=' complete; treating partial wrappers as literal
is the natural reading.
Plumbing: createRejectionCheck gains an optional failSeverity param;
testBase64Case gains a 'reject-info' mode. (This is also the hook for the
larger 400-MUST / -32001-SHOULD split that still needs doing.)
* server/http-standard-headers: split rejection check into status (MUST) + error-code (SHOULD/MUST)
SEP-2243 §Server Validation: 'servers MUST return HTTP status 400 ... and
SHOULD include a JSON-RPC error response using ... -32001'. So a server that
returns 400 with -32600 (or no error body) is compliant for standard headers.
The single conflated check FAILed it.
For custom headers (§Server Behavior for Custom Headers) -32001 IS a MUST,
so both halves stay FAILURE there.
createRejectionCheck → createRejectionChecks returning two rows: '<id>' for
the 400 status and '<id>-error-code' for -32001. testCase (standard) sets
errorCodeSeverity=WARNING; testBase64Case/testMissingCustomHeader (custom)
keep FAILURE; the two malformed-base64 INFO probes set both halves to INFO.
* rename check ids to sep-2243-* prefix; kebab-case throughout
Aligns with the sep-2243.yaml traceability ids and the repo convention
(sep-2164-*, sep-2207-*). Variant suffixes are kept (per-method, per-tool)
so per-case visibility isn't lost; the YAML's row id is a prefix of the
emitted ids, which is fine — extra ids not in the YAML are allowed.
Also fixes the mixed hyphen/underscore in reject-invalid-tool ids
(toolName has underscores; replace to hyphens).
* client: extract BaseHttpScenario to client/http-base.ts; both client scenarios extend it
http-standard-headers.ts had its own copy of the start/stop/handleRequest
boilerplate (~100 lines) that http-custom-headers.ts already abstracted as
BaseHttpScenario. Moved the abstract class to a shared file; both scenario
files now import it. HttpStandardHeadersScenario implements handlePost()
instead of handleRequest(); its handleInitialize() uses the base
sendInitialize() with the resources/prompts capability flags it needs.
Net: 465 → 361 lines for http-standard-headers.ts; the third inline copy of
the same HTTP-server scaffold is gone.
* use req.setEncoding('utf8') instead of per-chunk Buffer.toString()
Per-chunk .toString() corrupts a multi-byte UTF-8 char that straddles a TCP
chunk boundary (e.g. 0xC3 | 0xBC decodes to two replacement chars instead of
'ü'). The custom-headers scenario sends '日本語'/'naïve' in the body, so this
is reachable in principle. setEncoding('utf8') makes 'data' emit strings with
boundary handling done by Node's StringDecoder.
Fixed in both places: BaseHttpScenario.handleRequest (request body) and
sendRawRequest (response body).
* fix: migrate SEP-2243 scenarios from specVersions to source (introducedIn/removedIn)
The introducedIn/removedIn refactor (#265) replaced the specVersions array
with a source property on all scenario interfaces. The SEP-2243 scenarios
still used the old specVersions field, causing matchesSpecVersion() to
receive undefined and crash in spec-version.test.ts.
Replace specVersions with source = { introducedIn: DRAFT_PROTOCOL_VERSION }
in BaseHttpScenario and the two server ClientScenario classes. Remove the
now-unused SpecVersion imports.
* server/http-standard-headers: malformed-base64 padding/chars back to FAILURE
Per discussion on #259: the SEP-2243 conformance-test-case table is the
approved source of truth and lists these as reject cases, so they're
FAILURE-level even though the spec body itself only says 'MUST decode them
accordingly'. SDKs whose stdlib base64 is lenient (Node, browsers) will need
to validate before decoding; if that's burdensome we'll revisit.
Removes the now-unused 'reject-info' mode and statusSeverity/INFO plumbing
introduced for these two probes.
---------
Co-authored-by: Tarek Mahmoud Sayed <tarekms@microsoft.com>
Co-authored-by: Tarek Mahmoud Sayed <tarekms@ntdev.microsoft.com>
Co-authored-by: Tarek Mahmoud Sayed <10833894+tarekgh@users.noreply.github.com>
Co-authored-by: Paul Carleton <paulc@anthropic.com>
* feat: add new-sep scaffolding command
Related #243.
This commit adds the new-sep <NNNN> subcommand, which writes the file src/scenarios/<target>/sep-<NNNN>.yaml. This file contains the "sep", "spec_url", and "requirements" rows in the same style as sep-2164.yaml, with single quotes and two spaces. The spec source resolves in order: --spec-url → --spec-path → GitHub API against modelcontextprotocol/modelcontextprotocol (explicit --pr or title search for "SEP-NNNN").
The target directory is inferred from the spec path (`server/`, `client/`, or `basic/authorization*`); the --target flag overrides this. It refuses to overwrite without the --force flag.
Token resolution reuses the "gh auth token" fallback pattern from tier-check.
It also ships the .claude/skills/new-sep directory, which drives the CLI. It fetches the spec diff via "gh api" and extracts RFC 2119 sentences. There is a warning against regex-only matching. Bullets inherit keywords from their lead-in sentence. It rewrites the placeholder rows per the severity rules in AGENTS.md. MUST/SHALL/REQUIRED are checked for FAILURE. SHOULD is checked for WARNING. MAY/OPTIONAL are excluded.
Signed-off-by: Satoshi Ito <satoshi.ito.tf@hitachi.com>
* dropped --pr arg, list changed files, support multi-file seps w url override
* Consolidate SEP yamls into src/seps/ and tighten yaml ordering
- All sep-NNNN.yaml files now live in src/seps/ regardless of which
scenario suite their checks run in
- Drop --target / inferTarget; output dir is fixed
- renderYaml: check rows emit `check:` first, excluded rows grouped at
bottom with a blank-line separator
- SKILL.md: document key/row ordering, expand exclusion-confirmation step
- Move src/scenarios/server/sep-2164.yaml -> src/seps/
---------
Signed-off-by: Satoshi Ito <satoshi.ito.tf@hitachi.com>
Co-authored-by: Paul Carleton <paulc@anthropic.com>