Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/scenarios/client/draft-result-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
27 changes: 26 additions & 1 deletion src/scenarios/client/http-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand Down Expand Up @@ -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',
Expand Down
10 changes: 5 additions & 5 deletions src/scenarios/client/http-standard-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions src/scenarios/client/json-schema-ref-deref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(), () => {
Expand Down
16 changes: 16 additions & 0 deletions src/scenarios/client/mrtr-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading