From 6e81e860adfafa4a28e41aaefbcdf027d975156f Mon Sep 17 00:00:00 2001 From: Caitie McCaffrey Date: Fri, 13 Mar 2026 11:28:58 -0700 Subject: [PATCH 01/11] initial agent suggested changes + refactoring --- src/scenarios/index.ts | 72 +- src/scenarios/server/client-helper.ts | 152 ++- src/scenarios/server/mrtr-ephemeral.ts | 1133 +++++++++++++++++++++++ src/scenarios/server/mrtr-helpers.ts | 132 +++ src/scenarios/server/mrtr-tasks.ts | 758 +++++++++++++++ src/scenarios/server/mrtr-transition.ts | 219 +++++ src/scenarios/server/mrtr-validation.ts | 474 ++++++++++ 7 files changed, 2935 insertions(+), 5 deletions(-) create mode 100644 src/scenarios/server/mrtr-ephemeral.ts create mode 100644 src/scenarios/server/mrtr-helpers.ts create mode 100644 src/scenarios/server/mrtr-tasks.ts create mode 100644 src/scenarios/server/mrtr-transition.ts create mode 100644 src/scenarios/server/mrtr-validation.ts diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index d67fae4..bb4eee0 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -53,6 +53,32 @@ import { import { DNSRebindingProtectionScenario } from './server/dns-rebinding'; +// MRTR scenarios (SEP-2322) +import { + MrtrEphemeralBasicElicitationScenario, + MrtrEphemeralBasicSamplingScenario, + MrtrEphemeralRequestStateScenario, + MrtrEphemeralMultipleInputRequestsScenario, + MrtrEphemeralMultiRoundScenario, + MrtrEphemeralRequestStateOnlyScenario, + MrtrEphemeralMissingInputResponseScenario, + MrtrEphemeralNonToolRequestScenario +} from './server/mrtr-ephemeral'; + +import { + MrtrPersistentBasicScenario, + MrtrPersistentInputResponseAckScenario, + MrtrPersistentBadInputResponseScenario, + MrtrPersistentInputResponseIncompleteScenario +} from './server/mrtr-persistent'; + +import { + MrtrIncompleteResultStructureScenario, + MrtrInputRequestTypesScenario +} from './server/mrtr-validation'; + +import { MrtrEphemeralToPersistentScenario } from './server/mrtr-transition'; + import { authScenariosList, backcompatScenariosList, @@ -69,7 +95,26 @@ const pendingClientScenariosList: ClientScenario[] = [ // On hold until server-side SSE improvements are made // https://github.com/modelcontextprotocol/typescript-sdk/pull/1129 - new ServerSSEPollingScenario() + new ServerSSEPollingScenario(), + + // MRTR scenarios (SEP-2322) — pending until a conformance test server + // implements MRTR tools. These are draft spec scenarios intended to be + // run via `--spec-version draft` against MRTR-capable servers. + new MrtrEphemeralBasicElicitationScenario(), + new MrtrEphemeralBasicSamplingScenario(), + new MrtrEphemeralRequestStateScenario(), + new MrtrEphemeralMultipleInputRequestsScenario(), + new MrtrEphemeralMultiRoundScenario(), + new MrtrEphemeralRequestStateOnlyScenario(), + new MrtrEphemeralMissingInputResponseScenario(), + new MrtrEphemeralNonToolRequestScenario(), + new MrtrPersistentBasicScenario(), + new MrtrPersistentInputResponseAckScenario(), + new MrtrPersistentBadInputResponseScenario(), + new MrtrPersistentInputResponseIncompleteScenario(), + new MrtrIncompleteResultStructureScenario(), + new MrtrInputRequestTypesScenario(), + new MrtrEphemeralToPersistentScenario() ]; // All client scenarios @@ -124,7 +169,30 @@ const allClientScenariosList: ClientScenario[] = [ new PromptsGetWithImageScenario(), // Security scenarios - new DNSRebindingProtectionScenario() + new DNSRebindingProtectionScenario(), + + // MRTR Ephemeral Workflow scenarios (SEP-2322) + new MrtrEphemeralBasicElicitationScenario(), + new MrtrEphemeralBasicSamplingScenario(), + new MrtrEphemeralRequestStateScenario(), + new MrtrEphemeralMultipleInputRequestsScenario(), + new MrtrEphemeralMultiRoundScenario(), + new MrtrEphemeralRequestStateOnlyScenario(), + new MrtrEphemeralMissingInputResponseScenario(), + new MrtrEphemeralNonToolRequestScenario(), + + // MRTR Persistent Workflow scenarios (SEP-2322) + new MrtrPersistentBasicScenario(), + new MrtrPersistentInputResponseAckScenario(), + new MrtrPersistentBadInputResponseScenario(), + new MrtrPersistentInputResponseIncompleteScenario(), + + // MRTR Validation scenarios (SEP-2322) + new MrtrIncompleteResultStructureScenario(), + new MrtrInputRequestTypesScenario(), + + // MRTR Transition scenarios (SEP-2322) + new MrtrEphemeralToPersistentScenario() ]; // Active client scenarios (excludes pending) diff --git a/src/scenarios/server/client-helper.ts b/src/scenarios/server/client-helper.ts index eebbd9b..b53f41d 100644 --- a/src/scenarios/server/client-helper.ts +++ b/src/scenarios/server/client-helper.ts @@ -1,5 +1,13 @@ /** - * Helper utilities for creating MCP clients to test servers + * Helper utilities for creating MCP clients to test servers. + * + * Provides two connection modes: + * 1. SDK-based (connectToServer) — uses the MCP TypeScript SDK for standard + * protocol operations. + * 2. Raw JSON-RPC (RawMcpSession) — uses undici HTTP for draft/experimental + * features that the SDK does not yet support. + * + * Both modes share the same SDK-based initialize handshake and session ID. */ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; @@ -8,14 +16,27 @@ import { LoggingMessageNotificationSchema, ProgressNotificationSchema } from '@modelcontextprotocol/sdk/types.js'; +import { request } from 'undici'; + +// ─── JSON-RPC Types ────────────────────────────────────────────────────────── + +export interface JsonRpcResponse { + jsonrpc: '2.0'; + id: number; + result?: Record; + error?: { code: number; message: string; data?: unknown }; +} + +// ─── SDK-based Connection ──────────────────────────────────────────────────── export interface MCPClientConnection { client: Client; + transport: StreamableHTTPClientTransport; close: () => Promise; } /** - * Create and connect an MCP client to a server + * Create and connect an MCP client to a server using the SDK. */ export async function connectToServer( serverUrl: string @@ -40,15 +61,140 @@ export async function connectToServer( return { client, + transport, close: async () => { await client.close(); } }; } +// ─── Raw JSON-RPC Session ──────────────────────────────────────────────────── + /** - * Helper to collect notifications (logging and progress) + * A raw MCP session for testing draft/experimental protocol features that the + * SDK does not yet support. Uses the SDK for the standard initialize handshake, + * then sends raw JSON-RPC over HTTP via undici for subsequent requests. + * + * Usage: + * const session = await createRawSession(serverUrl); + * const response = await session.send('tools/call', { name: 'my-tool', arguments: {} }); */ +export class RawMcpSession { + private nextId = 1; + private sessionId: string | undefined; + private serverUrl: string; + private connection: MCPClientConnection | null = null; + + constructor(serverUrl: string) { + this.serverUrl = serverUrl; + } + + /** + * Initialize the MCP session using the SDK's connectToServer(), + * then extract the session ID for subsequent raw requests. + */ + async initialize(): Promise { + this.connection = await connectToServer(this.serverUrl); + this.sessionId = this.connection.transport.sessionId; + } + + /** + * Send a JSON-RPC request via raw HTTP. + * Automatically manages session ID and auto-incrementing JSON-RPC IDs. + * Handles both JSON and SSE response formats. + */ + async send( + method: string, + params?: Record + ): Promise { + const id = this.nextId++; + + const headers: Record = { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream' + }; + if (this.sessionId) { + headers['mcp-session-id'] = this.sessionId; + } + + const body = JSON.stringify({ + jsonrpc: '2.0', + id, + method, + params + }); + + const response = await request(this.serverUrl, { + method: 'POST', + headers, + body + }); + + // Update session ID if server sends a new one + const sid = response.headers['mcp-session-id']; + if (sid) { + this.sessionId = Array.isArray(sid) ? sid[0] : sid; + } + + const contentType = response.headers['content-type'] ?? ''; + + // Handle SSE responses — parse the last JSON-RPC message from the stream + if (contentType.includes('text/event-stream')) { + const text = await response.body.text(); + return parseSseResponse(text); + } + + // Handle direct JSON responses + return (await response.body.json()) as JsonRpcResponse; + } + + /** + * Close the underlying SDK connection. + */ + async close(): Promise { + if (this.connection) { + await this.connection.close(); + this.connection = null; + } + } + + getSessionId(): string | undefined { + return this.sessionId; + } +} + +/** + * Create an initialized raw MCP session ready for testing. + */ +export async function createRawSession( + serverUrl: string +): Promise { + const session = new RawMcpSession(serverUrl); + await session.initialize(); + return session; +} + +/** + * Parse the last JSON-RPC message from an SSE response body. + */ +export function parseSseResponse(sseText: string): JsonRpcResponse { + const lines = sseText.split('\n'); + let lastData: string | null = null; + + for (const line of lines) { + if (line.startsWith('data: ')) { + lastData = line.slice(6); + } + } + + if (!lastData) { + throw new Error('No data found in SSE stream'); + } + + return JSON.parse(lastData) as JsonRpcResponse; +} + +// ─── Notification Collector ────────────────────────────────────────────────── export class NotificationCollector { private loggingNotifications: any[] = []; private progressNotifications: any[] = []; diff --git a/src/scenarios/server/mrtr-ephemeral.ts b/src/scenarios/server/mrtr-ephemeral.ts new file mode 100644 index 0000000..316516e --- /dev/null +++ b/src/scenarios/server/mrtr-ephemeral.ts @@ -0,0 +1,1133 @@ +/** + * SEP-2322: Multi Round-Trip Requests (MRTR) - Ephemeral Workflow Tests + * + * Tests the ephemeral (stateless) workflow where servers respond with + * IncompleteResult containing inputRequests and/or requestState, and + * clients retry with inputResponses and echoed requestState. + */ + +import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types'; +import { + createMrtrSession, + isIncompleteResult, + isCompleteResult, + mockElicitResponse, + mockSamplingResponse, + mockListRootsResponse, + MRTR_SPEC_REFERENCES +} from './mrtr-helpers'; + +// ─── A1: Basic Elicitation ──────────────────────────────────────────────────── + +export class MrtrEphemeralBasicElicitationScenario implements ClientScenario { + name = 'mrtr-ephemeral-basic-elicitation'; + specVersions: SpecVersion[] = ['draft']; + description = `Test basic ephemeral MRTR flow with a single elicitation input request (SEP-2322). + +**Server Implementation Requirements:** + +Implement a tool named \`test_tool_with_elicitation\` (no arguments required). + +**Behavior (Round 1):** When called without \`inputResponses\`, return an \`IncompleteResult\`: + +\`\`\`json +{ + "result_type": "incomplete", + "inputRequests": { + "user_name": { + "method": "elicitation/create", + "params": { + "message": "What is your name?", + "requestedSchema": { + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": ["name"] + } + } + } + } +} +\`\`\` + +**Behavior (Round 2):** When called with \`inputResponses\` containing the key \`"user_name"\`, return a complete result: + +\`\`\`json +{ + "content": [{ "type": "text", "text": "Hello, !" }] +} +\`\`\``; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + // Round 1: Initial call — expect IncompleteResult + const r1 = await session.send('tools/call', { + name: 'test_tool_with_elicitation', + arguments: {} + }); + + const r1Result = r1.result; + const r1Errors: string[] = []; + + if (r1.error) { + r1Errors.push(`JSON-RPC error: ${r1.error.message}`); + } else if (!r1Result) { + r1Errors.push('No result in response'); + } else if (!isIncompleteResult(r1Result)) { + r1Errors.push( + 'Expected IncompleteResult but got a complete result. ' + + 'Server should return result_type: "incomplete" with inputRequests.' + ); + } else { + if (!r1Result.inputRequests) { + r1Errors.push('IncompleteResult missing inputRequests'); + } else if (!r1Result.inputRequests['user_name']) { + r1Errors.push( + 'inputRequests missing expected key "user_name"' + ); + } else { + const req = r1Result.inputRequests['user_name']; + if (req.method !== 'elicitation/create') { + r1Errors.push( + `Expected method "elicitation/create", got "${req.method}"` + ); + } + } + } + + checks.push({ + id: 'mrtr-ephemeral-elicitation-incomplete', + name: 'MRTREphemeralElicitationIncomplete', + description: + 'Server returns IncompleteResult with elicitation inputRequest', + status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r1Errors.length > 0 ? r1Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r1Result } + }); + + // Round 2: Retry with inputResponses — expect complete result + if (r1Errors.length === 0 && isIncompleteResult(r1Result)) { + const r2 = await session.send('tools/call', { + name: 'test_mrtr_ephemeral_elicitation', + arguments: {}, + inputResponses: { + user_name: mockElicitResponse({ name: 'Alice' }) + }, + ...(r1Result.requestState !== undefined + ? { requestState: r1Result.requestState } + : {}) + }); + + const r2Result = r2.result; + const r2Errors: string[] = []; + + if (r2.error) { + r2Errors.push(`JSON-RPC error: ${r2.error.message}`); + } else if (!r2Result) { + r2Errors.push('No result in response'); + } else if (!isCompleteResult(r2Result)) { + r2Errors.push( + 'Expected complete result after retry with inputResponses' + ); + } else { + const content = r2Result.content as + | Array<{ type: string; text?: string }> + | undefined; + if (!content || !Array.isArray(content) || content.length === 0) { + r2Errors.push('Complete result missing content array'); + } + } + + checks.push({ + id: 'mrtr-ephemeral-elicitation-complete', + name: 'MRTREphemeralElicitationComplete', + description: + 'Server returns complete result after retry with inputResponses', + status: r2Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r2Errors.length > 0 ? r2Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r2Result } + }); + } + } catch (error) { + checks.push({ + id: 'mrtr-ephemeral-elicitation-incomplete', + name: 'MRTREphemeralElicitationIncomplete', + description: + 'Server returns IncompleteResult with elicitation inputRequest', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} + +// ─── A2: Basic Sampling ────────────────────────────────────────────────────── + +export class MrtrEphemeralBasicSamplingScenario implements ClientScenario { + name = 'mrtr-ephemeral-basic-sampling'; + specVersions: SpecVersion[] = ['draft']; + description = `Test basic ephemeral MRTR flow with a single sampling input request (SEP-2322). + +**Server Implementation Requirements:** + +Implement a tool named \`test_mrtr_ephemeral_sampling\` (no arguments required). + +**Behavior (Round 1):** When called without \`inputResponses\`, return an \`IncompleteResult\`: + +\`\`\`json +{ + "result_type": "incomplete", + "inputRequests": { + "capital_question": { + "method": "sampling/createMessage", + "params": { + "messages": [{ + "role": "user", + "content": { "type": "text", "text": "What is the capital of France?" } + }], + "maxTokens": 100 + } + } + } +} +\`\`\` + +**Behavior (Round 2):** When called with \`inputResponses\` containing the key \`"capital_question"\`, return a complete result with the sampling response text.`; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + // Round 1: Initial call + const r1 = await session.send('tools/call', { + name: 'test_mrtr_ephemeral_sampling', + arguments: {} + }); + + const r1Result = r1.result; + const r1Errors: string[] = []; + + if (r1.error) { + r1Errors.push(`JSON-RPC error: ${r1.error.message}`); + } else if (!r1Result) { + r1Errors.push('No result in response'); + } else if (!isIncompleteResult(r1Result)) { + r1Errors.push('Expected IncompleteResult with sampling inputRequest'); + } else { + if (!r1Result.inputRequests) { + r1Errors.push('IncompleteResult missing inputRequests'); + } else { + const key = Object.keys(r1Result.inputRequests)[0]; + if (!key) { + r1Errors.push('inputRequests map is empty'); + } else { + const req = r1Result.inputRequests[key]; + if (req.method !== 'sampling/createMessage') { + r1Errors.push( + `Expected method "sampling/createMessage", got "${req.method}"` + ); + } + } + } + } + + checks.push({ + id: 'mrtr-ephemeral-sampling-incomplete', + name: 'MRTREphemeralSamplingIncomplete', + description: + 'Server returns IncompleteResult with sampling inputRequest', + status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r1Errors.length > 0 ? r1Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r1Result } + }); + + // Round 2: Retry with inputResponses + if (r1Errors.length === 0 && isIncompleteResult(r1Result)) { + const inputKey = Object.keys(r1Result.inputRequests!)[0]; + const r2 = await session.send('tools/call', { + name: 'test_mrtr_ephemeral_sampling', + arguments: {}, + inputResponses: { + [inputKey]: mockSamplingResponse('The capital of France is Paris.') + }, + ...(r1Result.requestState !== undefined + ? { requestState: r1Result.requestState } + : {}) + }); + + const r2Result = r2.result; + const r2Errors: string[] = []; + + if (r2.error) { + r2Errors.push(`JSON-RPC error: ${r2.error.message}`); + } else if (!r2Result) { + r2Errors.push('No result in response'); + } else if (!isCompleteResult(r2Result)) { + r2Errors.push( + 'Expected complete result after retry with sampling response' + ); + } + + checks.push({ + id: 'mrtr-ephemeral-sampling-complete', + name: 'MRTREphemeralSamplingComplete', + description: + 'Server returns complete result after retry with sampling response', + status: r2Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r2Errors.length > 0 ? r2Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r2Result } + }); + } + } catch (error) { + checks.push({ + id: 'mrtr-ephemeral-sampling-incomplete', + name: 'MRTREphemeralSamplingIncomplete', + description: + 'Server returns IncompleteResult with sampling inputRequest', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} + +// ─── A3: Request State ─────────────────────────────────────────────────────── + +export class MrtrEphemeralRequestStateScenario implements ClientScenario { + name = 'mrtr-ephemeral-request-state'; + specVersions: SpecVersion[] = ['draft']; + description = `Test that requestState is correctly round-tripped in ephemeral MRTR flow (SEP-2322). + +**Server Implementation Requirements:** + +Implement a tool named \`test_mrtr_request_state\` (no arguments required). + +**Behavior (Round 1):** Return an \`IncompleteResult\` with both \`inputRequests\` and \`requestState\`: + +\`\`\`json +{ + "result_type": "incomplete", + "inputRequests": { + "confirm": { + "method": "elicitation/create", + "params": { + "message": "Please confirm", + "requestedSchema": { + "type": "object", + "properties": { "ok": { "type": "boolean" } }, + "required": ["ok"] + } + } + } + }, + "requestState": "" +} +\`\`\` + +**Behavior (Round 2):** When called with \`inputResponses\` AND the echoed \`requestState\`, validate the state and return a complete result. The text content MUST include the word "state-ok" to confirm the server received and validated the requestState.`; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + // Round 1 + const r1 = await session.send('tools/call', { + name: 'test_mrtr_request_state', + arguments: {} + }); + + const r1Result = r1.result; + const r1Errors: string[] = []; + + if (r1.error) { + r1Errors.push(`JSON-RPC error: ${r1.error.message}`); + } else if (!r1Result || !isIncompleteResult(r1Result)) { + r1Errors.push('Expected IncompleteResult'); + } else { + if (!r1Result.requestState) { + r1Errors.push('IncompleteResult missing requestState'); + } + if (typeof r1Result.requestState !== 'string') { + r1Errors.push('requestState must be a string'); + } + if (!r1Result.inputRequests) { + r1Errors.push('IncompleteResult missing inputRequests'); + } + } + + checks.push({ + id: 'mrtr-ephemeral-request-state-incomplete', + name: 'MRTRRequestStateIncomplete', + description: + 'Server returns IncompleteResult with both inputRequests and requestState', + status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r1Errors.length > 0 ? r1Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r1Result } + }); + + // Round 2: Retry with inputResponses + requestState + if (r1Errors.length === 0 && isIncompleteResult(r1Result)) { + const inputKey = Object.keys(r1Result.inputRequests!)[0]; + const r2 = await session.send('tools/call', { + name: 'test_mrtr_request_state', + arguments: {}, + inputResponses: { + [inputKey]: mockElicitResponse({ ok: true }) + }, + requestState: r1Result.requestState + }); + + const r2Result = r2.result; + const r2Errors: string[] = []; + + if (r2.error) { + r2Errors.push(`JSON-RPC error: ${r2.error.message}`); + } else if (!r2Result) { + r2Errors.push('No result in response'); + } else if (!isCompleteResult(r2Result)) { + r2Errors.push('Expected complete result after retry with requestState'); + } else { + // Check that server confirmed it received the state + const content = r2Result.content as + | Array<{ type: string; text?: string }> + | undefined; + const text = content?.find((c) => c.type === 'text')?.text ?? ''; + if (!text.includes('state-ok')) { + r2Errors.push( + 'Server response text should include "state-ok" to confirm requestState was validated' + ); + } + } + + checks.push({ + id: 'mrtr-ephemeral-request-state-complete', + name: 'MRTRRequestStateComplete', + description: + 'Server validates echoed requestState and returns complete result', + status: r2Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r2Errors.length > 0 ? r2Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r2Result } + }); + } + } catch (error) { + checks.push({ + id: 'mrtr-ephemeral-request-state-incomplete', + name: 'MRTRRequestStateIncomplete', + description: + 'Server returns IncompleteResult with both inputRequests and requestState', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} + +// ─── A4: Multiple Input Requests ───────────────────────────────────────────── + +export class MrtrEphemeralMultipleInputRequestsScenario + implements ClientScenario +{ + name = 'mrtr-ephemeral-multiple-input-requests'; + specVersions: SpecVersion[] = ['draft']; + description = `Test multiple input requests in a single IncompleteResult (SEP-2322). + +**Server Implementation Requirements:** + +Implement a tool named \`test_mrtr_multiple_inputs\` (no arguments required). + +**Behavior (Round 1):** Return an \`IncompleteResult\` with multiple \`inputRequests\` — at least one elicitation AND one sampling: + +\`\`\`json +{ + "result_type": "incomplete", + "inputRequests": { + "user_name": { + "method": "elicitation/create", + "params": { + "message": "What is your name?", + "requestedSchema": { + "type": "object", + "properties": { "name": { "type": "string" } }, + "required": ["name"] + } + } + }, + "greeting": { + "method": "sampling/createMessage", + "params": { + "messages": [{ "role": "user", "content": { "type": "text", "text": "Generate a greeting" } }], + "maxTokens": 50 + } + } + } +} +\`\`\` + +**Behavior (Round 2):** When called with \`inputResponses\` containing ALL keys, return a complete result.`; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + // Round 1 + const r1 = await session.send('tools/call', { + name: 'test_mrtr_multiple_inputs', + arguments: {} + }); + + const r1Result = r1.result; + const r1Errors: string[] = []; + + if (r1.error) { + r1Errors.push(`JSON-RPC error: ${r1.error.message}`); + } else if (!r1Result || !isIncompleteResult(r1Result)) { + r1Errors.push('Expected IncompleteResult'); + } else if (!r1Result.inputRequests) { + r1Errors.push('IncompleteResult missing inputRequests'); + } else { + const keys = Object.keys(r1Result.inputRequests); + if (keys.length < 2) { + r1Errors.push( + `Expected at least 2 inputRequests, got ${keys.length}` + ); + } + // Check that we have different method types + const methods = new Set( + keys.map((k) => r1Result.inputRequests![k].method) + ); + if (methods.size < 2) { + r1Errors.push( + 'Expected inputRequests with different method types (e.g., elicitation + sampling)' + ); + } + } + + checks.push({ + id: 'mrtr-ephemeral-multiple-inputs-incomplete', + name: 'MRTRMultipleInputsIncomplete', + description: + 'Server returns IncompleteResult with multiple inputRequests of different types', + status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r1Errors.length > 0 ? r1Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r1Result } + }); + + // Round 2: Respond to all input requests + if (r1Errors.length === 0 && isIncompleteResult(r1Result)) { + const inputResponses: Record = {}; + for (const [key, req] of Object.entries(r1Result.inputRequests!)) { + if (req.method === 'elicitation/create') { + inputResponses[key] = mockElicitResponse({ name: 'Alice' }); + } else if (req.method === 'sampling/createMessage') { + inputResponses[key] = mockSamplingResponse('Hello there!'); + } else if (req.method === 'roots/list') { + inputResponses[key] = mockListRootsResponse(); + } + } + + const r2 = await session.send('tools/call', { + name: 'test_mrtr_multiple_inputs', + arguments: {}, + inputResponses, + ...(r1Result.requestState !== undefined + ? { requestState: r1Result.requestState } + : {}) + }); + + const r2Result = r2.result; + const r2Errors: string[] = []; + + if (r2.error) { + r2Errors.push(`JSON-RPC error: ${r2.error.message}`); + } else if (!r2Result) { + r2Errors.push('No result in response'); + } else if (!isCompleteResult(r2Result)) { + r2Errors.push( + 'Expected complete result after providing all inputResponses' + ); + } + + checks.push({ + id: 'mrtr-ephemeral-multiple-inputs-complete', + name: 'MRTRMultipleInputsComplete', + description: + 'Server returns complete result after all inputResponses are provided', + status: r2Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r2Errors.length > 0 ? r2Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r2Result } + }); + } + } catch (error) { + checks.push({ + id: 'mrtr-ephemeral-multiple-inputs-incomplete', + name: 'MRTRMultipleInputsIncomplete', + description: + 'Server returns IncompleteResult with multiple inputRequests of different types', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} + +// ─── A5: Multi-Round ───────────────────────────────────────────────────────── + +export class MrtrEphemeralMultiRoundScenario implements ClientScenario { + name = 'mrtr-ephemeral-multi-round'; + specVersions: SpecVersion[] = ['draft']; + description = `Test multi-round ephemeral MRTR flow with evolving requestState (SEP-2322). + +**Server Implementation Requirements:** + +Implement a tool named \`test_mrtr_multi_round\` (no arguments required). + +**Behavior (Round 1):** Return an \`IncompleteResult\` with an elicitation request and \`requestState\`: + +\`\`\`json +{ + "result_type": "incomplete", + "inputRequests": { + "step1": { + "method": "elicitation/create", + "params": { + "message": "Step 1: What is your name?", + "requestedSchema": { + "type": "object", + "properties": { "name": { "type": "string" } }, + "required": ["name"] + } + } + } + }, + "requestState": "" +} +\`\`\` + +**Behavior (Round 2):** When called with \`inputResponses\` for step1 + requestState, return ANOTHER \`IncompleteResult\` with a new elicitation and updated requestState: + +\`\`\`json +{ + "result_type": "incomplete", + "inputRequests": { + "step2": { + "method": "elicitation/create", + "params": { + "message": "Step 2: What is your favorite color?", + "requestedSchema": { + "type": "object", + "properties": { "color": { "type": "string" } }, + "required": ["color"] + } + } + } + }, + "requestState": "" +} +\`\`\` + +**Behavior (Round 3):** When called with \`inputResponses\` for step2 + updated requestState, return a complete result.`; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + // Round 1 + const r1 = await session.send('tools/call', { + name: 'test_mrtr_multi_round', + arguments: {} + }); + + const r1Result = r1.result; + let r1Ok = false; + + if ( + !r1.error && + r1Result && + isIncompleteResult(r1Result) && + r1Result.inputRequests && + r1Result.requestState + ) { + r1Ok = true; + } + + checks.push({ + id: 'mrtr-ephemeral-multi-round-r1', + name: 'MRTRMultiRoundR1', + description: 'Round 1: Server returns IncompleteResult with requestState', + status: r1Ok ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r1Ok + ? undefined + : 'Expected IncompleteResult with inputRequests and requestState', + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r1Result } + }); + + if (!r1Ok || !isIncompleteResult(r1Result)) return checks; + + // Round 2: Retry — expect another IncompleteResult + const r1InputKey = Object.keys(r1Result.inputRequests!)[0]; + const r2 = await session.send('tools/call', { + name: 'test_mrtr_multi_round', + arguments: {}, + inputResponses: { + [r1InputKey]: mockElicitResponse({ name: 'Alice' }) + }, + requestState: r1Result.requestState + }); + + const r2Result = r2.result; + let r2Ok = false; + + if ( + !r2.error && + r2Result && + isIncompleteResult(r2Result) && + r2Result.inputRequests && + r2Result.requestState + ) { + // requestState should have changed + if (r2Result.requestState !== r1Result.requestState) { + r2Ok = true; + } + } + + checks.push({ + id: 'mrtr-ephemeral-multi-round-r2', + name: 'MRTRMultiRoundR2', + description: + 'Round 2: Server returns another IncompleteResult with updated requestState', + status: r2Ok ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r2Ok + ? undefined + : 'Expected new IncompleteResult with different requestState', + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r2Result } + }); + + if (!r2Ok || !isIncompleteResult(r2Result)) return checks; + + // Round 3: Final retry — expect complete result + const r2InputKey = Object.keys(r2Result.inputRequests!)[0]; + const r3 = await session.send('tools/call', { + name: 'test_mrtr_multi_round', + arguments: {}, + inputResponses: { + [r2InputKey]: mockElicitResponse({ color: 'blue' }) + }, + requestState: r2Result.requestState + }); + + const r3Result = r3.result; + const r3Ok = + !r3.error && r3Result != null && isCompleteResult(r3Result); + + checks.push({ + id: 'mrtr-ephemeral-multi-round-r3', + name: 'MRTRMultiRoundR3', + description: 'Round 3: Server returns complete result', + status: r3Ok ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r3Ok + ? undefined + : 'Expected complete result after final retry', + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r3Result } + }); + } catch (error) { + checks.push({ + id: 'mrtr-ephemeral-multi-round-r1', + name: 'MRTRMultiRoundR1', + description: 'Round 1: Server returns IncompleteResult with requestState', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} + +// ─── A6: Request State Only (Load Shedding) ────────────────────────────────── + +export class MrtrEphemeralRequestStateOnlyScenario implements ClientScenario { + name = 'mrtr-ephemeral-request-state-only'; + specVersions: SpecVersion[] = ['draft']; + description = `Test IncompleteResult with requestState only — no inputRequests (load-shedding use case, SEP-2322). + +**Server Implementation Requirements:** + +Implement a tool named \`test_mrtr_state_only\` (no arguments required). + +**Behavior (Round 1):** Return an \`IncompleteResult\` with \`requestState\` but NO \`inputRequests\`: + +\`\`\`json +{ + "result_type": "incomplete", + "requestState": "" +} +\`\`\` + +**Behavior (Round 2):** When called with the echoed \`requestState\` (no \`inputResponses\`), return a complete result. + +This simulates load shedding where the server transfers accumulated computation state to be resumed by another instance.`; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + // Round 1: Expect IncompleteResult with requestState only + const r1 = await session.send('tools/call', { + name: 'test_mrtr_state_only', + arguments: {} + }); + + const r1Result = r1.result; + const r1Errors: string[] = []; + + if (r1.error) { + r1Errors.push(`JSON-RPC error: ${r1.error.message}`); + } else if (!r1Result || !isIncompleteResult(r1Result)) { + r1Errors.push('Expected IncompleteResult'); + } else { + if (!r1Result.requestState) { + r1Errors.push('IncompleteResult missing requestState'); + } + if (r1Result.inputRequests) { + r1Errors.push( + 'Load-shedding IncompleteResult should NOT have inputRequests' + ); + } + } + + checks.push({ + id: 'mrtr-ephemeral-state-only-incomplete', + name: 'MRTRStateOnlyIncomplete', + description: + 'Server returns IncompleteResult with requestState only (no inputRequests)', + status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r1Errors.length > 0 ? r1Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r1Result } + }); + + // Round 2: Retry with requestState only + if (r1Errors.length === 0 && isIncompleteResult(r1Result)) { + const r2 = await session.send('tools/call', { + name: 'test_mrtr_state_only', + arguments: {}, + requestState: r1Result.requestState + }); + + const r2Result = r2.result; + const r2Ok = + !r2.error && r2Result != null && isCompleteResult(r2Result); + + checks.push({ + id: 'mrtr-ephemeral-state-only-complete', + name: 'MRTRStateOnlyComplete', + description: + 'Server completes after receiving echoed requestState (no inputResponses needed)', + status: r2Ok ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r2Ok + ? undefined + : 'Expected complete result after retry with requestState', + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r2Result } + }); + } + } catch (error) { + checks.push({ + id: 'mrtr-ephemeral-state-only-incomplete', + name: 'MRTRStateOnlyIncomplete', + description: + 'Server returns IncompleteResult with requestState only (no inputRequests)', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} + +// ─── A7: Missing Input Response ────────────────────────────────────────────── + +export class MrtrEphemeralMissingInputResponseScenario + implements ClientScenario +{ + name = 'mrtr-ephemeral-missing-input-response'; + specVersions: SpecVersion[] = ['draft']; + description = `Test error handling when client sends wrong/missing inputResponses (SEP-2322). + +**Server Implementation Requirements:** + +Use the same tool as A1: \`test_mrtr_ephemeral_elicitation\`. + +**Behavior:** When the client retries with \`inputResponses\` that are missing required keys or contain wrong keys, the server SHOULD respond with a new \`IncompleteResult\` re-requesting the missing information (NOT a JSON-RPC error).`; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + // Round 1: Get the initial IncompleteResult + const r1 = await session.send('tools/call', { + name: 'test_mrtr_ephemeral_elicitation', + arguments: {} + }); + + if ( + r1.error || + !r1.result || + !isIncompleteResult(r1.result) || + !r1.result.inputRequests + ) { + checks.push({ + id: 'mrtr-ephemeral-missing-response-prereq', + name: 'MRTRMissingResponsePrereq', + description: 'Prerequisite: Server returns IncompleteResult', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + 'Could not get initial IncompleteResult to test error handling', + specReferences: MRTR_SPEC_REFERENCES + }); + return checks; + } + + // Round 2: Send wrong inputResponses (wrong key) + const r2 = await session.send('tools/call', { + name: 'test_mrtr_ephemeral_elicitation', + arguments: {}, + inputResponses: { + wrong_key: mockElicitResponse({ data: 'wrong' }) + }, + ...(r1.result.requestState !== undefined + ? { requestState: r1.result.requestState } + : {}) + }); + + const r2Result = r2.result; + const r2Errors: string[] = []; + + if (r2.error) { + // A JSON-RPC error is acceptable but the SEP prefers re-requesting + r2Errors.push( + 'Server returned JSON-RPC error instead of re-requesting via IncompleteResult. ' + + 'SEP-2322 recommends servers re-request missing information.' + ); + } else if (!r2Result) { + r2Errors.push('No result in response'); + } else if (!isIncompleteResult(r2Result)) { + r2Errors.push( + 'Expected IncompleteResult re-requesting missing information, ' + + 'but got a complete result' + ); + } + + checks.push({ + id: 'mrtr-ephemeral-missing-response-rerequests', + name: 'MRTRMissingResponseRerequests', + description: + 'Server re-requests missing inputResponses via new IncompleteResult', + status: r2Errors.length === 0 ? 'SUCCESS' : 'WARNING', + timestamp: new Date().toISOString(), + errorMessage: r2Errors.length > 0 ? r2Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r2Result } + }); + } catch (error) { + checks.push({ + id: 'mrtr-ephemeral-missing-response-rerequests', + name: 'MRTRMissingResponseRerequests', + description: + 'Server re-requests missing inputResponses via new IncompleteResult', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} + +// ─── A8: Non-Tool Request (prompts/get) ────────────────────────────────────── + +export class MrtrEphemeralNonToolRequestScenario implements ClientScenario { + name = 'mrtr-ephemeral-non-tool-request'; + specVersions: SpecVersion[] = ['draft']; + description = `Test IncompleteResult on a non-tool request (prompts/get) to verify MRTR is universal (SEP-2322). + +**Server Implementation Requirements:** + +Implement a prompt named \`test_mrtr_prompt\` that requires elicitation input. + +**Behavior (Round 1):** When \`prompts/get\` is called for \`test_mrtr_prompt\` without \`inputResponses\`, return an \`IncompleteResult\`: + +\`\`\`json +{ + "result_type": "incomplete", + "inputRequests": { + "user_context": { + "method": "elicitation/create", + "params": { + "message": "What context should the prompt use?", + "requestedSchema": { + "type": "object", + "properties": { "context": { "type": "string" } }, + "required": ["context"] + } + } + } + } +} +\`\`\` + +**Behavior (Round 2):** When called with \`inputResponses\`, return a complete \`GetPromptResult\`.`; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + // Round 1 + const r1 = await session.send('prompts/get', { + name: 'test_mrtr_prompt' + }); + + const r1Result = r1.result; + const r1Errors: string[] = []; + + if (r1.error) { + r1Errors.push(`JSON-RPC error: ${r1.error.message}`); + } else if (!r1Result || !isIncompleteResult(r1Result)) { + r1Errors.push('Expected IncompleteResult from prompts/get'); + } else if (!r1Result.inputRequests) { + r1Errors.push('IncompleteResult missing inputRequests'); + } + + checks.push({ + id: 'mrtr-ephemeral-non-tool-incomplete', + name: 'MRTRNonToolIncomplete', + description: + 'prompts/get returns IncompleteResult with inputRequests', + status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r1Errors.length > 0 ? r1Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r1Result } + }); + + // Round 2: Retry with inputResponses + if (r1Errors.length === 0 && isIncompleteResult(r1Result)) { + const inputKey = Object.keys(r1Result.inputRequests!)[0]; + const r2 = await session.send('prompts/get', { + name: 'test_mrtr_prompt', + inputResponses: { + [inputKey]: mockElicitResponse({ context: 'test context' }) + }, + ...(r1Result.requestState !== undefined + ? { requestState: r1Result.requestState } + : {}) + }); + + const r2Result = r2.result; + const r2Errors: string[] = []; + + if (r2.error) { + r2Errors.push(`JSON-RPC error: ${r2.error.message}`); + } else if (!r2Result) { + r2Errors.push('No result in response'); + } else if (!isCompleteResult(r2Result)) { + r2Errors.push('Expected complete GetPromptResult after retry'); + } else if (!r2Result.messages) { + r2Errors.push('Complete result missing messages (expected GetPromptResult)'); + } + + checks.push({ + id: 'mrtr-ephemeral-non-tool-complete', + name: 'MRTRNonToolComplete', + description: + 'prompts/get returns complete GetPromptResult after retry with inputResponses', + status: r2Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r2Errors.length > 0 ? r2Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r2Result } + }); + } + } catch (error) { + checks.push({ + id: 'mrtr-ephemeral-non-tool-incomplete', + name: 'MRTRNonToolIncomplete', + description: + 'prompts/get returns IncompleteResult with inputRequests', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} diff --git a/src/scenarios/server/mrtr-helpers.ts b/src/scenarios/server/mrtr-helpers.ts new file mode 100644 index 0000000..b7083fd --- /dev/null +++ b/src/scenarios/server/mrtr-helpers.ts @@ -0,0 +1,132 @@ +/** + * MRTR-specific helpers for SEP-2322 conformance tests. + * + * Uses RawMcpSession from client-helper.ts for connection management and + * raw JSON-RPC transport. This file adds only MRTR-specific type guards, + * mock response builders, and convenience wrappers. + */ + +import { + RawMcpSession, + createRawSession, + JsonRpcResponse +} from './client-helper'; + +// Re-export generic session types under MRTR-specific aliases for convenience +export type MrtrSession = RawMcpSession; +export type { JsonRpcResponse }; + +// ─── MRTR Types ────────────────────────────────────────────────────────────── + +export interface IncompleteResult { + result_type?: 'incomplete'; + inputRequests?: Record; + requestState?: string; + _meta?: Record; + [key: string]: unknown; +} + +export interface InputRequestObject { + method: string; + params?: Record; +} + +// ─── MRTR Type Guards ──────────────────────────────────────────────────────── + +/** + * Check if a JSON-RPC result is an IncompleteResult. + */ +export function isIncompleteResult( + result: Record | undefined +): result is IncompleteResult { + if (!result) return false; + if (result.result_type === 'incomplete') return true; + // Also detect by presence of MRTR fields (for servers that may not set result_type) + return 'inputRequests' in result || 'requestState' in result; +} + +/** + * Check if a JSON-RPC result is a complete result (not incomplete). + */ +export function isCompleteResult( + result: Record | undefined +): boolean { + if (!result) return false; + return !isIncompleteResult(result); +} + +/** + * Extract inputRequests from an IncompleteResult. + */ +export function getInputRequests( + result: IncompleteResult +): Record | undefined { + return result.inputRequests; +} + +// ─── Mock Response Builders ────────────────────────────────────────────────── + +/** + * Build a mock elicitation response (ElicitResult). + */ +export function mockElicitResponse( + content: Record +): Record { + return { + action: 'accept', + content + }; +} + +/** + * Build a mock sampling response (CreateMessageResult). + */ +export function mockSamplingResponse(text: string): Record { + return { + role: 'assistant', + content: { + type: 'text', + text + }, + model: 'test-model', + stopReason: 'endTurn' + }; +} + +/** + * Build a mock list roots response (ListRootsResult). + */ +export function mockListRootsResponse(): Record { + return { + roots: [ + { + uri: 'file:///test/root', + name: 'Test Root' + } + ] + }; +} + +// ─── Session Factory ───────────────────────────────────────────────────────── + +/** + * Create an initialized MRTR session ready for testing. + * Delegates to createRawSession from client-helper.ts. + */ +export async function createMrtrSession( + serverUrl: string +): Promise { + return createRawSession(serverUrl); +} + +// ─── Spec References ───────────────────────────────────────────────────────── + +/** + * SEP reference for MRTR tests. + */ +export const MRTR_SPEC_REFERENCES = [ + { + id: 'SEP-2322', + url: 'https://github.com/modelcontextprotocol/specification/pull/2322' + } +]; diff --git a/src/scenarios/server/mrtr-tasks.ts b/src/scenarios/server/mrtr-tasks.ts new file mode 100644 index 0000000..6dc5725 --- /dev/null +++ b/src/scenarios/server/mrtr-tasks.ts @@ -0,0 +1,758 @@ +/** + * SEP-2322: Multi Round-Trip Requests (MRTR) - Persistent Workflow Tests + * + * Tests the persistent (task-based) workflow where servers use Tasks to + * manage long-running operations that require additional input via + * tasks/get → input_required → tasks/result → tasks/input_response. + */ + +import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types'; +import { + createMrtrSession, + isIncompleteResult, + isCompleteResult, + mockElicitResponse, + MRTR_SPEC_REFERENCES, + MrtrSession +} from './mrtr-helpers'; + +/** + * Poll tasks/get until the task reaches the expected status or times out. + */ +async function pollTaskStatus( + session: MrtrSession, + taskId: string, + expectedStatus: string, + maxAttempts: number = 20, + intervalMs: number = 250 +): Promise | null> { + for (let i = 0; i < maxAttempts; i++) { + const response = await session.send('tasks/get', { taskId }); + if (response.error) return null; + const result = response.result; + if (!result) return null; + if (result.status === expectedStatus) return result; + // If already completed/failed, stop polling + if ( + result.status === 'completed' || + result.status === 'failed' || + result.status === 'cancelled' + ) { + return result; + } + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + return null; +} + +// ─── B1: Basic Persistent Workflow ─────────────────────────────────────────── + +export class MrtrPersistentBasicScenario implements ClientScenario { + name = 'mrtr-persistent-basic'; + specVersions: SpecVersion[] = ['draft']; + description = `Test full persistent MRTR workflow via Tasks API (SEP-2322). + +**Server Implementation Requirements:** + +Implement a tool named \`test_mrtr_persistent\` that supports task-augmented execution. + +**Behavior:** +1. When called with \`task\` metadata, return a \`CreateTaskResult\` with \`status: "working"\` +2. After a brief period, set task status to \`"input_required"\` +3. When \`tasks/result\` is called, return an \`IncompleteResult\` with \`inputRequests\`: + +\`\`\`json +{ + "result_type": "incomplete", + "inputRequests": { + "user_input": { + "method": "elicitation/create", + "params": { + "message": "What input should the task use?", + "requestedSchema": { + "type": "object", + "properties": { "input": { "type": "string" } }, + "required": ["input"] + } + } + } + } +} +\`\`\` + +4. When \`tasks/input_response\` is called with \`inputResponses\`, acknowledge and resume +5. Set task status to \`"completed"\` +6. When \`tasks/result\` is called again, return the final result with tool content`; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + // Step 1: Call tool with task metadata + const r1 = await session.send('tools/call', { + name: 'test_mrtr_persistent', + arguments: {}, + task: { ttl: 30000 } + }); + + const r1Result = r1.result; + const r1Errors: string[] = []; + let taskId: string | undefined; + + if (r1.error) { + r1Errors.push(`JSON-RPC error: ${r1.error.message}`); + } else if (!r1Result) { + r1Errors.push('No result in response'); + } else { + const task = r1Result.task as + | { taskId?: string; status?: string } + | undefined; + if (!task?.taskId) { + r1Errors.push( + 'Expected CreateTaskResult with task.taskId' + ); + } else { + taskId = task.taskId; + if (task.status !== 'working') { + r1Errors.push( + `Expected initial task status "working", got "${task.status}"` + ); + } + } + } + + checks.push({ + id: 'mrtr-persistent-task-created', + name: 'MRTRPersistentTaskCreated', + description: 'Server creates task with working status', + status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r1Errors.length > 0 ? r1Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r1Result, taskId } + }); + + if (!taskId) return checks; + + // Step 2: Poll tasks/get until input_required + const taskState = await pollTaskStatus( + session, + taskId, + 'input_required' + ); + + const pollErrors: string[] = []; + if (!taskState) { + pollErrors.push( + 'Task did not reach input_required status within timeout' + ); + } else if (taskState.status !== 'input_required') { + pollErrors.push( + `Expected status "input_required", got "${taskState.status}"` + ); + } + + checks.push({ + id: 'mrtr-persistent-input-required', + name: 'MRTRPersistentInputRequired', + description: 'Task reaches input_required status', + status: pollErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + pollErrors.length > 0 ? pollErrors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { taskState } + }); + + if (pollErrors.length > 0) return checks; + + // Step 3: Call tasks/result to get inputRequests + const r3 = await session.send('tasks/result', { taskId }); + const r3Result = r3.result; + const r3Errors: string[] = []; + + if (r3.error) { + r3Errors.push(`JSON-RPC error: ${r3.error.message}`); + } else if (!r3Result) { + r3Errors.push('No result from tasks/result'); + } else if (!isIncompleteResult(r3Result)) { + r3Errors.push( + 'Expected IncompleteResult with inputRequests from tasks/result' + ); + } else if (!r3Result.inputRequests) { + r3Errors.push('IncompleteResult from tasks/result missing inputRequests'); + } + + checks.push({ + id: 'mrtr-persistent-tasks-result-incomplete', + name: 'MRTRPersistentTasksResultIncomplete', + description: + 'tasks/result returns IncompleteResult with inputRequests', + status: r3Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r3Errors.length > 0 ? r3Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r3Result } + }); + + if (r3Errors.length > 0 || !isIncompleteResult(r3Result)) return checks; + + // Step 4: Call tasks/input_response with inputResponses + const inputKey = Object.keys(r3Result.inputRequests!)[0]; + const r4 = await session.send('tasks/input_response', { + inputResponses: { + [inputKey]: mockElicitResponse({ input: 'Hello World!' }) + }, + _meta: { + 'io.modelcontextprotocol/related-task': { taskId } + } + }); + + const r4Errors: string[] = []; + if (r4.error) { + r4Errors.push(`JSON-RPC error: ${r4.error.message}`); + } + + checks.push({ + id: 'mrtr-persistent-input-response-sent', + name: 'MRTRPersistentInputResponseSent', + description: 'tasks/input_response is acknowledged by server', + status: r4Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r4Errors.length > 0 ? r4Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r4.result } + }); + + if (r4Errors.length > 0) return checks; + + // Step 5: Poll until completed + const completedState = await pollTaskStatus( + session, + taskId, + 'completed' + ); + + const compErrors: string[] = []; + if (!completedState) { + compErrors.push( + 'Task did not reach completed status within timeout' + ); + } else if (completedState.status !== 'completed') { + compErrors.push( + `Expected status "completed", got "${completedState.status}"` + ); + } + + checks.push({ + id: 'mrtr-persistent-completed', + name: 'MRTRPersistentCompleted', + description: 'Task reaches completed status after input_response', + status: compErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + compErrors.length > 0 ? compErrors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { taskState: completedState } + }); + + if (compErrors.length > 0) return checks; + + // Step 6: Get final result + const r6 = await session.send('tasks/result', { taskId }); + const r6Result = r6.result; + const r6Errors: string[] = []; + + if (r6.error) { + r6Errors.push(`JSON-RPC error: ${r6.error.message}`); + } else if (!r6Result) { + r6Errors.push('No result from final tasks/result'); + } else if (!isCompleteResult(r6Result)) { + r6Errors.push('Expected complete result from final tasks/result'); + } else if (!r6Result.content) { + r6Errors.push('Final result missing content'); + } + + checks.push({ + id: 'mrtr-persistent-final-result', + name: 'MRTRPersistentFinalResult', + description: 'tasks/result returns complete final result', + status: r6Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r6Errors.length > 0 ? r6Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r6Result } + }); + } catch (error) { + checks.push({ + id: 'mrtr-persistent-task-created', + name: 'MRTRPersistentTaskCreated', + description: 'Server creates task with working status', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} + +// ─── B2: Input Response Acknowledgment ─────────────────────────────────────── + +export class MrtrPersistentInputResponseAckScenario implements ClientScenario { + name = 'mrtr-persistent-input-response-ack'; + specVersions: SpecVersion[] = ['draft']; + description = `Test that tasks/input_response returns proper acknowledgment with task metadata (SEP-2322). + +**Server Implementation Requirements:** + +Use the same tool as B1: \`test_mrtr_persistent\`. + +**Validation:** The \`tasks/input_response\` acknowledgment SHOULD include \`_meta\` with \`io.modelcontextprotocol/related-task\` containing the \`taskId\`.`; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + // Create task + const r1 = await session.send('tools/call', { + name: 'test_mrtr_persistent', + arguments: {}, + task: { ttl: 30000 } + }); + + const task = r1.result?.task as + | { taskId?: string } + | undefined; + if (!task?.taskId) { + checks.push({ + id: 'mrtr-persistent-ack-prereq', + name: 'MRTRPersistentAckPrereq', + description: 'Prerequisite: Task creation', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: 'Could not create task', + specReferences: MRTR_SPEC_REFERENCES + }); + return checks; + } + + const taskId = task.taskId; + + // Wait for input_required + await pollTaskStatus(session, taskId, 'input_required'); + + // Get input requests + const r3 = await session.send('tasks/result', { taskId }); + if ( + r3.error || + !r3.result || + !isIncompleteResult(r3.result) || + !r3.result.inputRequests + ) { + checks.push({ + id: 'mrtr-persistent-ack-prereq', + name: 'MRTRPersistentAckPrereq', + description: 'Prerequisite: Get inputRequests', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + 'Could not get inputRequests from tasks/result', + specReferences: MRTR_SPEC_REFERENCES + }); + return checks; + } + + // Send input_response and check acknowledgment + const inputKey = Object.keys(r3.result.inputRequests!)[0]; + const r4 = await session.send('tasks/input_response', { + inputResponses: { + [inputKey]: mockElicitResponse({ input: 'test' }) + }, + _meta: { + 'io.modelcontextprotocol/related-task': { taskId } + } + }); + + const r4Result = r4.result; + const errors: string[] = []; + + if (r4.error) { + errors.push(`JSON-RPC error: ${r4.error.message}`); + } else if (!r4Result) { + errors.push('No result from tasks/input_response'); + } else { + // Check for task metadata in acknowledgment + const meta = r4Result._meta as + | Record + | undefined; + const relatedTask = meta?.[ + 'io.modelcontextprotocol/related-task' + ] as { taskId?: string } | undefined; + if (!relatedTask?.taskId) { + errors.push( + 'Acknowledgment missing _meta.io.modelcontextprotocol/related-task.taskId' + ); + } else if (relatedTask.taskId !== taskId) { + errors.push( + `taskId mismatch: expected "${taskId}", got "${relatedTask.taskId}"` + ); + } + } + + checks.push({ + id: 'mrtr-persistent-ack-structure', + name: 'MRTRPersistentAckStructure', + description: + 'tasks/input_response acknowledgment includes task metadata', + status: errors.length === 0 ? 'SUCCESS' : 'WARNING', + timestamp: new Date().toISOString(), + errorMessage: errors.length > 0 ? errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r4Result } + }); + } catch (error) { + checks.push({ + id: 'mrtr-persistent-ack-structure', + name: 'MRTRPersistentAckStructure', + description: + 'tasks/input_response acknowledgment includes task metadata', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} + +// ─── B3: Bad Input Response ────────────────────────────────────────────────── + +export class MrtrPersistentBadInputResponseScenario implements ClientScenario { + name = 'mrtr-persistent-bad-input-response'; + specVersions: SpecVersion[] = ['draft']; + description = `Test error handling when tasks/input_response contains wrong data (SEP-2322). + +**Server Implementation Requirements:** + +Use the same tool as B1: \`test_mrtr_persistent\`. + +**Behavior:** When the client sends \`tasks/input_response\` with incorrect keys, the server SHOULD acknowledge the message but keep the task in \`input_required\` status. The next \`tasks/result\` call should return a new \`inputRequests\` re-requesting the needed information.`; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + // Create task and wait for input_required + const r1 = await session.send('tools/call', { + name: 'test_mrtr_persistent', + arguments: {}, + task: { ttl: 30000 } + }); + + const task = r1.result?.task as + | { taskId?: string } + | undefined; + if (!task?.taskId) { + checks.push({ + id: 'mrtr-persistent-bad-input-prereq', + name: 'MRTRPersistentBadInputPrereq', + description: 'Prerequisite: Task creation', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: 'Could not create task', + specReferences: MRTR_SPEC_REFERENCES + }); + return checks; + } + + const taskId = task.taskId; + await pollTaskStatus(session, taskId, 'input_required'); + + // Get input requests + const r3 = await session.send('tasks/result', { taskId }); + if ( + r3.error || + !r3.result || + !isIncompleteResult(r3.result) || + !r3.result.inputRequests + ) { + checks.push({ + id: 'mrtr-persistent-bad-input-prereq', + name: 'MRTRPersistentBadInputPrereq', + description: 'Prerequisite: Get inputRequests', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: 'Could not get inputRequests', + specReferences: MRTR_SPEC_REFERENCES + }); + return checks; + } + + // Send wrong inputResponses + const r4 = await session.send('tasks/input_response', { + inputResponses: { + wrong_key: mockElicitResponse({ wrong: 'data' }) + }, + _meta: { + 'io.modelcontextprotocol/related-task': { taskId } + } + }); + + const ackErrors: string[] = []; + if (r4.error) { + // Some servers may error; that's acceptable + ackErrors.push( + 'Server returned error for bad input (acceptable but not preferred)' + ); + } + + // Check task is still input_required + const stateAfter = await session.send('tasks/get', { taskId }); + const stillInputRequired = + stateAfter.result?.status === 'input_required'; + + // Try to get new inputRequests + let newInputRequests = false; + if (stillInputRequired) { + const r5 = await session.send('tasks/result', { taskId }); + if ( + r5.result && + isIncompleteResult(r5.result) && + r5.result.inputRequests + ) { + newInputRequests = true; + } + } + + const errors: string[] = []; + if (!stillInputRequired && ackErrors.length === 0) { + errors.push( + 'Task should remain in input_required after bad inputResponses' + ); + } + if (stillInputRequired && !newInputRequests) { + errors.push( + 'tasks/result should return new inputRequests after bad input_response' + ); + } + + checks.push({ + id: 'mrtr-persistent-bad-input-rerequests', + name: 'MRTRPersistentBadInputRerequests', + description: + 'Server keeps task in input_required and re-requests after bad inputResponses', + status: + errors.length === 0 + ? ackErrors.length === 0 + ? 'SUCCESS' + : 'WARNING' + : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + [...errors, ...ackErrors].length > 0 + ? [...errors, ...ackErrors].join('; ') + : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { + stillInputRequired, + newInputRequests, + ackResult: r4.result + } + }); + } catch (error) { + checks.push({ + id: 'mrtr-persistent-bad-input-rerequests', + name: 'MRTRPersistentBadInputRerequests', + description: + 'Server keeps task in input_required and re-requests after bad inputResponses', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} + +// ─── B4: tasks/input_response returning IncompleteResult ───────────────────── + +export class MrtrPersistentInputResponseIncompleteScenario + implements ClientScenario +{ + name = 'mrtr-persistent-input-response-incomplete'; + specVersions: SpecVersion[] = ['draft']; + description = `Test that tasks/input_response can itself return an IncompleteResult (SEP-2322). + +**Server Implementation Requirements:** + +Implement a tool named \`test_mrtr_persistent_multi_input\` that supports task-augmented execution and requires TWO rounds of input. + +**Behavior:** +1. Create task, transition to \`input_required\` +2. \`tasks/result\` returns IncompleteResult with first \`inputRequests\` +3. \`tasks/input_response\` returns an \`IncompleteResult\` with ADDITIONAL \`inputRequests\` +4. Client sends another \`tasks/input_response\` with the additional responses +5. Task completes + +This tests the schema: \`TaskInputResponseResultResponse.result: Result | IncompleteResult\``; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + // Create task + const r1 = await session.send('tools/call', { + name: 'test_mrtr_persistent_multi_input', + arguments: {}, + task: { ttl: 30000 } + }); + + const task = r1.result?.task as + | { taskId?: string } + | undefined; + if (!task?.taskId) { + checks.push({ + id: 'mrtr-persistent-multi-input-prereq', + name: 'MRTRPersistentMultiInputPrereq', + description: 'Prerequisite: Task creation', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: 'Could not create task', + specReferences: MRTR_SPEC_REFERENCES + }); + return checks; + } + + const taskId = task.taskId; + await pollTaskStatus(session, taskId, 'input_required'); + + // Get first inputRequests + const r3 = await session.send('tasks/result', { taskId }); + if ( + r3.error || + !r3.result || + !isIncompleteResult(r3.result) || + !r3.result.inputRequests + ) { + checks.push({ + id: 'mrtr-persistent-multi-input-prereq', + name: 'MRTRPersistentMultiInputPrereq', + description: 'Prerequisite: Get first inputRequests', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: 'Could not get inputRequests from tasks/result', + specReferences: MRTR_SPEC_REFERENCES + }); + return checks; + } + + // Send first input_response — expect IncompleteResult back + const inputKey1 = Object.keys(r3.result.inputRequests!)[0]; + const r4 = await session.send('tasks/input_response', { + inputResponses: { + [inputKey1]: mockElicitResponse({ input: 'step1' }) + }, + _meta: { + 'io.modelcontextprotocol/related-task': { taskId } + } + }); + + const r4Result = r4.result; + const r4Errors: string[] = []; + + if (r4.error) { + r4Errors.push(`JSON-RPC error: ${r4.error.message}`); + } else if (!r4Result) { + r4Errors.push('No result from tasks/input_response'); + } else if (!isIncompleteResult(r4Result)) { + r4Errors.push( + 'Expected IncompleteResult from tasks/input_response (additional input needed)' + ); + } else if (!r4Result.inputRequests) { + r4Errors.push( + 'IncompleteResult from tasks/input_response missing inputRequests' + ); + } + + checks.push({ + id: 'mrtr-persistent-input-response-returns-incomplete', + name: 'MRTRPersistentInputResponseReturnsIncomplete', + description: + 'tasks/input_response returns IncompleteResult with additional inputRequests', + status: r4Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r4Errors.length > 0 ? r4Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r4Result } + }); + + // Send second input_response — expect completion + if (r4Errors.length === 0 && isIncompleteResult(r4Result)) { + const inputKey2 = Object.keys(r4Result.inputRequests!)[0]; + const r5 = await session.send('tasks/input_response', { + inputResponses: { + [inputKey2]: mockElicitResponse({ input: 'step2' }) + }, + _meta: { + 'io.modelcontextprotocol/related-task': { taskId } + } + }); + + const r5Ok = !r5.error; + + // Poll for completion + if (r5Ok) { + const finalState = await pollTaskStatus( + session, + taskId, + 'completed' + ); + + checks.push({ + id: 'mrtr-persistent-multi-input-completed', + name: 'MRTRPersistentMultiInputCompleted', + description: + 'Task completes after second input_response', + status: + finalState?.status === 'completed' ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + finalState?.status !== 'completed' + ? 'Task did not complete after second input_response' + : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { taskState: finalState } + }); + } + } + } catch (error) { + checks.push({ + id: 'mrtr-persistent-input-response-returns-incomplete', + name: 'MRTRPersistentInputResponseReturnsIncomplete', + description: + 'tasks/input_response returns IncompleteResult with additional inputRequests', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} diff --git a/src/scenarios/server/mrtr-transition.ts b/src/scenarios/server/mrtr-transition.ts new file mode 100644 index 0000000..b95bbb4 --- /dev/null +++ b/src/scenarios/server/mrtr-transition.ts @@ -0,0 +1,219 @@ +/** + * SEP-2322: Multi Round-Trip Requests (MRTR) - Ephemeral-to-Persistent Transition Test + * + * Tests the transition from an ephemeral workflow to a persistent (task-based) + * workflow, as described in the SEP section "Interactions Between Ephemeral and + * Persistent Workflows." + */ + +import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types'; +import { + createMrtrSession, + isIncompleteResult, + mockElicitResponse, + MRTR_SPEC_REFERENCES, + MrtrSession +} from './mrtr-helpers'; + +/** + * Poll tasks/get until the task reaches the expected status or times out. + */ +async function pollTaskStatus( + session: MrtrSession, + taskId: string, + expectedStatus: string, + maxAttempts: number = 20, + intervalMs: number = 250 +): Promise | null> { + for (let i = 0; i < maxAttempts; i++) { + const response = await session.send('tasks/get', { taskId }); + if (response.error) return null; + const result = response.result; + if (!result) return null; + if (result.status === expectedStatus) return result; + if ( + result.status === 'completed' || + result.status === 'failed' || + result.status === 'cancelled' + ) { + return result; + } + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + return null; +} + +// ─── D1: Ephemeral-to-Persistent Transition ───────────────────────────────── + +export class MrtrEphemeralToPersistentScenario implements ClientScenario { + name = 'mrtr-ephemeral-to-persistent'; + specVersions: SpecVersion[] = ['draft']; + description = `Test transition from ephemeral to persistent workflow (SEP-2322). + +**Server Implementation Requirements:** + +Implement a tool named \`test_mrtr_transition\` that demonstrates the ephemeral-to-persistent workflow transition. + +**Behavior:** +1. When called with \`task\` metadata in params, the server initially responds with an ephemeral \`IncompleteResult\` (with \`inputRequests\`) rather than creating a task immediately +2. When the client retries the \`tools/call\` with \`inputResponses\` AND \`task\` metadata, the server now creates a persistent task and returns a \`CreateTaskResult\` with a task ID +3. The task can then be managed via the Tasks API + +This tests the pattern where a server gathers required input via ephemeral MRTR before committing to task creation, as described in the SEP section "Interactions Between Ephemeral and Persistent Workflows."`; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + // Step 1: Call tool with task metadata — server responds ephemerally + const r1 = await session.send('tools/call', { + name: 'test_mrtr_transition', + arguments: {}, + task: { ttl: 30000 } + }); + + const r1Result = r1.result; + const ephErrors: string[] = []; + + if (r1.error) { + ephErrors.push(`JSON-RPC error: ${r1.error.message}`); + } else if (!r1Result) { + ephErrors.push('No result in response'); + } else if (!isIncompleteResult(r1Result)) { + ephErrors.push( + 'Expected initial IncompleteResult (ephemeral response despite task metadata)' + ); + } else if (!r1Result.inputRequests) { + ephErrors.push('IncompleteResult missing inputRequests'); + } else { + // Verify there is NO task in the response — it should be ephemeral + if (r1Result.task) { + ephErrors.push( + 'Ephemeral step should not include task — expected no task creation yet' + ); + } + } + + checks.push({ + id: 'mrtr-transition-ephemeral-phase', + name: 'MRTRTransitionEphemeralPhase', + description: + 'Server responds with ephemeral IncompleteResult before creating task', + status: ephErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + ephErrors.length > 0 ? ephErrors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r1Result } + }); + + if (ephErrors.length > 0 || !isIncompleteResult(r1Result)) return checks; + + // Step 2: Retry with inputResponses + task metadata — server creates task + const inputKey = Object.keys( + r1Result.inputRequests as Record + )[0]; + const r2 = await session.send('tools/call', { + name: 'test_mrtr_transition', + arguments: {}, + inputResponses: { + [inputKey]: mockElicitResponse({ confirmed: true }) + }, + requestState: + typeof r1Result.requestState === 'string' + ? r1Result.requestState + : undefined, + task: { ttl: 30000 } + }); + + const r2Result = r2.result; + const transErrors: string[] = []; + let taskId: string | undefined; + + if (r2.error) { + transErrors.push(`JSON-RPC error: ${r2.error.message}`); + } else if (!r2Result) { + transErrors.push('No result from retry'); + } else { + // Should now have a task (persistent workflow) + const task = r2Result.task as + | { taskId?: string; status?: string } + | undefined; + if (!task?.taskId) { + transErrors.push( + 'Expected CreateTaskResult with task.taskId after providing input' + ); + } else { + taskId = task.taskId; + } + } + + checks.push({ + id: 'mrtr-transition-task-created', + name: 'MRTRTransitionTaskCreated', + description: + 'Server transitions to persistent workflow and creates task', + status: transErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + transErrors.length > 0 ? transErrors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r2Result, taskId } + }); + + if (!taskId) return checks; + + // Step 3: Verify the task is accessible via Tasks API + const taskState = await pollTaskStatus( + session, + taskId, + 'completed', + 40, + 250 + ); + + const taskErrors: string[] = []; + if (!taskState) { + taskErrors.push( + 'Could not retrieve task state via tasks/get' + ); + } else if ( + taskState.status !== 'completed' && + taskState.status !== 'working' + ) { + // Accept working or completed — just verify the task is real + taskErrors.push( + `Unexpected task status: "${taskState.status}"` + ); + } + + checks.push({ + id: 'mrtr-transition-task-accessible', + name: 'MRTRTransitionTaskAccessible', + description: + 'Created task is accessible via Tasks API after transition', + status: taskErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + taskErrors.length > 0 ? taskErrors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { taskState } + }); + } catch (error) { + checks.push({ + id: 'mrtr-transition-ephemeral-phase', + name: 'MRTRTransitionEphemeralPhase', + description: + 'Server responds with ephemeral IncompleteResult before creating task', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} diff --git a/src/scenarios/server/mrtr-validation.ts b/src/scenarios/server/mrtr-validation.ts new file mode 100644 index 0000000..102a6a7 --- /dev/null +++ b/src/scenarios/server/mrtr-validation.ts @@ -0,0 +1,474 @@ +/** + * SEP-2322: Multi Round-Trip Requests (MRTR) - Schema/Structure Validation Tests + * + * Tests that validate the structure and correctness of MRTR protocol + * messages, including IncompleteResult format, InputRequest types, and + * result_type field behavior. + */ + +import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types'; +import { + createMrtrSession, + isIncompleteResult, + mockElicitResponse, + mockSamplingResponse, + mockListRootsResponse, + MRTR_SPEC_REFERENCES +} from './mrtr-helpers'; + +// ─── C1: IncompleteResult Structure Validation ────────────────────────────── + +export class MrtrIncompleteResultStructureScenario implements ClientScenario { + name = 'mrtr-incomplete-result-structure'; + specVersions: SpecVersion[] = ['draft']; + description = `Validate the IncompleteResult structure conforms to the schema (SEP-2322). + +**Server Implementation Requirements:** + +Implement a tool named \`test_mrtr_validate_structure\` that returns an \`IncompleteResult\` with well-formed fields. + +**Behavior:** +1. When called, return an \`IncompleteResult\` with both \`inputRequests\` and \`requestState\`: + +\`\`\`json +{ + "result_type": "incomplete", + "inputRequests": { + "req1": { + "method": "elicitation/create", + "params": { + "message": "Validation test", + "requestedSchema": { + "type": "object", + "properties": { "value": { "type": "string" } }, + "required": ["value"] + } + } + } + }, + "requestState": "validation-state-token" +} +\`\`\` + +2. When retried with correct \`inputResponses\` and \`requestState\`, return a final result with \`result_type\` absent (testing default behavior). + +**Validation checks:** +- \`result_type\` is exactly \`"incomplete"\` (not any other string) +- \`inputRequests\` is present and is a map (object) +- Each \`inputRequests\` value has \`method\` and \`params\` +- \`requestState\` is a string +- Final result has \`result_type\` absent (backward compat for "complete")`; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + // Get IncompleteResult + const r1 = await session.send('tools/call', { + name: 'test_mrtr_validate_structure', + arguments: {} + }); + + const r1Result = r1.result; + const structureErrors: string[] = []; + + if (r1.error) { + structureErrors.push(`JSON-RPC error: ${r1.error.message}`); + } else if (!r1Result) { + structureErrors.push('No result in response'); + } else { + // Check result_type is exactly "incomplete" + if (r1Result.result_type !== 'incomplete') { + structureErrors.push( + `result_type should be "incomplete", got "${r1Result.result_type}"` + ); + } + + // Check inputRequests is present and is an object + if (!r1Result.inputRequests) { + structureErrors.push('inputRequests is missing'); + } else if ( + typeof r1Result.inputRequests !== 'object' || + Array.isArray(r1Result.inputRequests) + ) { + structureErrors.push( + 'inputRequests should be an object (map), not an array' + ); + } else { + // Validate each input request has method and params + const requests = r1Result.inputRequests as Record< + string, + Record + >; + for (const [key, value] of Object.entries(requests)) { + if (!value.method || typeof value.method !== 'string') { + structureErrors.push( + `inputRequests["${key}"] missing valid "method" field` + ); + } + if (!value.params || typeof value.params !== 'object') { + structureErrors.push( + `inputRequests["${key}"] missing valid "params" field` + ); + } + } + } + + // Check requestState if present + if ( + 'requestState' in r1Result && + typeof r1Result.requestState !== 'string' + ) { + structureErrors.push( + `requestState should be a string, got ${typeof r1Result.requestState}` + ); + } + } + + checks.push({ + id: 'mrtr-validate-incomplete-result-fields', + name: 'MRTRValidateIncompleteResultFields', + description: + 'IncompleteResult has correct result_type, inputRequests, and requestState', + status: structureErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + structureErrors.length > 0 + ? structureErrors.join('; ') + : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r1Result } + }); + + // Check backward compatibility: final result with absent result_type + if (r1Result && isIncompleteResult(r1Result)) { + const inputKey = Object.keys( + r1Result.inputRequests as Record + )[0]; + const r2 = await session.send('tools/call', { + name: 'test_mrtr_validate_structure', + arguments: {}, + inputResponses: { + [inputKey]: mockElicitResponse({ value: 'test' }) + }, + requestState: + typeof r1Result.requestState === 'string' + ? r1Result.requestState + : undefined + }); + + const r2Result = r2.result; + const compatErrors: string[] = []; + + if (r2.error) { + compatErrors.push(`JSON-RPC error: ${r2.error.message}`); + } else if (!r2Result) { + compatErrors.push('No result from retry'); + } else { + // result_type should be absent or "complete" for backward compat + if ( + 'result_type' in r2Result && + r2Result.result_type !== 'complete' && + r2Result.result_type !== undefined + ) { + compatErrors.push( + `Final result should have result_type absent or "complete", got "${r2Result.result_type}"` + ); + } + } + + checks.push({ + id: 'mrtr-validate-complete-result-default', + name: 'MRTRValidateCompleteResultDefault', + description: + 'Complete result has result_type absent or "complete" (backward compat)', + status: compatErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + compatErrors.length > 0 + ? compatErrors.join('; ') + : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r2Result } + }); + } + } catch (error) { + checks.push({ + id: 'mrtr-validate-incomplete-result-fields', + name: 'MRTRValidateIncompleteResultFields', + description: + 'IncompleteResult has correct result_type, inputRequests, and requestState', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} + +// ─── C2: InputRequest Types Validation ─────────────────────────────────────── + +export class MrtrInputRequestTypesScenario implements ClientScenario { + name = 'mrtr-input-request-types'; + specVersions: SpecVersion[] = ['draft']; + description = `Validate all three InputRequest types in IncompleteResult. + +**Server Implementation Requirements:** + +Implement a tool named \`test_mrtr_input_types\` that returns an \`IncompleteResult\` containing all three types of \`InputRequest\`. + +**Behavior:** + +When called, return: + +\`\`\`json +{ + "result_type": "incomplete", + "inputRequests": { + "elicit": { + "method": "elicitation/create", + "params": { + "message": "Please provide a value", + "requestedSchema": { + "type": "object", + "properties": { "value": { "type": "string" } }, + "required": ["value"] + } + } + }, + "sample": { + "method": "sampling/createMessage", + "params": { + "messages": [ + { "role": "user", "content": { "type": "text", "text": "Generate a response" } } + ], + "maxTokens": 100 + } + }, + "roots": { + "method": "roots/list", + "params": {} + } + } +} +\`\`\` + +When retried with valid \`inputResponses\` for all three, return a final result.`; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + const r1 = await session.send('tools/call', { + name: 'test_mrtr_input_types', + arguments: {} + }); + + const r1Result = r1.result; + + if (r1.error || !r1Result || !isIncompleteResult(r1Result)) { + checks.push({ + id: 'mrtr-validate-input-types-prereq', + name: 'MRTRValidateInputTypesPrereq', + description: 'Prerequisite: Get IncompleteResult', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r1.error + ? `JSON-RPC error: ${r1.error.message}` + : 'Expected IncompleteResult', + specReferences: MRTR_SPEC_REFERENCES + }); + return checks; + } + + const inputRequests = r1Result.inputRequests as + | Record> + | undefined; + + if (!inputRequests) { + checks.push({ + id: 'mrtr-validate-input-types-prereq', + name: 'MRTRValidateInputTypesPrereq', + description: 'Prerequisite: inputRequests present', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: 'inputRequests missing from IncompleteResult', + specReferences: MRTR_SPEC_REFERENCES + }); + return checks; + } + + // Find each type of InputRequest + const foundTypes: Record }> = {}; + for (const [key, value] of Object.entries(inputRequests)) { + const method = value.method as string; + if (method === 'elicitation/create') foundTypes['elicitation'] = { key, request: value }; + else if (method === 'sampling/createMessage') foundTypes['sampling'] = { key, request: value }; + else if (method === 'roots/list') foundTypes['roots'] = { key, request: value }; + } + + // Check elicitation + const elicitErrors: string[] = []; + if (!foundTypes['elicitation']) { + elicitErrors.push('No elicitation/create InputRequest found'); + } else { + const params = foundTypes['elicitation'].request.params as + | Record + | undefined; + if (!params) { + elicitErrors.push('elicitation/create missing params'); + } else { + if (typeof params.message !== 'string') { + elicitErrors.push( + 'elicitation/create params.message should be a string' + ); + } + if (!params.requestedSchema || typeof params.requestedSchema !== 'object') { + elicitErrors.push( + 'elicitation/create params.requestedSchema should be an object' + ); + } + } + } + + checks.push({ + id: 'mrtr-validate-elicitation-input-request', + name: 'MRTRValidateElicitationInputRequest', + description: + 'elicitation/create InputRequest has valid structure', + status: elicitErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + elicitErrors.length > 0 ? elicitErrors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { request: foundTypes['elicitation']?.request } + }); + + // Check sampling + const samplingErrors: string[] = []; + if (!foundTypes['sampling']) { + samplingErrors.push('No sampling/createMessage InputRequest found'); + } else { + const params = foundTypes['sampling'].request.params as + | Record + | undefined; + if (!params) { + samplingErrors.push('sampling/createMessage missing params'); + } else { + if (!Array.isArray(params.messages)) { + samplingErrors.push( + 'sampling/createMessage params.messages should be an array' + ); + } + if (typeof params.maxTokens !== 'number') { + samplingErrors.push( + 'sampling/createMessage params.maxTokens should be a number' + ); + } + } + } + + checks.push({ + id: 'mrtr-validate-sampling-input-request', + name: 'MRTRValidateSamplingInputRequest', + description: + 'sampling/createMessage InputRequest has valid structure', + status: samplingErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + samplingErrors.length > 0 ? samplingErrors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { request: foundTypes['sampling']?.request } + }); + + // Check roots + const rootsErrors: string[] = []; + if (!foundTypes['roots']) { + rootsErrors.push('No roots/list InputRequest found'); + } else { + // roots/list has minimal params, just check structure + if (!foundTypes['roots'].request.params) { + rootsErrors.push('roots/list missing params'); + } + } + + checks.push({ + id: 'mrtr-validate-roots-input-request', + name: 'MRTRValidateRootsInputRequest', + description: 'roots/list InputRequest has valid structure', + status: rootsErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + rootsErrors.length > 0 ? rootsErrors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { request: foundTypes['roots']?.request } + }); + + // Retry with responses for all three types + const inputResponses: Record = {}; + if (foundTypes['elicitation']) { + inputResponses[foundTypes['elicitation'].key] = + mockElicitResponse({ value: 'test' }); + } + if (foundTypes['sampling']) { + inputResponses[foundTypes['sampling'].key] = + mockSamplingResponse('Generated response text'); + } + if (foundTypes['roots']) { + inputResponses[foundTypes['roots'].key] = + mockListRootsResponse(); + } + + const r2 = await session.send('tools/call', { + name: 'test_mrtr_input_types', + arguments: {}, + inputResponses, + requestState: + typeof r1Result.requestState === 'string' + ? r1Result.requestState + : undefined + }); + + const retryErrors: string[] = []; + if (r2.error) { + retryErrors.push(`JSON-RPC error: ${r2.error.message}`); + } else if (!r2.result) { + retryErrors.push('No result from retry'); + } else if (isIncompleteResult(r2.result)) { + retryErrors.push('Expected complete result after providing all inputResponses'); + } + + checks.push({ + id: 'mrtr-validate-all-types-retry', + name: 'MRTRValidateAllTypesRetry', + description: + 'Retry with all three InputResponse types produces final result', + status: retryErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + retryErrors.length > 0 ? retryErrors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r2.result } + }); + } catch (error) { + checks.push({ + id: 'mrtr-validate-input-types-prereq', + name: 'MRTRValidateInputTypesPrereq', + description: 'Prerequisite: Get IncompleteResult with InputRequests', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} From c481e98425ffb9171c9377be0b66d1ab00d95d87 Mon Sep 17 00:00:00 2001 From: Caitie McCaffrey Date: Fri, 13 Mar 2026 11:31:06 -0700 Subject: [PATCH 02/11] initial agent suggested changes + refactoring --- src/scenarios/server/mrtr-ephemeral.ts | 27 +++---- .../{mrtr-tasks.ts => mrtr-persistent.ts} | 71 ++++++------------- src/scenarios/server/mrtr-transition.ts | 14 ++-- src/scenarios/server/mrtr-validation.ts | 50 +++++++------ 4 files changed, 67 insertions(+), 95 deletions(-) rename src/scenarios/server/{mrtr-tasks.ts => mrtr-persistent.ts} (93%) diff --git a/src/scenarios/server/mrtr-ephemeral.ts b/src/scenarios/server/mrtr-ephemeral.ts index 316516e..6d98aa4 100644 --- a/src/scenarios/server/mrtr-ephemeral.ts +++ b/src/scenarios/server/mrtr-ephemeral.ts @@ -87,9 +87,7 @@ Implement a tool named \`test_tool_with_elicitation\` (no arguments required). if (!r1Result.inputRequests) { r1Errors.push('IncompleteResult missing inputRequests'); } else if (!r1Result.inputRequests['user_name']) { - r1Errors.push( - 'inputRequests missing expected key "user_name"' - ); + r1Errors.push('inputRequests missing expected key "user_name"'); } else { const req = r1Result.inputRequests['user_name']; if (req.method !== 'elicitation/create') { @@ -412,7 +410,9 @@ Implement a tool named \`test_mrtr_request_state\` (no arguments required). } else if (!r2Result) { r2Errors.push('No result in response'); } else if (!isCompleteResult(r2Result)) { - r2Errors.push('Expected complete result after retry with requestState'); + r2Errors.push( + 'Expected complete result after retry with requestState' + ); } else { // Check that server confirmed it received the state const content = r2Result.content as @@ -698,7 +698,8 @@ Implement a tool named \`test_mrtr_multi_round\` (no arguments required). checks.push({ id: 'mrtr-ephemeral-multi-round-r1', name: 'MRTRMultiRoundR1', - description: 'Round 1: Server returns IncompleteResult with requestState', + description: + 'Round 1: Server returns IncompleteResult with requestState', status: r1Ok ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: r1Ok @@ -765,8 +766,7 @@ Implement a tool named \`test_mrtr_multi_round\` (no arguments required). }); const r3Result = r3.result; - const r3Ok = - !r3.error && r3Result != null && isCompleteResult(r3Result); + const r3Ok = !r3.error && r3Result != null && isCompleteResult(r3Result); checks.push({ id: 'mrtr-ephemeral-multi-round-r3', @@ -784,7 +784,8 @@ Implement a tool named \`test_mrtr_multi_round\` (no arguments required). checks.push({ id: 'mrtr-ephemeral-multi-round-r1', name: 'MRTRMultiRoundR1', - description: 'Round 1: Server returns IncompleteResult with requestState', + description: + 'Round 1: Server returns IncompleteResult with requestState', status: 'FAILURE', timestamp: new Date().toISOString(), errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, @@ -1068,8 +1069,7 @@ Implement a prompt named \`test_mrtr_prompt\` that requires elicitation input. checks.push({ id: 'mrtr-ephemeral-non-tool-incomplete', name: 'MRTRNonToolIncomplete', - description: - 'prompts/get returns IncompleteResult with inputRequests', + description: 'prompts/get returns IncompleteResult with inputRequests', status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: r1Errors.length > 0 ? r1Errors.join('; ') : undefined, @@ -1100,7 +1100,9 @@ Implement a prompt named \`test_mrtr_prompt\` that requires elicitation input. } else if (!isCompleteResult(r2Result)) { r2Errors.push('Expected complete GetPromptResult after retry'); } else if (!r2Result.messages) { - r2Errors.push('Complete result missing messages (expected GetPromptResult)'); + r2Errors.push( + 'Complete result missing messages (expected GetPromptResult)' + ); } checks.push({ @@ -1119,8 +1121,7 @@ Implement a prompt named \`test_mrtr_prompt\` that requires elicitation input. checks.push({ id: 'mrtr-ephemeral-non-tool-incomplete', name: 'MRTRNonToolIncomplete', - description: - 'prompts/get returns IncompleteResult with inputRequests', + description: 'prompts/get returns IncompleteResult with inputRequests', status: 'FAILURE', timestamp: new Date().toISOString(), errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, diff --git a/src/scenarios/server/mrtr-tasks.ts b/src/scenarios/server/mrtr-persistent.ts similarity index 93% rename from src/scenarios/server/mrtr-tasks.ts rename to src/scenarios/server/mrtr-persistent.ts index 6dc5725..196b344 100644 --- a/src/scenarios/server/mrtr-tasks.ts +++ b/src/scenarios/server/mrtr-persistent.ts @@ -110,9 +110,7 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe | { taskId?: string; status?: string } | undefined; if (!task?.taskId) { - r1Errors.push( - 'Expected CreateTaskResult with task.taskId' - ); + r1Errors.push('Expected CreateTaskResult with task.taskId'); } else { taskId = task.taskId; if (task.status !== 'working') { @@ -137,11 +135,7 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe if (!taskId) return checks; // Step 2: Poll tasks/get until input_required - const taskState = await pollTaskStatus( - session, - taskId, - 'input_required' - ); + const taskState = await pollTaskStatus(session, taskId, 'input_required'); const pollErrors: string[] = []; if (!taskState) { @@ -160,8 +154,7 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe description: 'Task reaches input_required status', status: pollErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), - errorMessage: - pollErrors.length > 0 ? pollErrors.join('; ') : undefined, + errorMessage: pollErrors.length > 0 ? pollErrors.join('; ') : undefined, specReferences: MRTR_SPEC_REFERENCES, details: { taskState } }); @@ -182,14 +175,15 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe 'Expected IncompleteResult with inputRequests from tasks/result' ); } else if (!r3Result.inputRequests) { - r3Errors.push('IncompleteResult from tasks/result missing inputRequests'); + r3Errors.push( + 'IncompleteResult from tasks/result missing inputRequests' + ); } checks.push({ id: 'mrtr-persistent-tasks-result-incomplete', name: 'MRTRPersistentTasksResultIncomplete', - description: - 'tasks/result returns IncompleteResult with inputRequests', + description: 'tasks/result returns IncompleteResult with inputRequests', status: r3Errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: r3Errors.length > 0 ? r3Errors.join('; ') : undefined, @@ -229,17 +223,11 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe if (r4Errors.length > 0) return checks; // Step 5: Poll until completed - const completedState = await pollTaskStatus( - session, - taskId, - 'completed' - ); + const completedState = await pollTaskStatus(session, taskId, 'completed'); const compErrors: string[] = []; if (!completedState) { - compErrors.push( - 'Task did not reach completed status within timeout' - ); + compErrors.push('Task did not reach completed status within timeout'); } else if (completedState.status !== 'completed') { compErrors.push( `Expected status "completed", got "${completedState.status}"` @@ -252,8 +240,7 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe description: 'Task reaches completed status after input_response', status: compErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), - errorMessage: - compErrors.length > 0 ? compErrors.join('; ') : undefined, + errorMessage: compErrors.length > 0 ? compErrors.join('; ') : undefined, specReferences: MRTR_SPEC_REFERENCES, details: { taskState: completedState } }); @@ -327,9 +314,7 @@ Use the same tool as B1: \`test_mrtr_persistent\`. task: { ttl: 30000 } }); - const task = r1.result?.task as - | { taskId?: string } - | undefined; + const task = r1.result?.task as { taskId?: string } | undefined; if (!task?.taskId) { checks.push({ id: 'mrtr-persistent-ack-prereq', @@ -362,8 +347,7 @@ Use the same tool as B1: \`test_mrtr_persistent\`. description: 'Prerequisite: Get inputRequests', status: 'FAILURE', timestamp: new Date().toISOString(), - errorMessage: - 'Could not get inputRequests from tasks/result', + errorMessage: 'Could not get inputRequests from tasks/result', specReferences: MRTR_SPEC_REFERENCES }); return checks; @@ -389,12 +373,10 @@ Use the same tool as B1: \`test_mrtr_persistent\`. errors.push('No result from tasks/input_response'); } else { // Check for task metadata in acknowledgment - const meta = r4Result._meta as - | Record + const meta = r4Result._meta as Record | undefined; + const relatedTask = meta?.['io.modelcontextprotocol/related-task'] as + | { taskId?: string } | undefined; - const relatedTask = meta?.[ - 'io.modelcontextprotocol/related-task' - ] as { taskId?: string } | undefined; if (!relatedTask?.taskId) { errors.push( 'Acknowledgment missing _meta.io.modelcontextprotocol/related-task.taskId' @@ -460,9 +442,7 @@ Use the same tool as B1: \`test_mrtr_persistent\`. task: { ttl: 30000 } }); - const task = r1.result?.task as - | { taskId?: string } - | undefined; + const task = r1.result?.task as { taskId?: string } | undefined; if (!task?.taskId) { checks.push({ id: 'mrtr-persistent-bad-input-prereq', @@ -519,8 +499,7 @@ Use the same tool as B1: \`test_mrtr_persistent\`. // Check task is still input_required const stateAfter = await session.send('tasks/get', { taskId }); - const stillInputRequired = - stateAfter.result?.status === 'input_required'; + const stillInputRequired = stateAfter.result?.status === 'input_required'; // Try to get new inputRequests let newInputRequests = false; @@ -622,9 +601,7 @@ This tests the schema: \`TaskInputResponseResultResponse.result: Result | Incomp task: { ttl: 30000 } }); - const task = r1.result?.task as - | { taskId?: string } - | undefined; + const task = r1.result?.task as { taskId?: string } | undefined; if (!task?.taskId) { checks.push({ id: 'mrtr-persistent-multi-input-prereq', @@ -717,19 +694,13 @@ This tests the schema: \`TaskInputResponseResultResponse.result: Result | Incomp // Poll for completion if (r5Ok) { - const finalState = await pollTaskStatus( - session, - taskId, - 'completed' - ); + const finalState = await pollTaskStatus(session, taskId, 'completed'); checks.push({ id: 'mrtr-persistent-multi-input-completed', name: 'MRTRPersistentMultiInputCompleted', - description: - 'Task completes after second input_response', - status: - finalState?.status === 'completed' ? 'SUCCESS' : 'FAILURE', + description: 'Task completes after second input_response', + status: finalState?.status === 'completed' ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: finalState?.status !== 'completed' diff --git a/src/scenarios/server/mrtr-transition.ts b/src/scenarios/server/mrtr-transition.ts index b95bbb4..6c38969 100644 --- a/src/scenarios/server/mrtr-transition.ts +++ b/src/scenarios/server/mrtr-transition.ts @@ -103,8 +103,7 @@ This tests the pattern where a server gathers required input via ephemeral MRTR 'Server responds with ephemeral IncompleteResult before creating task', status: ephErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), - errorMessage: - ephErrors.length > 0 ? ephErrors.join('; ') : undefined, + errorMessage: ephErrors.length > 0 ? ephErrors.join('; ') : undefined, specReferences: MRTR_SPEC_REFERENCES, details: { result: r1Result } }); @@ -176,17 +175,13 @@ This tests the pattern where a server gathers required input via ephemeral MRTR const taskErrors: string[] = []; if (!taskState) { - taskErrors.push( - 'Could not retrieve task state via tasks/get' - ); + taskErrors.push('Could not retrieve task state via tasks/get'); } else if ( taskState.status !== 'completed' && taskState.status !== 'working' ) { // Accept working or completed — just verify the task is real - taskErrors.push( - `Unexpected task status: "${taskState.status}"` - ); + taskErrors.push(`Unexpected task status: "${taskState.status}"`); } checks.push({ @@ -196,8 +191,7 @@ This tests the pattern where a server gathers required input via ephemeral MRTR 'Created task is accessible via Tasks API after transition', status: taskErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), - errorMessage: - taskErrors.length > 0 ? taskErrors.join('; ') : undefined, + errorMessage: taskErrors.length > 0 ? taskErrors.join('; ') : undefined, specReferences: MRTR_SPEC_REFERENCES, details: { taskState } }); diff --git a/src/scenarios/server/mrtr-validation.ts b/src/scenarios/server/mrtr-validation.ts index 102a6a7..fa45a3b 100644 --- a/src/scenarios/server/mrtr-validation.ts +++ b/src/scenarios/server/mrtr-validation.ts @@ -135,9 +135,7 @@ Implement a tool named \`test_mrtr_validate_structure\` that returns an \`Incomp status: structureErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: - structureErrors.length > 0 - ? structureErrors.join('; ') - : undefined, + structureErrors.length > 0 ? structureErrors.join('; ') : undefined, specReferences: MRTR_SPEC_REFERENCES, details: { result: r1Result } }); @@ -187,9 +185,7 @@ Implement a tool named \`test_mrtr_validate_structure\` that returns an \`Incomp status: compatErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: - compatErrors.length > 0 - ? compatErrors.join('; ') - : undefined, + compatErrors.length > 0 ? compatErrors.join('; ') : undefined, specReferences: MRTR_SPEC_REFERENCES, details: { result: r2Result } }); @@ -306,12 +302,18 @@ When retried with valid \`inputResponses\` for all three, return a final result. } // Find each type of InputRequest - const foundTypes: Record }> = {}; + const foundTypes: Record< + string, + { key: string; request: Record } + > = {}; for (const [key, value] of Object.entries(inputRequests)) { const method = value.method as string; - if (method === 'elicitation/create') foundTypes['elicitation'] = { key, request: value }; - else if (method === 'sampling/createMessage') foundTypes['sampling'] = { key, request: value }; - else if (method === 'roots/list') foundTypes['roots'] = { key, request: value }; + if (method === 'elicitation/create') + foundTypes['elicitation'] = { key, request: value }; + else if (method === 'sampling/createMessage') + foundTypes['sampling'] = { key, request: value }; + else if (method === 'roots/list') + foundTypes['roots'] = { key, request: value }; } // Check elicitation @@ -330,7 +332,10 @@ When retried with valid \`inputResponses\` for all three, return a final result. 'elicitation/create params.message should be a string' ); } - if (!params.requestedSchema || typeof params.requestedSchema !== 'object') { + if ( + !params.requestedSchema || + typeof params.requestedSchema !== 'object' + ) { elicitErrors.push( 'elicitation/create params.requestedSchema should be an object' ); @@ -341,8 +346,7 @@ When retried with valid \`inputResponses\` for all three, return a final result. checks.push({ id: 'mrtr-validate-elicitation-input-request', name: 'MRTRValidateElicitationInputRequest', - description: - 'elicitation/create InputRequest has valid structure', + description: 'elicitation/create InputRequest has valid structure', status: elicitErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: @@ -378,8 +382,7 @@ When retried with valid \`inputResponses\` for all three, return a final result. checks.push({ id: 'mrtr-validate-sampling-input-request', name: 'MRTRValidateSamplingInputRequest', - description: - 'sampling/createMessage InputRequest has valid structure', + description: 'sampling/createMessage InputRequest has valid structure', status: samplingErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: @@ -414,16 +417,17 @@ When retried with valid \`inputResponses\` for all three, return a final result. // Retry with responses for all three types const inputResponses: Record = {}; if (foundTypes['elicitation']) { - inputResponses[foundTypes['elicitation'].key] = - mockElicitResponse({ value: 'test' }); + inputResponses[foundTypes['elicitation'].key] = mockElicitResponse({ + value: 'test' + }); } if (foundTypes['sampling']) { - inputResponses[foundTypes['sampling'].key] = - mockSamplingResponse('Generated response text'); + inputResponses[foundTypes['sampling'].key] = mockSamplingResponse( + 'Generated response text' + ); } if (foundTypes['roots']) { - inputResponses[foundTypes['roots'].key] = - mockListRootsResponse(); + inputResponses[foundTypes['roots'].key] = mockListRootsResponse(); } const r2 = await session.send('tools/call', { @@ -442,7 +446,9 @@ When retried with valid \`inputResponses\` for all three, return a final result. } else if (!r2.result) { retryErrors.push('No result from retry'); } else if (isIncompleteResult(r2.result)) { - retryErrors.push('Expected complete result after providing all inputResponses'); + retryErrors.push( + 'Expected complete result after providing all inputResponses' + ); } checks.push({ From ab655cbe3798b3552f690e7b7df982657fa26d6e Mon Sep 17 00:00:00 2001 From: Caitie McCaffrey Date: Fri, 13 Mar 2026 11:43:27 -0700 Subject: [PATCH 03/11] add ListRoot Tests --- src/scenarios/index.ts | 3 + src/scenarios/server/mrtr-ephemeral.ts | 146 ++++++++++++++++++++++++- 2 files changed, 143 insertions(+), 6 deletions(-) diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index bb4eee0..0803bd7 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -57,6 +57,7 @@ import { DNSRebindingProtectionScenario } from './server/dns-rebinding'; import { MrtrEphemeralBasicElicitationScenario, MrtrEphemeralBasicSamplingScenario, + MrtrEphemeralBasicListRootsScenario, MrtrEphemeralRequestStateScenario, MrtrEphemeralMultipleInputRequestsScenario, MrtrEphemeralMultiRoundScenario, @@ -102,6 +103,7 @@ const pendingClientScenariosList: ClientScenario[] = [ // run via `--spec-version draft` against MRTR-capable servers. new MrtrEphemeralBasicElicitationScenario(), new MrtrEphemeralBasicSamplingScenario(), + new MrtrEphemeralBasicListRootsScenario(), new MrtrEphemeralRequestStateScenario(), new MrtrEphemeralMultipleInputRequestsScenario(), new MrtrEphemeralMultiRoundScenario(), @@ -174,6 +176,7 @@ const allClientScenariosList: ClientScenario[] = [ // MRTR Ephemeral Workflow scenarios (SEP-2322) new MrtrEphemeralBasicElicitationScenario(), new MrtrEphemeralBasicSamplingScenario(), + new MrtrEphemeralBasicListRootsScenario(), new MrtrEphemeralRequestStateScenario(), new MrtrEphemeralMultipleInputRequestsScenario(), new MrtrEphemeralMultiRoundScenario(), diff --git a/src/scenarios/server/mrtr-ephemeral.ts b/src/scenarios/server/mrtr-ephemeral.ts index 6d98aa4..21b4622 100644 --- a/src/scenarios/server/mrtr-ephemeral.ts +++ b/src/scenarios/server/mrtr-ephemeral.ts @@ -312,7 +312,141 @@ Implement a tool named \`test_mrtr_ephemeral_sampling\` (no arguments required). } } -// ─── A3: Request State ─────────────────────────────────────────────────────── +// ─── A3: Basic ListRoots ───────────────────────────────────────────────────── + +export class MrtrEphemeralBasicListRootsScenario implements ClientScenario { + name = 'mrtr-ephemeral-basic-list-roots'; + specVersions: SpecVersion[] = ['draft']; + description = `Test basic ephemeral MRTR flow with a single roots/list input request (SEP-2322). + +**Server Implementation Requirements:** + +Implement a tool named \`test_mrtr_ephemeral_list_roots\` (no arguments required). + +**Behavior (Round 1):** When called without \`inputResponses\`, return an \`IncompleteResult\`: + +\`\`\`json +{ + "result_type": "incomplete", + "inputRequests": { + "client_roots": { + "method": "roots/list", + "params": {} + } + } +} +\`\`\` + +**Behavior (Round 2):** When called with \`inputResponses\` containing the key \`"client_roots"\` (a ListRootsResult with a \`roots\` array), return a complete result that references the provided roots.`; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const session = await createMrtrSession(serverUrl); + + // Round 1: Initial call + const r1 = await session.send('tools/call', { + name: 'test_mrtr_ephemeral_list_roots', + arguments: {} + }); + + const r1Result = r1.result; + const r1Errors: string[] = []; + + if (r1.error) { + r1Errors.push(`JSON-RPC error: ${r1.error.message}`); + } else if (!r1Result) { + r1Errors.push('No result in response'); + } else if (!isIncompleteResult(r1Result)) { + r1Errors.push('Expected IncompleteResult with roots/list inputRequest'); + } else { + if (!r1Result.inputRequests) { + r1Errors.push('IncompleteResult missing inputRequests'); + } else { + const key = Object.keys(r1Result.inputRequests)[0]; + if (!key) { + r1Errors.push('inputRequests map is empty'); + } else { + const req = r1Result.inputRequests[key]; + if (req.method !== 'roots/list') { + r1Errors.push( + `Expected method "roots/list", got "${req.method}"` + ); + } + } + } + } + + checks.push({ + id: 'mrtr-ephemeral-list-roots-incomplete', + name: 'MRTREphemeralListRootsIncomplete', + description: + 'Server returns IncompleteResult with roots/list inputRequest', + status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r1Errors.length > 0 ? r1Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r1Result } + }); + + // Round 2: Retry with inputResponses + if (r1Errors.length === 0 && isIncompleteResult(r1Result)) { + const inputKey = Object.keys(r1Result.inputRequests!)[0]; + const r2 = await session.send('tools/call', { + name: 'test_mrtr_ephemeral_list_roots', + arguments: {}, + inputResponses: { + [inputKey]: mockListRootsResponse() + }, + ...(r1Result.requestState !== undefined + ? { requestState: r1Result.requestState } + : {}) + }); + + const r2Result = r2.result; + const r2Errors: string[] = []; + + if (r2.error) { + r2Errors.push(`JSON-RPC error: ${r2.error.message}`); + } else if (!r2Result) { + r2Errors.push('No result in response'); + } else if (!isCompleteResult(r2Result)) { + r2Errors.push( + 'Expected complete result after retry with roots response' + ); + } + + checks.push({ + id: 'mrtr-ephemeral-list-roots-complete', + name: 'MRTREphemeralListRootsComplete', + description: + 'Server returns complete result after retry with roots response', + status: r2Errors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: r2Errors.length > 0 ? r2Errors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r2Result } + }); + } + } catch (error) { + checks.push({ + id: 'mrtr-ephemeral-list-roots-incomplete', + name: 'MRTREphemeralListRootsIncomplete', + description: + 'Server returns IncompleteResult with roots/list inputRequest', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: MRTR_SPEC_REFERENCES + }); + } + + return checks; + } +} + +// ─── A4: Request State ────────────────────────────────────────────────────── export class MrtrEphemeralRequestStateScenario implements ClientScenario { name = 'mrtr-ephemeral-request-state'; @@ -455,7 +589,7 @@ Implement a tool named \`test_mrtr_request_state\` (no arguments required). } } -// ─── A4: Multiple Input Requests ───────────────────────────────────────────── +// ─── A5: Multiple Input Requests ───────────────────────────────────────────── export class MrtrEphemeralMultipleInputRequestsScenario implements ClientScenario @@ -613,7 +747,7 @@ Implement a tool named \`test_mrtr_multiple_inputs\` (no arguments required). } } -// ─── A5: Multi-Round ───────────────────────────────────────────────────────── +// ─── A6: Multi-Round ───────────────────────────────────────────────────────── export class MrtrEphemeralMultiRoundScenario implements ClientScenario { name = 'mrtr-ephemeral-multi-round'; @@ -797,7 +931,7 @@ Implement a tool named \`test_mrtr_multi_round\` (no arguments required). } } -// ─── A6: Request State Only (Load Shedding) ────────────────────────────────── +// ─── A7: Request State Only ────────────────────────────────── export class MrtrEphemeralRequestStateOnlyScenario implements ClientScenario { name = 'mrtr-ephemeral-request-state-only'; @@ -906,7 +1040,7 @@ This simulates load shedding where the server transfers accumulated computation } } -// ─── A7: Missing Input Response ────────────────────────────────────────────── +// ─── A8: Missing Input Response ────────────────────────────────────────────── export class MrtrEphemeralMissingInputResponseScenario implements ClientScenario @@ -1010,7 +1144,7 @@ Use the same tool as A1: \`test_mrtr_ephemeral_elicitation\`. } } -// ─── A8: Non-Tool Request (prompts/get) ────────────────────────────────────── +// ─── A9: Non-Tool Request (prompts/get) ────────────────────────────────────── export class MrtrEphemeralNonToolRequestScenario implements ClientScenario { name = 'mrtr-ephemeral-non-tool-request'; From 3d04a7cd55d3d5782a19d980736e3fad76cc9095 Mon Sep 17 00:00:00 2001 From: Caitie McCaffrey Date: Fri, 13 Mar 2026 12:00:23 -0700 Subject: [PATCH 04/11] Remove duplicate test --- src/scenarios/index.ts | 3 - src/scenarios/server/mrtr-persistent.ts | 169 ++++++------------------ 2 files changed, 40 insertions(+), 132 deletions(-) diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index 0803bd7..3a22954 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -68,7 +68,6 @@ import { import { MrtrPersistentBasicScenario, - MrtrPersistentInputResponseAckScenario, MrtrPersistentBadInputResponseScenario, MrtrPersistentInputResponseIncompleteScenario } from './server/mrtr-persistent'; @@ -111,7 +110,6 @@ const pendingClientScenariosList: ClientScenario[] = [ new MrtrEphemeralMissingInputResponseScenario(), new MrtrEphemeralNonToolRequestScenario(), new MrtrPersistentBasicScenario(), - new MrtrPersistentInputResponseAckScenario(), new MrtrPersistentBadInputResponseScenario(), new MrtrPersistentInputResponseIncompleteScenario(), new MrtrIncompleteResultStructureScenario(), @@ -186,7 +184,6 @@ const allClientScenariosList: ClientScenario[] = [ // MRTR Persistent Workflow scenarios (SEP-2322) new MrtrPersistentBasicScenario(), - new MrtrPersistentInputResponseAckScenario(), new MrtrPersistentBadInputResponseScenario(), new MrtrPersistentInputResponseIncompleteScenario(), diff --git a/src/scenarios/server/mrtr-persistent.ts b/src/scenarios/server/mrtr-persistent.ts index 196b344..a2a8d47 100644 --- a/src/scenarios/server/mrtr-persistent.ts +++ b/src/scenarios/server/mrtr-persistent.ts @@ -220,6 +220,45 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe details: { result: r4.result } }); + // Validate acknowledgment includes task metadata (SHOULD per spec) + if (r4Errors.length === 0) { + const r4Result = r4.result; + const ackErrors: string[] = []; + + if (!r4Result) { + ackErrors.push('No result from tasks/input_response'); + } else { + const meta = r4Result._meta as + | Record + | undefined; + const relatedTask = meta?.[ + 'io.modelcontextprotocol/related-task' + ] as { taskId?: string } | undefined; + if (!relatedTask?.taskId) { + ackErrors.push( + 'Acknowledgment missing _meta.io.modelcontextprotocol/related-task.taskId' + ); + } else if (relatedTask.taskId !== taskId) { + ackErrors.push( + `taskId mismatch: expected "${taskId}", got "${relatedTask.taskId}"` + ); + } + } + + checks.push({ + id: 'mrtr-persistent-ack-structure', + name: 'MRTRPersistentAckStructure', + description: + 'tasks/input_response acknowledgment includes task metadata', + status: ackErrors.length === 0 ? 'SUCCESS' : 'WARNING', + timestamp: new Date().toISOString(), + errorMessage: + ackErrors.length > 0 ? ackErrors.join('; ') : undefined, + specReferences: MRTR_SPEC_REFERENCES, + details: { result: r4Result } + }); + } + if (r4Errors.length > 0) return checks; // Step 5: Poll until completed @@ -288,135 +327,7 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe } } -// ─── B2: Input Response Acknowledgment ─────────────────────────────────────── - -export class MrtrPersistentInputResponseAckScenario implements ClientScenario { - name = 'mrtr-persistent-input-response-ack'; - specVersions: SpecVersion[] = ['draft']; - description = `Test that tasks/input_response returns proper acknowledgment with task metadata (SEP-2322). - -**Server Implementation Requirements:** - -Use the same tool as B1: \`test_mrtr_persistent\`. - -**Validation:** The \`tasks/input_response\` acknowledgment SHOULD include \`_meta\` with \`io.modelcontextprotocol/related-task\` containing the \`taskId\`.`; - - async run(serverUrl: string): Promise { - const checks: ConformanceCheck[] = []; - - try { - const session = await createMrtrSession(serverUrl); - - // Create task - const r1 = await session.send('tools/call', { - name: 'test_mrtr_persistent', - arguments: {}, - task: { ttl: 30000 } - }); - - const task = r1.result?.task as { taskId?: string } | undefined; - if (!task?.taskId) { - checks.push({ - id: 'mrtr-persistent-ack-prereq', - name: 'MRTRPersistentAckPrereq', - description: 'Prerequisite: Task creation', - status: 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: 'Could not create task', - specReferences: MRTR_SPEC_REFERENCES - }); - return checks; - } - - const taskId = task.taskId; - - // Wait for input_required - await pollTaskStatus(session, taskId, 'input_required'); - - // Get input requests - const r3 = await session.send('tasks/result', { taskId }); - if ( - r3.error || - !r3.result || - !isIncompleteResult(r3.result) || - !r3.result.inputRequests - ) { - checks.push({ - id: 'mrtr-persistent-ack-prereq', - name: 'MRTRPersistentAckPrereq', - description: 'Prerequisite: Get inputRequests', - status: 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: 'Could not get inputRequests from tasks/result', - specReferences: MRTR_SPEC_REFERENCES - }); - return checks; - } - - // Send input_response and check acknowledgment - const inputKey = Object.keys(r3.result.inputRequests!)[0]; - const r4 = await session.send('tasks/input_response', { - inputResponses: { - [inputKey]: mockElicitResponse({ input: 'test' }) - }, - _meta: { - 'io.modelcontextprotocol/related-task': { taskId } - } - }); - - const r4Result = r4.result; - const errors: string[] = []; - - if (r4.error) { - errors.push(`JSON-RPC error: ${r4.error.message}`); - } else if (!r4Result) { - errors.push('No result from tasks/input_response'); - } else { - // Check for task metadata in acknowledgment - const meta = r4Result._meta as Record | undefined; - const relatedTask = meta?.['io.modelcontextprotocol/related-task'] as - | { taskId?: string } - | undefined; - if (!relatedTask?.taskId) { - errors.push( - 'Acknowledgment missing _meta.io.modelcontextprotocol/related-task.taskId' - ); - } else if (relatedTask.taskId !== taskId) { - errors.push( - `taskId mismatch: expected "${taskId}", got "${relatedTask.taskId}"` - ); - } - } - - checks.push({ - id: 'mrtr-persistent-ack-structure', - name: 'MRTRPersistentAckStructure', - description: - 'tasks/input_response acknowledgment includes task metadata', - status: errors.length === 0 ? 'SUCCESS' : 'WARNING', - timestamp: new Date().toISOString(), - errorMessage: errors.length > 0 ? errors.join('; ') : undefined, - specReferences: MRTR_SPEC_REFERENCES, - details: { result: r4Result } - }); - } catch (error) { - checks.push({ - id: 'mrtr-persistent-ack-structure', - name: 'MRTRPersistentAckStructure', - description: - 'tasks/input_response acknowledgment includes task metadata', - status: 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, - specReferences: MRTR_SPEC_REFERENCES - }); - } - - return checks; - } -} - -// ─── B3: Bad Input Response ────────────────────────────────────────────────── +// ─── B2: Bad Input Response ────────────────────────────────────────────────── export class MrtrPersistentBadInputResponseScenario implements ClientScenario { name = 'mrtr-persistent-bad-input-response'; From b78e78383d9c6188fb3a85a1e5e2924bb507cf74 Mon Sep 17 00:00:00 2001 From: Caitie McCaffrey Date: Mon, 16 Mar 2026 09:19:18 -0700 Subject: [PATCH 05/11] rename tests --- src/scenarios/index.ts | 118 ++++----- ...elpers.ts => incomplete-result-helpers.ts} | 24 +- ...rsistent.ts => incomplete-result-tasks.ts} | 123 +++++---- ...ion.ts => incomplete-result-transition.ts} | 72 +++--- ...ion.ts => incomplete-result-validation.ts} | 72 +++--- ...mrtr-ephemeral.ts => incomplete-result.ts} | 242 +++++++++--------- 6 files changed, 327 insertions(+), 324 deletions(-) rename src/scenarios/server/{mrtr-helpers.ts => incomplete-result-helpers.ts} (77%) rename src/scenarios/server/{mrtr-persistent.ts => incomplete-result-tasks.ts} (84%) rename src/scenarios/server/{mrtr-transition.ts => incomplete-result-transition.ts} (70%) rename src/scenarios/server/{mrtr-validation.ts => incomplete-result-validation.ts} (86%) rename src/scenarios/server/{mrtr-ephemeral.ts => incomplete-result.ts} (84%) diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index 3a22954..04fb1db 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -53,31 +53,31 @@ import { import { DNSRebindingProtectionScenario } from './server/dns-rebinding'; -// MRTR scenarios (SEP-2322) +// IncompleteResult scenarios (SEP-2322) import { - MrtrEphemeralBasicElicitationScenario, - MrtrEphemeralBasicSamplingScenario, - MrtrEphemeralBasicListRootsScenario, - MrtrEphemeralRequestStateScenario, - MrtrEphemeralMultipleInputRequestsScenario, - MrtrEphemeralMultiRoundScenario, - MrtrEphemeralRequestStateOnlyScenario, - MrtrEphemeralMissingInputResponseScenario, - MrtrEphemeralNonToolRequestScenario -} from './server/mrtr-ephemeral'; + IncompleteResultBasicElicitationScenario, + IncompleteResultBasicSamplingScenario, + IncompleteResultBasicListRootsScenario, + IncompleteResultRequestStateScenario, + IncompleteResultMultipleInputRequestsScenario, + IncompleteResultMultiRoundScenario, + IncompleteResultRequestStateOnlyScenario, + IncompleteResultMissingInputResponseScenario, + IncompleteResultNonToolRequestScenario +} from './server/incomplete-result'; import { - MrtrPersistentBasicScenario, - MrtrPersistentBadInputResponseScenario, - MrtrPersistentInputResponseIncompleteScenario -} from './server/mrtr-persistent'; + IncompleteResultTaskBasicScenario, + IncompleteResultTaskBadInputResponseScenario, + IncompleteResultTaskInputResponseIncompleteScenario +} from './server/incomplete-result-tasks'; import { - MrtrIncompleteResultStructureScenario, - MrtrInputRequestTypesScenario -} from './server/mrtr-validation'; + IncompleteResultStructureScenario, + InputRequestTypesScenario +} from './server/incomplete-result-validation'; -import { MrtrEphemeralToPersistentScenario } from './server/mrtr-transition'; +import { IncompleteResultToTaskTransitionScenario } from './server/incomplete-result-transition'; import { authScenariosList, @@ -97,24 +97,24 @@ const pendingClientScenariosList: ClientScenario[] = [ // https://github.com/modelcontextprotocol/typescript-sdk/pull/1129 new ServerSSEPollingScenario(), - // MRTR scenarios (SEP-2322) — pending until a conformance test server - // implements MRTR tools. These are draft spec scenarios intended to be - // run via `--spec-version draft` against MRTR-capable servers. - new MrtrEphemeralBasicElicitationScenario(), - new MrtrEphemeralBasicSamplingScenario(), - new MrtrEphemeralBasicListRootsScenario(), - new MrtrEphemeralRequestStateScenario(), - new MrtrEphemeralMultipleInputRequestsScenario(), - new MrtrEphemeralMultiRoundScenario(), - new MrtrEphemeralRequestStateOnlyScenario(), - new MrtrEphemeralMissingInputResponseScenario(), - new MrtrEphemeralNonToolRequestScenario(), - new MrtrPersistentBasicScenario(), - new MrtrPersistentBadInputResponseScenario(), - new MrtrPersistentInputResponseIncompleteScenario(), - new MrtrIncompleteResultStructureScenario(), - new MrtrInputRequestTypesScenario(), - new MrtrEphemeralToPersistentScenario() + // IncompleteResult scenarios (SEP-2322) — pending until a conformance test + // server implements IncompleteResult tools. These are draft spec scenarios + // intended to be run via `--spec-version draft` against capable servers. + new IncompleteResultBasicElicitationScenario(), + new IncompleteResultBasicSamplingScenario(), + new IncompleteResultBasicListRootsScenario(), + new IncompleteResultRequestStateScenario(), + new IncompleteResultMultipleInputRequestsScenario(), + new IncompleteResultMultiRoundScenario(), + new IncompleteResultRequestStateOnlyScenario(), + new IncompleteResultMissingInputResponseScenario(), + new IncompleteResultNonToolRequestScenario(), + new IncompleteResultTaskBasicScenario(), + new IncompleteResultTaskBadInputResponseScenario(), + new IncompleteResultTaskInputResponseIncompleteScenario(), + new IncompleteResultStructureScenario(), + new InputRequestTypesScenario(), + new IncompleteResultToTaskTransitionScenario() ]; // All client scenarios @@ -171,28 +171,28 @@ const allClientScenariosList: ClientScenario[] = [ // Security scenarios new DNSRebindingProtectionScenario(), - // MRTR Ephemeral Workflow scenarios (SEP-2322) - new MrtrEphemeralBasicElicitationScenario(), - new MrtrEphemeralBasicSamplingScenario(), - new MrtrEphemeralBasicListRootsScenario(), - new MrtrEphemeralRequestStateScenario(), - new MrtrEphemeralMultipleInputRequestsScenario(), - new MrtrEphemeralMultiRoundScenario(), - new MrtrEphemeralRequestStateOnlyScenario(), - new MrtrEphemeralMissingInputResponseScenario(), - new MrtrEphemeralNonToolRequestScenario(), - - // MRTR Persistent Workflow scenarios (SEP-2322) - new MrtrPersistentBasicScenario(), - new MrtrPersistentBadInputResponseScenario(), - new MrtrPersistentInputResponseIncompleteScenario(), - - // MRTR Validation scenarios (SEP-2322) - new MrtrIncompleteResultStructureScenario(), - new MrtrInputRequestTypesScenario(), - - // MRTR Transition scenarios (SEP-2322) - new MrtrEphemeralToPersistentScenario() + // IncompleteResult scenarios (SEP-2322) + new IncompleteResultBasicElicitationScenario(), + new IncompleteResultBasicSamplingScenario(), + new IncompleteResultBasicListRootsScenario(), + new IncompleteResultRequestStateScenario(), + new IncompleteResultMultipleInputRequestsScenario(), + new IncompleteResultMultiRoundScenario(), + new IncompleteResultRequestStateOnlyScenario(), + new IncompleteResultMissingInputResponseScenario(), + new IncompleteResultNonToolRequestScenario(), + + // IncompleteResult Task scenarios (SEP-2322) + new IncompleteResultTaskBasicScenario(), + new IncompleteResultTaskBadInputResponseScenario(), + new IncompleteResultTaskInputResponseIncompleteScenario(), + + // IncompleteResult Validation scenarios (SEP-2322) + new IncompleteResultStructureScenario(), + new InputRequestTypesScenario(), + + // IncompleteResult Transition scenarios (SEP-2322) + new IncompleteResultToTaskTransitionScenario() ]; // Active client scenarios (excludes pending) diff --git a/src/scenarios/server/mrtr-helpers.ts b/src/scenarios/server/incomplete-result-helpers.ts similarity index 77% rename from src/scenarios/server/mrtr-helpers.ts rename to src/scenarios/server/incomplete-result-helpers.ts index b7083fd..82b128c 100644 --- a/src/scenarios/server/mrtr-helpers.ts +++ b/src/scenarios/server/incomplete-result-helpers.ts @@ -1,9 +1,9 @@ /** - * MRTR-specific helpers for SEP-2322 conformance tests. + * IncompleteResult helpers for SEP-2322 conformance tests. * * Uses RawMcpSession from client-helper.ts for connection management and - * raw JSON-RPC transport. This file adds only MRTR-specific type guards, - * mock response builders, and convenience wrappers. + * raw JSON-RPC transport. This file adds IncompleteResult-specific type + * guards, mock response builders, and convenience wrappers. */ import { @@ -12,11 +12,9 @@ import { JsonRpcResponse } from './client-helper'; -// Re-export generic session types under MRTR-specific aliases for convenience -export type MrtrSession = RawMcpSession; -export type { JsonRpcResponse }; +export type { RawMcpSession, JsonRpcResponse }; -// ─── MRTR Types ────────────────────────────────────────────────────────────── +// ─── IncompleteResult Types ────────────────────────────────────────────────── export interface IncompleteResult { result_type?: 'incomplete'; @@ -31,7 +29,7 @@ export interface InputRequestObject { params?: Record; } -// ─── MRTR Type Guards ──────────────────────────────────────────────────────── +// ─── Type Guards ───────────────────────────────────────────────────────────── /** * Check if a JSON-RPC result is an IncompleteResult. @@ -41,7 +39,7 @@ export function isIncompleteResult( ): result is IncompleteResult { if (!result) return false; if (result.result_type === 'incomplete') return true; - // Also detect by presence of MRTR fields (for servers that may not set result_type) + // Also detect by presence of IncompleteResult fields return 'inputRequests' in result || 'requestState' in result; } @@ -110,19 +108,19 @@ export function mockListRootsResponse(): Record { // ─── Session Factory ───────────────────────────────────────────────────────── /** - * Create an initialized MRTR session ready for testing. + * Create an initialized raw MCP session for IncompleteResult testing. * Delegates to createRawSession from client-helper.ts. */ -export async function createMrtrSession( +export async function createIncompleteResultSession( serverUrl: string -): Promise { +): Promise { return createRawSession(serverUrl); } // ─── Spec References ───────────────────────────────────────────────────────── /** - * SEP reference for MRTR tests. + * SEP reference for IncompleteResult / MRTR tests. */ export const MRTR_SPEC_REFERENCES = [ { diff --git a/src/scenarios/server/mrtr-persistent.ts b/src/scenarios/server/incomplete-result-tasks.ts similarity index 84% rename from src/scenarios/server/mrtr-persistent.ts rename to src/scenarios/server/incomplete-result-tasks.ts index a2a8d47..770165b 100644 --- a/src/scenarios/server/mrtr-persistent.ts +++ b/src/scenarios/server/incomplete-result-tasks.ts @@ -1,5 +1,5 @@ /** - * SEP-2322: Multi Round-Trip Requests (MRTR) - Persistent Workflow Tests + * SEP-2322: IncompleteResult - Persistent Workflow Tests * * Tests the persistent (task-based) workflow where servers use Tasks to * manage long-running operations that require additional input via @@ -8,19 +8,19 @@ import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types'; import { - createMrtrSession, + createIncompleteResultSession, isIncompleteResult, isCompleteResult, mockElicitResponse, MRTR_SPEC_REFERENCES, - MrtrSession -} from './mrtr-helpers'; + RawMcpSession +} from './incomplete-result-helpers'; /** * Poll tasks/get until the task reaches the expected status or times out. */ async function pollTaskStatus( - session: MrtrSession, + session: RawMcpSession, taskId: string, expectedStatus: string, maxAttempts: number = 20, @@ -47,14 +47,14 @@ async function pollTaskStatus( // ─── B1: Basic Persistent Workflow ─────────────────────────────────────────── -export class MrtrPersistentBasicScenario implements ClientScenario { - name = 'mrtr-persistent-basic'; +export class IncompleteResultTaskBasicScenario implements ClientScenario { + name = 'incomplete-result-task-basic'; specVersions: SpecVersion[] = ['draft']; - description = `Test full persistent MRTR workflow via Tasks API (SEP-2322). + description = `Test full persistent IncompleteResult workflow via Tasks API (SEP-2322). **Server Implementation Requirements:** -Implement a tool named \`test_mrtr_persistent\` that supports task-augmented execution. +Implement a tool named \`test_incomplete_result_task\` that supports task-augmented execution. **Behavior:** 1. When called with \`task\` metadata, return a \`CreateTaskResult\` with \`status: "working"\` @@ -88,11 +88,11 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe const checks: ConformanceCheck[] = []; try { - const session = await createMrtrSession(serverUrl); + const session = await createIncompleteResultSession(serverUrl); // Step 1: Call tool with task metadata const r1 = await session.send('tools/call', { - name: 'test_mrtr_persistent', + name: 'test_incomplete_result_task', arguments: {}, task: { ttl: 30000 } }); @@ -122,8 +122,8 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe } checks.push({ - id: 'mrtr-persistent-task-created', - name: 'MRTRPersistentTaskCreated', + id: 'incomplete-result-task-created', + name: 'IncompleteResultTaskCreated', description: 'Server creates task with working status', status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), @@ -149,8 +149,8 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe } checks.push({ - id: 'mrtr-persistent-input-required', - name: 'MRTRPersistentInputRequired', + id: 'incomplete-result-task-input-required', + name: 'IncompleteResultTaskInputRequired', description: 'Task reaches input_required status', status: pollErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), @@ -181,8 +181,8 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe } checks.push({ - id: 'mrtr-persistent-tasks-result-incomplete', - name: 'MRTRPersistentTasksResultIncomplete', + id: 'incomplete-result-task-tasks-result-incomplete', + name: 'IncompleteResultTaskTasksResultIncomplete', description: 'tasks/result returns IncompleteResult with inputRequests', status: r3Errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), @@ -210,8 +210,8 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe } checks.push({ - id: 'mrtr-persistent-input-response-sent', - name: 'MRTRPersistentInputResponseSent', + id: 'incomplete-result-task-input-response-sent', + name: 'IncompleteResultTaskInputResponseSent', description: 'tasks/input_response is acknowledged by server', status: r4Errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), @@ -228,12 +228,10 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe if (!r4Result) { ackErrors.push('No result from tasks/input_response'); } else { - const meta = r4Result._meta as - | Record + const meta = r4Result._meta as Record | undefined; + const relatedTask = meta?.['io.modelcontextprotocol/related-task'] as + | { taskId?: string } | undefined; - const relatedTask = meta?.[ - 'io.modelcontextprotocol/related-task' - ] as { taskId?: string } | undefined; if (!relatedTask?.taskId) { ackErrors.push( 'Acknowledgment missing _meta.io.modelcontextprotocol/related-task.taskId' @@ -246,14 +244,13 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe } checks.push({ - id: 'mrtr-persistent-ack-structure', - name: 'MRTRPersistentAckStructure', + id: 'incomplete-result-task-ack-structure', + name: 'IncompleteResultTaskAckStructure', description: 'tasks/input_response acknowledgment includes task metadata', status: ackErrors.length === 0 ? 'SUCCESS' : 'WARNING', timestamp: new Date().toISOString(), - errorMessage: - ackErrors.length > 0 ? ackErrors.join('; ') : undefined, + errorMessage: ackErrors.length > 0 ? ackErrors.join('; ') : undefined, specReferences: MRTR_SPEC_REFERENCES, details: { result: r4Result } }); @@ -274,8 +271,8 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe } checks.push({ - id: 'mrtr-persistent-completed', - name: 'MRTRPersistentCompleted', + id: 'incomplete-result-task-completed', + name: 'IncompleteResultTaskCompleted', description: 'Task reaches completed status after input_response', status: compErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), @@ -302,8 +299,8 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe } checks.push({ - id: 'mrtr-persistent-final-result', - name: 'MRTRPersistentFinalResult', + id: 'incomplete-result-task-final-result', + name: 'IncompleteResultTaskFinalResult', description: 'tasks/result returns complete final result', status: r6Errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), @@ -313,8 +310,8 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe }); } catch (error) { checks.push({ - id: 'mrtr-persistent-task-created', - name: 'MRTRPersistentTaskCreated', + id: 'incomplete-result-task-created', + name: 'IncompleteResultTaskCreated', description: 'Server creates task with working status', status: 'FAILURE', timestamp: new Date().toISOString(), @@ -329,14 +326,16 @@ Implement a tool named \`test_mrtr_persistent\` that supports task-augmented exe // ─── B2: Bad Input Response ────────────────────────────────────────────────── -export class MrtrPersistentBadInputResponseScenario implements ClientScenario { - name = 'mrtr-persistent-bad-input-response'; +export class IncompleteResultTaskBadInputResponseScenario + implements ClientScenario +{ + name = 'incomplete-result-task-bad-input-response'; specVersions: SpecVersion[] = ['draft']; description = `Test error handling when tasks/input_response contains wrong data (SEP-2322). **Server Implementation Requirements:** -Use the same tool as B1: \`test_mrtr_persistent\`. +Use the same tool as B1: \`test_incomplete_result_task\`. **Behavior:** When the client sends \`tasks/input_response\` with incorrect keys, the server SHOULD acknowledge the message but keep the task in \`input_required\` status. The next \`tasks/result\` call should return a new \`inputRequests\` re-requesting the needed information.`; @@ -344,11 +343,11 @@ Use the same tool as B1: \`test_mrtr_persistent\`. const checks: ConformanceCheck[] = []; try { - const session = await createMrtrSession(serverUrl); + const session = await createIncompleteResultSession(serverUrl); // Create task and wait for input_required const r1 = await session.send('tools/call', { - name: 'test_mrtr_persistent', + name: 'test_incomplete_result_task', arguments: {}, task: { ttl: 30000 } }); @@ -356,8 +355,8 @@ Use the same tool as B1: \`test_mrtr_persistent\`. const task = r1.result?.task as { taskId?: string } | undefined; if (!task?.taskId) { checks.push({ - id: 'mrtr-persistent-bad-input-prereq', - name: 'MRTRPersistentBadInputPrereq', + id: 'incomplete-result-task-bad-input-prereq', + name: 'IncompleteResultTaskBadInputPrereq', description: 'Prerequisite: Task creation', status: 'FAILURE', timestamp: new Date().toISOString(), @@ -379,8 +378,8 @@ Use the same tool as B1: \`test_mrtr_persistent\`. !r3.result.inputRequests ) { checks.push({ - id: 'mrtr-persistent-bad-input-prereq', - name: 'MRTRPersistentBadInputPrereq', + id: 'incomplete-result-task-bad-input-prereq', + name: 'IncompleteResultTaskBadInputPrereq', description: 'Prerequisite: Get inputRequests', status: 'FAILURE', timestamp: new Date().toISOString(), @@ -438,8 +437,8 @@ Use the same tool as B1: \`test_mrtr_persistent\`. } checks.push({ - id: 'mrtr-persistent-bad-input-rerequests', - name: 'MRTRPersistentBadInputRerequests', + id: 'incomplete-result-task-bad-input-rerequests', + name: 'IncompleteResultTaskBadInputRerequests', description: 'Server keeps task in input_required and re-requests after bad inputResponses', status: @@ -462,8 +461,8 @@ Use the same tool as B1: \`test_mrtr_persistent\`. }); } catch (error) { checks.push({ - id: 'mrtr-persistent-bad-input-rerequests', - name: 'MRTRPersistentBadInputRerequests', + id: 'incomplete-result-task-bad-input-rerequests', + name: 'IncompleteResultTaskBadInputRerequests', description: 'Server keeps task in input_required and re-requests after bad inputResponses', status: 'FAILURE', @@ -479,16 +478,16 @@ Use the same tool as B1: \`test_mrtr_persistent\`. // ─── B4: tasks/input_response returning IncompleteResult ───────────────────── -export class MrtrPersistentInputResponseIncompleteScenario +export class IncompleteResultTaskInputResponseIncompleteScenario implements ClientScenario { - name = 'mrtr-persistent-input-response-incomplete'; + name = 'incomplete-result-task-input-response-incomplete'; specVersions: SpecVersion[] = ['draft']; description = `Test that tasks/input_response can itself return an IncompleteResult (SEP-2322). **Server Implementation Requirements:** -Implement a tool named \`test_mrtr_persistent_multi_input\` that supports task-augmented execution and requires TWO rounds of input. +Implement a tool named \`test_incomplete_result_task_multi_input\` that supports task-augmented execution and requires TWO rounds of input. **Behavior:** 1. Create task, transition to \`input_required\` @@ -503,11 +502,11 @@ This tests the schema: \`TaskInputResponseResultResponse.result: Result | Incomp const checks: ConformanceCheck[] = []; try { - const session = await createMrtrSession(serverUrl); + const session = await createIncompleteResultSession(serverUrl); // Create task const r1 = await session.send('tools/call', { - name: 'test_mrtr_persistent_multi_input', + name: 'test_incomplete_result_task_multi_input', arguments: {}, task: { ttl: 30000 } }); @@ -515,8 +514,8 @@ This tests the schema: \`TaskInputResponseResultResponse.result: Result | Incomp const task = r1.result?.task as { taskId?: string } | undefined; if (!task?.taskId) { checks.push({ - id: 'mrtr-persistent-multi-input-prereq', - name: 'MRTRPersistentMultiInputPrereq', + id: 'incomplete-result-task-multi-input-prereq', + name: 'IncompleteResultTaskMultiInputPrereq', description: 'Prerequisite: Task creation', status: 'FAILURE', timestamp: new Date().toISOString(), @@ -538,8 +537,8 @@ This tests the schema: \`TaskInputResponseResultResponse.result: Result | Incomp !r3.result.inputRequests ) { checks.push({ - id: 'mrtr-persistent-multi-input-prereq', - name: 'MRTRPersistentMultiInputPrereq', + id: 'incomplete-result-task-multi-input-prereq', + name: 'IncompleteResultTaskMultiInputPrereq', description: 'Prerequisite: Get first inputRequests', status: 'FAILURE', timestamp: new Date().toISOString(), @@ -578,8 +577,8 @@ This tests the schema: \`TaskInputResponseResultResponse.result: Result | Incomp } checks.push({ - id: 'mrtr-persistent-input-response-returns-incomplete', - name: 'MRTRPersistentInputResponseReturnsIncomplete', + id: 'incomplete-result-task-input-response-returns-incomplete', + name: 'IncompleteResultTaskInputResponseReturnsIncomplete', description: 'tasks/input_response returns IncompleteResult with additional inputRequests', status: r4Errors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -608,8 +607,8 @@ This tests the schema: \`TaskInputResponseResultResponse.result: Result | Incomp const finalState = await pollTaskStatus(session, taskId, 'completed'); checks.push({ - id: 'mrtr-persistent-multi-input-completed', - name: 'MRTRPersistentMultiInputCompleted', + id: 'incomplete-result-task-multi-input-completed', + name: 'IncompleteResultTaskMultiInputCompleted', description: 'Task completes after second input_response', status: finalState?.status === 'completed' ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), @@ -624,8 +623,8 @@ This tests the schema: \`TaskInputResponseResultResponse.result: Result | Incomp } } catch (error) { checks.push({ - id: 'mrtr-persistent-input-response-returns-incomplete', - name: 'MRTRPersistentInputResponseReturnsIncomplete', + id: 'incomplete-result-task-input-response-returns-incomplete', + name: 'IncompleteResultTaskInputResponseReturnsIncomplete', description: 'tasks/input_response returns IncompleteResult with additional inputRequests', status: 'FAILURE', diff --git a/src/scenarios/server/mrtr-transition.ts b/src/scenarios/server/incomplete-result-transition.ts similarity index 70% rename from src/scenarios/server/mrtr-transition.ts rename to src/scenarios/server/incomplete-result-transition.ts index 6c38969..1719ba2 100644 --- a/src/scenarios/server/mrtr-transition.ts +++ b/src/scenarios/server/incomplete-result-transition.ts @@ -1,25 +1,25 @@ /** - * SEP-2322: Multi Round-Trip Requests (MRTR) - Ephemeral-to-Persistent Transition Test + * SEP-2322: IncompleteResult - IncompleteResult-to-Task Transition Test * - * Tests the transition from an ephemeral workflow to a persistent (task-based) - * workflow, as described in the SEP section "Interactions Between Ephemeral and - * Persistent Workflows." + * Tests the transition from an IncompleteResult workflow to a task-based + * workflow, as described in the SEP section "Interactions Between IncompleteResult + * and Task-Based Workflows." */ import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types'; import { - createMrtrSession, + createIncompleteResultSession, isIncompleteResult, mockElicitResponse, MRTR_SPEC_REFERENCES, - MrtrSession -} from './mrtr-helpers'; + RawMcpSession +} from './incomplete-result-helpers'; /** * Poll tasks/get until the task reaches the expected status or times out. */ async function pollTaskStatus( - session: MrtrSession, + session: RawMcpSession, taskId: string, expectedStatus: string, maxAttempts: number = 20, @@ -43,33 +43,35 @@ async function pollTaskStatus( return null; } -// ─── D1: Ephemeral-to-Persistent Transition ───────────────────────────────── +// ─── D1: IncompleteResult-to-Task Transition ───────────────────────────────── -export class MrtrEphemeralToPersistentScenario implements ClientScenario { - name = 'mrtr-ephemeral-to-persistent'; +export class IncompleteResultToTaskTransitionScenario + implements ClientScenario +{ + name = 'incomplete-result-to-task-transition'; specVersions: SpecVersion[] = ['draft']; - description = `Test transition from ephemeral to persistent workflow (SEP-2322). + description = `Test transition from IncompleteResult to task-based workflow (SEP-2322). **Server Implementation Requirements:** -Implement a tool named \`test_mrtr_transition\` that demonstrates the ephemeral-to-persistent workflow transition. +Implement a tool named \`test_incomplete_result_transition\` that demonstrates the IncompleteResult-to-task-based workflow transition. **Behavior:** -1. When called with \`task\` metadata in params, the server initially responds with an ephemeral \`IncompleteResult\` (with \`inputRequests\`) rather than creating a task immediately -2. When the client retries the \`tools/call\` with \`inputResponses\` AND \`task\` metadata, the server now creates a persistent task and returns a \`CreateTaskResult\` with a task ID +1. When called with \`task\` metadata in params, the server initially responds with an \`IncompleteResult\` (with \`inputRequests\`) rather than creating a task immediately +2. When the client retries the \`tools/call\` with \`inputResponses\` AND \`task\` metadata, the server now creates a task and returns a \`CreateTaskResult\` with a task ID 3. The task can then be managed via the Tasks API -This tests the pattern where a server gathers required input via ephemeral MRTR before committing to task creation, as described in the SEP section "Interactions Between Ephemeral and Persistent Workflows."`; +This tests the pattern where a server gathers required input via IncompleteResult before committing to task creation, as described in the SEP section "Interactions Between IncompleteResult and Task-Based Workflows."`; async run(serverUrl: string): Promise { const checks: ConformanceCheck[] = []; try { - const session = await createMrtrSession(serverUrl); + const session = await createIncompleteResultSession(serverUrl); - // Step 1: Call tool with task metadata — server responds ephemerally + // Step 1: Call tool with task metadata — server responds with IncompleteResult const r1 = await session.send('tools/call', { - name: 'test_mrtr_transition', + name: 'test_incomplete_result_transition', arguments: {}, task: { ttl: 30000 } }); @@ -83,24 +85,24 @@ This tests the pattern where a server gathers required input via ephemeral MRTR ephErrors.push('No result in response'); } else if (!isIncompleteResult(r1Result)) { ephErrors.push( - 'Expected initial IncompleteResult (ephemeral response despite task metadata)' + 'Expected initial IncompleteResult (IncompleteResult response despite task metadata)' ); } else if (!r1Result.inputRequests) { ephErrors.push('IncompleteResult missing inputRequests'); } else { - // Verify there is NO task in the response — it should be ephemeral + // Verify there is NO task in the response — it should be IncompleteResult if (r1Result.task) { ephErrors.push( - 'Ephemeral step should not include task — expected no task creation yet' + 'IncompleteResult step should not include task — expected no task creation yet' ); } } checks.push({ - id: 'mrtr-transition-ephemeral-phase', - name: 'MRTRTransitionEphemeralPhase', + id: 'incomplete-result-transition-ephemeral-phase', + name: 'IncompleteResultTransitionEphemeralPhase', description: - 'Server responds with ephemeral IncompleteResult before creating task', + 'Server responds with IncompleteResult before creating task', status: ephErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: ephErrors.length > 0 ? ephErrors.join('; ') : undefined, @@ -115,7 +117,7 @@ This tests the pattern where a server gathers required input via ephemeral MRTR r1Result.inputRequests as Record )[0]; const r2 = await session.send('tools/call', { - name: 'test_mrtr_transition', + name: 'test_incomplete_result_transition', arguments: {}, inputResponses: { [inputKey]: mockElicitResponse({ confirmed: true }) @@ -136,7 +138,7 @@ This tests the pattern where a server gathers required input via ephemeral MRTR } else if (!r2Result) { transErrors.push('No result from retry'); } else { - // Should now have a task (persistent workflow) + // Should now have a task (task-based workflow) const task = r2Result.task as | { taskId?: string; status?: string } | undefined; @@ -150,10 +152,10 @@ This tests the pattern where a server gathers required input via ephemeral MRTR } checks.push({ - id: 'mrtr-transition-task-created', - name: 'MRTRTransitionTaskCreated', + id: 'incomplete-result-transition-task-created', + name: 'IncompleteResultTransitionTaskCreated', description: - 'Server transitions to persistent workflow and creates task', + 'Server transitions to task-based workflow and creates task', status: transErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), errorMessage: @@ -185,8 +187,8 @@ This tests the pattern where a server gathers required input via ephemeral MRTR } checks.push({ - id: 'mrtr-transition-task-accessible', - name: 'MRTRTransitionTaskAccessible', + id: 'incomplete-result-transition-task-accessible', + name: 'IncompleteResultTransitionTaskAccessible', description: 'Created task is accessible via Tasks API after transition', status: taskErrors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -197,10 +199,10 @@ This tests the pattern where a server gathers required input via ephemeral MRTR }); } catch (error) { checks.push({ - id: 'mrtr-transition-ephemeral-phase', - name: 'MRTRTransitionEphemeralPhase', + id: 'incomplete-result-transition-ephemeral-phase', + name: 'IncompleteResultTransitionEphemeralPhase', description: - 'Server responds with ephemeral IncompleteResult before creating task', + 'Server responds with IncompleteResult before creating task', status: 'FAILURE', timestamp: new Date().toISOString(), errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, diff --git a/src/scenarios/server/mrtr-validation.ts b/src/scenarios/server/incomplete-result-validation.ts similarity index 86% rename from src/scenarios/server/mrtr-validation.ts rename to src/scenarios/server/incomplete-result-validation.ts index fa45a3b..7c6636f 100644 --- a/src/scenarios/server/mrtr-validation.ts +++ b/src/scenarios/server/incomplete-result-validation.ts @@ -1,31 +1,31 @@ /** - * SEP-2322: Multi Round-Trip Requests (MRTR) - Schema/Structure Validation Tests + * SEP-2322: IncompleteResult - Schema/Structure Validation Tests * - * Tests that validate the structure and correctness of MRTR protocol + * Tests that validate the structure and correctness of IncompleteResult protocol * messages, including IncompleteResult format, InputRequest types, and * result_type field behavior. */ import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types'; import { - createMrtrSession, + createIncompleteResultSession, isIncompleteResult, mockElicitResponse, mockSamplingResponse, mockListRootsResponse, MRTR_SPEC_REFERENCES -} from './mrtr-helpers'; +} from './incomplete-result-helpers'; // ─── C1: IncompleteResult Structure Validation ────────────────────────────── -export class MrtrIncompleteResultStructureScenario implements ClientScenario { - name = 'mrtr-incomplete-result-structure'; +export class IncompleteResultStructureScenario implements ClientScenario { + name = 'incomplete-result-structure'; specVersions: SpecVersion[] = ['draft']; description = `Validate the IncompleteResult structure conforms to the schema (SEP-2322). **Server Implementation Requirements:** -Implement a tool named \`test_mrtr_validate_structure\` that returns an \`IncompleteResult\` with well-formed fields. +Implement a tool named \`test_incomplete_result_validate_structure\` that returns an \`IncompleteResult\` with well-formed fields. **Behavior:** 1. When called, return an \`IncompleteResult\` with both \`inputRequests\` and \`requestState\`: @@ -63,11 +63,11 @@ Implement a tool named \`test_mrtr_validate_structure\` that returns an \`Incomp const checks: ConformanceCheck[] = []; try { - const session = await createMrtrSession(serverUrl); + const session = await createIncompleteResultSession(serverUrl); // Get IncompleteResult const r1 = await session.send('tools/call', { - name: 'test_mrtr_validate_structure', + name: 'test_incomplete_result_validate_structure', arguments: {} }); @@ -128,8 +128,8 @@ Implement a tool named \`test_mrtr_validate_structure\` that returns an \`Incomp } checks.push({ - id: 'mrtr-validate-incomplete-result-fields', - name: 'MRTRValidateIncompleteResultFields', + id: 'incomplete-result-validate-incomplete-result-fields', + name: 'IncompleteResultValidateIncompleteResultFields', description: 'IncompleteResult has correct result_type, inputRequests, and requestState', status: structureErrors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -146,7 +146,7 @@ Implement a tool named \`test_mrtr_validate_structure\` that returns an \`Incomp r1Result.inputRequests as Record )[0]; const r2 = await session.send('tools/call', { - name: 'test_mrtr_validate_structure', + name: 'test_incomplete_result_validate_structure', arguments: {}, inputResponses: { [inputKey]: mockElicitResponse({ value: 'test' }) @@ -178,8 +178,8 @@ Implement a tool named \`test_mrtr_validate_structure\` that returns an \`Incomp } checks.push({ - id: 'mrtr-validate-complete-result-default', - name: 'MRTRValidateCompleteResultDefault', + id: 'incomplete-result-validate-complete-result-default', + name: 'IncompleteResultValidateCompleteResultDefault', description: 'Complete result has result_type absent or "complete" (backward compat)', status: compatErrors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -192,8 +192,8 @@ Implement a tool named \`test_mrtr_validate_structure\` that returns an \`Incomp } } catch (error) { checks.push({ - id: 'mrtr-validate-incomplete-result-fields', - name: 'MRTRValidateIncompleteResultFields', + id: 'incomplete-result-validate-incomplete-result-fields', + name: 'IncompleteResultValidateIncompleteResultFields', description: 'IncompleteResult has correct result_type, inputRequests, and requestState', status: 'FAILURE', @@ -209,14 +209,14 @@ Implement a tool named \`test_mrtr_validate_structure\` that returns an \`Incomp // ─── C2: InputRequest Types Validation ─────────────────────────────────────── -export class MrtrInputRequestTypesScenario implements ClientScenario { - name = 'mrtr-input-request-types'; +export class InputRequestTypesScenario implements ClientScenario { + name = 'input-request-types'; specVersions: SpecVersion[] = ['draft']; description = `Validate all three InputRequest types in IncompleteResult. **Server Implementation Requirements:** -Implement a tool named \`test_mrtr_input_types\` that returns an \`IncompleteResult\` containing all three types of \`InputRequest\`. +Implement a tool named \`test_incomplete_result_input_types\` that returns an \`IncompleteResult\` containing all three types of \`InputRequest\`. **Behavior:** @@ -260,10 +260,10 @@ When retried with valid \`inputResponses\` for all three, return a final result. const checks: ConformanceCheck[] = []; try { - const session = await createMrtrSession(serverUrl); + const session = await createIncompleteResultSession(serverUrl); const r1 = await session.send('tools/call', { - name: 'test_mrtr_input_types', + name: 'test_incomplete_result_input_types', arguments: {} }); @@ -271,8 +271,8 @@ When retried with valid \`inputResponses\` for all three, return a final result. if (r1.error || !r1Result || !isIncompleteResult(r1Result)) { checks.push({ - id: 'mrtr-validate-input-types-prereq', - name: 'MRTRValidateInputTypesPrereq', + id: 'incomplete-result-validate-input-types-prereq', + name: 'IncompleteResultValidateInputTypesPrereq', description: 'Prerequisite: Get IncompleteResult', status: 'FAILURE', timestamp: new Date().toISOString(), @@ -290,8 +290,8 @@ When retried with valid \`inputResponses\` for all three, return a final result. if (!inputRequests) { checks.push({ - id: 'mrtr-validate-input-types-prereq', - name: 'MRTRValidateInputTypesPrereq', + id: 'incomplete-result-validate-input-types-prereq', + name: 'IncompleteResultValidateInputTypesPrereq', description: 'Prerequisite: inputRequests present', status: 'FAILURE', timestamp: new Date().toISOString(), @@ -344,8 +344,8 @@ When retried with valid \`inputResponses\` for all three, return a final result. } checks.push({ - id: 'mrtr-validate-elicitation-input-request', - name: 'MRTRValidateElicitationInputRequest', + id: 'incomplete-result-validate-elicitation-input-request', + name: 'IncompleteResultValidateElicitationInputRequest', description: 'elicitation/create InputRequest has valid structure', status: elicitErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), @@ -380,8 +380,8 @@ When retried with valid \`inputResponses\` for all three, return a final result. } checks.push({ - id: 'mrtr-validate-sampling-input-request', - name: 'MRTRValidateSamplingInputRequest', + id: 'incomplete-result-validate-sampling-input-request', + name: 'IncompleteResultValidateSamplingInputRequest', description: 'sampling/createMessage InputRequest has valid structure', status: samplingErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), @@ -403,8 +403,8 @@ When retried with valid \`inputResponses\` for all three, return a final result. } checks.push({ - id: 'mrtr-validate-roots-input-request', - name: 'MRTRValidateRootsInputRequest', + id: 'incomplete-result-validate-roots-input-request', + name: 'IncompleteResultValidateRootsInputRequest', description: 'roots/list InputRequest has valid structure', status: rootsErrors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), @@ -431,7 +431,7 @@ When retried with valid \`inputResponses\` for all three, return a final result. } const r2 = await session.send('tools/call', { - name: 'test_mrtr_input_types', + name: 'test_incomplete_result_input_types', arguments: {}, inputResponses, requestState: @@ -452,8 +452,8 @@ When retried with valid \`inputResponses\` for all three, return a final result. } checks.push({ - id: 'mrtr-validate-all-types-retry', - name: 'MRTRValidateAllTypesRetry', + id: 'incomplete-result-validate-all-types-retry', + name: 'IncompleteResultValidateAllTypesRetry', description: 'Retry with all three InputResponse types produces final result', status: retryErrors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -465,8 +465,8 @@ When retried with valid \`inputResponses\` for all three, return a final result. }); } catch (error) { checks.push({ - id: 'mrtr-validate-input-types-prereq', - name: 'MRTRValidateInputTypesPrereq', + id: 'incomplete-result-validate-input-types-prereq', + name: 'IncompleteResultValidateInputTypesPrereq', description: 'Prerequisite: Get IncompleteResult with InputRequests', status: 'FAILURE', timestamp: new Date().toISOString(), diff --git a/src/scenarios/server/mrtr-ephemeral.ts b/src/scenarios/server/incomplete-result.ts similarity index 84% rename from src/scenarios/server/mrtr-ephemeral.ts rename to src/scenarios/server/incomplete-result.ts index 21b4622..ad7342c 100644 --- a/src/scenarios/server/mrtr-ephemeral.ts +++ b/src/scenarios/server/incomplete-result.ts @@ -1,5 +1,5 @@ /** - * SEP-2322: Multi Round-Trip Requests (MRTR) - Ephemeral Workflow Tests + * SEP-2322: IncompleteResult - Ephemeral Workflow Tests * * Tests the ephemeral (stateless) workflow where servers respond with * IncompleteResult containing inputRequests and/or requestState, and @@ -8,21 +8,23 @@ import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types'; import { - createMrtrSession, + createIncompleteResultSession, isIncompleteResult, isCompleteResult, mockElicitResponse, mockSamplingResponse, mockListRootsResponse, MRTR_SPEC_REFERENCES -} from './mrtr-helpers'; +} from './incomplete-result-helpers'; // ─── A1: Basic Elicitation ──────────────────────────────────────────────────── -export class MrtrEphemeralBasicElicitationScenario implements ClientScenario { - name = 'mrtr-ephemeral-basic-elicitation'; +export class IncompleteResultBasicElicitationScenario + implements ClientScenario +{ + name = 'incomplete-result-basic-elicitation'; specVersions: SpecVersion[] = ['draft']; - description = `Test basic ephemeral MRTR flow with a single elicitation input request (SEP-2322). + description = `Test basic ephemeral IncompleteResult flow with a single elicitation input request (SEP-2322). **Server Implementation Requirements:** @@ -63,7 +65,7 @@ Implement a tool named \`test_tool_with_elicitation\` (no arguments required). const checks: ConformanceCheck[] = []; try { - const session = await createMrtrSession(serverUrl); + const session = await createIncompleteResultSession(serverUrl); // Round 1: Initial call — expect IncompleteResult const r1 = await session.send('tools/call', { @@ -99,8 +101,8 @@ Implement a tool named \`test_tool_with_elicitation\` (no arguments required). } checks.push({ - id: 'mrtr-ephemeral-elicitation-incomplete', - name: 'MRTREphemeralElicitationIncomplete', + id: 'incomplete-result-elicitation-incomplete', + name: 'IncompleteResultElicitationIncomplete', description: 'Server returns IncompleteResult with elicitation inputRequest', status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -113,7 +115,7 @@ Implement a tool named \`test_tool_with_elicitation\` (no arguments required). // Round 2: Retry with inputResponses — expect complete result if (r1Errors.length === 0 && isIncompleteResult(r1Result)) { const r2 = await session.send('tools/call', { - name: 'test_mrtr_ephemeral_elicitation', + name: 'test_incomplete_result_elicitation', arguments: {}, inputResponses: { user_name: mockElicitResponse({ name: 'Alice' }) @@ -144,8 +146,8 @@ Implement a tool named \`test_tool_with_elicitation\` (no arguments required). } checks.push({ - id: 'mrtr-ephemeral-elicitation-complete', - name: 'MRTREphemeralElicitationComplete', + id: 'incomplete-result-elicitation-complete', + name: 'IncompleteResultElicitationComplete', description: 'Server returns complete result after retry with inputResponses', status: r2Errors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -157,8 +159,8 @@ Implement a tool named \`test_tool_with_elicitation\` (no arguments required). } } catch (error) { checks.push({ - id: 'mrtr-ephemeral-elicitation-incomplete', - name: 'MRTREphemeralElicitationIncomplete', + id: 'incomplete-result-elicitation-incomplete', + name: 'IncompleteResultElicitationIncomplete', description: 'Server returns IncompleteResult with elicitation inputRequest', status: 'FAILURE', @@ -174,14 +176,14 @@ Implement a tool named \`test_tool_with_elicitation\` (no arguments required). // ─── A2: Basic Sampling ────────────────────────────────────────────────────── -export class MrtrEphemeralBasicSamplingScenario implements ClientScenario { - name = 'mrtr-ephemeral-basic-sampling'; +export class IncompleteResultBasicSamplingScenario implements ClientScenario { + name = 'incomplete-result-basic-sampling'; specVersions: SpecVersion[] = ['draft']; - description = `Test basic ephemeral MRTR flow with a single sampling input request (SEP-2322). + description = `Test basic ephemeral IncompleteResult flow with a single sampling input request (SEP-2322). **Server Implementation Requirements:** -Implement a tool named \`test_mrtr_ephemeral_sampling\` (no arguments required). +Implement a tool named \`test_incomplete_result_sampling\` (no arguments required). **Behavior (Round 1):** When called without \`inputResponses\`, return an \`IncompleteResult\`: @@ -209,11 +211,11 @@ Implement a tool named \`test_mrtr_ephemeral_sampling\` (no arguments required). const checks: ConformanceCheck[] = []; try { - const session = await createMrtrSession(serverUrl); + const session = await createIncompleteResultSession(serverUrl); // Round 1: Initial call const r1 = await session.send('tools/call', { - name: 'test_mrtr_ephemeral_sampling', + name: 'test_incomplete_result_sampling', arguments: {} }); @@ -245,8 +247,8 @@ Implement a tool named \`test_mrtr_ephemeral_sampling\` (no arguments required). } checks.push({ - id: 'mrtr-ephemeral-sampling-incomplete', - name: 'MRTREphemeralSamplingIncomplete', + id: 'incomplete-result-sampling-incomplete', + name: 'IncompleteResultSamplingIncomplete', description: 'Server returns IncompleteResult with sampling inputRequest', status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -260,7 +262,7 @@ Implement a tool named \`test_mrtr_ephemeral_sampling\` (no arguments required). if (r1Errors.length === 0 && isIncompleteResult(r1Result)) { const inputKey = Object.keys(r1Result.inputRequests!)[0]; const r2 = await session.send('tools/call', { - name: 'test_mrtr_ephemeral_sampling', + name: 'test_incomplete_result_sampling', arguments: {}, inputResponses: { [inputKey]: mockSamplingResponse('The capital of France is Paris.') @@ -284,8 +286,8 @@ Implement a tool named \`test_mrtr_ephemeral_sampling\` (no arguments required). } checks.push({ - id: 'mrtr-ephemeral-sampling-complete', - name: 'MRTREphemeralSamplingComplete', + id: 'incomplete-result-sampling-complete', + name: 'IncompleteResultSamplingComplete', description: 'Server returns complete result after retry with sampling response', status: r2Errors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -297,8 +299,8 @@ Implement a tool named \`test_mrtr_ephemeral_sampling\` (no arguments required). } } catch (error) { checks.push({ - id: 'mrtr-ephemeral-sampling-incomplete', - name: 'MRTREphemeralSamplingIncomplete', + id: 'incomplete-result-sampling-incomplete', + name: 'IncompleteResultSamplingIncomplete', description: 'Server returns IncompleteResult with sampling inputRequest', status: 'FAILURE', @@ -314,14 +316,14 @@ Implement a tool named \`test_mrtr_ephemeral_sampling\` (no arguments required). // ─── A3: Basic ListRoots ───────────────────────────────────────────────────── -export class MrtrEphemeralBasicListRootsScenario implements ClientScenario { - name = 'mrtr-ephemeral-basic-list-roots'; +export class IncompleteResultBasicListRootsScenario implements ClientScenario { + name = 'incomplete-result-basic-list-roots'; specVersions: SpecVersion[] = ['draft']; - description = `Test basic ephemeral MRTR flow with a single roots/list input request (SEP-2322). + description = `Test basic ephemeral IncompleteResult flow with a single roots/list input request (SEP-2322). **Server Implementation Requirements:** -Implement a tool named \`test_mrtr_ephemeral_list_roots\` (no arguments required). +Implement a tool named \`test_incomplete_result_list_roots\` (no arguments required). **Behavior (Round 1):** When called without \`inputResponses\`, return an \`IncompleteResult\`: @@ -343,11 +345,11 @@ Implement a tool named \`test_mrtr_ephemeral_list_roots\` (no arguments required const checks: ConformanceCheck[] = []; try { - const session = await createMrtrSession(serverUrl); + const session = await createIncompleteResultSession(serverUrl); // Round 1: Initial call const r1 = await session.send('tools/call', { - name: 'test_mrtr_ephemeral_list_roots', + name: 'test_incomplete_result_list_roots', arguments: {} }); @@ -379,8 +381,8 @@ Implement a tool named \`test_mrtr_ephemeral_list_roots\` (no arguments required } checks.push({ - id: 'mrtr-ephemeral-list-roots-incomplete', - name: 'MRTREphemeralListRootsIncomplete', + id: 'incomplete-result-list-roots-incomplete', + name: 'IncompleteResultListRootsIncomplete', description: 'Server returns IncompleteResult with roots/list inputRequest', status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -394,7 +396,7 @@ Implement a tool named \`test_mrtr_ephemeral_list_roots\` (no arguments required if (r1Errors.length === 0 && isIncompleteResult(r1Result)) { const inputKey = Object.keys(r1Result.inputRequests!)[0]; const r2 = await session.send('tools/call', { - name: 'test_mrtr_ephemeral_list_roots', + name: 'test_incomplete_result_list_roots', arguments: {}, inputResponses: { [inputKey]: mockListRootsResponse() @@ -418,8 +420,8 @@ Implement a tool named \`test_mrtr_ephemeral_list_roots\` (no arguments required } checks.push({ - id: 'mrtr-ephemeral-list-roots-complete', - name: 'MRTREphemeralListRootsComplete', + id: 'incomplete-result-list-roots-complete', + name: 'IncompleteResultListRootsComplete', description: 'Server returns complete result after retry with roots response', status: r2Errors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -431,8 +433,8 @@ Implement a tool named \`test_mrtr_ephemeral_list_roots\` (no arguments required } } catch (error) { checks.push({ - id: 'mrtr-ephemeral-list-roots-incomplete', - name: 'MRTREphemeralListRootsIncomplete', + id: 'incomplete-result-list-roots-incomplete', + name: 'IncompleteResultListRootsIncomplete', description: 'Server returns IncompleteResult with roots/list inputRequest', status: 'FAILURE', @@ -448,14 +450,14 @@ Implement a tool named \`test_mrtr_ephemeral_list_roots\` (no arguments required // ─── A4: Request State ────────────────────────────────────────────────────── -export class MrtrEphemeralRequestStateScenario implements ClientScenario { - name = 'mrtr-ephemeral-request-state'; +export class IncompleteResultRequestStateScenario implements ClientScenario { + name = 'incomplete-result-request-state'; specVersions: SpecVersion[] = ['draft']; - description = `Test that requestState is correctly round-tripped in ephemeral MRTR flow (SEP-2322). + description = `Test that requestState is correctly round-tripped in ephemeral IncompleteResult flow (SEP-2322). **Server Implementation Requirements:** -Implement a tool named \`test_mrtr_request_state\` (no arguments required). +Implement a tool named \`test_incomplete_result_request_state\` (no arguments required). **Behavior (Round 1):** Return an \`IncompleteResult\` with both \`inputRequests\` and \`requestState\`: @@ -485,11 +487,11 @@ Implement a tool named \`test_mrtr_request_state\` (no arguments required). const checks: ConformanceCheck[] = []; try { - const session = await createMrtrSession(serverUrl); + const session = await createIncompleteResultSession(serverUrl); // Round 1 const r1 = await session.send('tools/call', { - name: 'test_mrtr_request_state', + name: 'test_incomplete_result_request_state', arguments: {} }); @@ -513,8 +515,8 @@ Implement a tool named \`test_mrtr_request_state\` (no arguments required). } checks.push({ - id: 'mrtr-ephemeral-request-state-incomplete', - name: 'MRTRRequestStateIncomplete', + id: 'incomplete-result-request-state-incomplete', + name: 'IncompleteResultRequestStateIncomplete', description: 'Server returns IncompleteResult with both inputRequests and requestState', status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -528,7 +530,7 @@ Implement a tool named \`test_mrtr_request_state\` (no arguments required). if (r1Errors.length === 0 && isIncompleteResult(r1Result)) { const inputKey = Object.keys(r1Result.inputRequests!)[0]; const r2 = await session.send('tools/call', { - name: 'test_mrtr_request_state', + name: 'test_incomplete_result_request_state', arguments: {}, inputResponses: { [inputKey]: mockElicitResponse({ ok: true }) @@ -561,8 +563,8 @@ Implement a tool named \`test_mrtr_request_state\` (no arguments required). } checks.push({ - id: 'mrtr-ephemeral-request-state-complete', - name: 'MRTRRequestStateComplete', + id: 'incomplete-result-request-state-complete', + name: 'IncompleteResultRequestStateComplete', description: 'Server validates echoed requestState and returns complete result', status: r2Errors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -574,8 +576,8 @@ Implement a tool named \`test_mrtr_request_state\` (no arguments required). } } catch (error) { checks.push({ - id: 'mrtr-ephemeral-request-state-incomplete', - name: 'MRTRRequestStateIncomplete', + id: 'incomplete-result-request-state-incomplete', + name: 'IncompleteResultRequestStateIncomplete', description: 'Server returns IncompleteResult with both inputRequests and requestState', status: 'FAILURE', @@ -591,16 +593,16 @@ Implement a tool named \`test_mrtr_request_state\` (no arguments required). // ─── A5: Multiple Input Requests ───────────────────────────────────────────── -export class MrtrEphemeralMultipleInputRequestsScenario +export class IncompleteResultMultipleInputRequestsScenario implements ClientScenario { - name = 'mrtr-ephemeral-multiple-input-requests'; + name = 'incomplete-result-multiple-input-requests'; specVersions: SpecVersion[] = ['draft']; description = `Test multiple input requests in a single IncompleteResult (SEP-2322). **Server Implementation Requirements:** -Implement a tool named \`test_mrtr_multiple_inputs\` (no arguments required). +Implement a tool named \`test_incomplete_result_multiple_inputs\` (no arguments required). **Behavior (Round 1):** Return an \`IncompleteResult\` with multiple \`inputRequests\` — at least one elicitation AND one sampling: @@ -636,11 +638,11 @@ Implement a tool named \`test_mrtr_multiple_inputs\` (no arguments required). const checks: ConformanceCheck[] = []; try { - const session = await createMrtrSession(serverUrl); + const session = await createIncompleteResultSession(serverUrl); // Round 1 const r1 = await session.send('tools/call', { - name: 'test_mrtr_multiple_inputs', + name: 'test_incomplete_result_multiple_inputs', arguments: {} }); @@ -672,8 +674,8 @@ Implement a tool named \`test_mrtr_multiple_inputs\` (no arguments required). } checks.push({ - id: 'mrtr-ephemeral-multiple-inputs-incomplete', - name: 'MRTRMultipleInputsIncomplete', + id: 'incomplete-result-multiple-inputs-incomplete', + name: 'IncompleteResultMultipleInputsIncomplete', description: 'Server returns IncompleteResult with multiple inputRequests of different types', status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -697,7 +699,7 @@ Implement a tool named \`test_mrtr_multiple_inputs\` (no arguments required). } const r2 = await session.send('tools/call', { - name: 'test_mrtr_multiple_inputs', + name: 'test_incomplete_result_multiple_inputs', arguments: {}, inputResponses, ...(r1Result.requestState !== undefined @@ -719,8 +721,8 @@ Implement a tool named \`test_mrtr_multiple_inputs\` (no arguments required). } checks.push({ - id: 'mrtr-ephemeral-multiple-inputs-complete', - name: 'MRTRMultipleInputsComplete', + id: 'incomplete-result-multiple-inputs-complete', + name: 'IncompleteResultMultipleInputsComplete', description: 'Server returns complete result after all inputResponses are provided', status: r2Errors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -732,8 +734,8 @@ Implement a tool named \`test_mrtr_multiple_inputs\` (no arguments required). } } catch (error) { checks.push({ - id: 'mrtr-ephemeral-multiple-inputs-incomplete', - name: 'MRTRMultipleInputsIncomplete', + id: 'incomplete-result-multiple-inputs-incomplete', + name: 'IncompleteResultMultipleInputsIncomplete', description: 'Server returns IncompleteResult with multiple inputRequests of different types', status: 'FAILURE', @@ -749,14 +751,14 @@ Implement a tool named \`test_mrtr_multiple_inputs\` (no arguments required). // ─── A6: Multi-Round ───────────────────────────────────────────────────────── -export class MrtrEphemeralMultiRoundScenario implements ClientScenario { - name = 'mrtr-ephemeral-multi-round'; +export class IncompleteResultMultiRoundScenario implements ClientScenario { + name = 'incomplete-result-multi-round'; specVersions: SpecVersion[] = ['draft']; - description = `Test multi-round ephemeral MRTR flow with evolving requestState (SEP-2322). + description = `Test multi-round ephemeral IncompleteResult flow with evolving requestState (SEP-2322). **Server Implementation Requirements:** -Implement a tool named \`test_mrtr_multi_round\` (no arguments required). +Implement a tool named \`test_incomplete_result_multi_round\` (no arguments required). **Behavior (Round 1):** Return an \`IncompleteResult\` with an elicitation request and \`requestState\`: @@ -808,11 +810,11 @@ Implement a tool named \`test_mrtr_multi_round\` (no arguments required). const checks: ConformanceCheck[] = []; try { - const session = await createMrtrSession(serverUrl); + const session = await createIncompleteResultSession(serverUrl); // Round 1 const r1 = await session.send('tools/call', { - name: 'test_mrtr_multi_round', + name: 'test_incomplete_result_multi_round', arguments: {} }); @@ -830,8 +832,8 @@ Implement a tool named \`test_mrtr_multi_round\` (no arguments required). } checks.push({ - id: 'mrtr-ephemeral-multi-round-r1', - name: 'MRTRMultiRoundR1', + id: 'incomplete-result-multi-round-r1', + name: 'IncompleteResultMultiRoundR1', description: 'Round 1: Server returns IncompleteResult with requestState', status: r1Ok ? 'SUCCESS' : 'FAILURE', @@ -848,7 +850,7 @@ Implement a tool named \`test_mrtr_multi_round\` (no arguments required). // Round 2: Retry — expect another IncompleteResult const r1InputKey = Object.keys(r1Result.inputRequests!)[0]; const r2 = await session.send('tools/call', { - name: 'test_mrtr_multi_round', + name: 'test_incomplete_result_multi_round', arguments: {}, inputResponses: { [r1InputKey]: mockElicitResponse({ name: 'Alice' }) @@ -873,8 +875,8 @@ Implement a tool named \`test_mrtr_multi_round\` (no arguments required). } checks.push({ - id: 'mrtr-ephemeral-multi-round-r2', - name: 'MRTRMultiRoundR2', + id: 'incomplete-result-multi-round-r2', + name: 'IncompleteResultMultiRoundR2', description: 'Round 2: Server returns another IncompleteResult with updated requestState', status: r2Ok ? 'SUCCESS' : 'FAILURE', @@ -891,7 +893,7 @@ Implement a tool named \`test_mrtr_multi_round\` (no arguments required). // Round 3: Final retry — expect complete result const r2InputKey = Object.keys(r2Result.inputRequests!)[0]; const r3 = await session.send('tools/call', { - name: 'test_mrtr_multi_round', + name: 'test_incomplete_result_multi_round', arguments: {}, inputResponses: { [r2InputKey]: mockElicitResponse({ color: 'blue' }) @@ -903,8 +905,8 @@ Implement a tool named \`test_mrtr_multi_round\` (no arguments required). const r3Ok = !r3.error && r3Result != null && isCompleteResult(r3Result); checks.push({ - id: 'mrtr-ephemeral-multi-round-r3', - name: 'MRTRMultiRoundR3', + id: 'incomplete-result-multi-round-r3', + name: 'IncompleteResultMultiRoundR3', description: 'Round 3: Server returns complete result', status: r3Ok ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), @@ -916,8 +918,8 @@ Implement a tool named \`test_mrtr_multi_round\` (no arguments required). }); } catch (error) { checks.push({ - id: 'mrtr-ephemeral-multi-round-r1', - name: 'MRTRMultiRoundR1', + id: 'incomplete-result-multi-round-r1', + name: 'IncompleteResultMultiRoundR1', description: 'Round 1: Server returns IncompleteResult with requestState', status: 'FAILURE', @@ -933,14 +935,16 @@ Implement a tool named \`test_mrtr_multi_round\` (no arguments required). // ─── A7: Request State Only ────────────────────────────────── -export class MrtrEphemeralRequestStateOnlyScenario implements ClientScenario { - name = 'mrtr-ephemeral-request-state-only'; +export class IncompleteResultRequestStateOnlyScenario + implements ClientScenario +{ + name = 'incomplete-result-request-state-only'; specVersions: SpecVersion[] = ['draft']; description = `Test IncompleteResult with requestState only — no inputRequests (load-shedding use case, SEP-2322). **Server Implementation Requirements:** -Implement a tool named \`test_mrtr_state_only\` (no arguments required). +Implement a tool named \`test_incomplete_result_state_only\` (no arguments required). **Behavior (Round 1):** Return an \`IncompleteResult\` with \`requestState\` but NO \`inputRequests\`: @@ -959,11 +963,11 @@ This simulates load shedding where the server transfers accumulated computation const checks: ConformanceCheck[] = []; try { - const session = await createMrtrSession(serverUrl); + const session = await createIncompleteResultSession(serverUrl); // Round 1: Expect IncompleteResult with requestState only const r1 = await session.send('tools/call', { - name: 'test_mrtr_state_only', + name: 'test_incomplete_result_state_only', arguments: {} }); @@ -986,8 +990,8 @@ This simulates load shedding where the server transfers accumulated computation } checks.push({ - id: 'mrtr-ephemeral-state-only-incomplete', - name: 'MRTRStateOnlyIncomplete', + id: 'incomplete-result-state-only-incomplete', + name: 'IncompleteResultStateOnlyIncomplete', description: 'Server returns IncompleteResult with requestState only (no inputRequests)', status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -1000,7 +1004,7 @@ This simulates load shedding where the server transfers accumulated computation // Round 2: Retry with requestState only if (r1Errors.length === 0 && isIncompleteResult(r1Result)) { const r2 = await session.send('tools/call', { - name: 'test_mrtr_state_only', + name: 'test_incomplete_result_state_only', arguments: {}, requestState: r1Result.requestState }); @@ -1010,8 +1014,8 @@ This simulates load shedding where the server transfers accumulated computation !r2.error && r2Result != null && isCompleteResult(r2Result); checks.push({ - id: 'mrtr-ephemeral-state-only-complete', - name: 'MRTRStateOnlyComplete', + id: 'incomplete-result-state-only-complete', + name: 'IncompleteResultStateOnlyComplete', description: 'Server completes after receiving echoed requestState (no inputResponses needed)', status: r2Ok ? 'SUCCESS' : 'FAILURE', @@ -1025,8 +1029,8 @@ This simulates load shedding where the server transfers accumulated computation } } catch (error) { checks.push({ - id: 'mrtr-ephemeral-state-only-incomplete', - name: 'MRTRStateOnlyIncomplete', + id: 'incomplete-result-state-only-incomplete', + name: 'IncompleteResultStateOnlyIncomplete', description: 'Server returns IncompleteResult with requestState only (no inputRequests)', status: 'FAILURE', @@ -1042,16 +1046,16 @@ This simulates load shedding where the server transfers accumulated computation // ─── A8: Missing Input Response ────────────────────────────────────────────── -export class MrtrEphemeralMissingInputResponseScenario +export class IncompleteResultMissingInputResponseScenario implements ClientScenario { - name = 'mrtr-ephemeral-missing-input-response'; + name = 'incomplete-result-missing-input-response'; specVersions: SpecVersion[] = ['draft']; description = `Test error handling when client sends wrong/missing inputResponses (SEP-2322). **Server Implementation Requirements:** -Use the same tool as A1: \`test_mrtr_ephemeral_elicitation\`. +Use the same tool as A1: \`test_incomplete_result_elicitation\`. **Behavior:** When the client retries with \`inputResponses\` that are missing required keys or contain wrong keys, the server SHOULD respond with a new \`IncompleteResult\` re-requesting the missing information (NOT a JSON-RPC error).`; @@ -1059,11 +1063,11 @@ Use the same tool as A1: \`test_mrtr_ephemeral_elicitation\`. const checks: ConformanceCheck[] = []; try { - const session = await createMrtrSession(serverUrl); + const session = await createIncompleteResultSession(serverUrl); // Round 1: Get the initial IncompleteResult const r1 = await session.send('tools/call', { - name: 'test_mrtr_ephemeral_elicitation', + name: 'test_incomplete_result_elicitation', arguments: {} }); @@ -1074,8 +1078,8 @@ Use the same tool as A1: \`test_mrtr_ephemeral_elicitation\`. !r1.result.inputRequests ) { checks.push({ - id: 'mrtr-ephemeral-missing-response-prereq', - name: 'MRTRMissingResponsePrereq', + id: 'incomplete-result-missing-response-prereq', + name: 'IncompleteResultMissingResponsePrereq', description: 'Prerequisite: Server returns IncompleteResult', status: 'FAILURE', timestamp: new Date().toISOString(), @@ -1088,7 +1092,7 @@ Use the same tool as A1: \`test_mrtr_ephemeral_elicitation\`. // Round 2: Send wrong inputResponses (wrong key) const r2 = await session.send('tools/call', { - name: 'test_mrtr_ephemeral_elicitation', + name: 'test_incomplete_result_elicitation', arguments: {}, inputResponses: { wrong_key: mockElicitResponse({ data: 'wrong' }) @@ -1117,8 +1121,8 @@ Use the same tool as A1: \`test_mrtr_ephemeral_elicitation\`. } checks.push({ - id: 'mrtr-ephemeral-missing-response-rerequests', - name: 'MRTRMissingResponseRerequests', + id: 'incomplete-result-missing-response-rerequests', + name: 'IncompleteResultMissingResponseRerequests', description: 'Server re-requests missing inputResponses via new IncompleteResult', status: r2Errors.length === 0 ? 'SUCCESS' : 'WARNING', @@ -1129,8 +1133,8 @@ Use the same tool as A1: \`test_mrtr_ephemeral_elicitation\`. }); } catch (error) { checks.push({ - id: 'mrtr-ephemeral-missing-response-rerequests', - name: 'MRTRMissingResponseRerequests', + id: 'incomplete-result-missing-response-rerequests', + name: 'IncompleteResultMissingResponseRerequests', description: 'Server re-requests missing inputResponses via new IncompleteResult', status: 'FAILURE', @@ -1146,16 +1150,16 @@ Use the same tool as A1: \`test_mrtr_ephemeral_elicitation\`. // ─── A9: Non-Tool Request (prompts/get) ────────────────────────────────────── -export class MrtrEphemeralNonToolRequestScenario implements ClientScenario { - name = 'mrtr-ephemeral-non-tool-request'; +export class IncompleteResultNonToolRequestScenario implements ClientScenario { + name = 'incomplete-result-non-tool-request'; specVersions: SpecVersion[] = ['draft']; - description = `Test IncompleteResult on a non-tool request (prompts/get) to verify MRTR is universal (SEP-2322). + description = `Test IncompleteResult on a non-tool request (prompts/get) to verify IncompleteResult is universal (SEP-2322). **Server Implementation Requirements:** -Implement a prompt named \`test_mrtr_prompt\` that requires elicitation input. +Implement a prompt named \`test_incomplete_result_prompt\` that requires elicitation input. -**Behavior (Round 1):** When \`prompts/get\` is called for \`test_mrtr_prompt\` without \`inputResponses\`, return an \`IncompleteResult\`: +**Behavior (Round 1):** When \`prompts/get\` is called for \`test_incomplete_result_prompt\` without \`inputResponses\`, return an \`IncompleteResult\`: \`\`\`json { @@ -1182,11 +1186,11 @@ Implement a prompt named \`test_mrtr_prompt\` that requires elicitation input. const checks: ConformanceCheck[] = []; try { - const session = await createMrtrSession(serverUrl); + const session = await createIncompleteResultSession(serverUrl); // Round 1 const r1 = await session.send('prompts/get', { - name: 'test_mrtr_prompt' + name: 'test_incomplete_result_prompt' }); const r1Result = r1.result; @@ -1201,8 +1205,8 @@ Implement a prompt named \`test_mrtr_prompt\` that requires elicitation input. } checks.push({ - id: 'mrtr-ephemeral-non-tool-incomplete', - name: 'MRTRNonToolIncomplete', + id: 'incomplete-result-non-tool-incomplete', + name: 'IncompleteResultNonToolIncomplete', description: 'prompts/get returns IncompleteResult with inputRequests', status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), @@ -1215,7 +1219,7 @@ Implement a prompt named \`test_mrtr_prompt\` that requires elicitation input. if (r1Errors.length === 0 && isIncompleteResult(r1Result)) { const inputKey = Object.keys(r1Result.inputRequests!)[0]; const r2 = await session.send('prompts/get', { - name: 'test_mrtr_prompt', + name: 'test_incomplete_result_prompt', inputResponses: { [inputKey]: mockElicitResponse({ context: 'test context' }) }, @@ -1240,8 +1244,8 @@ Implement a prompt named \`test_mrtr_prompt\` that requires elicitation input. } checks.push({ - id: 'mrtr-ephemeral-non-tool-complete', - name: 'MRTRNonToolComplete', + id: 'incomplete-result-non-tool-complete', + name: 'IncompleteResultNonToolComplete', description: 'prompts/get returns complete GetPromptResult after retry with inputResponses', status: r2Errors.length === 0 ? 'SUCCESS' : 'FAILURE', @@ -1253,8 +1257,8 @@ Implement a prompt named \`test_mrtr_prompt\` that requires elicitation input. } } catch (error) { checks.push({ - id: 'mrtr-ephemeral-non-tool-incomplete', - name: 'MRTRNonToolIncomplete', + id: 'incomplete-result-non-tool-incomplete', + name: 'IncompleteResultNonToolIncomplete', description: 'prompts/get returns IncompleteResult with inputRequests', status: 'FAILURE', timestamp: new Date().toISOString(), From 78032aa091533ef098133a571a9c98cfaf879663 Mon Sep 17 00:00:00 2001 From: Caitie McCaffrey Date: Tue, 17 Mar 2026 15:24:46 -0700 Subject: [PATCH 06/11] update rawMCP connection logic to not use sessionId, remove unnecessary wrapper class --- src/scenarios/index.ts | 2 +- src/scenarios/server/client-helper.ts | 17 ++-------------- .../server/incomplete-result-helpers.ts | 20 +++++-------------- .../server/incomplete-result-tasks.ts | 10 +++++----- .../server/incomplete-result-transition.ts | 4 ++-- .../server/incomplete-result-validation.ts | 6 +++--- src/scenarios/server/incomplete-result.ts | 20 +++++++++---------- 7 files changed, 28 insertions(+), 51 deletions(-) diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index 04fb1db..9cc9aaf 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -53,7 +53,7 @@ import { import { DNSRebindingProtectionScenario } from './server/dns-rebinding'; -// IncompleteResult scenarios (SEP-2322) +// IncompleteResult scenarios from (SEP-2322) import { IncompleteResultBasicElicitationScenario, IncompleteResultBasicSamplingScenario, diff --git a/src/scenarios/server/client-helper.ts b/src/scenarios/server/client-helper.ts index b53f41d..a651b68 100644 --- a/src/scenarios/server/client-helper.ts +++ b/src/scenarios/server/client-helper.ts @@ -81,7 +81,6 @@ export async function connectToServer( */ export class RawMcpSession { private nextId = 1; - private sessionId: string | undefined; private serverUrl: string; private connection: MCPClientConnection | null = null; @@ -95,7 +94,6 @@ export class RawMcpSession { */ async initialize(): Promise { this.connection = await connectToServer(this.serverUrl); - this.sessionId = this.connection.transport.sessionId; } /** @@ -113,9 +111,6 @@ export class RawMcpSession { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream' }; - if (this.sessionId) { - headers['mcp-session-id'] = this.sessionId; - } const body = JSON.stringify({ jsonrpc: '2.0', @@ -130,15 +125,11 @@ export class RawMcpSession { body }); - // Update session ID if server sends a new one - const sid = response.headers['mcp-session-id']; - if (sid) { - this.sessionId = Array.isArray(sid) ? sid[0] : sid; - } - const contentType = response.headers['content-type'] ?? ''; // Handle SSE responses — parse the last JSON-RPC message from the stream + // Not doing proper handling of SSE here since none of the MRTR features under test currently require it. + // This can be expanded if necessary for new features. if (contentType.includes('text/event-stream')) { const text = await response.body.text(); return parseSseResponse(text); @@ -157,10 +148,6 @@ export class RawMcpSession { this.connection = null; } } - - getSessionId(): string | undefined { - return this.sessionId; - } } /** diff --git a/src/scenarios/server/incomplete-result-helpers.ts b/src/scenarios/server/incomplete-result-helpers.ts index 82b128c..76beea8 100644 --- a/src/scenarios/server/incomplete-result-helpers.ts +++ b/src/scenarios/server/incomplete-result-helpers.ts @@ -1,14 +1,13 @@ /** - * IncompleteResult helpers for SEP-2322 conformance tests. + * Helpers for SEP-2322 conformance tests. * * Uses RawMcpSession from client-helper.ts for connection management and * raw JSON-RPC transport. This file adds IncompleteResult-specific type - * guards, mock response builders, and convenience wrappers. + * guards and mock response builders. */ import { RawMcpSession, - createRawSession, JsonRpcResponse } from './client-helper'; @@ -45,11 +44,14 @@ export function isIncompleteResult( /** * Check if a JSON-RPC result is a complete result (not incomplete). + * complete is the default so if result_type is missing we assume it's complete. */ export function isCompleteResult( result: Record | undefined ): boolean { if (!result) return false; + if (result.result_type === 'complete') return true; + if (!('result_type' in result)) return true; return !isIncompleteResult(result); } @@ -105,18 +107,6 @@ export function mockListRootsResponse(): Record { }; } -// ─── Session Factory ───────────────────────────────────────────────────────── - -/** - * Create an initialized raw MCP session for IncompleteResult testing. - * Delegates to createRawSession from client-helper.ts. - */ -export async function createIncompleteResultSession( - serverUrl: string -): Promise { - return createRawSession(serverUrl); -} - // ─── Spec References ───────────────────────────────────────────────────────── /** diff --git a/src/scenarios/server/incomplete-result-tasks.ts b/src/scenarios/server/incomplete-result-tasks.ts index 770165b..c2c9488 100644 --- a/src/scenarios/server/incomplete-result-tasks.ts +++ b/src/scenarios/server/incomplete-result-tasks.ts @@ -1,5 +1,5 @@ /** - * SEP-2322: IncompleteResult - Persistent Workflow Tests + * SEP-2322: MRTR Tests for Persistent Workflow aka Tasks * * Tests the persistent (task-based) workflow where servers use Tasks to * manage long-running operations that require additional input via @@ -7,8 +7,8 @@ */ import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types'; +import { createRawSession } from './client-helper'; import { - createIncompleteResultSession, isIncompleteResult, isCompleteResult, mockElicitResponse, @@ -88,7 +88,7 @@ Implement a tool named \`test_incomplete_result_task\` that supports task-augmen const checks: ConformanceCheck[] = []; try { - const session = await createIncompleteResultSession(serverUrl); + const session = await createRawSession(serverUrl); // Step 1: Call tool with task metadata const r1 = await session.send('tools/call', { @@ -343,7 +343,7 @@ Use the same tool as B1: \`test_incomplete_result_task\`. const checks: ConformanceCheck[] = []; try { - const session = await createIncompleteResultSession(serverUrl); + const session = await createRawSession(serverUrl); // Create task and wait for input_required const r1 = await session.send('tools/call', { @@ -502,7 +502,7 @@ This tests the schema: \`TaskInputResponseResultResponse.result: Result | Incomp const checks: ConformanceCheck[] = []; try { - const session = await createIncompleteResultSession(serverUrl); + const session = await createRawSession(serverUrl); // Create task const r1 = await session.send('tools/call', { diff --git a/src/scenarios/server/incomplete-result-transition.ts b/src/scenarios/server/incomplete-result-transition.ts index 1719ba2..dce4390 100644 --- a/src/scenarios/server/incomplete-result-transition.ts +++ b/src/scenarios/server/incomplete-result-transition.ts @@ -7,8 +7,8 @@ */ import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types'; +import { createRawSession } from './client-helper'; import { - createIncompleteResultSession, isIncompleteResult, mockElicitResponse, MRTR_SPEC_REFERENCES, @@ -67,7 +67,7 @@ This tests the pattern where a server gathers required input via IncompleteResul const checks: ConformanceCheck[] = []; try { - const session = await createIncompleteResultSession(serverUrl); + const session = await createRawSession(serverUrl); // Step 1: Call tool with task metadata — server responds with IncompleteResult const r1 = await session.send('tools/call', { diff --git a/src/scenarios/server/incomplete-result-validation.ts b/src/scenarios/server/incomplete-result-validation.ts index 7c6636f..0386ffa 100644 --- a/src/scenarios/server/incomplete-result-validation.ts +++ b/src/scenarios/server/incomplete-result-validation.ts @@ -7,8 +7,8 @@ */ import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types'; +import { createRawSession } from './client-helper'; import { - createIncompleteResultSession, isIncompleteResult, mockElicitResponse, mockSamplingResponse, @@ -63,7 +63,7 @@ Implement a tool named \`test_incomplete_result_validate_structure\` that return const checks: ConformanceCheck[] = []; try { - const session = await createIncompleteResultSession(serverUrl); + const session = await createRawSession(serverUrl); // Get IncompleteResult const r1 = await session.send('tools/call', { @@ -260,7 +260,7 @@ When retried with valid \`inputResponses\` for all three, return a final result. const checks: ConformanceCheck[] = []; try { - const session = await createIncompleteResultSession(serverUrl); + const session = await createRawSession(serverUrl); const r1 = await session.send('tools/call', { name: 'test_incomplete_result_input_types', diff --git a/src/scenarios/server/incomplete-result.ts b/src/scenarios/server/incomplete-result.ts index ad7342c..7e71406 100644 --- a/src/scenarios/server/incomplete-result.ts +++ b/src/scenarios/server/incomplete-result.ts @@ -7,8 +7,8 @@ */ import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types'; +import { createRawSession } from './client-helper'; import { - createIncompleteResultSession, isIncompleteResult, isCompleteResult, mockElicitResponse, @@ -65,7 +65,7 @@ Implement a tool named \`test_tool_with_elicitation\` (no arguments required). const checks: ConformanceCheck[] = []; try { - const session = await createIncompleteResultSession(serverUrl); + const session = await createRawSession(serverUrl); // Round 1: Initial call — expect IncompleteResult const r1 = await session.send('tools/call', { @@ -211,7 +211,7 @@ Implement a tool named \`test_incomplete_result_sampling\` (no arguments require const checks: ConformanceCheck[] = []; try { - const session = await createIncompleteResultSession(serverUrl); + const session = await createRawSession(serverUrl); // Round 1: Initial call const r1 = await session.send('tools/call', { @@ -345,7 +345,7 @@ Implement a tool named \`test_incomplete_result_list_roots\` (no arguments requi const checks: ConformanceCheck[] = []; try { - const session = await createIncompleteResultSession(serverUrl); + const session = await createRawSession(serverUrl); // Round 1: Initial call const r1 = await session.send('tools/call', { @@ -487,7 +487,7 @@ Implement a tool named \`test_incomplete_result_request_state\` (no arguments re const checks: ConformanceCheck[] = []; try { - const session = await createIncompleteResultSession(serverUrl); + const session = await createRawSession(serverUrl); // Round 1 const r1 = await session.send('tools/call', { @@ -638,7 +638,7 @@ Implement a tool named \`test_incomplete_result_multiple_inputs\` (no arguments const checks: ConformanceCheck[] = []; try { - const session = await createIncompleteResultSession(serverUrl); + const session = await createRawSession(serverUrl); // Round 1 const r1 = await session.send('tools/call', { @@ -810,7 +810,7 @@ Implement a tool named \`test_incomplete_result_multi_round\` (no arguments requ const checks: ConformanceCheck[] = []; try { - const session = await createIncompleteResultSession(serverUrl); + const session = await createRawSession(serverUrl); // Round 1 const r1 = await session.send('tools/call', { @@ -963,7 +963,7 @@ This simulates load shedding where the server transfers accumulated computation const checks: ConformanceCheck[] = []; try { - const session = await createIncompleteResultSession(serverUrl); + const session = await createRawSession(serverUrl); // Round 1: Expect IncompleteResult with requestState only const r1 = await session.send('tools/call', { @@ -1063,7 +1063,7 @@ Use the same tool as A1: \`test_incomplete_result_elicitation\`. const checks: ConformanceCheck[] = []; try { - const session = await createIncompleteResultSession(serverUrl); + const session = await createRawSession(serverUrl); // Round 1: Get the initial IncompleteResult const r1 = await session.send('tools/call', { @@ -1186,7 +1186,7 @@ Implement a prompt named \`test_incomplete_result_prompt\` that requires elicita const checks: ConformanceCheck[] = []; try { - const session = await createIncompleteResultSession(serverUrl); + const session = await createRawSession(serverUrl); // Round 1 const r1 = await session.send('prompts/get', { From b8621d43797b279b986760e5beaa758723be05a8 Mon Sep 17 00:00:00 2001 From: Caitie McCaffrey Date: Tue, 17 Mar 2026 15:56:14 -0700 Subject: [PATCH 07/11] update all tests to include all types of inputRequests --- src/scenarios/server/incomplete-result.ts | 37 +++++++++++++++++------ 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/scenarios/server/incomplete-result.ts b/src/scenarios/server/incomplete-result.ts index 7e71406..05955bd 100644 --- a/src/scenarios/server/incomplete-result.ts +++ b/src/scenarios/server/incomplete-result.ts @@ -115,7 +115,7 @@ Implement a tool named \`test_tool_with_elicitation\` (no arguments required). // Round 2: Retry with inputResponses — expect complete result if (r1Errors.length === 0 && isIncompleteResult(r1Result)) { const r2 = await session.send('tools/call', { - name: 'test_incomplete_result_elicitation', + name: 'test_tool_with_elicitation', arguments: {}, inputResponses: { user_name: mockElicitResponse({ name: 'Alice' }) @@ -604,7 +604,7 @@ export class IncompleteResultMultipleInputRequestsScenario Implement a tool named \`test_incomplete_result_multiple_inputs\` (no arguments required). -**Behavior (Round 1):** Return an \`IncompleteResult\` with multiple \`inputRequests\` — at least one elicitation AND one sampling: +**Behavior (Round 1):** Return an \`IncompleteResult\` with multiple \`inputRequests\` — elicitation, sampling, and roots/list — plus \`requestState\`: \`\`\`json { @@ -627,12 +627,17 @@ Implement a tool named \`test_incomplete_result_multiple_inputs\` (no arguments "messages": [{ "role": "user", "content": { "type": "text", "text": "Generate a greeting" } }], "maxTokens": 50 } + }, + "client_roots": { + "method": "roots/list", + "params": {} } - } + }, + "requestState": "" } \`\`\` -**Behavior (Round 2):** When called with \`inputResponses\` containing ALL keys, return a complete result.`; +**Behavior (Round 2):** When called with \`inputResponses\` containing ALL keys and the echoed \`requestState\`, return a complete result.`; async run(serverUrl: string): Promise { const checks: ConformanceCheck[] = []; @@ -656,19 +661,33 @@ Implement a tool named \`test_incomplete_result_multiple_inputs\` (no arguments } else if (!r1Result.inputRequests) { r1Errors.push('IncompleteResult missing inputRequests'); } else { + if (!r1Result.requestState) { + r1Errors.push('IncompleteResult missing requestState'); + } + const keys = Object.keys(r1Result.inputRequests); - if (keys.length < 2) { + if (keys.length < 3) { r1Errors.push( - `Expected at least 2 inputRequests, got ${keys.length}` + `Expected at least 3 inputRequests, got ${keys.length}` ); } - // Check that we have different method types + + // Check that required method types are present const methods = new Set( keys.map((k) => r1Result.inputRequests![k].method) ); - if (methods.size < 2) { + if (!methods.has('elicitation/create')) { + r1Errors.push('Expected an elicitation/create inputRequest'); + } + if (!methods.has('sampling/createMessage')) { + r1Errors.push('Expected a sampling/createMessage inputRequest'); + } + if (!methods.has('roots/list')) { + r1Errors.push('Expected a roots/list inputRequest'); + } + if (methods.size < 3) { r1Errors.push( - 'Expected inputRequests with different method types (e.g., elicitation + sampling)' + 'Expected inputRequests with different method types (elicitation + sampling + roots/list)' ); } } From 85ef0af479165d417f038ad664bf1c263b2305ce Mon Sep 17 00:00:00 2001 From: Caitie McCaffrey Date: Tue, 17 Mar 2026 16:08:40 -0700 Subject: [PATCH 08/11] removing duplicative tests --- .../server/incomplete-result-transition.ts | 215 -------- .../server/incomplete-result-validation.ts | 480 ------------------ src/scenarios/server/incomplete-result.ts | 193 +------ 3 files changed, 27 insertions(+), 861 deletions(-) delete mode 100644 src/scenarios/server/incomplete-result-transition.ts delete mode 100644 src/scenarios/server/incomplete-result-validation.ts diff --git a/src/scenarios/server/incomplete-result-transition.ts b/src/scenarios/server/incomplete-result-transition.ts deleted file mode 100644 index dce4390..0000000 --- a/src/scenarios/server/incomplete-result-transition.ts +++ /dev/null @@ -1,215 +0,0 @@ -/** - * SEP-2322: IncompleteResult - IncompleteResult-to-Task Transition Test - * - * Tests the transition from an IncompleteResult workflow to a task-based - * workflow, as described in the SEP section "Interactions Between IncompleteResult - * and Task-Based Workflows." - */ - -import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types'; -import { createRawSession } from './client-helper'; -import { - isIncompleteResult, - mockElicitResponse, - MRTR_SPEC_REFERENCES, - RawMcpSession -} from './incomplete-result-helpers'; - -/** - * Poll tasks/get until the task reaches the expected status or times out. - */ -async function pollTaskStatus( - session: RawMcpSession, - taskId: string, - expectedStatus: string, - maxAttempts: number = 20, - intervalMs: number = 250 -): Promise | null> { - for (let i = 0; i < maxAttempts; i++) { - const response = await session.send('tasks/get', { taskId }); - if (response.error) return null; - const result = response.result; - if (!result) return null; - if (result.status === expectedStatus) return result; - if ( - result.status === 'completed' || - result.status === 'failed' || - result.status === 'cancelled' - ) { - return result; - } - await new Promise((resolve) => setTimeout(resolve, intervalMs)); - } - return null; -} - -// ─── D1: IncompleteResult-to-Task Transition ───────────────────────────────── - -export class IncompleteResultToTaskTransitionScenario - implements ClientScenario -{ - name = 'incomplete-result-to-task-transition'; - specVersions: SpecVersion[] = ['draft']; - description = `Test transition from IncompleteResult to task-based workflow (SEP-2322). - -**Server Implementation Requirements:** - -Implement a tool named \`test_incomplete_result_transition\` that demonstrates the IncompleteResult-to-task-based workflow transition. - -**Behavior:** -1. When called with \`task\` metadata in params, the server initially responds with an \`IncompleteResult\` (with \`inputRequests\`) rather than creating a task immediately -2. When the client retries the \`tools/call\` with \`inputResponses\` AND \`task\` metadata, the server now creates a task and returns a \`CreateTaskResult\` with a task ID -3. The task can then be managed via the Tasks API - -This tests the pattern where a server gathers required input via IncompleteResult before committing to task creation, as described in the SEP section "Interactions Between IncompleteResult and Task-Based Workflows."`; - - async run(serverUrl: string): Promise { - const checks: ConformanceCheck[] = []; - - try { - const session = await createRawSession(serverUrl); - - // Step 1: Call tool with task metadata — server responds with IncompleteResult - const r1 = await session.send('tools/call', { - name: 'test_incomplete_result_transition', - arguments: {}, - task: { ttl: 30000 } - }); - - const r1Result = r1.result; - const ephErrors: string[] = []; - - if (r1.error) { - ephErrors.push(`JSON-RPC error: ${r1.error.message}`); - } else if (!r1Result) { - ephErrors.push('No result in response'); - } else if (!isIncompleteResult(r1Result)) { - ephErrors.push( - 'Expected initial IncompleteResult (IncompleteResult response despite task metadata)' - ); - } else if (!r1Result.inputRequests) { - ephErrors.push('IncompleteResult missing inputRequests'); - } else { - // Verify there is NO task in the response — it should be IncompleteResult - if (r1Result.task) { - ephErrors.push( - 'IncompleteResult step should not include task — expected no task creation yet' - ); - } - } - - checks.push({ - id: 'incomplete-result-transition-ephemeral-phase', - name: 'IncompleteResultTransitionEphemeralPhase', - description: - 'Server responds with IncompleteResult before creating task', - status: ephErrors.length === 0 ? 'SUCCESS' : 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: ephErrors.length > 0 ? ephErrors.join('; ') : undefined, - specReferences: MRTR_SPEC_REFERENCES, - details: { result: r1Result } - }); - - if (ephErrors.length > 0 || !isIncompleteResult(r1Result)) return checks; - - // Step 2: Retry with inputResponses + task metadata — server creates task - const inputKey = Object.keys( - r1Result.inputRequests as Record - )[0]; - const r2 = await session.send('tools/call', { - name: 'test_incomplete_result_transition', - arguments: {}, - inputResponses: { - [inputKey]: mockElicitResponse({ confirmed: true }) - }, - requestState: - typeof r1Result.requestState === 'string' - ? r1Result.requestState - : undefined, - task: { ttl: 30000 } - }); - - const r2Result = r2.result; - const transErrors: string[] = []; - let taskId: string | undefined; - - if (r2.error) { - transErrors.push(`JSON-RPC error: ${r2.error.message}`); - } else if (!r2Result) { - transErrors.push('No result from retry'); - } else { - // Should now have a task (task-based workflow) - const task = r2Result.task as - | { taskId?: string; status?: string } - | undefined; - if (!task?.taskId) { - transErrors.push( - 'Expected CreateTaskResult with task.taskId after providing input' - ); - } else { - taskId = task.taskId; - } - } - - checks.push({ - id: 'incomplete-result-transition-task-created', - name: 'IncompleteResultTransitionTaskCreated', - description: - 'Server transitions to task-based workflow and creates task', - status: transErrors.length === 0 ? 'SUCCESS' : 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: - transErrors.length > 0 ? transErrors.join('; ') : undefined, - specReferences: MRTR_SPEC_REFERENCES, - details: { result: r2Result, taskId } - }); - - if (!taskId) return checks; - - // Step 3: Verify the task is accessible via Tasks API - const taskState = await pollTaskStatus( - session, - taskId, - 'completed', - 40, - 250 - ); - - const taskErrors: string[] = []; - if (!taskState) { - taskErrors.push('Could not retrieve task state via tasks/get'); - } else if ( - taskState.status !== 'completed' && - taskState.status !== 'working' - ) { - // Accept working or completed — just verify the task is real - taskErrors.push(`Unexpected task status: "${taskState.status}"`); - } - - checks.push({ - id: 'incomplete-result-transition-task-accessible', - name: 'IncompleteResultTransitionTaskAccessible', - description: - 'Created task is accessible via Tasks API after transition', - status: taskErrors.length === 0 ? 'SUCCESS' : 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: taskErrors.length > 0 ? taskErrors.join('; ') : undefined, - specReferences: MRTR_SPEC_REFERENCES, - details: { taskState } - }); - } catch (error) { - checks.push({ - id: 'incomplete-result-transition-ephemeral-phase', - name: 'IncompleteResultTransitionEphemeralPhase', - description: - 'Server responds with IncompleteResult before creating task', - status: 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, - specReferences: MRTR_SPEC_REFERENCES - }); - } - - return checks; - } -} diff --git a/src/scenarios/server/incomplete-result-validation.ts b/src/scenarios/server/incomplete-result-validation.ts deleted file mode 100644 index 0386ffa..0000000 --- a/src/scenarios/server/incomplete-result-validation.ts +++ /dev/null @@ -1,480 +0,0 @@ -/** - * SEP-2322: IncompleteResult - Schema/Structure Validation Tests - * - * Tests that validate the structure and correctness of IncompleteResult protocol - * messages, including IncompleteResult format, InputRequest types, and - * result_type field behavior. - */ - -import { ClientScenario, ConformanceCheck, SpecVersion } from '../../types'; -import { createRawSession } from './client-helper'; -import { - isIncompleteResult, - mockElicitResponse, - mockSamplingResponse, - mockListRootsResponse, - MRTR_SPEC_REFERENCES -} from './incomplete-result-helpers'; - -// ─── C1: IncompleteResult Structure Validation ────────────────────────────── - -export class IncompleteResultStructureScenario implements ClientScenario { - name = 'incomplete-result-structure'; - specVersions: SpecVersion[] = ['draft']; - description = `Validate the IncompleteResult structure conforms to the schema (SEP-2322). - -**Server Implementation Requirements:** - -Implement a tool named \`test_incomplete_result_validate_structure\` that returns an \`IncompleteResult\` with well-formed fields. - -**Behavior:** -1. When called, return an \`IncompleteResult\` with both \`inputRequests\` and \`requestState\`: - -\`\`\`json -{ - "result_type": "incomplete", - "inputRequests": { - "req1": { - "method": "elicitation/create", - "params": { - "message": "Validation test", - "requestedSchema": { - "type": "object", - "properties": { "value": { "type": "string" } }, - "required": ["value"] - } - } - } - }, - "requestState": "validation-state-token" -} -\`\`\` - -2. When retried with correct \`inputResponses\` and \`requestState\`, return a final result with \`result_type\` absent (testing default behavior). - -**Validation checks:** -- \`result_type\` is exactly \`"incomplete"\` (not any other string) -- \`inputRequests\` is present and is a map (object) -- Each \`inputRequests\` value has \`method\` and \`params\` -- \`requestState\` is a string -- Final result has \`result_type\` absent (backward compat for "complete")`; - - async run(serverUrl: string): Promise { - const checks: ConformanceCheck[] = []; - - try { - const session = await createRawSession(serverUrl); - - // Get IncompleteResult - const r1 = await session.send('tools/call', { - name: 'test_incomplete_result_validate_structure', - arguments: {} - }); - - const r1Result = r1.result; - const structureErrors: string[] = []; - - if (r1.error) { - structureErrors.push(`JSON-RPC error: ${r1.error.message}`); - } else if (!r1Result) { - structureErrors.push('No result in response'); - } else { - // Check result_type is exactly "incomplete" - if (r1Result.result_type !== 'incomplete') { - structureErrors.push( - `result_type should be "incomplete", got "${r1Result.result_type}"` - ); - } - - // Check inputRequests is present and is an object - if (!r1Result.inputRequests) { - structureErrors.push('inputRequests is missing'); - } else if ( - typeof r1Result.inputRequests !== 'object' || - Array.isArray(r1Result.inputRequests) - ) { - structureErrors.push( - 'inputRequests should be an object (map), not an array' - ); - } else { - // Validate each input request has method and params - const requests = r1Result.inputRequests as Record< - string, - Record - >; - for (const [key, value] of Object.entries(requests)) { - if (!value.method || typeof value.method !== 'string') { - structureErrors.push( - `inputRequests["${key}"] missing valid "method" field` - ); - } - if (!value.params || typeof value.params !== 'object') { - structureErrors.push( - `inputRequests["${key}"] missing valid "params" field` - ); - } - } - } - - // Check requestState if present - if ( - 'requestState' in r1Result && - typeof r1Result.requestState !== 'string' - ) { - structureErrors.push( - `requestState should be a string, got ${typeof r1Result.requestState}` - ); - } - } - - checks.push({ - id: 'incomplete-result-validate-incomplete-result-fields', - name: 'IncompleteResultValidateIncompleteResultFields', - description: - 'IncompleteResult has correct result_type, inputRequests, and requestState', - status: structureErrors.length === 0 ? 'SUCCESS' : 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: - structureErrors.length > 0 ? structureErrors.join('; ') : undefined, - specReferences: MRTR_SPEC_REFERENCES, - details: { result: r1Result } - }); - - // Check backward compatibility: final result with absent result_type - if (r1Result && isIncompleteResult(r1Result)) { - const inputKey = Object.keys( - r1Result.inputRequests as Record - )[0]; - const r2 = await session.send('tools/call', { - name: 'test_incomplete_result_validate_structure', - arguments: {}, - inputResponses: { - [inputKey]: mockElicitResponse({ value: 'test' }) - }, - requestState: - typeof r1Result.requestState === 'string' - ? r1Result.requestState - : undefined - }); - - const r2Result = r2.result; - const compatErrors: string[] = []; - - if (r2.error) { - compatErrors.push(`JSON-RPC error: ${r2.error.message}`); - } else if (!r2Result) { - compatErrors.push('No result from retry'); - } else { - // result_type should be absent or "complete" for backward compat - if ( - 'result_type' in r2Result && - r2Result.result_type !== 'complete' && - r2Result.result_type !== undefined - ) { - compatErrors.push( - `Final result should have result_type absent or "complete", got "${r2Result.result_type}"` - ); - } - } - - checks.push({ - id: 'incomplete-result-validate-complete-result-default', - name: 'IncompleteResultValidateCompleteResultDefault', - description: - 'Complete result has result_type absent or "complete" (backward compat)', - status: compatErrors.length === 0 ? 'SUCCESS' : 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: - compatErrors.length > 0 ? compatErrors.join('; ') : undefined, - specReferences: MRTR_SPEC_REFERENCES, - details: { result: r2Result } - }); - } - } catch (error) { - checks.push({ - id: 'incomplete-result-validate-incomplete-result-fields', - name: 'IncompleteResultValidateIncompleteResultFields', - description: - 'IncompleteResult has correct result_type, inputRequests, and requestState', - status: 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, - specReferences: MRTR_SPEC_REFERENCES - }); - } - - return checks; - } -} - -// ─── C2: InputRequest Types Validation ─────────────────────────────────────── - -export class InputRequestTypesScenario implements ClientScenario { - name = 'input-request-types'; - specVersions: SpecVersion[] = ['draft']; - description = `Validate all three InputRequest types in IncompleteResult. - -**Server Implementation Requirements:** - -Implement a tool named \`test_incomplete_result_input_types\` that returns an \`IncompleteResult\` containing all three types of \`InputRequest\`. - -**Behavior:** - -When called, return: - -\`\`\`json -{ - "result_type": "incomplete", - "inputRequests": { - "elicit": { - "method": "elicitation/create", - "params": { - "message": "Please provide a value", - "requestedSchema": { - "type": "object", - "properties": { "value": { "type": "string" } }, - "required": ["value"] - } - } - }, - "sample": { - "method": "sampling/createMessage", - "params": { - "messages": [ - { "role": "user", "content": { "type": "text", "text": "Generate a response" } } - ], - "maxTokens": 100 - } - }, - "roots": { - "method": "roots/list", - "params": {} - } - } -} -\`\`\` - -When retried with valid \`inputResponses\` for all three, return a final result.`; - - async run(serverUrl: string): Promise { - const checks: ConformanceCheck[] = []; - - try { - const session = await createRawSession(serverUrl); - - const r1 = await session.send('tools/call', { - name: 'test_incomplete_result_input_types', - arguments: {} - }); - - const r1Result = r1.result; - - if (r1.error || !r1Result || !isIncompleteResult(r1Result)) { - checks.push({ - id: 'incomplete-result-validate-input-types-prereq', - name: 'IncompleteResultValidateInputTypesPrereq', - description: 'Prerequisite: Get IncompleteResult', - status: 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: r1.error - ? `JSON-RPC error: ${r1.error.message}` - : 'Expected IncompleteResult', - specReferences: MRTR_SPEC_REFERENCES - }); - return checks; - } - - const inputRequests = r1Result.inputRequests as - | Record> - | undefined; - - if (!inputRequests) { - checks.push({ - id: 'incomplete-result-validate-input-types-prereq', - name: 'IncompleteResultValidateInputTypesPrereq', - description: 'Prerequisite: inputRequests present', - status: 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: 'inputRequests missing from IncompleteResult', - specReferences: MRTR_SPEC_REFERENCES - }); - return checks; - } - - // Find each type of InputRequest - const foundTypes: Record< - string, - { key: string; request: Record } - > = {}; - for (const [key, value] of Object.entries(inputRequests)) { - const method = value.method as string; - if (method === 'elicitation/create') - foundTypes['elicitation'] = { key, request: value }; - else if (method === 'sampling/createMessage') - foundTypes['sampling'] = { key, request: value }; - else if (method === 'roots/list') - foundTypes['roots'] = { key, request: value }; - } - - // Check elicitation - const elicitErrors: string[] = []; - if (!foundTypes['elicitation']) { - elicitErrors.push('No elicitation/create InputRequest found'); - } else { - const params = foundTypes['elicitation'].request.params as - | Record - | undefined; - if (!params) { - elicitErrors.push('elicitation/create missing params'); - } else { - if (typeof params.message !== 'string') { - elicitErrors.push( - 'elicitation/create params.message should be a string' - ); - } - if ( - !params.requestedSchema || - typeof params.requestedSchema !== 'object' - ) { - elicitErrors.push( - 'elicitation/create params.requestedSchema should be an object' - ); - } - } - } - - checks.push({ - id: 'incomplete-result-validate-elicitation-input-request', - name: 'IncompleteResultValidateElicitationInputRequest', - description: 'elicitation/create InputRequest has valid structure', - status: elicitErrors.length === 0 ? 'SUCCESS' : 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: - elicitErrors.length > 0 ? elicitErrors.join('; ') : undefined, - specReferences: MRTR_SPEC_REFERENCES, - details: { request: foundTypes['elicitation']?.request } - }); - - // Check sampling - const samplingErrors: string[] = []; - if (!foundTypes['sampling']) { - samplingErrors.push('No sampling/createMessage InputRequest found'); - } else { - const params = foundTypes['sampling'].request.params as - | Record - | undefined; - if (!params) { - samplingErrors.push('sampling/createMessage missing params'); - } else { - if (!Array.isArray(params.messages)) { - samplingErrors.push( - 'sampling/createMessage params.messages should be an array' - ); - } - if (typeof params.maxTokens !== 'number') { - samplingErrors.push( - 'sampling/createMessage params.maxTokens should be a number' - ); - } - } - } - - checks.push({ - id: 'incomplete-result-validate-sampling-input-request', - name: 'IncompleteResultValidateSamplingInputRequest', - description: 'sampling/createMessage InputRequest has valid structure', - status: samplingErrors.length === 0 ? 'SUCCESS' : 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: - samplingErrors.length > 0 ? samplingErrors.join('; ') : undefined, - specReferences: MRTR_SPEC_REFERENCES, - details: { request: foundTypes['sampling']?.request } - }); - - // Check roots - const rootsErrors: string[] = []; - if (!foundTypes['roots']) { - rootsErrors.push('No roots/list InputRequest found'); - } else { - // roots/list has minimal params, just check structure - if (!foundTypes['roots'].request.params) { - rootsErrors.push('roots/list missing params'); - } - } - - checks.push({ - id: 'incomplete-result-validate-roots-input-request', - name: 'IncompleteResultValidateRootsInputRequest', - description: 'roots/list InputRequest has valid structure', - status: rootsErrors.length === 0 ? 'SUCCESS' : 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: - rootsErrors.length > 0 ? rootsErrors.join('; ') : undefined, - specReferences: MRTR_SPEC_REFERENCES, - details: { request: foundTypes['roots']?.request } - }); - - // Retry with responses for all three types - const inputResponses: Record = {}; - if (foundTypes['elicitation']) { - inputResponses[foundTypes['elicitation'].key] = mockElicitResponse({ - value: 'test' - }); - } - if (foundTypes['sampling']) { - inputResponses[foundTypes['sampling'].key] = mockSamplingResponse( - 'Generated response text' - ); - } - if (foundTypes['roots']) { - inputResponses[foundTypes['roots'].key] = mockListRootsResponse(); - } - - const r2 = await session.send('tools/call', { - name: 'test_incomplete_result_input_types', - arguments: {}, - inputResponses, - requestState: - typeof r1Result.requestState === 'string' - ? r1Result.requestState - : undefined - }); - - const retryErrors: string[] = []; - if (r2.error) { - retryErrors.push(`JSON-RPC error: ${r2.error.message}`); - } else if (!r2.result) { - retryErrors.push('No result from retry'); - } else if (isIncompleteResult(r2.result)) { - retryErrors.push( - 'Expected complete result after providing all inputResponses' - ); - } - - checks.push({ - id: 'incomplete-result-validate-all-types-retry', - name: 'IncompleteResultValidateAllTypesRetry', - description: - 'Retry with all three InputResponse types produces final result', - status: retryErrors.length === 0 ? 'SUCCESS' : 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: - retryErrors.length > 0 ? retryErrors.join('; ') : undefined, - specReferences: MRTR_SPEC_REFERENCES, - details: { result: r2.result } - }); - } catch (error) { - checks.push({ - id: 'incomplete-result-validate-input-types-prereq', - name: 'IncompleteResultValidateInputTypesPrereq', - description: 'Prerequisite: Get IncompleteResult with InputRequests', - status: 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, - specReferences: MRTR_SPEC_REFERENCES - }); - } - - return checks; - } -} diff --git a/src/scenarios/server/incomplete-result.ts b/src/scenarios/server/incomplete-result.ts index 05955bd..f4fb284 100644 --- a/src/scenarios/server/incomplete-result.ts +++ b/src/scenarios/server/incomplete-result.ts @@ -838,7 +838,7 @@ Implement a tool named \`test_incomplete_result_multi_round\` (no arguments requ }); const r1Result = r1.result; - let r1Ok = false; + let round1Complete = false; if ( !r1.error && @@ -847,7 +847,7 @@ Implement a tool named \`test_incomplete_result_multi_round\` (no arguments requ r1Result.inputRequests && r1Result.requestState ) { - r1Ok = true; + round1Complete = true; } checks.push({ @@ -855,16 +855,16 @@ Implement a tool named \`test_incomplete_result_multi_round\` (no arguments requ name: 'IncompleteResultMultiRoundR1', description: 'Round 1: Server returns IncompleteResult with requestState', - status: r1Ok ? 'SUCCESS' : 'FAILURE', + status: round1Complete ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), - errorMessage: r1Ok + errorMessage: round1Complete ? undefined : 'Expected IncompleteResult with inputRequests and requestState', specReferences: MRTR_SPEC_REFERENCES, details: { result: r1Result } }); - if (!r1Ok || !isIncompleteResult(r1Result)) return checks; + if (!round1Complete || !isIncompleteResult(r1Result)) return checks; // Round 2: Retry — expect another IncompleteResult const r1InputKey = Object.keys(r1Result.inputRequests!)[0]; @@ -878,7 +878,7 @@ Implement a tool named \`test_incomplete_result_multi_round\` (no arguments requ }); const r2Result = r2.result; - let r2Ok = false; + let round2Complete = false; if ( !r2.error && @@ -889,7 +889,7 @@ Implement a tool named \`test_incomplete_result_multi_round\` (no arguments requ ) { // requestState should have changed if (r2Result.requestState !== r1Result.requestState) { - r2Ok = true; + round2Complete = true; } } @@ -898,16 +898,16 @@ Implement a tool named \`test_incomplete_result_multi_round\` (no arguments requ name: 'IncompleteResultMultiRoundR2', description: 'Round 2: Server returns another IncompleteResult with updated requestState', - status: r2Ok ? 'SUCCESS' : 'FAILURE', + status: round2Complete ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), - errorMessage: r2Ok + errorMessage: round2Complete ? undefined : 'Expected new IncompleteResult with different requestState', specReferences: MRTR_SPEC_REFERENCES, details: { result: r2Result } }); - if (!r2Ok || !isIncompleteResult(r2Result)) return checks; + if (!round2Complete || !isIncompleteResult(r2Result)) return checks; // Round 3: Final retry — expect complete result const r2InputKey = Object.keys(r2Result.inputRequests!)[0]; @@ -921,15 +921,15 @@ Implement a tool named \`test_incomplete_result_multi_round\` (no arguments requ }); const r3Result = r3.result; - const r3Ok = !r3.error && r3Result != null && isCompleteResult(r3Result); + const round3Complete = !r3.error && r3Result != null && isCompleteResult(r3Result); checks.push({ id: 'incomplete-result-multi-round-r3', name: 'IncompleteResultMultiRoundR3', description: 'Round 3: Server returns complete result', - status: r3Ok ? 'SUCCESS' : 'FAILURE', + status: round3Complete ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), - errorMessage: r3Ok + errorMessage: round3Complete ? undefined : 'Expected complete result after final retry', specReferences: MRTR_SPEC_REFERENCES, @@ -952,118 +952,7 @@ Implement a tool named \`test_incomplete_result_multi_round\` (no arguments requ } } -// ─── A7: Request State Only ────────────────────────────────── - -export class IncompleteResultRequestStateOnlyScenario - implements ClientScenario -{ - name = 'incomplete-result-request-state-only'; - specVersions: SpecVersion[] = ['draft']; - description = `Test IncompleteResult with requestState only — no inputRequests (load-shedding use case, SEP-2322). - -**Server Implementation Requirements:** - -Implement a tool named \`test_incomplete_result_state_only\` (no arguments required). - -**Behavior (Round 1):** Return an \`IncompleteResult\` with \`requestState\` but NO \`inputRequests\`: - -\`\`\`json -{ - "result_type": "incomplete", - "requestState": "" -} -\`\`\` - -**Behavior (Round 2):** When called with the echoed \`requestState\` (no \`inputResponses\`), return a complete result. - -This simulates load shedding where the server transfers accumulated computation state to be resumed by another instance.`; - - async run(serverUrl: string): Promise { - const checks: ConformanceCheck[] = []; - - try { - const session = await createRawSession(serverUrl); - - // Round 1: Expect IncompleteResult with requestState only - const r1 = await session.send('tools/call', { - name: 'test_incomplete_result_state_only', - arguments: {} - }); - - const r1Result = r1.result; - const r1Errors: string[] = []; - - if (r1.error) { - r1Errors.push(`JSON-RPC error: ${r1.error.message}`); - } else if (!r1Result || !isIncompleteResult(r1Result)) { - r1Errors.push('Expected IncompleteResult'); - } else { - if (!r1Result.requestState) { - r1Errors.push('IncompleteResult missing requestState'); - } - if (r1Result.inputRequests) { - r1Errors.push( - 'Load-shedding IncompleteResult should NOT have inputRequests' - ); - } - } - - checks.push({ - id: 'incomplete-result-state-only-incomplete', - name: 'IncompleteResultStateOnlyIncomplete', - description: - 'Server returns IncompleteResult with requestState only (no inputRequests)', - status: r1Errors.length === 0 ? 'SUCCESS' : 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: r1Errors.length > 0 ? r1Errors.join('; ') : undefined, - specReferences: MRTR_SPEC_REFERENCES, - details: { result: r1Result } - }); - - // Round 2: Retry with requestState only - if (r1Errors.length === 0 && isIncompleteResult(r1Result)) { - const r2 = await session.send('tools/call', { - name: 'test_incomplete_result_state_only', - arguments: {}, - requestState: r1Result.requestState - }); - - const r2Result = r2.result; - const r2Ok = - !r2.error && r2Result != null && isCompleteResult(r2Result); - - checks.push({ - id: 'incomplete-result-state-only-complete', - name: 'IncompleteResultStateOnlyComplete', - description: - 'Server completes after receiving echoed requestState (no inputResponses needed)', - status: r2Ok ? 'SUCCESS' : 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: r2Ok - ? undefined - : 'Expected complete result after retry with requestState', - specReferences: MRTR_SPEC_REFERENCES, - details: { result: r2Result } - }); - } - } catch (error) { - checks.push({ - id: 'incomplete-result-state-only-incomplete', - name: 'IncompleteResultStateOnlyIncomplete', - description: - 'Server returns IncompleteResult with requestState only (no inputRequests)', - status: 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, - specReferences: MRTR_SPEC_REFERENCES - }); - } - - return checks; - } -} - -// ─── A8: Missing Input Response ────────────────────────────────────────────── +// ─── A7: Missing Input Response ────────────────────────────────────────────── export class IncompleteResultMissingInputResponseScenario implements ClientScenario @@ -1084,56 +973,28 @@ Use the same tool as A1: \`test_incomplete_result_elicitation\`. try { const session = await createRawSession(serverUrl); - // Round 1: Get the initial IncompleteResult + // Round 1: Send wrong inputResponses (wrong key) const r1 = await session.send('tools/call', { - name: 'test_incomplete_result_elicitation', - arguments: {} - }); - - if ( - r1.error || - !r1.result || - !isIncompleteResult(r1.result) || - !r1.result.inputRequests - ) { - checks.push({ - id: 'incomplete-result-missing-response-prereq', - name: 'IncompleteResultMissingResponsePrereq', - description: 'Prerequisite: Server returns IncompleteResult', - status: 'FAILURE', - timestamp: new Date().toISOString(), - errorMessage: - 'Could not get initial IncompleteResult to test error handling', - specReferences: MRTR_SPEC_REFERENCES - }); - return checks; - } - - // Round 2: Send wrong inputResponses (wrong key) - const r2 = await session.send('tools/call', { name: 'test_incomplete_result_elicitation', arguments: {}, inputResponses: { wrong_key: mockElicitResponse({ data: 'wrong' }) - }, - ...(r1.result.requestState !== undefined - ? { requestState: r1.result.requestState } - : {}) + } }); - const r2Result = r2.result; - const r2Errors: string[] = []; + const r1Result = r1.result; + const r1Errors: string[] = []; - if (r2.error) { + if (r1.error) { // A JSON-RPC error is acceptable but the SEP prefers re-requesting - r2Errors.push( + r1Errors.push( 'Server returned JSON-RPC error instead of re-requesting via IncompleteResult. ' + 'SEP-2322 recommends servers re-request missing information.' ); - } else if (!r2Result) { - r2Errors.push('No result in response'); - } else if (!isIncompleteResult(r2Result)) { - r2Errors.push( + } else if (!r1Result) { + r1Errors.push('No result in response'); + } else if (!isIncompleteResult(r1Result)) { + r1Errors.push( 'Expected IncompleteResult re-requesting missing information, ' + 'but got a complete result' ); @@ -1144,11 +1005,11 @@ Use the same tool as A1: \`test_incomplete_result_elicitation\`. name: 'IncompleteResultMissingResponseRerequests', description: 'Server re-requests missing inputResponses via new IncompleteResult', - status: r2Errors.length === 0 ? 'SUCCESS' : 'WARNING', + status: r1Errors.length === 0 ? 'SUCCESS' : 'WARNING', timestamp: new Date().toISOString(), - errorMessage: r2Errors.length > 0 ? r2Errors.join('; ') : undefined, + errorMessage: r1Errors.length > 0 ? r1Errors.join('; ') : undefined, specReferences: MRTR_SPEC_REFERENCES, - details: { result: r2Result } + details: { result: r1Result } }); } catch (error) { checks.push({ From 9d86c97bb93d052de614b2aaa13f6d9c422757cc Mon Sep 17 00:00:00 2001 From: Caitie McCaffrey Date: Tue, 17 Mar 2026 16:09:57 -0700 Subject: [PATCH 09/11] removing duplicative tests --- src/scenarios/index.ts | 8 -------- src/scenarios/server/client-helper.ts | 2 +- src/scenarios/server/incomplete-result-helpers.ts | 7 ++----- src/scenarios/server/incomplete-result.ts | 3 ++- 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index 9cc9aaf..f43bb4d 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -61,7 +61,6 @@ import { IncompleteResultRequestStateScenario, IncompleteResultMultipleInputRequestsScenario, IncompleteResultMultiRoundScenario, - IncompleteResultRequestStateOnlyScenario, IncompleteResultMissingInputResponseScenario, IncompleteResultNonToolRequestScenario } from './server/incomplete-result'; @@ -72,13 +71,6 @@ import { IncompleteResultTaskInputResponseIncompleteScenario } from './server/incomplete-result-tasks'; -import { - IncompleteResultStructureScenario, - InputRequestTypesScenario -} from './server/incomplete-result-validation'; - -import { IncompleteResultToTaskTransitionScenario } from './server/incomplete-result-transition'; - import { authScenariosList, backcompatScenariosList, diff --git a/src/scenarios/server/client-helper.ts b/src/scenarios/server/client-helper.ts index a651b68..5e99374 100644 --- a/src/scenarios/server/client-helper.ts +++ b/src/scenarios/server/client-helper.ts @@ -129,7 +129,7 @@ export class RawMcpSession { // Handle SSE responses — parse the last JSON-RPC message from the stream // Not doing proper handling of SSE here since none of the MRTR features under test currently require it. - // This can be expanded if necessary for new features. + // This can be expanded if necessary for new features. if (contentType.includes('text/event-stream')) { const text = await response.body.text(); return parseSseResponse(text); diff --git a/src/scenarios/server/incomplete-result-helpers.ts b/src/scenarios/server/incomplete-result-helpers.ts index 76beea8..e79189c 100644 --- a/src/scenarios/server/incomplete-result-helpers.ts +++ b/src/scenarios/server/incomplete-result-helpers.ts @@ -6,10 +6,7 @@ * guards and mock response builders. */ -import { - RawMcpSession, - JsonRpcResponse -} from './client-helper'; +import { RawMcpSession, JsonRpcResponse } from './client-helper'; export type { RawMcpSession, JsonRpcResponse }; @@ -44,7 +41,7 @@ export function isIncompleteResult( /** * Check if a JSON-RPC result is a complete result (not incomplete). - * complete is the default so if result_type is missing we assume it's complete. + * complete is the default so if result_type is missing we assume it's complete. */ export function isCompleteResult( result: Record | undefined diff --git a/src/scenarios/server/incomplete-result.ts b/src/scenarios/server/incomplete-result.ts index f4fb284..7b4b63c 100644 --- a/src/scenarios/server/incomplete-result.ts +++ b/src/scenarios/server/incomplete-result.ts @@ -921,7 +921,8 @@ Implement a tool named \`test_incomplete_result_multi_round\` (no arguments requ }); const r3Result = r3.result; - const round3Complete = !r3.error && r3Result != null && isCompleteResult(r3Result); + const round3Complete = + !r3.error && r3Result != null && isCompleteResult(r3Result); checks.push({ id: 'incomplete-result-multi-round-r3', From d446a35f8a33b2019fa53bf34567e88d7784c016 Mon Sep 17 00:00:00 2001 From: Caitie McCaffrey Date: Tue, 17 Mar 2026 16:11:07 -0700 Subject: [PATCH 10/11] removing duplicative tests --- src/scenarios/index.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index f43bb4d..c04402c 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -98,15 +98,11 @@ const pendingClientScenariosList: ClientScenario[] = [ new IncompleteResultRequestStateScenario(), new IncompleteResultMultipleInputRequestsScenario(), new IncompleteResultMultiRoundScenario(), - new IncompleteResultRequestStateOnlyScenario(), new IncompleteResultMissingInputResponseScenario(), new IncompleteResultNonToolRequestScenario(), new IncompleteResultTaskBasicScenario(), new IncompleteResultTaskBadInputResponseScenario(), new IncompleteResultTaskInputResponseIncompleteScenario(), - new IncompleteResultStructureScenario(), - new InputRequestTypesScenario(), - new IncompleteResultToTaskTransitionScenario() ]; // All client scenarios @@ -170,7 +166,6 @@ const allClientScenariosList: ClientScenario[] = [ new IncompleteResultRequestStateScenario(), new IncompleteResultMultipleInputRequestsScenario(), new IncompleteResultMultiRoundScenario(), - new IncompleteResultRequestStateOnlyScenario(), new IncompleteResultMissingInputResponseScenario(), new IncompleteResultNonToolRequestScenario(), @@ -178,13 +173,6 @@ const allClientScenariosList: ClientScenario[] = [ new IncompleteResultTaskBasicScenario(), new IncompleteResultTaskBadInputResponseScenario(), new IncompleteResultTaskInputResponseIncompleteScenario(), - - // IncompleteResult Validation scenarios (SEP-2322) - new IncompleteResultStructureScenario(), - new InputRequestTypesScenario(), - - // IncompleteResult Transition scenarios (SEP-2322) - new IncompleteResultToTaskTransitionScenario() ]; // Active client scenarios (excludes pending) From a8fa73650ae944f7a5995f863275cd7a1b6343aa Mon Sep 17 00:00:00 2001 From: Caitie McCaffrey Date: Tue, 17 Mar 2026 16:12:24 -0700 Subject: [PATCH 11/11] Fix formatting --- src/scenarios/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index c04402c..9018b11 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -102,7 +102,7 @@ const pendingClientScenariosList: ClientScenario[] = [ new IncompleteResultNonToolRequestScenario(), new IncompleteResultTaskBasicScenario(), new IncompleteResultTaskBadInputResponseScenario(), - new IncompleteResultTaskInputResponseIncompleteScenario(), + new IncompleteResultTaskInputResponseIncompleteScenario() ]; // All client scenarios @@ -172,7 +172,7 @@ const allClientScenariosList: ClientScenario[] = [ // IncompleteResult Task scenarios (SEP-2322) new IncompleteResultTaskBasicScenario(), new IncompleteResultTaskBadInputResponseScenario(), - new IncompleteResultTaskInputResponseIncompleteScenario(), + new IncompleteResultTaskInputResponseIncompleteScenario() ]; // Active client scenarios (excludes pending)