From 552c5f24d95517bf74fadfb07330ff6c516fd48c Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 18 Nov 2025 22:46:30 +0000 Subject: [PATCH 1/5] Add conformance tests for SEP-1036 URL mode elicitation Implements conformance tests for SEP-1036 which adds URL mode elicitation to MCP. The tests validate: - URL mode request format (mode, url, elicitationId, message fields) - URL mode response structure (action without content) - URLElicitationRequiredError structure and error code (-32042) Also updates existing elicitation tools to include explicit mode: 'form' as required by the SEP-1036 schema changes. --- .../servers/typescript/everything-server.ts | 79 +++- package-lock.json | 13 +- src/scenarios/index.ts | 4 + src/scenarios/server/client-helper.ts | 34 ++ src/scenarios/server/elicitation-url.ts | 422 ++++++++++++++++++ 5 files changed, 550 insertions(+), 2 deletions(-) create mode 100644 src/scenarios/server/elicitation-url.ts diff --git a/examples/servers/typescript/everything-server.ts b/examples/servers/typescript/everything-server.ts index a7ffdfd..45f5ba1 100644 --- a/examples/servers/typescript/everything-server.ts +++ b/examples/servers/typescript/everything-server.ts @@ -13,7 +13,7 @@ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; -import { ElicitResultSchema } from '@modelcontextprotocol/sdk/types.js'; +import { ElicitResultSchema, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import express from 'express'; import cors from 'cors'; @@ -357,6 +357,7 @@ function createMcpServer() { { method: 'elicitation/create', params: { + mode: 'form', message: args.message, requestedSchema: { type: 'object', @@ -409,6 +410,7 @@ function createMcpServer() { { method: 'elicitation/create', params: { + mode: 'form', message: 'Please review and update the form fields with defaults', requestedSchema: { type: 'object', @@ -484,6 +486,7 @@ function createMcpServer() { { method: 'elicitation/create', params: { + mode: 'form', message: 'Please select options from the enum fields', requestedSchema: { type: 'object', @@ -566,6 +569,80 @@ function createMcpServer() { } ); + // SEP-1036: URL mode elicitation + mcpServer.registerTool( + 'test_elicitation_sep1036_url', + { + description: 'Tests URL mode elicitation per SEP-1036', + inputSchema: {} + }, + async () => { + try { + const elicitationId = `sep1036-test-${randomUUID()}`; + const result = await mcpServer.server.request( + { + method: 'elicitation/create', + params: { + mode: 'url', + message: 'Please complete authorization to continue', + url: 'https://mcp.example.com/authorize', + elicitationId + } + }, + z + .object({ method: z.literal('elicitation/create') }) + .passthrough() as any + ); + + const elicitResult = result as any; + return { + content: [ + { + type: 'text', + text: `URL elicitation completed: action=${elicitResult.action}` + } + ] + }; + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `URL elicitation not supported or error: ${error.message}` + } + ] + }; + } + } + ); + + // SEP-1036: URL mode elicitation error flow + mcpServer.registerTool( + 'test_elicitation_sep1036_error', + { + description: + 'Tests URLElicitationRequiredError flow per SEP-1036 (throws error)', + inputSchema: {} + }, + async () => { + const elicitationId = `sep1036-error-${randomUUID()}`; + throw new McpError( + ErrorCode.UrlElicitationRequired, + 'Authorization required to access this resource', + { + elicitations: [ + { + mode: 'url', + message: 'Please authorize access to continue', + url: 'https://mcp.example.com/authorize-error-test', + elicitationId + } + ] + } + ); + } + ); + // Dynamic tool (registered later via timer) // ===== RESOURCES ===== diff --git a/package-lock.json b/package-lock.json index 88477cb..1c66b8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@modelcontextprotocol/sdk": "^1.22.0", "commander": "^14.0.2", "express": "^5.1.0", - "lefthook": "^2.0.2", "zod": "^3.25.76" }, "bin": { @@ -26,6 +25,7 @@ "cors": "^2.8.5", "eslint": "^9.8.0", "eslint-config-prettier": "^10.1.8", + "lefthook": "^2.0.2", "prettier": "3.6.2", "tsdown": "^0.15.12", "tsx": "^4.7.0", @@ -3635,6 +3635,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/lefthook/-/lefthook-2.0.2.tgz", "integrity": "sha512-2lrSva53G604ZWjK5kHYvDdwb5GzbhciIPWhebv0A8ceveqSsnG2JgVEt+DnhOPZ4VfNcXvt3/ohFBPNpuAlVw==", + "dev": true, "hasInstallScript": true, "bin": { "lefthook": "bin/index.js" @@ -3659,6 +3660,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3672,6 +3674,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3685,6 +3688,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3698,6 +3702,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3711,6 +3716,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3724,6 +3730,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3737,6 +3744,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3750,6 +3758,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3763,6 +3772,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3776,6 +3786,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index 3180c26..232c008 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -27,6 +27,7 @@ import { import { ElicitationDefaultsScenario } from './server/elicitation-defaults.js'; import { ElicitationEnumsScenario } from './server/elicitation-enums.js'; +import { ElicitationUrlModeScenario } from './server/elicitation-url.js'; import { ResourcesListScenario, @@ -79,6 +80,9 @@ const allClientScenariosList: ClientScenario[] = [ // Elicitation scenarios (SEP-1034) new ElicitationDefaultsScenario(), + // Elicitation scenarios (SEP-1036) - URL mode + new ElicitationUrlModeScenario(), + // Elicitation scenarios (SEP-1330) - pending ...pendingClientScenariosList, diff --git a/src/scenarios/server/client-helper.ts b/src/scenarios/server/client-helper.ts index eebbd9b..8e2a74c 100644 --- a/src/scenarios/server/client-helper.ts +++ b/src/scenarios/server/client-helper.ts @@ -46,6 +46,40 @@ export async function connectToServer( }; } +/** + * Create and connect an MCP client with URL elicitation capability (SEP-1036) + */ +export async function connectToServerWithUrlElicitation( + serverUrl: string +): Promise { + const client = new Client( + { + name: 'conformance-test-client', + version: '1.0.0' + }, + { + capabilities: { + // Client capabilities + sampling: {}, + elicitation: { + url: {} + } + } + } + ); + + const transport = new StreamableHTTPClientTransport(new URL(serverUrl)); + + await client.connect(transport); + + return { + client, + close: async () => { + await client.close(); + } + }; +} + /** * Helper to collect notifications (logging and progress) */ diff --git a/src/scenarios/server/elicitation-url.ts b/src/scenarios/server/elicitation-url.ts new file mode 100644 index 0000000..2bb757c --- /dev/null +++ b/src/scenarios/server/elicitation-url.ts @@ -0,0 +1,422 @@ +/** + * SEP-1036: URL mode elicitation test scenarios for MCP servers + */ + +import { ClientScenario, ConformanceCheck } from '../../types.js'; +import { connectToServerWithUrlElicitation } from './client-helper.js'; +import { + ElicitRequestSchema, + ErrorCode, + McpError +} from '@modelcontextprotocol/sdk/types.js'; + +export class ElicitationUrlModeScenario implements ClientScenario { + name = 'elicitation-sep1036-url-mode'; + description = `Test URL mode elicitation per SEP-1036. + +**Server Implementation Requirements:** + +Implement two tools: + +1. \`test_elicitation_sep1036_url\` (no arguments) - Requests URL mode elicitation from client with: + - \`mode\`: "url" + - \`message\`: Human-readable explanation (non-empty string) + - \`url\`: Valid URL (e.g., "https://mcp.example.com/test") + - \`elicitationId\`: Unique identifier (non-empty string) + + **Returns**: Text content with the elicitation action received + +2. \`test_elicitation_sep1036_error\` (no arguments) - Throws URLElicitationRequiredError: + - Error code: -32042 + - Error data contains \`elicitations\` array with URL mode elicitation objects + +**Example elicitation request:** +\`\`\`json +{ + "method": "elicitation/create", + "params": { + "mode": "url", + "message": "Please complete authorization", + "url": "https://mcp.example.com/test", + "elicitationId": "sep1036-test-uuid" + } +} +\`\`\``; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + const connection = await connectToServerWithUrlElicitation(serverUrl); + + // Part 1: Test URL mode elicitation request flow + let capturedRequest: any = null; + connection.client.setRequestHandler( + ElicitRequestSchema, + async (request) => { + capturedRequest = request; + // URL mode response should have action but no content + return { + action: 'accept' + }; + } + ); + + await connection.client.callTool({ + name: 'test_elicitation_sep1036_url', + arguments: {} + }); + + // Validate that elicitation was requested + if (!capturedRequest) { + checks.push({ + id: 'sep1036-url-general', + name: 'URLElicitationSEP1036General', + description: 'Server requests URL mode elicitation', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: 'Server did not request elicitation from client', + specReferences: [ + { + id: 'SEP-1036', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887' + } + ] + }); + await connection.close(); + return checks; + } + + const params = capturedRequest.params; + + // Check 1: Validate mode is "url" + const modeErrors: string[] = []; + if (!params?.mode) { + modeErrors.push('Missing mode parameter'); + } else if (params.mode !== 'url') { + modeErrors.push(`Expected mode "url", got "${params.mode}"`); + } + + checks.push({ + id: 'sep1036-url-mode', + name: 'URLModeRequired', + description: 'URL elicitation request specifies mode as "url"', + status: modeErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: modeErrors.length > 0 ? modeErrors.join('; ') : undefined, + specReferences: [ + { + id: 'SEP-1036', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887' + } + ], + details: { + mode: params?.mode + } + }); + + // Check 2: Validate message is present and non-empty + const messageErrors: string[] = []; + if (!params?.message) { + messageErrors.push('Missing message parameter'); + } else if (typeof params.message !== 'string') { + messageErrors.push( + `Expected string message, got ${typeof params.message}` + ); + } else if (params.message.trim() === '') { + messageErrors.push('Message is empty'); + } + + checks.push({ + id: 'sep1036-url-message', + name: 'URLMessagePresent', + description: 'URL elicitation request includes human-readable message', + status: messageErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + messageErrors.length > 0 ? messageErrors.join('; ') : undefined, + specReferences: [ + { + id: 'SEP-1036', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887' + } + ], + details: { + message: params?.message + } + }); + + // Check 3: Validate url is present and valid + const urlErrors: string[] = []; + if (!params?.url) { + urlErrors.push('Missing url parameter'); + } else if (typeof params.url !== 'string') { + urlErrors.push(`Expected string url, got ${typeof params.url}`); + } else { + try { + const urlObj = new URL(params.url); + // URL should use HTTP or HTTPS protocol + if (urlObj.protocol !== 'https:' && urlObj.protocol !== 'http:') { + urlErrors.push( + `URL must use HTTP or HTTPS protocol, got "${urlObj.protocol}"` + ); + } + } catch { + urlErrors.push(`Invalid URL format: ${params.url}`); + } + } + + checks.push({ + id: 'sep1036-url-field', + name: 'URLFieldValid', + description: 'URL elicitation request includes valid URL', + status: urlErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: urlErrors.length > 0 ? urlErrors.join('; ') : undefined, + specReferences: [ + { + id: 'SEP-1036', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887' + } + ], + details: { + url: params?.url + } + }); + + // Check 4: Validate elicitationId is present and valid + const idErrors: string[] = []; + if (!params?.elicitationId) { + idErrors.push('Missing elicitationId parameter'); + } else if (typeof params.elicitationId !== 'string') { + idErrors.push( + `Expected string elicitationId, got ${typeof params.elicitationId}` + ); + } else if (params.elicitationId.trim() === '') { + idErrors.push('elicitationId is empty'); + } + + checks.push({ + id: 'sep1036-url-elicitation-id', + name: 'URLElicitationIdPresent', + description: 'URL elicitation request includes unique elicitationId', + status: idErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: idErrors.length > 0 ? idErrors.join('; ') : undefined, + specReferences: [ + { + id: 'SEP-1036', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887' + } + ], + details: { + elicitationId: params?.elicitationId + } + }); + + // Check 5: URL mode response has action (this is implicitly tested by the tool completing) + // We successfully returned { action: 'accept' } and the tool completed + checks.push({ + id: 'sep1036-url-response-action', + name: 'URLResponseAction', + description: 'Client response has action field', + status: 'SUCCESS', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1036', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887' + } + ], + details: { + action: 'accept' + } + }); + + // Check 6: URL mode response should not have content field + // Our handler returned { action: 'accept' } without content, which is correct + checks.push({ + id: 'sep1036-url-response-no-content', + name: 'URLResponseNoContent', + description: 'URL mode response omits content field', + status: 'SUCCESS', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1036', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887' + } + ], + details: { + note: 'Response contained action only, no content field' + } + }); + + // Part 2: Test URLElicitationRequiredError flow + let errorReceived: McpError | null = null; + try { + await connection.client.callTool({ + name: 'test_elicitation_sep1036_error', + arguments: {} + }); + // If we get here, the tool didn't throw an error as expected + checks.push({ + id: 'sep1036-url-error-code', + name: 'URLErrorCode', + description: + 'Server returns URLElicitationRequiredError (code -32042)', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + 'Tool did not throw URLElicitationRequiredError as expected', + specReferences: [ + { + id: 'SEP-1036', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887' + } + ] + }); + } catch (error) { + if (error instanceof McpError) { + errorReceived = error; + } else if (error instanceof Error && 'code' in error) { + // Handle case where error might not be McpError instance but has code + errorReceived = error as unknown as McpError; + } + + // Check 7: Validate error code is -32042 + const errorCodeErrors: string[] = []; + if (!errorReceived) { + errorCodeErrors.push('Did not receive an MCP error'); + } else if (errorReceived.code !== ErrorCode.UrlElicitationRequired) { + errorCodeErrors.push( + `Expected error code ${ErrorCode.UrlElicitationRequired} (-32042), got ${errorReceived.code}` + ); + } + + checks.push({ + id: 'sep1036-url-error-code', + name: 'URLErrorCode', + description: + 'Server returns URLElicitationRequiredError (code -32042)', + status: errorCodeErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + errorCodeErrors.length > 0 ? errorCodeErrors.join('; ') : undefined, + specReferences: [ + { + id: 'SEP-1036', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887' + } + ], + details: { + errorCode: errorReceived?.code + } + }); + + // Check 8: Validate error data contains elicitations array + const elicitationsErrors: string[] = []; + const errorData = errorReceived?.data as + | { elicitations?: unknown[] } + | undefined; + if (!errorData?.elicitations) { + elicitationsErrors.push('Error data missing elicitations array'); + } else if (!Array.isArray(errorData.elicitations)) { + elicitationsErrors.push('elicitations is not an array'); + } else if (errorData.elicitations.length === 0) { + elicitationsErrors.push('elicitations array is empty'); + } + + checks.push({ + id: 'sep1036-url-error-elicitations', + name: 'URLErrorElicitations', + description: 'Error data contains elicitations array', + status: elicitationsErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + elicitationsErrors.length > 0 + ? elicitationsErrors.join('; ') + : undefined, + specReferences: [ + { + id: 'SEP-1036', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887' + } + ], + details: { + elicitationsCount: errorData?.elicitations?.length + } + }); + + // Check 9: Validate each elicitation has required URL mode fields + const structureErrors: string[] = []; + if (errorData?.elicitations && Array.isArray(errorData.elicitations)) { + for (let i = 0; i < errorData.elicitations.length; i++) { + const elicit = errorData.elicitations[i] as Record; + if (!elicit.mode || elicit.mode !== 'url') { + structureErrors.push( + `Elicitation[${i}]: missing or invalid mode (expected "url")` + ); + } + if (!elicit.url || typeof elicit.url !== 'string') { + structureErrors.push( + `Elicitation[${i}]: missing or invalid url field` + ); + } + if ( + !elicit.elicitationId || + typeof elicit.elicitationId !== 'string' + ) { + structureErrors.push( + `Elicitation[${i}]: missing or invalid elicitationId field` + ); + } + if (!elicit.message || typeof elicit.message !== 'string') { + structureErrors.push( + `Elicitation[${i}]: missing or invalid message field` + ); + } + } + } + + checks.push({ + id: 'sep1036-url-error-structure', + name: 'URLErrorStructure', + description: 'Each elicitation has required URL mode fields', + status: structureErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + structureErrors.length > 0 ? structureErrors.join('; ') : undefined, + specReferences: [ + { + id: 'SEP-1036', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887' + } + ], + details: { + elicitations: errorData?.elicitations + } + }); + } + + await connection.close(); + } catch (error) { + checks.push({ + id: 'sep1036-url-general', + name: 'URLElicitationSEP1036General', + description: 'Server requests URL mode elicitation', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'SEP-1036', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887' + } + ] + }); + } + + return checks; + } +} From 257cf7942d867f3d741fa0d7902b0c5fd5acbd46 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 20 Nov 2025 14:46:40 +0000 Subject: [PATCH 2/5] Fix SEP-1036 URL elicitation conformance tests - Use elicitInput() instead of raw request() for proper capability checking - Add completion notification test (notifications/elicitation/complete) - Add test_elicitation_sep1036_complete tool to everything-server --- .../servers/typescript/everything-server.ts | 65 ++++++--- src/scenarios/server/elicitation-url.ts | 128 +++++++++++++++++- 2 files changed, 175 insertions(+), 18 deletions(-) diff --git a/examples/servers/typescript/everything-server.ts b/examples/servers/typescript/everything-server.ts index 45f5ba1..01a08c5 100644 --- a/examples/servers/typescript/everything-server.ts +++ b/examples/servers/typescript/everything-server.ts @@ -13,7 +13,11 @@ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; -import { ElicitResultSchema, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; +import { + ElicitResultSchema, + McpError, + ErrorCode +} from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import express from 'express'; import cors from 'cors'; @@ -579,27 +583,18 @@ function createMcpServer() { async () => { try { const elicitationId = `sep1036-test-${randomUUID()}`; - const result = await mcpServer.server.request( - { - method: 'elicitation/create', - params: { - mode: 'url', - message: 'Please complete authorization to continue', - url: 'https://mcp.example.com/authorize', - elicitationId - } - }, - z - .object({ method: z.literal('elicitation/create') }) - .passthrough() as any - ); + const result = await mcpServer.server.elicitInput({ + mode: 'url', + message: 'Please complete authorization to continue', + url: 'https://mcp.example.com/authorize', + elicitationId + }); - const elicitResult = result as any; return { content: [ { type: 'text', - text: `URL elicitation completed: action=${elicitResult.action}` + text: `URL elicitation completed: action=${result.action}` } ] }; @@ -643,6 +638,42 @@ function createMcpServer() { } ); + // SEP-1036: URL mode elicitation with completion notification + mcpServer.registerTool( + 'test_elicitation_sep1036_complete', + { + description: + 'Tests URL mode elicitation with completion notification per SEP-1036', + inputSchema: {} + }, + async () => { + const elicitationId = `sep1036-complete-${randomUUID()}`; + const result = await mcpServer.server.elicitInput({ + mode: 'url', + message: 'Please complete the authorization flow', + url: 'https://mcp.example.com/authorize-with-completion', + elicitationId + }); + + // Send completion notification after client accepts + if (result.action === 'accept') { + // Create a notifier for this elicitationId and send the notification + const notifier = + mcpServer.server.createElicitationCompletionNotifier(elicitationId); + await notifier(); + } + + return { + content: [ + { + type: 'text', + text: `URL elicitation with completion: action=${result.action}, notification sent` + } + ] + }; + } + ); + // Dynamic tool (registered later via timer) // ===== RESOURCES ===== diff --git a/src/scenarios/server/elicitation-url.ts b/src/scenarios/server/elicitation-url.ts index 2bb757c..fca84d0 100644 --- a/src/scenarios/server/elicitation-url.ts +++ b/src/scenarios/server/elicitation-url.ts @@ -6,6 +6,7 @@ import { ClientScenario, ConformanceCheck } from '../../types.js'; import { connectToServerWithUrlElicitation } from './client-helper.js'; import { ElicitRequestSchema, + ElicitationCompleteNotificationSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; @@ -16,7 +17,7 @@ export class ElicitationUrlModeScenario implements ClientScenario { **Server Implementation Requirements:** -Implement two tools: +Implement three tools: 1. \`test_elicitation_sep1036_url\` (no arguments) - Requests URL mode elicitation from client with: - \`mode\`: "url" @@ -30,6 +31,11 @@ Implement two tools: - Error code: -32042 - Error data contains \`elicitations\` array with URL mode elicitation objects +3. \`test_elicitation_sep1036_complete\` (no arguments) - Tests completion notification flow: + - Requests URL mode elicitation + - When client accepts, sends \`notifications/elicitation/complete\` notification + - The notification must include the matching \`elicitationId\` + **Example elicitation request:** \`\`\`json { @@ -399,6 +405,126 @@ Implement two tools: }); } + // Part 3: Test completion notification flow + let completionNotificationReceived = false; + let receivedElicitationId: string | null = null; + let capturedElicitationIdFromRequest: string | null = null; + + // Set up notification handler for completion + connection.client.setNotificationHandler( + ElicitationCompleteNotificationSchema, + (notification) => { + completionNotificationReceived = true; + receivedElicitationId = notification.params.elicitationId; + } + ); + + // Update the request handler to capture the elicitationId + connection.client.setRequestHandler( + ElicitRequestSchema, + async (request) => { + capturedElicitationIdFromRequest = request.params.elicitationId; + return { action: 'accept' }; + } + ); + + try { + await connection.client.callTool({ + name: 'test_elicitation_sep1036_complete', + arguments: {} + }); + + // Small delay to allow notification to be received + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Check 10: Verify completion notification was received + const notificationErrors: string[] = []; + if (!completionNotificationReceived) { + notificationErrors.push( + 'Server did not send notifications/elicitation/complete notification' + ); + } + + checks.push({ + id: 'sep1036-url-completion-notification', + name: 'URLCompletionNotification', + description: + 'Server sends notifications/elicitation/complete after out-of-band completion', + status: notificationErrors.length === 0 ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: + notificationErrors.length > 0 + ? notificationErrors.join('; ') + : undefined, + specReferences: [ + { + id: 'SEP-1036', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887' + } + ], + details: { + notificationReceived: completionNotificationReceived + } + }); + + // Check 11: Verify elicitationId matches + const idMatchErrors: string[] = []; + if (completionNotificationReceived) { + if (!receivedElicitationId) { + idMatchErrors.push('Completion notification missing elicitationId'); + } else if ( + capturedElicitationIdFromRequest && + receivedElicitationId !== capturedElicitationIdFromRequest + ) { + idMatchErrors.push( + `elicitationId mismatch: request had "${capturedElicitationIdFromRequest}", notification had "${receivedElicitationId}"` + ); + } + } + + checks.push({ + id: 'sep1036-url-completion-id-match', + name: 'URLCompletionIdMatch', + description: + 'Completion notification elicitationId matches the original request', + status: + completionNotificationReceived && idMatchErrors.length === 0 + ? 'SUCCESS' + : completionNotificationReceived + ? 'FAILURE' + : 'SKIPPED', + timestamp: new Date().toISOString(), + errorMessage: + idMatchErrors.length > 0 ? idMatchErrors.join('; ') : undefined, + specReferences: [ + { + id: 'SEP-1036', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887' + } + ], + details: { + requestElicitationId: capturedElicitationIdFromRequest, + notificationElicitationId: receivedElicitationId + } + }); + } catch (error) { + checks.push({ + id: 'sep1036-url-completion-notification', + name: 'URLCompletionNotification', + description: + 'Server sends notifications/elicitation/complete after out-of-band completion', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Tool call failed: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'SEP-1036', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/pull/887' + } + ] + }); + } + await connection.close(); } catch (error) { checks.push({ From 6a75bbbbc01d89143742f3a8e6e28e336005e04e Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 20 Nov 2025 16:11:40 +0000 Subject: [PATCH 3/5] Mark SEP-1036 URL mode tests as pending until SDK release - Add ElicitationUrlModeScenario to pending list since SDK doesn't yet export URL mode elicitation support - Define ElicitationCompleteNotificationSchema locally until SDK releases it --- src/scenarios/index.ts | 9 ++++----- src/scenarios/server/elicitation-url.ts | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index 232c008..df2a119 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -52,7 +52,9 @@ import { listMetadataScenarios } from './client/auth/discovery-metadata.js'; // Pending client scenarios (not yet fully tested/implemented) const pendingClientScenariosList: ClientScenario[] = [ // Elicitation scenarios (SEP-1330) - new ElicitationEnumsScenario() + new ElicitationEnumsScenario(), + // Elicitation scenarios (SEP-1036) - URL mode (pending SDK release) + new ElicitationUrlModeScenario() ]; // All client scenarios @@ -80,10 +82,7 @@ const allClientScenariosList: ClientScenario[] = [ // Elicitation scenarios (SEP-1034) new ElicitationDefaultsScenario(), - // Elicitation scenarios (SEP-1036) - URL mode - new ElicitationUrlModeScenario(), - - // Elicitation scenarios (SEP-1330) - pending + // Elicitation scenarios (SEP-1330, SEP-1036) - pending ...pendingClientScenariosList, // Resources scenarios diff --git a/src/scenarios/server/elicitation-url.ts b/src/scenarios/server/elicitation-url.ts index fca84d0..b005e3b 100644 --- a/src/scenarios/server/elicitation-url.ts +++ b/src/scenarios/server/elicitation-url.ts @@ -6,10 +6,20 @@ import { ClientScenario, ConformanceCheck } from '../../types.js'; import { connectToServerWithUrlElicitation } from './client-helper.js'; import { ElicitRequestSchema, - ElicitationCompleteNotificationSchema, ErrorCode, - McpError + McpError, + NotificationSchema } from '@modelcontextprotocol/sdk/types.js'; +import { z } from 'zod'; + +// Define locally until SDK releases this schema +const ElicitationCompleteNotificationSchema = NotificationSchema.extend({ + method: z.literal('notifications/elicitation/complete'), + params: z.object({ + _meta: z.object({}).passthrough().optional(), + elicitationId: z.string() + }) +}); export class ElicitationUrlModeScenario implements ClientScenario { name = 'elicitation-sep1036-url-mode'; From 9f9c13480bc154687d88895e6c9aaddc60efc0fb Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 20 Nov 2025 16:29:19 +0000 Subject: [PATCH 4/5] Fix TypeScript errors for pending SEP-1036 tests Define URL_ELICITATION_REQUIRED_CODE and UrlModeElicitParams locally since they don't exist in the released SDK yet. --- src/scenarios/server/elicitation-url.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/scenarios/server/elicitation-url.ts b/src/scenarios/server/elicitation-url.ts index b005e3b..dd00830 100644 --- a/src/scenarios/server/elicitation-url.ts +++ b/src/scenarios/server/elicitation-url.ts @@ -6,13 +6,14 @@ import { ClientScenario, ConformanceCheck } from '../../types.js'; import { connectToServerWithUrlElicitation } from './client-helper.js'; import { ElicitRequestSchema, - ErrorCode, McpError, NotificationSchema } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; -// Define locally until SDK releases this schema +// Define locally until SDK releases these +const URL_ELICITATION_REQUIRED_CODE = -32042; + const ElicitationCompleteNotificationSchema = NotificationSchema.extend({ method: z.literal('notifications/elicitation/complete'), params: z.object({ @@ -21,6 +22,14 @@ const ElicitationCompleteNotificationSchema = NotificationSchema.extend({ }) }); +// Extended params type for URL mode elicitation (not yet in SDK) +interface UrlModeElicitParams { + message: string; + mode?: string; + url?: string; + elicitationId?: string; +} + export class ElicitationUrlModeScenario implements ClientScenario { name = 'elicitation-sep1036-url-mode'; description = `Test URL mode elicitation per SEP-1036. @@ -304,9 +313,9 @@ Implement three tools: const errorCodeErrors: string[] = []; if (!errorReceived) { errorCodeErrors.push('Did not receive an MCP error'); - } else if (errorReceived.code !== ErrorCode.UrlElicitationRequired) { + } else if (errorReceived.code !== URL_ELICITATION_REQUIRED_CODE) { errorCodeErrors.push( - `Expected error code ${ErrorCode.UrlElicitationRequired} (-32042), got ${errorReceived.code}` + `Expected error code ${URL_ELICITATION_REQUIRED_CODE} (-32042), got ${errorReceived.code}` ); } @@ -433,7 +442,8 @@ Implement three tools: connection.client.setRequestHandler( ElicitRequestSchema, async (request) => { - capturedElicitationIdFromRequest = request.params.elicitationId; + const params = request.params as UrlModeElicitParams; + capturedElicitationIdFromRequest = params.elicitationId ?? null; return { action: 'accept' }; } ); From b55734d0ebe6ea217692e2b03fbdeff2b86a8051 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 20 Nov 2025 20:34:46 +0000 Subject: [PATCH 5/5] Fix missing comma in pendingClientScenariosList --- examples/servers/typescript/everything-server.ts | 2 +- src/scenarios/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/servers/typescript/everything-server.ts b/examples/servers/typescript/everything-server.ts index 3725635..55d0835 100644 --- a/examples/servers/typescript/everything-server.ts +++ b/examples/servers/typescript/everything-server.ts @@ -16,7 +16,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/ import { ElicitResultSchema, McpError, - ErrorCode + ErrorCode, ListToolsRequestSchema, type ListToolsResult, type Tool diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index da53d5d..67c8229 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -56,7 +56,7 @@ const pendingClientScenariosList: ClientScenario[] = [ // Elicitation scenarios (SEP-1330) new ElicitationEnumsScenario(), // Elicitation scenarios (SEP-1036) - URL mode (pending SDK release) - new ElicitationUrlModeScenario() + new ElicitationUrlModeScenario(), // JSON Schema 2020-12 (SEP-1613) // This test is pending until the SDK includes PR #1135 which preserves