fix: serve server/discover from the hand-rolled client-scenario mocks (#347)

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.
This commit is contained in:
Felix Weinberger 2026-06-18 15:30:19 +01:00 • committed by GitHub
commit 206709f8a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 135 additions and 6 deletions

View file

@ -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();

View file

@ -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',

View file

@ -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 {

View file

@ -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<string, unknown> | undefined)?.method ===
'server/discover'
) {
return res.json({
jsonrpc: '2.0',
id: (req.body as Record<string, unknown>).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(), () => {

View file

@ -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;