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:
parent
28f83384ef
commit
b9041ea41b
9 changed files with 612 additions and 17 deletions
|
|
@ -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({
|
||||
|
|
|
|||
296
src/scenarios/client/draft-result-fields.test.ts
Normal file
296
src/scenarios/client/draft-result-fields.test.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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, {})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' }]
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Reference in a new issue