feat: support "draft" as a first-class spec version target (#255)

* feat: support "draft" as a first-class spec version target

- Add LATEST_SPEC_VERSION and DATED_SPEC_VERSIONS constants so the next
  spec release is a one-line change in types.ts.
- Accept "draft" as a valid protocolVersion in the initialize check and
  mock-server response.
- --spec-version draft now selects latest-dated scenarios plus draft-tagged
  ones, so SEP authors can run the full suite against an SDK tracking the
  in-progress spec without retagging core scenarios.
- Forward --spec-version to the client process via
  MCP_CONFORMANCE_SPEC_VERSION so SDK examples can pick the matching
  protocolVersion.

Closes #253

* review: collapse SpecVersion to wire strings; dedup NEGOTIABLE_PROTOCOL_VERSIONS; split out 'extension'

Addresses review feedback from @felixweinberger and @mikekistler:

- SpecVersion is now always a wire protocolVersion string
  (DatedSpecVersion | typeof DRAFT_PROTOCOL_VERSION). The separate
  'draft' tag literal is gone from the type system; 'draft' survives
  only as a CLI input alias in resolveSpecVersion. Removes the
  tag-vs-wire confusion and the specVersionToProtocolVersion mapping.
- NEGOTIABLE_PROTOCOL_VERSIONS in types.ts is the single source for
  what the mock server accepts on initialize (was duplicated in
  checks/client.ts and scenarios/client/initialize.ts).
- 'extension' moved out of SpecVersion into a separate ScenarioSpecTag
  type. --spec-version extension is no longer valid; extension scenarios
  remain reachable via --suite extensions.
- Draft-tagged scenarios now use the DRAFT_PROTOCOL_VERSION constant
  so a draft revision bump is a one-line change in types.ts.
This commit is contained in:
Paul Carleton 2026-04-30 11:37:32 +01:00 • committed by GitHub
commit 17f1f93cc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 221 additions and 78 deletions

View file

@ -4,7 +4,7 @@ import type {
Scenario,
ConformanceCheck,
ScenarioUrls,
SpecVersion
ScenarioSpecTag
} from '../../../types';
import { createAuthServer } from './helpers/createAuthServer';
import { createServer } from './helpers/createServer';
@ -37,7 +37,7 @@ async function generateTestKeypair(): Promise<{
*/
export class ClientCredentialsJwtScenario implements Scenario {
name = 'auth/client-credentials-jwt';
specVersions: SpecVersion[] = ['extension'];
specVersions: ScenarioSpecTag[] = ['extension'];
description =
'Tests OAuth client_credentials flow with private_key_jwt authentication (SEP-1046)';
@ -256,7 +256,7 @@ export class ClientCredentialsJwtScenario implements Scenario {
*/
export class ClientCredentialsBasicScenario implements Scenario {
name = 'auth/client-credentials-basic';
specVersions: SpecVersion[] = ['extension'];
specVersions: ScenarioSpecTag[] = ['extension'];
description =
'Tests OAuth client_credentials flow with client_secret_basic authentication';

View file

@ -5,7 +5,7 @@ import type {
Scenario,
ConformanceCheck,
ScenarioUrls,
SpecVersion
ScenarioSpecTag
} from '../../../types';
import { createAuthServer } from './helpers/createAuthServer';
import { createServer } from './helpers/createServer';
@ -60,7 +60,7 @@ async function createIdpIdToken(
*/
export class CrossAppAccessCompleteFlowScenario implements Scenario {
name = 'auth/cross-app-access-complete-flow';
specVersions: SpecVersion[] = ['extension'];
specVersions: ScenarioSpecTag[] = ['extension'];
description =
'Tests complete SEP-990 flow: token exchange + JWT bearer grant (Enterprise Managed OAuth)';

View file

@ -1,5 +1,9 @@
import type { Scenario, ConformanceCheck } from '../../../types';
import { ScenarioUrls, SpecVersion } from '../../../types';
import {
ScenarioUrls,
SpecVersion,
DRAFT_PROTOCOL_VERSION
} from '../../../types';
import { createAuthServer } from './helpers/createAuthServer';
import { createServer } from './helpers/createServer';
import { ServerLifecycle } from './helpers/serverLifecycle';
@ -23,7 +27,7 @@ import { MockTokenVerifier } from './helpers/mockTokenVerifier';
*/
export class OfflineAccessScopeScenario implements Scenario {
name = 'auth/offline-access-scope';
specVersions: SpecVersion[] = ['draft'];
specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION];
description =
'Tests that a client that wants a refresh token handles offline_access scope and refresh_token grant type when AS supports them (SEP-2207)';
@ -227,7 +231,7 @@ export class OfflineAccessScopeScenario implements Scenario {
*/
export class OfflineAccessNotSupportedScenario implements Scenario {
name = 'auth/offline-access-not-supported';
specVersions: SpecVersion[] = ['draft'];
specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION];
description =
'Tests that client does not request offline_access when AS does not list it in scopes_supported (SEP-2207)';

View file

@ -1,5 +1,9 @@
import type { Scenario, ConformanceCheck } from '../../../types.js';
import { ScenarioUrls, SpecVersion } from '../../../types.js';
import {
ScenarioUrls,
SpecVersion,
DRAFT_PROTOCOL_VERSION
} from '../../../types.js';
import { createAuthServer } from './helpers/createAuthServer.js';
import { createServer } from './helpers/createServer.js';
import { ServerLifecycle } from './helpers/serverLifecycle.js';
@ -27,7 +31,7 @@ import { MockTokenVerifier } from './helpers/mockTokenVerifier.js';
*/
export class ResourceMismatchScenario implements Scenario {
name = 'auth/resource-mismatch';
specVersions: SpecVersion[] = ['draft'];
specVersions: SpecVersion[] = [DRAFT_PROTOCOL_VERSION];
description =
'Tests that client rejects when PRM resource does not match server URL';
allowClientError = true;

View file

@ -3,7 +3,9 @@ import {
Scenario,
ScenarioUrls,
ConformanceCheck,
SpecVersion
SpecVersion,
LATEST_SPEC_VERSION,
NEGOTIABLE_PROTOCOL_VERSIONS
} from '../../types';
import { clientChecks } from '../../checks/index';
@ -117,11 +119,10 @@ export class InitializeScenario implements Scenario {
this.checks.push(clientChecks.createServerInfoCheck(serverInfo));
// Echo back client's version if valid, otherwise use latest
const VALID_VERSIONS = ['2025-06-18', '2025-11-25'];
const clientVersion = initializeRequest?.protocolVersion;
const responseVersion = VALID_VERSIONS.includes(clientVersion)
const responseVersion = NEGOTIABLE_PROTOCOL_VERSIONS.includes(clientVersion)
? clientVersion
: '2025-11-25';
: LATEST_SPEC_VERSION;
const response = {
jsonrpc: '2.0',