fix: include required result members in 2026-07-28 mock responses (#341)

At the 2026-07-28 revision every server result must include resultType,
and cacheable results (server/discover, tools/list, prompts/list,
resources/list, resources/templates/list, resources/read) must also
include ttlMs and cacheScope. The client-direction mock servers omitted
these, so a strictly-validating client was failed by the suite's own
non-conformant mock.

The shared stateless mock now stamps the missing members onto handler
results and its server/discover response (withRequiredDraftResultFields),
and the hand-rolled per-scenario servers set them explicitly or route
their fallbacks through the helper. The deliberately resultType-less
probe in sep-2322-client-request-state is unchanged. New tests pin the
response shapes.
This commit is contained in:
Felix Weinberger 2026-06-16 15:13:23 +01:00 • committed by GitHub
commit b9041ea41b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 612 additions and 17 deletions

View file

@ -56,5 +56,10 @@ export interface ScenarioContext {
}
export { createServerStateful } from './stateful';
export { createServerStateless, validateStatelessRequest } from './stateless';
export {
createServerStateless,
validateStatelessRequest,
withRequiredDraftResultFields,
CACHEABLE_RESULT_METHODS
} from './stateless';
export { createServerFor } from './select';

View file

@ -1,7 +1,12 @@
import { describe, it, expect } from 'vitest';
import { createServerFor } from './select';
import { createServerStateful } from './stateful';
import { createServerStateless, validateStatelessRequest } from './stateless';
import {
createServerStateless,
validateStatelessRequest,
withRequiredDraftResultFields,
CACHEABLE_RESULT_METHODS
} from './stateless';
import { STATELESS_SPEC_VERSIONS } from '../connection/select';
import { DRAFT_PROTOCOL_VERSION } from '../types';
@ -73,6 +78,28 @@ describe('validateStatelessRequest', () => {
expect(v).toMatchObject({ kind: 'route', id: 1, method: 'tools/list' });
});
it('includes the draft-required result members on the server/discover result', () => {
const v = validateStatelessRequest(
{
headers,
body: {
jsonrpc: '2.0',
id: 1,
method: 'server/discover',
params: { _meta: meta }
}
},
{},
[DRAFT_PROTOCOL_VERSION]
);
expect(v).toMatchObject({
kind: 'handled',
body: {
result: { resultType: 'complete', ttlMs: 0, cacheScope: 'private' }
}
});
});
it('rejects versions outside the supported list with -32004 and echoes it', () => {
const v = validateStatelessRequest(
{
@ -105,6 +132,53 @@ describe('validateStatelessRequest', () => {
});
});
describe('withRequiredDraftResultFields', () => {
it('stamps resultType "complete" when the handler omitted it', () => {
expect(
withRequiredDraftResultFields('tools/call', { content: [] })
).toEqual({ resultType: 'complete', content: [] });
});
it('adds ttlMs and cacheScope for every cacheable method', () => {
for (const method of CACHEABLE_RESULT_METHODS) {
expect(withRequiredDraftResultFields(method, {})).toEqual({
resultType: 'complete',
ttlMs: 0,
cacheScope: 'private'
});
}
});
it('does not add caching hints to non-cacheable results', () => {
const result = withRequiredDraftResultFields('tools/call', {
content: []
});
expect(result).not.toHaveProperty('ttlMs');
expect(result).not.toHaveProperty('cacheScope');
});
it('preserves members the handler set itself', () => {
expect(
withRequiredDraftResultFields('tools/call', {
resultType: 'input_required',
inputRequests: {}
})
).toMatchObject({ resultType: 'input_required' });
expect(
withRequiredDraftResultFields('tools/list', {
ttlMs: 5000,
cacheScope: 'public',
tools: []
})
).toMatchObject({ ttlMs: 5000, cacheScope: 'public' });
});
it('passes non-object results through untouched', () => {
expect(withRequiredDraftResultFields('tools/call', null)).toBeNull();
expect(withRequiredDraftResultFields('tools/call', [1])).toEqual([1]);
});
});
describe('createServerFor', () => {
it('returns stateful for dated 2025-x versions', () => {
expect(createServerFor('2025-06-18')).toBe(createServerStateful);
@ -286,6 +360,68 @@ describe('createServerStateless', () => {
}
});
it('stamps the draft-required result members onto handler results', async () => {
const srv = await createServerStateless({
'tools/list': () => ({ tools: [{ name: 'x' }] }),
'tools/call': () => ({ content: [{ type: 'text', text: 'ok' }] })
});
try {
const list = await post(
srv.url,
{
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: { _meta: meta }
},
headers
);
expect(list.body.result).toMatchObject({
resultType: 'complete',
ttlMs: 0,
cacheScope: 'private',
tools: [{ name: 'x' }]
});
const call = await post(
srv.url,
{
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: { _meta: meta, name: 'x' }
},
headers
);
expect(call.body.result).toMatchObject({ resultType: 'complete' });
expect(call.body.result).not.toHaveProperty('ttlMs');
expect(call.body.result).not.toHaveProperty('cacheScope');
} finally {
await srv.close();
}
});
it('preserves a resultType the handler set itself', async () => {
const srv = await createServerStateless({
'tools/call': () => ({ resultType: 'input_required', inputRequests: {} })
});
try {
const { body } = await post(
srv.url,
{
jsonrpc: '2.0',
id: 1,
method: 'tools/call',
params: { _meta: meta }
},
headers
);
expect(body.result.resultType).toBe('input_required');
} finally {
await srv.close();
}
});
it('returns -32601 for unknown methods', async () => {
const srv = await createServerStateless({});
try {

View file

@ -20,6 +20,45 @@ const META_KEYS = [
'io.modelcontextprotocol/clientCapabilities'
] as const;
/**
* Operations whose results the 2026-07-28 revision marks cacheable: servers
* MUST include the caching hints `ttlMs` and `cacheScope` on these results
* (draft `CacheableResult`, server/utilities/caching).
*/
export const CACHEABLE_RESULT_METHODS: ReadonlySet<string> = new Set([
'server/discover',
'tools/list',
'prompts/list',
'resources/list',
'resources/templates/list',
'resources/read'
]);
/**
* Fill in the result members the 2026-07-28 revision requires of servers when
* the handler did not set them itself: every result MUST carry `resultType`,
* and results of the cacheable operations MUST also carry `ttlMs` and
* `cacheScope`. Members the handler set are preserved (e.g. a handler may
* return `resultType: 'input_required'`); only absent (or undefined) ones are
* filled. A scenario that needs to send a deliberately non-conformant result
* must build its own server instead of routing through this mock.
*/
export function withRequiredDraftResultFields(
method: string,
result: unknown
): unknown {
if (result === null || typeof result !== 'object' || Array.isArray(result)) {
return result;
}
const stamped: Record<string, unknown> = { ...result };
stamped.resultType ??= 'complete';
if (CACHEABLE_RESULT_METHODS.has(method)) {
stamped.ttlMs ??= 0;
stamped.cacheScope ??= 'private';
}
return stamped;
}
type IncomingHeaders = Record<string, string | string[] | undefined>;
export type StatelessValidation =
@ -114,11 +153,11 @@ export function validateStatelessRequest(
body: {
jsonrpc: '2.0',
id,
result: {
result: withRequiredDraftResultFields(method, {
supportedVersions,
capabilities,
serverInfo: { name: 'conformance-mock-server', version: '1.0.0' }
}
})
}
};
}
@ -166,7 +205,11 @@ export async function createServerStateless(
}
try {
const result = await handler(params, req.body as JSONRPCRequest);
return res.json({ jsonrpc: '2.0', id, result });
return res.json({
jsonrpc: '2.0',
id,
result: withRequiredDraftResultFields(method, result)
});
} catch (e) {
return error(500, -32603, e instanceof Error ? e.message : String(e));
}

View file

@ -12,6 +12,7 @@ import express, { Request, Response, NextFunction } from 'express';
import type { ConformanceCheck } from '../../../../types';
import {
validateStatelessRequest,
withRequiredDraftResultFields,
type ScenarioContext
} from '../../../../mock-server';
import { isStatefulVersion } from '../../../../connection/select';
@ -210,16 +211,18 @@ export function createServer(
return res.json({
jsonrpc: '2.0',
id,
result: {
result: withRequiredDraftResultFields(method, {
tools: [{ name: 'test-tool', inputSchema: { type: 'object' } }]
}
})
});
}
if (method === 'tools/call') {
return res.json({
jsonrpc: '2.0',
id,
result: { content: [{ type: 'text', text: 'test' }] }
result: withRequiredDraftResultFields(method, {
content: [{ type: 'text', text: 'test' }]
})
});
}
return res.status(404).json({

View file

@ -0,0 +1,296 @@
import { describe, it, expect } from 'vitest';
import { testScenarioContext } from '../../mock-server/testing';
import { DRAFT_PROTOCOL_VERSION } from '../../types';
import { HttpStandardHeadersScenario } from './http-standard-headers';
import {
HttpCustomHeadersScenario,
HttpInvalidToolHeadersScenario
} from './http-custom-headers';
import { RequestMetadataScenario } from './request-metadata';
import { MRTRClientScenario } from './mrtr-client';
/**
* Pins that the hand-rolled mock servers used by client-direction scenarios
* at 2026-07-28 return spec-valid results: every result carries `resultType`,
* and the cacheable list/read/discover results also carry `ttlMs` and
* `cacheScope`. Without these, a strictly-conforming client is failed by the
* suite's own non-conformant mock. The shared stateless mock is covered in
* src/mock-server/mock-server.test.ts.
*/
const meta = {
'io.modelcontextprotocol/protocolVersion': DRAFT_PROTOCOL_VERSION,
'io.modelcontextprotocol/clientInfo': { name: 'test', version: '1.0' },
'io.modelcontextprotocol/clientCapabilities': {}
};
async function post(
url: string,
body: object,
headers: Record<string, string> = {}
) {
const r = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...headers },
body: JSON.stringify(body)
});
return { status: r.status, body: await r.json() };
}
const CACHEABLE_FIELDS = {
resultType: 'complete',
ttlMs: 0,
cacheScope: 'private'
};
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();
const { serverUrl } = await scenario.start(
testScenarioContext(DRAFT_PROTOCOL_VERSION)
);
try {
const cases: Array<{
method: string;
params?: object;
cacheable: boolean;
}> = [
{ method: 'initialize', cacheable: false },
{ method: 'tools/list', cacheable: true },
{
method: 'tools/call',
params: { name: 'test_headers', arguments: {} },
cacheable: false
},
{ method: 'resources/list', cacheable: true },
{
method: 'resources/read',
params: { uri: 'file:///path/to/file%20name.txt' },
cacheable: true
},
// Not explicitly handled by the scenario — exercises the method-aware
// generic fallback, which must still add the caching hints.
{ method: 'resources/templates/list', cacheable: true },
{ method: 'prompts/list', cacheable: true },
{
method: 'prompts/get',
params: { name: 'test_prompt' },
cacheable: false
},
{ method: 'unknown/method', cacheable: false }
];
let id = 1;
for (const c of cases) {
const { status, body } = await post(
serverUrl,
{
jsonrpc: '2.0',
id: id++,
method: c.method,
params: c.params ?? {}
},
{ 'Mcp-Method': c.method }
);
expect(status, c.method).toBe(200);
expect(body.result.resultType, c.method).toBe('complete');
if (c.cacheable) {
expect(body.result, c.method).toMatchObject({
ttlMs: 0,
cacheScope: 'private'
});
}
}
} finally {
await scenario.stop();
}
});
});
describe('http-custom-headers mock results (2026-07-28)', () => {
it('carries the draft-required result members', async () => {
const scenario = new HttpCustomHeadersScenario();
const { serverUrl } = await scenario.start(
testScenarioContext(DRAFT_PROTOCOL_VERSION)
);
try {
const list = await post(serverUrl, {
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {}
});
expect(list.body.result).toMatchObject(CACHEABLE_FIELDS);
const call = await post(serverUrl, {
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'test_custom_headers',
arguments: { region: 'us-east', priority: 1, query: 'q' }
}
});
expect(call.body.result.resultType).toBe('complete');
} finally {
await scenario.stop();
}
});
});
describe('http-invalid-tool-headers mock results (2026-07-28)', () => {
it('carries the draft-required result members', async () => {
const scenario = new HttpInvalidToolHeadersScenario();
const { serverUrl } = await scenario.start(
testScenarioContext(DRAFT_PROTOCOL_VERSION)
);
try {
const list = await post(serverUrl, {
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {}
});
expect(list.body.result).toMatchObject(CACHEABLE_FIELDS);
const call = await post(serverUrl, {
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: { name: 'valid_tool', arguments: { region: 'us-east' } }
});
expect(call.body.result.resultType).toBe('complete');
} finally {
await scenario.stop();
}
});
});
describe('request-metadata mock results (2026-07-28)', () => {
it('carries the draft-required result members after the simulated rejection', async () => {
const scenario = new RequestMetadataScenario();
const { serverUrl } = await scenario.start(
testScenarioContext(DRAFT_PROTOCOL_VERSION)
);
const headers = { 'MCP-Protocol-Version': DRAFT_PROTOCOL_VERSION };
try {
// The first request is always answered with the simulated -32004
// rejection (retry probe); results are served from the second on.
const first = await post(
serverUrl,
{
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: { _meta: meta }
},
headers
);
expect(first.status).toBe(400);
const discover = await post(
serverUrl,
{
jsonrpc: '2.0',
id: 2,
method: 'server/discover',
params: { _meta: meta }
},
headers
);
expect(discover.body.result).toMatchObject(CACHEABLE_FIELDS);
const list = await post(
serverUrl,
{
jsonrpc: '2.0',
id: 3,
method: 'tools/list',
params: { _meta: meta }
},
headers
);
expect(list.body.result).toMatchObject(CACHEABLE_FIELDS);
const call = await post(
serverUrl,
{
jsonrpc: '2.0',
id: 4,
method: 'tools/call',
params: { _meta: meta, name: 'x' }
},
headers
);
expect(call.body.result.resultType).toBe('complete');
const other = await post(
serverUrl,
{ jsonrpc: '2.0', id: 5, method: 'ping', params: { _meta: meta } },
headers
);
expect(other.body.result.resultType).toBe('complete');
} finally {
await scenario.stop();
}
});
});
describe('sep-2322-client-request-state mock results (2026-07-28)', () => {
it('carries the draft-required result members on conformant results and keeps the deliberate omission', async () => {
const scenario = new MRTRClientScenario();
const { serverUrl } = await scenario.start(
testScenarioContext(DRAFT_PROTOCOL_VERSION)
);
try {
const list = await post(serverUrl, {
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {}
});
expect(list.body.result).toMatchObject(CACHEABLE_FIELDS);
const initial = await post(serverUrl, {
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: { name: 'test_mrtr_echo_state', arguments: {} }
});
expect(initial.body.result.resultType).toBe('input_required');
const retry = await post(serverUrl, {
jsonrpc: '2.0',
id: 3,
method: 'tools/call',
params: {
name: 'test_mrtr_echo_state',
arguments: {},
inputResponses: { confirm: { action: 'accept' } },
requestState: initial.body.result.requestState
}
});
expect(retry.body.result.resultType).toBe('complete');
const unrelated = await post(serverUrl, {
jsonrpc: '2.0',
id: 4,
method: 'tools/call',
params: { name: 'test_mrtr_unrelated', arguments: {} }
});
expect(unrelated.body.result.resultType).toBe('complete');
// The default-resultType probe deliberately omits resultType — that is
// the check's stimulus and must not be "fixed".
const noResultType = await post(serverUrl, {
jsonrpc: '2.0',
id: 5,
method: 'tools/call',
params: { name: 'test_mrtr_no_result_type', arguments: {} }
});
expect(noResultType.body.result.content).toBeDefined();
expect(noResultType.body.result).not.toHaveProperty('resultType');
} finally {
await scenario.stop();
}
});
});

View file

@ -1,4 +1,7 @@
import type { ScenarioContext } from '../../mock-server';
import {
withRequiredDraftResultFields,
type ScenarioContext
} from '../../mock-server';
/**
* Shared HTTP test-server scaffold for client-under-test SEP-2243 scenarios.
*
@ -135,6 +138,7 @@ export abstract class BaseHttpScenario implements Scenario {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
protocolVersion: DRAFT_PROTOCOL_VERSION,
serverInfo: { name: this.name + '-server', version: '1.0.0' },
capabilities
@ -151,7 +155,9 @@ export abstract class BaseHttpScenario implements Scenario {
this.sendJson(res, {
jsonrpc: '2.0',
id: request.id,
result: {}
// Method-aware so cacheable methods that fall through to the generic
// reply still carry the ttlMs/cacheScope the draft revision requires.
result: withRequiredDraftResultFields(request.method, {})
});
}
}

View file

@ -276,6 +276,9 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
ttlMs: 0,
cacheScope: 'private',
tools: [
{
name: 'test_custom_headers',
@ -591,6 +594,7 @@ export class HttpCustomHeadersScenario extends BaseHttpScenario {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
content: [{ type: 'text', text: 'Custom headers test completed' }]
}
});
@ -760,6 +764,9 @@ export class HttpInvalidToolHeadersScenario extends BaseHttpScenario {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
ttlMs: 0,
cacheScope: 'private',
tools: [
// ── Valid tool (should be kept by client) ──
{
@ -938,6 +945,7 @@ export class HttpInvalidToolHeadersScenario extends BaseHttpScenario {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
content: [{ type: 'text', text: 'Tool call received' }]
}
});

View file

@ -221,6 +221,9 @@ export class HttpStandardHeadersScenario extends BaseHttpScenario {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
ttlMs: 0,
cacheScope: 'private',
tools: [
{
name: 'test_headers',
@ -259,6 +262,7 @@ export class HttpStandardHeadersScenario extends BaseHttpScenario {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
content: [
{
type: 'text',
@ -281,6 +285,9 @@ export class HttpStandardHeadersScenario extends BaseHttpScenario {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
ttlMs: 0,
cacheScope: 'private',
resources: [
{
uri: 'file:///path/to/file%20name.txt',
@ -309,6 +316,9 @@ export class HttpStandardHeadersScenario extends BaseHttpScenario {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
ttlMs: 0,
cacheScope: 'private',
contents: []
}
})
@ -326,6 +336,9 @@ export class HttpStandardHeadersScenario extends BaseHttpScenario {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
ttlMs: 0,
cacheScope: 'private',
prompts: [
{
name: 'test_prompt',
@ -348,6 +361,7 @@ export class HttpStandardHeadersScenario extends BaseHttpScenario {
jsonrpc: '2.0',
id: request.id,
result: {
resultType: 'complete',
messages: []
}
})

View file

@ -44,6 +44,9 @@ function createMcpServer(canaryUrl: string, onToolsListed: () => void): Server {
server.setRequestHandler(ListToolsRequestSchema, async () => {
onToolsListed();
return {
resultType: 'complete',
ttlMs: 0,
cacheScope: 'private',
tools: [
{
name: TOOL_NAME,

View file

@ -96,7 +96,12 @@ function createMRTRServer(checks: ConformanceCheck[]): express.Application {
res.json({
jsonrpc: '2.0',
id,
result: { tools: TOOLS }
result: {
resultType: 'complete',
ttlMs: 0,
cacheScope: 'private',
tools: TOOLS
}
});
return;
}
@ -141,7 +146,11 @@ function createMRTRServer(checks: ConformanceCheck[]): express.Application {
res.json({
jsonrpc: '2.0',
id,
result: { action: 'accept', content: { confirmed: true } }
result: {
resultType: 'complete',
action: 'accept',
content: { confirmed: true }
}
});
return;
}
@ -258,6 +267,7 @@ function createMRTRServer(checks: ConformanceCheck[]): express.Application {
jsonrpc: '2.0',
id,
result: {
resultType: 'complete',
content: [{ type: 'text', text: 'echo-state-ok' }]
}
});
@ -324,6 +334,7 @@ function createMRTRServer(checks: ConformanceCheck[]): express.Application {
jsonrpc: '2.0',
id,
result: {
resultType: 'complete',
content: [{ type: 'text', text: 'no-state-ok' }]
}
});
@ -370,6 +381,7 @@ function createMRTRServer(checks: ConformanceCheck[]): express.Application {
jsonrpc: '2.0',
id,
result: {
resultType: 'complete',
content: [{ type: 'text', text: 'unrelated-ok' }]
}
});
@ -403,7 +415,10 @@ function createMRTRServer(checks: ConformanceCheck[]): express.Application {
res.json({
jsonrpc: '2.0',
id,
result: { content: [{ type: 'text', text: 'unexpected-retry' }] }
result: {
resultType: 'complete',
content: [{ type: 'text', text: 'unexpected-retry' }]
}
});
return;
}

View file

@ -1,4 +1,7 @@
import type { ScenarioContext } from '../../mock-server';
import {
withRequiredDraftResultFields,
type ScenarioContext
} from '../../mock-server';
import http from 'http';
import {
Scenario,
@ -319,11 +322,11 @@ export class RequestMetadataScenario implements Scenario {
JSON.stringify({
jsonrpc: '2.0',
id: request.id,
result: {
result: withRequiredDraftResultFields(request.method, {
supportedVersions: [DRAFT_PROTOCOL_VERSION],
capabilities: {},
serverInfo: { name: 'test', version: '1.0' }
}
})
})
);
return;
@ -337,7 +340,13 @@ export class RequestMetadataScenario implements Scenario {
result = { content: [] };
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, result }));
res.end(
JSON.stringify({
jsonrpc: '2.0',
id: request.id,
result: withRequiredDraftResultFields(request.method, result)
})
);
});
}
}

View file

@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { ToolsCallScenario } from './tools_call';
import { DRAFT_PROTOCOL_VERSION } from '../../types';
describe('tools_call scenario', () => {
it('emits a single FAILURE check when the tool was never called', async () => {
@ -20,6 +21,62 @@ describe('tools_call scenario', () => {
}
});
it('serves spec-valid results at the draft (2026-07-28) version', async () => {
const meta = {
'io.modelcontextprotocol/protocolVersion': DRAFT_PROTOCOL_VERSION,
'io.modelcontextprotocol/clientInfo': {
name: 'test-client',
version: '1.0.0'
},
'io.modelcontextprotocol/clientCapabilities': {}
};
async function post(url: string, body: object) {
const r = await fetch(url, {
method: 'POST',
headers: {
'content-type': 'application/json',
'mcp-protocol-version': DRAFT_PROTOCOL_VERSION
},
body: JSON.stringify(body)
});
return { status: r.status, body: await r.json() };
}
const scenario = new ToolsCallScenario();
const { serverUrl } = await scenario.start(
testScenarioContext(DRAFT_PROTOCOL_VERSION)
);
try {
const list = await post(serverUrl, {
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: { _meta: meta }
});
expect(list.status).toBe(200);
expect(list.body.result).toMatchObject({
resultType: 'complete',
ttlMs: 0,
cacheScope: 'private'
});
const call = await post(serverUrl, {
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: { _meta: meta, name: 'add_numbers', arguments: { a: 2, b: 3 } }
});
expect(call.status).toBe(200);
expect(call.body.result.resultType).toBe('complete');
const checks = scenario.getChecks();
expect(checks).toHaveLength(1);
expect(checks[0].status).toBe('SUCCESS');
} finally {
await scenario.stop();
}
});
it('emits SUCCESS after a valid tools/call and getChecks() is idempotent', async () => {
const scenario = new ToolsCallScenario();
const { serverUrl } = await scenario.start(testScenarioContext());