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

View file

@ -2,7 +2,11 @@ import {
Scenario,
ClientScenario,
ClientScenarioForAuthorizationServer,
SpecVersion
SpecVersion,
ScenarioSpecTag,
DATED_SPEC_VERSIONS,
DRAFT_PROTOCOL_VERSION,
LATEST_SPEC_VERSION
} from '../types';
import { InitializeScenario } from './client/initialize';
import { ToolsCallScenario } from './client/tools_call';
@ -256,32 +260,50 @@ export function listDraftScenarios(): string[] {
export { listMetadataScenarios };
// All valid spec versions, used by the CLI to validate --spec-version input.
// 'extension' is intentionally excluded — extension scenarios are off-timeline
// and selected via `--suite extensions`, not `--spec-version`.
export const ALL_SPEC_VERSIONS: SpecVersion[] = [
'2025-03-26',
'2025-06-18',
'2025-11-25',
'draft',
'extension'
...DATED_SPEC_VERSIONS,
DRAFT_PROTOCOL_VERSION
];
export function resolveSpecVersion(value: string): SpecVersion {
if (value === 'draft') return DRAFT_PROTOCOL_VERSION;
if (ALL_SPEC_VERSIONS.includes(value as SpecVersion)) {
return value as SpecVersion;
}
console.error(`Unknown spec version: ${value}`);
console.error(`Valid versions: ${ALL_SPEC_VERSIONS.join(', ')}`);
console.error(
`Valid versions: ${ALL_SPEC_VERSIONS.join(', ')} (or 'draft' as an alias for ${DRAFT_PROTOCOL_VERSION})`
);
process.exit(1);
}
// The draft version selects everything in the latest dated release plus
// scenarios tagged draft-only, so SEP authors can run the full suite against an
// SDK tracking the in-progress spec without retagging core scenarios.
function matchesSpecVersion(
scenario: { specVersions: ScenarioSpecTag[] },
version: SpecVersion
): boolean {
if (version === DRAFT_PROTOCOL_VERSION) {
return (
scenario.specVersions.includes(DRAFT_PROTOCOL_VERSION) ||
scenario.specVersions.includes(LATEST_SPEC_VERSION)
);
}
return scenario.specVersions.includes(version);
}
export function listScenariosForSpec(version: SpecVersion): string[] {
return scenariosList
.filter((s) => s.specVersions.includes(version))
.filter((s) => matchesSpecVersion(s, version))
.map((s) => s.name);
}
export function listClientScenariosForSpec(version: SpecVersion): string[] {
return allClientScenariosList
.filter((s) => s.specVersions.includes(version))
.filter((s) => matchesSpecVersion(s, version))
.map((s) => s.name);
}
@ -289,13 +311,13 @@ export function listClientScenariosForAuthorizationServerForSpec(
version: SpecVersion
): string[] {
return allClientScenariosListForAuthorizationServer
.filter((s) => s.specVersions.includes(version))
.filter((s) => matchesSpecVersion(s, version))
.map((s) => s.name);
}
export function getScenarioSpecVersions(
name: string
): SpecVersion[] | undefined {
): ScenarioSpecTag[] | undefined {
return (
scenarios.get(name)?.specVersions ??
clientScenarios.get(name)?.specVersions ??
@ -303,4 +325,4 @@ export function getScenarioSpecVersions(
);
}
export type { SpecVersion };
export type { SpecVersion, ScenarioSpecTag };