conformance/src/types.ts
Bill Prin 8445dd3aa0
fix: gate SEP-2350 scope-union check to 2026-07-28+ (#337)
The auth/scope-step-up scenario emitted sep-2350-scope-union-on-reauth at
every spec version, but the set-wise union requirement (SEP-2350) was only
introduced in 2026-07-28. This wrongly warned clients that are conformant at
2025-11-25 and drop the previously-granted scope on re-authorization (e.g.
python-sdk v1, surfaced while wiring up its runner in #329).

Gate the check behind specVersionAtLeast(specVersion, DRAFT_PROTOCOL_VERSION),
a small timeline-ordering helper added to types.ts (mirrors the ordering used
by scenario applicability). Add tests pinning both sides of the branch:
enforced at draft, silent at 2025-11-25.

Follow-up to #329.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:55:31 +01:00

143 lines
4.1 KiB
TypeScript

import type { RunContext } from './connection';
import type { ScenarioContext } from './mock-server';
export type CheckStatus =
| 'SUCCESS'
| 'FAILURE'
| 'WARNING'
| 'SKIPPED'
| 'INFO';
export interface SpecReference {
id: string;
url?: string;
}
export interface ConformanceCheck {
id: string;
name: string;
description: string;
status: CheckStatus;
timestamp: string;
specReferences?: SpecReference[];
details?: Record<string, unknown>;
metadata?: Record<string, unknown>;
errorMessage?: string;
logs?: string[];
}
export const DATED_SPEC_VERSIONS = [
'2025-03-26',
'2025-06-18',
'2025-11-25'
] as const;
export type DatedSpecVersion = (typeof DATED_SPEC_VERSIONS)[number];
export const LATEST_SPEC_VERSION: DatedSpecVersion = '2025-11-25';
/**
* Wire `protocolVersion` for the in-progress spec. Mirrors
* `LATEST_PROTOCOL_VERSION` in the spec repo's `schema/draft/schema.ts`;
* bump when that constant changes.
*/
export const DRAFT_PROTOCOL_VERSION = '2026-07-28';
// Wire protocolVersion strings the mock server will negotiate on initialize.
export const NEGOTIABLE_PROTOCOL_VERSIONS: readonly string[] = [
'2025-06-18',
LATEST_SPEC_VERSION,
DRAFT_PROTOCOL_VERSION
];
/**
* A spec revision the conformance suite can target via `--spec-version`.
* Always a wire `protocolVersion` string. The CLI also accepts `'draft'` as
* an alias for {@link DRAFT_PROTOCOL_VERSION}.
*/
export type SpecVersion = DatedSpecVersion | typeof DRAFT_PROTOCOL_VERSION;
/** Spec versions in timeline order, dated revisions followed by the draft. */
const SPEC_VERSION_TIMELINE: readonly SpecVersion[] = [
...DATED_SPEC_VERSIONS,
DRAFT_PROTOCOL_VERSION
];
/**
* True when `v` is at or after `threshold` on the spec timeline. Lets a check
* gate itself to the version that introduced its requirement (e.g. a draft-only
* requirement passes `DRAFT_PROTOCOL_VERSION` as the threshold).
*/
export function specVersionAtLeast(
v: SpecVersion,
threshold: SpecVersion
): boolean {
return (
SPEC_VERSION_TIMELINE.indexOf(v) >= SPEC_VERSION_TIMELINE.indexOf(threshold)
);
}
// Scenarios may also be tagged 'extension' to mark them as off-timeline
// (selectable via --suite extensions, never via --spec-version). See #256.
export type ScenarioSpecTag = SpecVersion | 'extension';
/**
* Known protocol extensions that this suite has scenarios for.
* Values are SEP-2133 extension identifiers (the keys used in
* `capabilities.extensions`).
*/
export const EXTENSION_IDS = [
'io.modelcontextprotocol/oauth-client-credentials',
'io.modelcontextprotocol/enterprise-managed-authorization'
] as const;
export type ExtensionId = (typeof EXTENSION_IDS)[number];
/**
* Where a scenario's requirement comes from. Either the dated spec timeline
* (`introducedIn`/`removedIn`) or a named protocol extension that lives
* outside the spec release cycle. Extensions never match `--spec-version`.
*/
export type ScenarioSource =
| {
introducedIn: DatedSpecVersion | typeof DRAFT_PROTOCOL_VERSION;
removedIn?: DatedSpecVersion | typeof DRAFT_PROTOCOL_VERSION;
}
| { extensionId: ExtensionId };
export interface ScenarioUrls {
serverUrl: string;
authUrl?: string;
/**
* Optional context to pass to the client via MCP_CONFORMANCE_CONTEXT env var.
* This is a JSON-serializable object containing scenario-specific data like credentials.
*/
context?: Record<string, unknown>;
}
export interface Scenario {
name: string;
description: string;
source: ScenarioSource;
/**
* If true, a non-zero client exit code is expected and will not cause the test to fail.
* Use this for scenarios where the client is expected to error (e.g., rejecting invalid auth).
*/
allowClientError?: boolean;
start(ctx: ScenarioContext): Promise<ScenarioUrls>;
stop(): Promise<void>;
getChecks(): ConformanceCheck[];
}
export interface ClientScenario {
name: string;
description: string;
source: ScenarioSource;
run(ctx: RunContext): Promise<ConformanceCheck[]>;
}
export interface ClientScenarioForAuthorizationServer {
name: string;
description: string;
source: ScenarioSource;
run(serverUrl: string): Promise<ConformanceCheck[]>;
}