From 8f7bf1072130d5159144570210606a0816f780f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 17:48:48 +0000 Subject: [PATCH 1/5] Add client check: preserve query params in PRM discovery URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per RFC 9728 §3.1 the well-known suffix is inserted between the host and the path and/or query components of the resource identifier, so a client told to connect to https://host/mcp?tenant=a must request /.well-known/oauth-protected-resource/mcp?tenant=a, not drop the query. Adds a metadata-query-params variant to the metadata discovery scenario family with a new prm-query-preserved check (WARNING severity), plus a deliberately-broken example client and a negative test. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BnD1ZkuBPVVB235hJQxjA4 --- .../typescript/auth-test-strip-query.ts | 84 +++++++++++++++++++ .../clients/typescript/everything-client.ts | 1 + .../client/auth/discovery-metadata.ts | 57 ++++++++++++- .../client/auth/helpers/createServer.ts | 55 +++++++++++- src/scenarios/client/auth/index.test.ts | 8 ++ 5 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 examples/clients/typescript/auth-test-strip-query.ts diff --git a/examples/clients/typescript/auth-test-strip-query.ts b/examples/clients/typescript/auth-test-strip-query.ts new file mode 100644 index 00000000..954fd713 --- /dev/null +++ b/examples/clients/typescript/auth-test-strip-query.ts @@ -0,0 +1,84 @@ +#!/usr/bin/env node + +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import { + auth, + UnauthorizedError +} from '@modelcontextprotocol/sdk/client/auth.js'; +import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js'; +import { withOAuthRetry } from './helpers/withOAuthRetry'; +import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider'; +import { runAsCli } from './helpers/cliRunner'; +import { logger } from './helpers/logger'; + +/** + * Broken client that drops the MCP server URL's query component when + * constructing the PRM well-known URL. + * BUG: RFC 9728 §3.1 inserts the well-known suffix between the host and the + * path AND/OR QUERY components, so for https://host/mcp?tenant=alpha the PRM + * URL is /.well-known/oauth-protected-resource/mcp?tenant=alpha — this client + * requests /.well-known/oauth-protected-resource/mcp instead. + */ +export async function runClient(serverUrl: string): Promise { + const handle401Broken = async ( + response: Response, + provider: ConformanceOAuthProvider, + next: FetchLike, + serverUrl: string | URL + ): Promise => { + // BUG: insert the well-known suffix before the path but discard the query + const url = new URL(serverUrl); + const resourceMetadataUrl = new URL( + `/.well-known/oauth-protected-resource${url.pathname}`, + url.origin + ); + + let result = await auth(provider, { + serverUrl, + resourceMetadataUrl, + fetchFn: next + }); + + if (result === 'REDIRECT') { + const authorizationCode = await provider.getAuthCode(); + result = await auth(provider, { + serverUrl, + resourceMetadataUrl, + authorizationCode, + fetchFn: next + }); + if (result !== 'AUTHORIZED') { + throw new UnauthorizedError( + `Authentication failed with result: ${result}` + ); + } + } + }; + + const client = new Client( + { name: 'test-auth-client-strip-query', version: '1.0.0' }, + { capabilities: {} } + ); + + const oauthFetch = withOAuthRetry( + 'test-auth-client-strip-query', + new URL(serverUrl), + handle401Broken + )(fetch); + + const transport = new StreamableHTTPClientTransport(new URL(serverUrl), { + fetch: oauthFetch + }); + + await client.connect(transport); + logger.debug('✅ Successfully connected to MCP server'); + + await client.listTools(); + logger.debug('✅ Successfully listed tools'); + + await transport.close(); + logger.debug('✅ Connection closed successfully'); +} + +runAsCli(runClient, import.meta.url, 'auth-test-strip-query '); diff --git a/examples/clients/typescript/everything-client.ts b/examples/clients/typescript/everything-client.ts index ff6dd35f..25e597ac 100644 --- a/examples/clients/typescript/everything-client.ts +++ b/examples/clients/typescript/everything-client.ts @@ -380,6 +380,7 @@ registerScenarios( 'auth/metadata-var1', 'auth/metadata-var2', 'auth/metadata-var3', + 'auth/metadata-query-params', // Backcompat scenarios 'auth/2025-03-26-oauth-metadata-backcompat', 'auth/2025-03-26-oauth-endpoint-fallback', diff --git a/src/scenarios/client/auth/discovery-metadata.ts b/src/scenarios/client/auth/discovery-metadata.ts index 3c53e08d..70b21b5e 100644 --- a/src/scenarios/client/auth/discovery-metadata.ts +++ b/src/scenarios/client/auth/discovery-metadata.ts @@ -13,6 +13,7 @@ import { createAuthServer } from './helpers/createAuthServer'; import { createServer } from './helpers/createServer'; import { ServerLifecycle } from './helpers/serverLifecycle'; import { SpecReferences } from './spec-references'; +import { untestableCheck } from '../../untestable'; import { Request, Response } from 'express'; /** @@ -27,6 +28,12 @@ interface MetadataScenarioConfig { authRoutePrefix?: string; /** If true, add a trap for root PRM requests */ trapRootPrm?: boolean; + /** + * Query string (without '?') appended to the MCP server URL handed to the + * client. The client must preserve it when constructing the PRM well-known + * URL (RFC 9728 §3.1). + */ + serverUrlQuery?: string; } /** @@ -38,6 +45,12 @@ interface MetadataScenarioConfig { * | metadata-var1 | /.well-known/oauth-protected-resource/mcp | No | /.well-known/openid-configuration | * | metadata-var2 | /.well-known/oauth-protected-resource | No | /.well-known/oauth-authorization-server/tenant1| * | metadata-var3 | /custom/metadata/location.json | Yes | /tenant1/.well-known/openid-configuration | + * + * metadata-query-params reuses the metadata-var1 layout (path-based PRM, not + * in WWW-Authenticate) but hands the client an MCP server URL with a query + * component. Per RFC 9728 §3.1 the well-known suffix is inserted between the + * host and the path and/or query components, so the client's PRM request must + * keep the query: /.well-known/oauth-protected-resource/mcp?tenant=alpha. */ const SCENARIO_CONFIGS: MetadataScenarioConfig[] = [ { @@ -66,6 +79,13 @@ const SCENARIO_CONFIGS: MetadataScenarioConfig[] = [ inWwwAuth: true, oauthMetadataLocation: '/tenant1/.well-known/openid-configuration', authRoutePrefix: '/tenant1' + }, + { + name: 'metadata-query-params', + prmLocation: '/.well-known/oauth-protected-resource/mcp', + inWwwAuth: false, + oauthMetadataLocation: '/.well-known/oauth-authorization-server', + serverUrlQuery: 'tenant=alpha' } ]; @@ -93,7 +113,7 @@ function createMetadataScenario(config: MetadataScenarioConfig): Scenario { **PRM:** ${config.prmLocation}${config.inWwwAuth ? '' : ' (not in WWW-Authenticate)'} **OAuth metadata:** ${config.oauthMetadataLocation} -`, +${config.serverUrlQuery ? `**Server URL query:** ?${config.serverUrlQuery} (client should preserve it in the PRM well-known URL per RFC 9728 §3.1)\n` : ''}`, async start(ctx: ScenarioContext): Promise { checks = []; @@ -134,7 +154,10 @@ function createMetadataScenario(config: MetadataScenarioConfig): Scenario { const app = createServer(ctx, checks, server.getUrl, getAuthServerUrl, { prmPath: config.prmLocation, - includePrmInWwwAuth: config.inWwwAuth + includePrmInWwwAuth: config.inWwwAuth, + ...(config.serverUrlQuery && { + expectedPrmQuery: config.serverUrlQuery + }) }); // Add trap for root PRM requests if configured @@ -169,7 +192,9 @@ function createMetadataScenario(config: MetadataScenarioConfig): Scenario { await server.start(app); - return { serverUrl: `${server.getUrl()}/mcp` }; + return { + serverUrl: `${server.getUrl()}/mcp${config.serverUrlQuery ? `?${config.serverUrlQuery}` : ''}` + }; }, async stop() { @@ -198,6 +223,29 @@ function createMetadataScenario(config: MetadataScenarioConfig): Scenario { } } + // If the client never reached the PRM well-known URL at all, query + // preservation could not be observed. Report the check as untestable at + // the requirement's severity (WARNING — see createServer) rather than + // letting it silently disappear. + if ( + config.serverUrlQuery && + !checks.find((c) => c.id === 'prm-query-preserved') + ) { + checks.push( + untestableCheck( + 'prm-query-preserved', + 'PRMQueryPreserved', + 'Client SHOULD preserve the MCP server URL query component when constructing the PRM well-known URL (RFC 9728 §3.1)', + 'client never requested the path-based PRM well-known URL, so query preservation could not be verified', + [ + SpecReferences.RFC_PRM_DISCOVERY, + SpecReferences.MCP_PRM_DISCOVERY + ], + 'WARNING' + ) + ); + } + return checks; } }; @@ -216,6 +264,9 @@ export const AuthMetadataVar2Scenario = createMetadataScenario( export const AuthMetadataVar3Scenario = createMetadataScenario( SCENARIO_CONFIGS[3] ); +export const AuthMetadataQueryParamsScenario = createMetadataScenario( + SCENARIO_CONFIGS[4] +); // Export all scenarios as an array for convenience export const metadataScenarios = SCENARIO_CONFIGS.map(createMetadataScenario); diff --git a/src/scenarios/client/auth/helpers/createServer.ts b/src/scenarios/client/auth/helpers/createServer.ts index cd32d21b..71fcdee6 100644 --- a/src/scenarios/client/auth/helpers/createServer.ts +++ b/src/scenarios/client/auth/helpers/createServer.ts @@ -30,6 +30,15 @@ export interface ServerOptions { tokenVerifier?: MockTokenVerifier; /** Override the resource field in PRM response (for testing resource mismatch) */ prmResourceOverride?: string; + /** + * Query string (without '?') that the MCP server URL carries and that the + * client is expected to preserve when constructing the PRM well-known URL + * (RFC 9728 §3.1 inserts the well-known suffix between the host and the + * path and/or query components). When set, the PRM route emits the + * `prm-query-preserved` check and includes the query in the `resource` + * value. The metadata is served either way so the flow can continue. + */ + expectedPrmQuery?: string; } export function createServer( @@ -46,7 +55,8 @@ export function createServer( includePrmInWwwAuth = true, includeScopeInWwwAuth = false, tokenVerifier, - prmResourceOverride + prmResourceOverride, + expectedPrmQuery } = options; // Factory: create a fresh Server per request to avoid "Already connected" errors // after the v1.26.0 security fix (GHSA-345p-7cg4-v4c7) @@ -121,6 +131,47 @@ export function createServer( } }); + // RFC 9728 §3.1: for a resource identifier with a query component, the + // well-known suffix is inserted between the host and the path and/or + // query, so the query must survive into the PRM request. A stripped + // query is recorded as a WARNING (the query-bearing identifier itself + // is a SHOULD NOT-discouraged configuration per RFC 9728 §1.2), and the + // metadata is served either way so the rest of the flow can proceed. + if (expectedPrmQuery !== undefined) { + const queryIndex = req.originalUrl.indexOf('?'); + const actualQuery = + queryIndex >= 0 ? req.originalUrl.slice(queryIndex + 1) : ''; + const expectedParams = new URLSearchParams(expectedPrmQuery); + const actualParams = new URLSearchParams(actualQuery); + expectedParams.sort(); + actualParams.sort(); + const preserved = expectedParams.toString() === actualParams.toString(); + + checks.push({ + id: 'prm-query-preserved', + name: 'PRMQueryPreserved', + description: preserved + ? 'Client preserved the MCP server URL query component in the PRM well-known URL' + : 'Client SHOULD preserve the MCP server URL query component when constructing the PRM well-known URL (RFC 9728 §3.1)', + status: preserved ? 'SUCCESS' : 'WARNING', + timestamp: new Date().toISOString(), + specReferences: [ + SpecReferences.RFC_PRM_DISCOVERY, + SpecReferences.MCP_PRM_DISCOVERY + ], + ...(preserved + ? {} + : { + errorMessage: `Expected PRM request query "?${expectedPrmQuery}" but got "${actualQuery ? `?${actualQuery}` : '(no query)'}"` + }), + details: { + url: req.originalUrl, + expectedQuery: expectedPrmQuery, + actualQuery + } + }); + } + // Resource is usually $baseUrl/mcp, but if PRM is at the root, // the resource identifier is the root. // Can be overridden via prmResourceOverride for testing resource mismatch. @@ -128,7 +179,7 @@ export function createServer( prmResourceOverride ?? (prmPath === '/.well-known/oauth-protected-resource' ? getBaseUrl() - : `${getBaseUrl()}/mcp`); + : `${getBaseUrl()}/mcp${expectedPrmQuery ? `?${expectedPrmQuery}` : ''}`); const prmResponse: any = { resource, diff --git a/src/scenarios/client/auth/index.test.ts b/src/scenarios/client/auth/index.test.ts index fbb50a28..b45ef171 100644 --- a/src/scenarios/client/auth/index.test.ts +++ b/src/scenarios/client/auth/index.test.ts @@ -9,6 +9,7 @@ import { InlineClientRunner } from './test_helpers/testClient'; import { runClient as badPrmClient } from '../../../../examples/clients/typescript/auth-test-bad-prm'; +import { runClient as stripQueryClient } from '../../../../examples/clients/typescript/auth-test-strip-query'; import { runWifJwtBearerWrongAudience, runWifJwtBearerMissingAssertion, @@ -113,6 +114,13 @@ describe('Negative tests', () => { }); }); + test('client drops query component when constructing PRM well-known URL', async () => { + const runner = new InlineClientRunner(stripQueryClient); + await runClientAgainstScenario(runner, 'auth/metadata-query-params', { + expectedFailureSlugs: ['prm-query-preserved'] + }); + }); + test('client ignores scope from WWW-Authenticate header', async () => { const runner = new InlineClientRunner(ignoreScopeClient); await runClientAgainstScenario(runner, 'auth/scope-from-www-authenticate', { From 1845b06ee9ea5d436c820383b1a30429fce11510 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 19:40:44 +0000 Subject: [PATCH 2/5] sdk: wire the go-sdk conformance client; fix moved server fixture path The go-sdk repo grew a client conformance fixture (conformance/ everything-client) and its server fixture moved from examples/server/conformance to conformance/everything-server, so the old entry's build command no longer compiled. Build both fixtures, add the client command, point the server URL at /mcp (matching go-sdk's own conformance CI), and adopt the repo's conformance/baseline.yml. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BnD1ZkuBPVVB235hJQxjA4 --- src/sdk-runner/known-sdks.ts | 15 +++++++++++---- src/sdk-runner/sdk-runner.test.ts | 9 +++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/sdk-runner/known-sdks.ts b/src/sdk-runner/known-sdks.ts index 8a6b30fd..eb90880b 100644 --- a/src/sdk-runner/known-sdks.ts +++ b/src/sdk-runner/known-sdks.ts @@ -38,13 +38,20 @@ export const KNOWN_SDKS: Record = { }, expectedFailures: 'test/conformance/conformance-baseline.yml' }, + // Fixtures live under conformance/ (the server moved there from + // examples/server/conformance); one build compiles both. Same fixtures and + // baseline the go-sdk repo's own conformance CI uses. 'go-sdk': { - build: 'go build -o ./.conformance-server ./examples/server/conformance', - // Upstream go-sdk has no client conformance fixture yet (see go-sdk#859). + build: + 'go build -o ./.conformance-client ./conformance/everything-client && go build -o ./.conformance-server ./conformance/everything-server', + client: { + command: './.conformance-client' + }, server: { command: './.conformance-server -http=:3000', - url: 'http://localhost:3000' - } + url: 'http://localhost:3000/mcp' + }, + expectedFailures: 'conformance/baseline.yml' }, // v1.x — the stable, published line of the python-sdk, analogous to // typescript-sdk-v1 (v2/main is mid-refactor and noisy). Clones the diff --git a/src/sdk-runner/sdk-runner.test.ts b/src/sdk-runner/sdk-runner.test.ts index 6c6353e5..77210807 100644 --- a/src/sdk-runner/sdk-runner.test.ts +++ b/src/sdk-runner/sdk-runner.test.ts @@ -104,6 +104,15 @@ describe('lookupBuiltinConfig', () => { expect(lookupBuiltinConfig('typescript-sdk')?.specVersion).toBeUndefined(); }); + it('exposes go-sdk with both fixture commands and the repo baseline', () => { + const go = lookupBuiltinConfig('go-sdk'); + expect(go?.build).toContain('./conformance/everything-client'); + expect(go?.build).toContain('./conformance/everything-server'); + expect(go?.client?.command).toBe('./.conformance-client'); + expect(go?.server?.url).toBe('http://localhost:3000/mcp'); + expect(go?.expectedFailures).toBe('conformance/baseline.yml'); + }); + it('every built-in entry validates against SdkConfigSchema', () => { for (const [name, cfg] of Object.entries(KNOWN_SDKS)) { expect(() => SdkConfigSchema.parse(cfg), name).not.toThrow(); From 114a1a9dcaf271068a04d6bb8a63202dc03ec47f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 19:41:14 +0000 Subject: [PATCH 3/5] sdk: wire the csharp-sdk conformance client The csharp-sdk client fixture (tests/ModelContextProtocol. ConformanceClient) expects the scenario as its first positional argument instead of reading MCP_CONFORMANCE_SCENARIO, so the client command is a small sh -c shim that forwards the env var plus the runner-appended server URL. Client-only: the repo's server fixture is exercised through dotnet test rather than a standalone command, and no expected-failures baseline exists upstream. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BnD1ZkuBPVVB235hJQxjA4 --- src/sdk-runner/known-sdks.ts | 15 +++++++++++++++ src/sdk-runner/sdk-runner.test.ts | 9 +++++++++ 2 files changed, 24 insertions(+) diff --git a/src/sdk-runner/known-sdks.ts b/src/sdk-runner/known-sdks.ts index eb90880b..e97aeb2d 100644 --- a/src/sdk-runner/known-sdks.ts +++ b/src/sdk-runner/known-sdks.ts @@ -53,6 +53,21 @@ export const KNOWN_SDKS: Record = { }, expectedFailures: 'conformance/baseline.yml' }, + // Client fixture: tests/ModelContextProtocol.ConformanceClient. The fixture + // takes the scenario as its first positional argument rather than reading + // MCP_CONFORMANCE_SCENARIO, and the runner appends the server URL as the + // last argument, so the command goes through `sh -c` to forward both + // ("$1" is the appended URL). POSIX sh only — same constraint as the other + // shell-based commands here. The csharp-sdk repo runs conformance + // per-scenario via `dotnet test` and keeps no baseline file, so there is no + // expectedFailures entry. Requires the .NET 10 SDK (global.json). + 'csharp-sdk': { + build: + 'dotnet build tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj -f net10.0 -c Release', + client: { + command: `sh -c 'exec ./artifacts/bin/ModelContextProtocol.ConformanceClient/Release/net10.0/ModelContextProtocol.ConformanceClient "$MCP_CONFORMANCE_SCENARIO" "$1"' conformance-client` + } + }, // v1.x — the stable, published line of the python-sdk, analogous to // typescript-sdk-v1 (v2/main is mid-refactor and noisy). Clones the // python-sdk repo, defaulting to the `v1.x` branch, and targets the latest diff --git a/src/sdk-runner/sdk-runner.test.ts b/src/sdk-runner/sdk-runner.test.ts index 77210807..0041251e 100644 --- a/src/sdk-runner/sdk-runner.test.ts +++ b/src/sdk-runner/sdk-runner.test.ts @@ -113,6 +113,15 @@ describe('lookupBuiltinConfig', () => { expect(go?.expectedFailures).toBe('conformance/baseline.yml'); }); + it('exposes csharp-sdk as client-only, forwarding scenario and URL', () => { + const cs = lookupBuiltinConfig('csharp-sdk'); + expect(cs?.build).toContain('ModelContextProtocol.ConformanceClient'); + // The fixture wants as positional args; the sh -c shim + // maps the env var and the runner-appended URL onto them. + expect(cs?.client?.command).toContain('"$MCP_CONFORMANCE_SCENARIO" "$1"'); + expect(cs?.server).toBeUndefined(); + }); + it('every built-in entry validates against SdkConfigSchema', () => { for (const [name, cfg] of Object.entries(KNOWN_SDKS)) { expect(() => SdkConfigSchema.parse(cfg), name).not.toThrow(); From fbbbd02118b7addd76489ad2792688cc81e1eaf6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 19:41:41 +0000 Subject: [PATCH 4/5] sdk: add a python-sdk (main) entry tracking the draft spec Same fixtures as python-sdk-v1 but on main with no pinned spec version, mirroring the bare typescript-sdk entry and the python-sdk repo's own conformance CI. Trims the v1 comment's claim that main is mid-refactor: main's conformance client baseline is empty upstream. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BnD1ZkuBPVVB235hJQxjA4 --- src/sdk-runner/known-sdks.ts | 19 ++++++++++++++++++- src/sdk-runner/sdk-runner.test.ts | 9 +++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/sdk-runner/known-sdks.ts b/src/sdk-runner/known-sdks.ts index e97aeb2d..3287e83c 100644 --- a/src/sdk-runner/known-sdks.ts +++ b/src/sdk-runner/known-sdks.ts @@ -68,8 +68,25 @@ export const KNOWN_SDKS: Record = { command: `sh -c 'exec ./artifacts/bin/ModelContextProtocol.ConformanceClient/Release/net10.0/ModelContextProtocol.ConformanceClient "$MCP_CONFORMANCE_SCENARIO" "$1"' conformance-client` } }, + // main — the development line, targeting the draft spec (analogous to the + // bare typescript-sdk entry). Same fixture layout as python-sdk-v1: the + // conformance client and the everything-server are uv workspace members, so + // one `uv sync --all-packages` covers both modes. Matches the repo's own + // conformance CI (.github/workflows/conformance.yml), including its + // expected-failures baseline. + 'python-sdk': { + build: 'uv sync --frozen --all-extras --all-packages', + client: { + command: '.venv/bin/python .github/actions/conformance/client.py' + }, + server: { + command: 'uv run --frozen mcp-everything-server --port 3000', + url: 'http://localhost:3000/mcp' + }, + expectedFailures: '.github/actions/conformance/expected-failures.yml' + }, // v1.x — the stable, published line of the python-sdk, analogous to - // typescript-sdk-v1 (v2/main is mid-refactor and noisy). Clones the + // typescript-sdk-v1. Clones the // python-sdk repo, defaulting to the `v1.x` branch, and targets the latest // dated spec so draft-only scenarios/checks are excluded by default. uv // workspace: the `mcp` (client) and `mcp-everything-server` (server) packages diff --git a/src/sdk-runner/sdk-runner.test.ts b/src/sdk-runner/sdk-runner.test.ts index 0041251e..c762f761 100644 --- a/src/sdk-runner/sdk-runner.test.ts +++ b/src/sdk-runner/sdk-runner.test.ts @@ -122,6 +122,15 @@ describe('lookupBuiltinConfig', () => { expect(cs?.server).toBeUndefined(); }); + it('exposes bare python-sdk (main) tracking the draft spec', () => { + const py = lookupBuiltinConfig('python-sdk'); + expect(py?.defaultRef).toBeUndefined(); + expect(py?.specVersion).toBeUndefined(); + expect(py?.client?.command).toContain('client.py'); + expect(py?.server?.command).toContain('mcp-everything-server'); + expect(py?.expectedFailures).toContain('expected-failures.yml'); + }); + it('every built-in entry validates against SdkConfigSchema', () => { for (const [name, cfg] of Object.entries(KNOWN_SDKS)) { expect(() => SdkConfigSchema.parse(cfg), name).not.toThrow(); From 4e7edbb2fb8426129b8e9d3a352b6e228cf8f992 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 21:11:00 +0000 Subject: [PATCH 5/5] address panel review: comment accuracy, scenario rationale, check wording - discovery-metadata: fix the metadata-query-params comment (it uses var1's PRM placement but the default OAuth metadata location, not the var1 layout), add the scenario to the config table, and state why it is a separate scenario (query-bearing resource identifiers are SHOULD NOT-discouraged per RFC 9728 section 1.2 - the mutually-exclusive-config carve-out) - known-sdks: note the go-sdk server entry runs only the fixture's default -stateless setting; per-mode configs are a follow-up - createServer/discovery-metadata: reword prm-query-preserved descriptions so they don't imply a literal RFC SHOULD (the formation rule is MUST-level; WARNING reflects the discouraged configuration) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BnD1ZkuBPVVB235hJQxjA4 --- .../client/auth/discovery-metadata.ts | 31 ++++++++++++------- .../client/auth/helpers/createServer.ts | 2 +- src/sdk-runner/known-sdks.ts | 5 ++- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/scenarios/client/auth/discovery-metadata.ts b/src/scenarios/client/auth/discovery-metadata.ts index 70b21b5e..20957c0f 100644 --- a/src/scenarios/client/auth/discovery-metadata.ts +++ b/src/scenarios/client/auth/discovery-metadata.ts @@ -39,18 +39,25 @@ interface MetadataScenarioConfig { /** * Scenario configurations table: * - * | Scenario | PRM Location | In WWW-Auth | OAuth Metadata Location | - * |------------------|-------------------------------------------|-------------|------------------------------------------------| - * | metadata-default | /.well-known/oauth-protected-resource/mcp | Yes | /.well-known/oauth-authorization-server | - * | metadata-var1 | /.well-known/oauth-protected-resource/mcp | No | /.well-known/openid-configuration | - * | metadata-var2 | /.well-known/oauth-protected-resource | No | /.well-known/oauth-authorization-server/tenant1| - * | metadata-var3 | /custom/metadata/location.json | Yes | /tenant1/.well-known/openid-configuration | + * | Scenario | PRM Location | In WWW-Auth | OAuth Metadata Location | + * |-----------------------|-------------------------------------------|-------------|------------------------------------------------| + * | metadata-default | /.well-known/oauth-protected-resource/mcp | Yes | /.well-known/oauth-authorization-server | + * | metadata-var1 | /.well-known/oauth-protected-resource/mcp | No | /.well-known/openid-configuration | + * | metadata-var2 | /.well-known/oauth-protected-resource | No | /.well-known/oauth-authorization-server/tenant1| + * | metadata-var3 | /custom/metadata/location.json | Yes | /tenant1/.well-known/openid-configuration | + * | metadata-query-params | /.well-known/oauth-protected-resource/mcp | No | /.well-known/oauth-authorization-server | * - * metadata-query-params reuses the metadata-var1 layout (path-based PRM, not - * in WWW-Authenticate) but hands the client an MCP server URL with a query - * component. Per RFC 9728 §3.1 the well-known suffix is inserted between the - * host and the path and/or query components, so the client's PRM request must - * keep the query: /.well-known/oauth-protected-resource/mcp?tenant=alpha. + * metadata-query-params uses var1's PRM placement (path-based, not advertised + * in WWW-Authenticate) with the default OAuth metadata location, and hands + * the client an MCP server URL with a query component. Per RFC 9728 §3.1 the + * well-known suffix is inserted between the host and the path and/or query + * components, so the client's PRM request must keep the query: + * /.well-known/oauth-protected-resource/mcp?tenant=alpha. This is a separate + * scenario rather than a tweak to an existing config because a query-bearing + * resource identifier is itself a SHOULD NOT-discouraged configuration + * (RFC 9728 §1.2), so it must not contaminate the mainline metadata + * scenarios' server URL (the mutually-exclusive-config carve-out in + * AGENTS.md). */ const SCENARIO_CONFIGS: MetadataScenarioConfig[] = [ { @@ -235,7 +242,7 @@ ${config.serverUrlQuery ? `**Server URL query:** ?${config.serverUrlQuery} (clie untestableCheck( 'prm-query-preserved', 'PRMQueryPreserved', - 'Client SHOULD preserve the MCP server URL query component when constructing the PRM well-known URL (RFC 9728 §3.1)', + 'Client is expected to preserve the MCP server URL query component when constructing the PRM well-known URL (RFC 9728 §3.1)', 'client never requested the path-based PRM well-known URL, so query preservation could not be verified', [ SpecReferences.RFC_PRM_DISCOVERY, diff --git a/src/scenarios/client/auth/helpers/createServer.ts b/src/scenarios/client/auth/helpers/createServer.ts index 71fcdee6..0ed42c56 100644 --- a/src/scenarios/client/auth/helpers/createServer.ts +++ b/src/scenarios/client/auth/helpers/createServer.ts @@ -152,7 +152,7 @@ export function createServer( name: 'PRMQueryPreserved', description: preserved ? 'Client preserved the MCP server URL query component in the PRM well-known URL' - : 'Client SHOULD preserve the MCP server URL query component when constructing the PRM well-known URL (RFC 9728 §3.1)', + : 'Client did not preserve the MCP server URL query component when constructing the PRM well-known URL; RFC 9728 §3.1 requires the query to be kept (reported as WARNING because a query-bearing resource identifier is itself a discouraged configuration per RFC 9728 §1.2)', status: preserved ? 'SUCCESS' : 'WARNING', timestamp: new Date().toISOString(), specReferences: [ diff --git a/src/sdk-runner/known-sdks.ts b/src/sdk-runner/known-sdks.ts index 3287e83c..30c45576 100644 --- a/src/sdk-runner/known-sdks.ts +++ b/src/sdk-runner/known-sdks.ts @@ -40,7 +40,10 @@ export const KNOWN_SDKS: Record = { }, // Fixtures live under conformance/ (the server moved there from // examples/server/conformance); one build compiles both. Same fixtures and - // baseline the go-sdk repo's own conformance CI uses. + // baseline the go-sdk repo's own conformance CI uses. The server entry runs + // the fixture in its default stateless setting only; go-sdk's own + // conformance CI runs it both ways (-stateless=false and -stateless). + // Per-mode configs here are a follow-up. 'go-sdk': { build: 'go build -o ./.conformance-client ./conformance/everything-client && go build -o ./.conformance-server ./conformance/everything-server',