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:
parent
8445dd3aa0
commit
206709f8a6
5 changed files with 135 additions and 6 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(), () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue