From f257966c8420f2ebf0e77e85ee9edb770cd57e60 Mon Sep 17 00:00:00 2001 From: Youngseok Oh Date: Mon, 28 Sep 2026 11:55:07 +0900 Subject: [PATCH] fix: validate successful request-state completion --- .../typescript/sep-2322-mrtr-broken-server.ts | 62 ++++++++++++++++++ src/scenarios/server/input-required-result.ts | 22 +++++++ src/scenarios/server/negative-mrtr.test.ts | 63 ++++++++++++++++++- 3 files changed, 144 insertions(+), 3 deletions(-) diff --git a/examples/servers/typescript/sep-2322-mrtr-broken-server.ts b/examples/servers/typescript/sep-2322-mrtr-broken-server.ts index 72cf6021..bee97923 100644 --- a/examples/servers/typescript/sep-2322-mrtr-broken-server.ts +++ b/examples/servers/typescript/sep-2322-mrtr-broken-server.ts @@ -104,6 +104,68 @@ handlers['tools/call'] = (params) => { }; } + case 'test_input_required_result_request_state': { + if (!inputResponses) { + return { + resultType: 'input_required', + inputRequests: { + confirm: { + method: 'elicitation/create', + params: { + message: 'Confirm?', + requestedSchema: { + type: 'object', + properties: { ok: { type: 'boolean' } }, + required: ['ok'] + } + } + } + }, + requestState: 'request-state-negative-fixture' + }; + } + if ( + params.requestState !== 'request-state-negative-fixture' || + !inputResponses['confirm'] + ) { + throw { + code: -32602, + message: 'Expected echoed state and confirm response' + }; + } + const mode = process.env.MRTR_REQUEST_STATE_MODE || 'missing-marker'; + if (mode === 'jsonrpc-error') { + throw { code: -32602, message: 'State rejected' }; + } + if (mode === 'input-required') { + return { resultType: 'input_required', requestState: 'still-pending' }; + } + const isError = + mode === 'tool-error' || mode === 'tool-error-with-marker'; + const marker = + mode === 'valid' || + mode === 'valid-second-text' || + mode === 'tool-error-with-marker'; + return { + resultType: 'complete', + isError, + content: + mode === 'empty-content' + ? [] + : [ + ...(mode === 'valid-second-text' + ? [{ type: 'text', text: 'Context' }] + : []), + { + type: 'text', + text: marker + ? 'state-ok: requestState validated' + : 'State rejected' + } + ] + }; + } + case 'test_input_required_result_capabilities': { // BUG 4: Conformant on its face — `requestState` satisfies "at least one // of inputRequests or requestState" — but it names no input request, so diff --git a/src/scenarios/server/input-required-result.ts b/src/scenarios/server/input-required-result.ts index 64ad4ba9..0cd001f8 100644 --- a/src/scenarios/server/input-required-result.ts +++ b/src/scenarios/server/input-required-result.ts @@ -558,6 +558,28 @@ Implement a tool named \`test_input_required_result_request_state\` (no argument r2Errors.push( 'Expected complete result after retry with requestState' ); + } else { + if (r2Result.isError === true) { + r2Errors.push( + 'Tool result reported isError: true after retry with requestState' + ); + } + const content = r2Result.content; + const hasStateMarker = + Array.isArray(content) && + content.some( + (block) => + typeof block === 'object' && + block !== null && + block.type === 'text' && + typeof block.text === 'string' && + block.text.includes('state-ok') + ); + if (!hasStateMarker) { + r2Errors.push( + 'Expected text content containing the fixture marker "state-ok"' + ); + } } checks.push({ diff --git a/src/scenarios/server/negative-mrtr.test.ts b/src/scenarios/server/negative-mrtr.test.ts index 871ad73b..95b42a61 100644 --- a/src/scenarios/server/negative-mrtr.test.ts +++ b/src/scenarios/server/negative-mrtr.test.ts @@ -14,7 +14,8 @@ import { InputRequiredResultResultTypeScenario, InputRequiredResultUnsupportedMethodsScenario, InputRequiredResultTamperedStateScenario, - InputRequiredResultCapabilityCheckScenario + InputRequiredResultCapabilityCheckScenario, + InputRequiredResultRequestStateScenario } from './input-required-result'; import { formatWireViolation, @@ -47,11 +48,15 @@ function getFreePort(): Promise { }); } -function startServer(scriptPath: string, port: number): Promise { +function startServer( + scriptPath: string, + port: number, + extraEnv: NodeJS.ProcessEnv = {} +): Promise { return new Promise((resolve, reject) => { const isWindows = process.platform === 'win32'; const proc = spawn('npx', ['tsx', scriptPath], { - env: { ...process.env, PORT: port.toString() }, + env: { ...process.env, ...extraEnv, PORT: port.toString() }, stdio: ['ignore', 'pipe', 'pipe'], shell: isWindows }); @@ -158,3 +163,55 @@ describe('SEP-2322 MRTR negative tests', () => { expect(capabilityCheck?.details?.untestable).toBe(true); }, 10000); }); + +// Issue #505: completion type alone does not establish fixture success. +describe('SEP-2322 request-state completion semantics', () => { + it.each([ + ['valid', 'SUCCESS'], + ['valid-second-text', 'SUCCESS'], + ['missing-marker', 'FAILURE'], + ['empty-content', 'FAILURE'], + ['tool-error', 'FAILURE'], + ['tool-error-with-marker', 'FAILURE'], + ['jsonrpc-error', 'FAILURE'], + ['input-required', 'FAILURE'] + ])( + 'request-state %s emits %s', + async (mode, expected) => { + const port = await getFreePort(); + let proc: ChildProcess | null = null; + try { + proc = await startServer( + path.join( + process.cwd(), + 'examples/servers/typescript/sep-2322-mrtr-broken-server.ts' + ), + port, + { MRTR_REQUEST_STATE_MODE: mode } + ); + const checks = await new InputRequiredResultRequestStateScenario().run( + testContext(`http://localhost:${port}/mcp`) + ); + // Prove the second check was reached with a valid round-1 prerequisite. + expect( + checks.find((c) => c.id === 'sep-2322-request-state-incomplete') + ?.status + ).toBe('SUCCESS'); + const complete = checks.filter( + (c) => c.id === 'sep-2322-request-state-complete' + ); + expect(complete).toHaveLength(1); + expect(complete[0].status).toBe(expected); + if (mode.startsWith('tool-error')) { + expect(complete[0].errorMessage).toContain('isError'); + } + if (mode === 'missing-marker' || mode === 'empty-content') { + expect(complete[0].errorMessage).toContain('state-ok'); + } + } finally { + await stopServer(proc); + } + }, + 20000 + ); +});