From 17e53b3b4166749ba764ad0b7ec038d78081df7b Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 18 Jun 2026 14:07:39 +0000 Subject: [PATCH] fix: serve server/discover from the hand-rolled client-scenario mocks The SEP-2243, SEP-2322, and SEP-2106 client-direction scenarios build their own mock servers and did not answer server/discover, so a 2026-07-28 client that negotiates via discover before its scenario calls hit a -32601 or generic fallback and had to synthesize the response itself. The shared stateless mock and the request-metadata and auth scenarios already served it. BaseHttpScenario now answers server/discover with a valid DiscoverResult (supportedVersions, capabilities from an overridable discoverCapabilities(), serverInfo, plus the cacheable-result members) before delegating to handlePost; http-standard-headers overrides the capabilities to match the methods it serves. mrtr-client and json-schema-ref-no-deref answer it directly. New tests pin the response shape for all five scenarios. --- .../client/draft-result-fields.test.ts | 65 +++++++++++++++++++ src/scenarios/client/http-base.ts | 27 +++++++- src/scenarios/client/http-standard-headers.ts | 10 +-- src/scenarios/client/json-schema-ref-deref.ts | 23 +++++++ src/scenarios/client/mrtr-client.ts | 16 +++++ 5 files changed, 135 insertions(+), 6 deletions(-) diff --git a/src/scenarios/client/draft-result-fields.test.ts b/src/scenarios/client/draft-result-fields.test.ts index 3d5c37ae..54f6f9be 100644 --- a/src/scenarios/client/draft-result-fields.test.ts +++ b/src/scenarios/client/draft-result-fields.test.ts @@ -8,6 +8,7 @@ import { } from './http-custom-headers'; import { RequestMetadataScenario } from './request-metadata'; import { MRTRClientScenario } from './mrtr-client'; +import { JsonSchemaRefDerefScenario } from './json-schema-ref-deref'; /** * Pins that the hand-rolled mock servers used by client-direction scenarios @@ -43,6 +44,70 @@ const CACHEABLE_FIELDS = { cacheScope: 'private' }; +const DISCOVER_FIELDS = { + ...CACHEABLE_FIELDS, + supportedVersions: [DRAFT_PROTOCOL_VERSION] +}; + +describe('hand-rolled mock servers serve server/discover (2026-07-28)', () => { + const cases = [ + { + name: 'http-standard-headers', + make: () => new HttpStandardHeadersScenario(), + capabilities: { tools: {}, resources: {}, prompts: {} } + }, + { + name: 'http-custom-headers', + make: () => new HttpCustomHeadersScenario(), + capabilities: { tools: {} } + }, + { + name: 'http-invalid-tool-headers', + make: () => new HttpInvalidToolHeadersScenario(), + capabilities: { tools: {} } + }, + { + name: 'sep-2322-client-request-state', + make: () => new MRTRClientScenario(), + capabilities: { tools: {} } + }, + { + name: 'json-schema-ref-no-deref', + make: () => new JsonSchemaRefDerefScenario(), + capabilities: { tools: {} } + } + ]; + + for (const c of cases) { + it(`${c.name} returns a valid DiscoverResult`, async () => { + const scenario = c.make(); + const { serverUrl } = await scenario.start( + testScenarioContext(DRAFT_PROTOCOL_VERSION) + ); + try { + const { status, body } = await post( + serverUrl, + { + jsonrpc: '2.0', + id: 1, + method: 'server/discover', + params: { _meta: meta } + }, + { 'mcp-protocol-version': DRAFT_PROTOCOL_VERSION } + ); + expect(status).toBe(200); + expect(body.result).toMatchObject({ + ...DISCOVER_FIELDS, + capabilities: c.capabilities + }); + expect(body.result.serverInfo?.name).toBeTypeOf('string'); + } finally { + await scenario.stop(); + } + }); + } +}); + describe('http-standard-headers mock results (2026-07-28)', () => { it('carries the draft-required result members on every handled method', async () => { const scenario = new HttpStandardHeadersScenario(); diff --git a/src/scenarios/client/http-base.ts b/src/scenarios/client/http-base.ts index 866ed65a..3e8cbe8e 100644 --- a/src/scenarios/client/http-base.ts +++ b/src/scenarios/client/http-base.ts @@ -102,6 +102,10 @@ export abstract class BaseHttpScenario implements Scenario { req.on('end', () => { try { const request = JSON.parse(body); + if (request.method === 'server/discover') { + this.sendDiscover(res, request); + return; + } this.handlePost(req, res, request); } catch (error) { res.writeHead(400, { 'Content-Type': 'application/json' }); @@ -129,10 +133,31 @@ export abstract class BaseHttpScenario implements Scenario { res.end(JSON.stringify(body)); } + /** + * Capabilities advertised to a 2026-07-28 client via `server/discover` (and + * defaulted in the legacy `initialize` reply). Subclasses override to match + * the methods they actually serve. + */ + protected discoverCapabilities(): object { + return { tools: {} }; + } + + protected sendDiscover(res: http.ServerResponse, request: any): void { + this.sendJson(res, { + jsonrpc: '2.0', + id: request.id, + result: withRequiredDraftResultFields('server/discover', { + supportedVersions: [DRAFT_PROTOCOL_VERSION], + capabilities: this.discoverCapabilities(), + serverInfo: { name: this.name + '-server', version: '1.0.0' } + }) + }); + } + protected sendInitialize( res: http.ServerResponse, request: any, - capabilities: object = { tools: {} } + capabilities: object = this.discoverCapabilities() ): void { this.sendJson(res, { jsonrpc: '2.0', diff --git a/src/scenarios/client/http-standard-headers.ts b/src/scenarios/client/http-standard-headers.ts index dcc45f67..227546b2 100644 --- a/src/scenarios/client/http-standard-headers.ts +++ b/src/scenarios/client/http-standard-headers.ts @@ -202,12 +202,12 @@ export class HttpStandardHeadersScenario extends BaseHttpScenario { }); } + protected discoverCapabilities(): object { + return { tools: {}, resources: {}, prompts: {} }; + } + private handleInitialize(res: http.ServerResponse, request: any): void { - this.sendInitialize(res, request, { - tools: {}, - resources: {}, - prompts: {} - }); + this.sendInitialize(res, request); } private handleToolsList(res: http.ServerResponse, request: any): void { diff --git a/src/scenarios/client/json-schema-ref-deref.ts b/src/scenarios/client/json-schema-ref-deref.ts index 1783d57d..ebf1367c 100644 --- a/src/scenarios/client/json-schema-ref-deref.ts +++ b/src/scenarios/client/json-schema-ref-deref.ts @@ -109,6 +109,29 @@ The scenario advertises a tool whose inputSchema contains a \`$ref\` pointing at }); app.post('/mcp', async (req: Request, res: Response) => { + // The bundled SDK server below predates the 2026-07-28 lifecycle and + // does not implement server/discover; answer it directly so a client + // that negotiates first can proceed to tools/list. + if ( + (req.body as Record | undefined)?.method === + 'server/discover' + ) { + return res.json({ + jsonrpc: '2.0', + id: (req.body as Record).id ?? null, + result: { + resultType: 'complete', + ttlMs: 0, + cacheScope: 'private', + supportedVersions: [DRAFT_PROTOCOL_VERSION], + capabilities: { tools: {} }, + serverInfo: { + name: 'json-schema-ref-deref-server', + version: '1.0.0' + } + } + }); + } try { // Stateless: fresh server and transport per request const server = createMcpServer(this.canaryUrl(), () => { diff --git a/src/scenarios/client/mrtr-client.ts b/src/scenarios/client/mrtr-client.ts index eaeb4143..2bd5692e 100644 --- a/src/scenarios/client/mrtr-client.ts +++ b/src/scenarios/client/mrtr-client.ts @@ -87,6 +87,22 @@ function createMRTRServer(checks: ConformanceCheck[]): express.Application { const { id, method, params } = body; switch (method) { + case 'server/discover': { + res.json({ + jsonrpc: '2.0', + id, + result: { + resultType: 'complete', + ttlMs: 0, + cacheScope: 'private', + supportedVersions: [DRAFT_PROTOCOL_VERSION], + capabilities: { tools: {} }, + serverInfo: { name: 'mrtr-mock-server', version: '1.0.0' } + } + }); + return; + } + case 'notifications/initialized': { res.status(204).end(); return;