From 8a4601d4a690dcb249da1addf27506a549cb5f37 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Fri, 14 Nov 2025 15:59:48 +0000 Subject: [PATCH 01/17] Add conformance tests for SEP-1699 SSE polling Adds conformance tests for SEP-1699 (SSE Polling via Server-Side Disconnect) which allows servers to close connections while maintaining SSE streams for long-polling behavior. New scenarios: - server-sse-polling: Tests server sends priming event (id + empty data) and retry field before closing connection (SHOULD requirements) - sse-retry: Tests client respects SSE retry field timing when reconnecting (MUST requirement) These tests will initially fail for TypeScript SDK as it doesn't currently respect the retry field from SSE events. --- examples/clients/typescript/sse-retry-test.ts | 87 +++++ src/scenarios/client/sse-retry.ts | 305 ++++++++++++++++++ src/scenarios/index.ts | 6 + src/scenarios/server/sse-polling.ts | 234 ++++++++++++++ 4 files changed, 632 insertions(+) create mode 100644 examples/clients/typescript/sse-retry-test.ts create mode 100644 src/scenarios/client/sse-retry.ts create mode 100644 src/scenarios/server/sse-polling.ts diff --git a/examples/clients/typescript/sse-retry-test.ts b/examples/clients/typescript/sse-retry-test.ts new file mode 100644 index 0000000..a2266f5 --- /dev/null +++ b/examples/clients/typescript/sse-retry-test.ts @@ -0,0 +1,87 @@ +#!/usr/bin/env node + +/** + * SSE Retry Test Client + * + * Tests that the MCP client respects the SSE retry field when reconnecting. + * This client connects to a test server that sends retry: field and closes + * the connection, then validates that the client waits the appropriate time. + */ + +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; + +async function main(): Promise { + const serverUrl = process.argv[2]; + + if (!serverUrl) { + console.error('Usage: sse-retry-test '); + process.exit(1); + } + + console.log(`Connecting to MCP server at: ${serverUrl}`); + console.log('This test validates SSE retry field compliance (SEP-1699)'); + + try { + const client = new Client( + { + name: 'sse-retry-test-client', + version: '1.0.0' + }, + { + capabilities: {} + } + ); + + const transport = new StreamableHTTPClientTransport(new URL(serverUrl), { + reconnectionOptions: { + initialReconnectionDelay: 1000, + maxReconnectionDelay: 10000, + reconnectionDelayGrowFactor: 1.5, + maxRetries: 3 + } + }); + + // Track reconnection events + transport.onerror = (error) => { + console.log(`Transport error: ${error.message}`); + }; + + transport.onclose = () => { + console.log('Transport closed'); + }; + + console.log('Initiating connection...'); + await client.connect(transport); + console.log('Connected to MCP server'); + + // Keep connection alive to observe reconnection behavior + // The server will disconnect and the client should reconnect + console.log('Waiting for reconnection cycle...'); + console.log( + 'Server will close connection and client should wait for retry field timing' + ); + + // Wait long enough for: + // 1. Server to send retry field and close (100ms) + // 2. Client to wait for retry period (2000ms expected) + // 3. Client to reconnect (100ms) + // 4. Second disconnect cycle (optional) + await new Promise((resolve) => setTimeout(resolve, 6000)); + + console.log('Test duration complete'); + + await transport.close(); + console.log('Connection closed successfully'); + + process.exit(0); + } catch (error) { + console.error('Test failed:', error); + process.exit(1); + } +} + +main().catch((error) => { + console.error('Unhandled error:', error); + process.exit(1); +}); diff --git a/src/scenarios/client/sse-retry.ts b/src/scenarios/client/sse-retry.ts new file mode 100644 index 0000000..7f6bd2e --- /dev/null +++ b/src/scenarios/client/sse-retry.ts @@ -0,0 +1,305 @@ +/** + * SSE Retry conformance test scenarios for MCP clients (SEP-1699) + * + * Tests that clients properly respect the SSE retry field by: + * - Waiting the specified milliseconds before reconnecting + * - Sending Last-Event-ID header on reconnection + */ + +import http from 'http'; +import { Scenario, ScenarioUrls, ConformanceCheck } from '../../types.js'; + +export class SSERetryScenario implements Scenario { + name = 'sse-retry'; + description = 'Tests that client respects SSE retry field timing (SEP-1699)'; + + private server: http.Server | null = null; + private checks: ConformanceCheck[] = []; + private port: number = 0; + + // Timing tracking + private connectionTimestamps: number[] = []; + private lastEventIds: (string | undefined)[] = []; + private retryValue: number = 2000; // 2 seconds + private eventIdCounter: number = 0; + + // Tolerances for timing validation + private readonly EARLY_TOLERANCE = 50; // Allow 50ms early for scheduler variance + private readonly LATE_TOLERANCE = 200; // Allow 200ms late for network/event loop + + async start(): Promise { + return new Promise((resolve, reject) => { + this.server = http.createServer((req, res) => { + this.handleRequest(req, res); + }); + + this.server.on('error', reject); + + this.server.listen(0, () => { + const address = this.server!.address(); + if (address && typeof address === 'object') { + this.port = address.port; + resolve({ + serverUrl: `http://localhost:${this.port}` + }); + } else { + reject(new Error('Failed to get server address')); + } + }); + }); + } + + async stop(): Promise { + return new Promise((resolve, reject) => { + if (this.server) { + this.server.close((err) => { + if (err) { + reject(err); + } else { + this.server = null; + resolve(); + } + }); + } else { + resolve(); + } + }); + } + + getChecks(): ConformanceCheck[] { + // Generate checks based on observed behavior + this.generateChecks(); + return this.checks; + } + + private handleRequest( + req: http.IncomingMessage, + res: http.ServerResponse + ): void { + const timestamp = performance.now(); + this.connectionTimestamps.push(timestamp); + + // Track Last-Event-ID header + const lastEventId = req.headers['last-event-id'] as string | undefined; + this.lastEventIds.push(lastEventId); + + if (req.method === 'GET') { + // Handle SSE stream request + this.handleSSEStream(req, res); + } else if (req.method === 'POST') { + // Handle JSON-RPC requests (for initialization) + this.handleJSONRPC(req, res); + } else { + res.writeHead(405); + res.end('Method Not Allowed'); + } + } + + private handleSSEStream( + req: http.IncomingMessage, + res: http.ServerResponse + ): void { + // Set SSE headers + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive' + }); + + // Generate event ID + this.eventIdCounter++; + const eventId = `event-${this.eventIdCounter}`; + + // Send priming event with ID and empty data + res.write(`id: ${eventId}\n`); + res.write(`retry: ${this.retryValue}\n`); + res.write(`data: \n\n`); + + // Close connection after a short delay to trigger client reconnection + setTimeout(() => { + res.end(); + }, 100); + } + + private handleJSONRPC( + req: http.IncomingMessage, + res: http.ServerResponse + ): void { + let body = ''; + + req.on('data', (chunk) => { + body += chunk.toString(); + }); + + req.on('end', () => { + try { + const request = JSON.parse(body); + + if (request.method === 'initialize') { + // Respond to initialize request with SSE stream + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive' + }); + + // Generate event ID + this.eventIdCounter++; + const eventId = `event-${this.eventIdCounter}`; + + // Send priming event + res.write(`id: ${eventId}\n`); + res.write(`retry: ${this.retryValue}\n`); + res.write(`data: \n\n`); + + // Send initialize response + const response = { + jsonrpc: '2.0', + id: request.id, + result: { + protocolVersion: '2025-06-18', + serverInfo: { + name: 'sse-retry-test-server', + version: '1.0.0' + }, + capabilities: {} + } + }; + + res.write(`event: message\n`); + res.write(`id: event-${++this.eventIdCounter}\n`); + res.write(`data: ${JSON.stringify(response)}\n\n`); + + // Close connection after sending response to trigger reconnection + setTimeout(() => { + res.end(); + }, 100); + } else { + // For other requests, send a simple JSON response + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + jsonrpc: '2.0', + id: request.id, + result: {} + }) + ); + } + } catch (error) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32700, + message: `Parse error: ${error}` + } + }) + ); + } + }); + } + + private generateChecks(): void { + // Check 1: Client should have reconnected + if (this.connectionTimestamps.length < 2) { + this.checks.push({ + id: 'client-sse-retry-reconnect', + name: 'ClientReconnects', + description: 'Client reconnects after server disconnect', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Expected at least 2 connections, got ${this.connectionTimestamps.length}. Client may not have attempted to reconnect.`, + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + connectionCount: this.connectionTimestamps.length, + retryValue: this.retryValue + } + }); + return; + } + + // Check 2: Client MUST respect retry field timing + const actualDelay = + this.connectionTimestamps[1] - this.connectionTimestamps[0]; + const minExpected = this.retryValue - this.EARLY_TOLERANCE; + const maxExpected = this.retryValue + this.LATE_TOLERANCE; + + const tooEarly = actualDelay < minExpected; + const tooLate = actualDelay > maxExpected; + const withinTolerance = !tooEarly && !tooLate; + + let status: 'SUCCESS' | 'FAILURE' | 'WARNING' = 'SUCCESS'; + let errorMessage: string | undefined; + + if (tooEarly) { + // Client reconnected too soon - MUST violation + status = 'FAILURE'; + errorMessage = `Client reconnected too early (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). Client MUST respect the retry field and wait the specified time.`; + } else if (tooLate) { + // Client reconnected too late - not a spec violation but suspicious + status = 'WARNING'; + errorMessage = `Client reconnected late (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). This is acceptable but may indicate the client is ignoring the retry field and using its own backoff.`; + } + + this.checks.push({ + id: 'client-sse-retry-timing', + name: 'ClientRespectsRetryField', + description: + 'Client MUST respect the retry field, waiting the given number of milliseconds before attempting to reconnect', + status, + timestamp: new Date().toISOString(), + errorMessage, + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + expectedRetryMs: this.retryValue, + actualDelayMs: Math.round(actualDelay), + minAcceptableMs: minExpected, + maxAcceptableMs: maxExpected, + earlyToleranceMs: this.EARLY_TOLERANCE, + lateToleranceMs: this.LATE_TOLERANCE, + withinTolerance, + tooEarly, + tooLate, + connectionCount: this.connectionTimestamps.length + } + }); + + // Check 3: Client SHOULD send Last-Event-ID header on reconnection + const hasLastEventId = + this.lastEventIds.length > 1 && this.lastEventIds[1] !== undefined; + + this.checks.push({ + id: 'client-sse-last-event-id', + name: 'ClientSendsLastEventId', + description: + 'Client SHOULD send Last-Event-ID header on reconnection for resumability', + status: hasLastEventId ? 'SUCCESS' : 'WARNING', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + hasLastEventId, + lastEventIds: this.lastEventIds, + connectionCount: this.connectionTimestamps.length + }, + errorMessage: !hasLastEventId + ? 'Client did not send Last-Event-ID header on reconnection. This is a SHOULD requirement for resumability.' + : undefined + }); + } +} diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index 5d50dff..03dcddf 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -2,6 +2,7 @@ import { Scenario, ClientScenario } from '../types.js'; import { InitializeScenario } from './client/initialize.js'; import { ToolsCallScenario } from './client/tools_call.js'; import { ElicitationClientDefaultsScenario } from './client/elicitation-defaults.js'; +import { SSERetryScenario } from './client/sse-retry.js'; // Import all new server test scenarios import { ServerInitializeScenario } from './server/lifecycle.js'; @@ -27,6 +28,7 @@ import { import { ElicitationDefaultsScenario } from './server/elicitation-defaults.js'; import { ElicitationEnumsScenario } from './server/elicitation-enums.js'; +import { ServerSSEPollingScenario } from './server/sse-polling.js'; import { ResourcesListScenario, @@ -78,6 +80,9 @@ const allClientScenariosList: ClientScenario[] = [ // Elicitation scenarios (SEP-1034) new ElicitationDefaultsScenario(), + // SSE Polling scenarios (SEP-1699) + new ServerSSEPollingScenario(), + // Elicitation scenarios (SEP-1330) - pending ...pendingClientScenariosList, @@ -116,6 +121,7 @@ const scenariosList: Scenario[] = [ new InitializeScenario(), new ToolsCallScenario(), new ElicitationClientDefaultsScenario(), + new SSERetryScenario(), ...authScenariosList ]; diff --git a/src/scenarios/server/sse-polling.ts b/src/scenarios/server/sse-polling.ts new file mode 100644 index 0000000..d73d280 --- /dev/null +++ b/src/scenarios/server/sse-polling.ts @@ -0,0 +1,234 @@ +/** + * SSE Polling conformance test scenarios for MCP servers (SEP-1699) + * + * Tests that servers properly implement SSE polling behavior including: + * - Sending priming events with event ID and empty data + * - Sending retry field before closing connection + */ + +import { ClientScenario, ConformanceCheck } from '../../types.js'; +import { EventSourceParserStream } from 'eventsource-parser/stream'; + +export class ServerSSEPollingScenario implements ClientScenario { + name = 'server-sse-polling'; + description = + 'Test server sends SSE priming event and retry field (SEP-1699)'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + try { + // Make a GET request to establish SSE stream + const response = await fetch(serverUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream' + } + }); + + // Check if server supports SSE GET endpoint + if (response.status === 405) { + checks.push({ + id: 'server-sse-polling-endpoint', + name: 'ServerSSEPollingEndpoint', + description: 'Server supports SSE GET endpoint', + status: 'INFO', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + serverUrl, + statusCode: response.status, + message: + 'Server does not support SSE GET endpoint (405 Method Not Allowed)' + } + }); + return checks; + } + + if (!response.ok) { + checks.push({ + id: 'server-sse-polling-connection', + name: 'ServerSSEPollingConnection', + description: 'Server accepts SSE GET request', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Server returned HTTP ${response.status}`, + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ] + }); + return checks; + } + + // Parse SSE stream + let hasEventId = false; + let hasPrimingEvent = false; + let hasRetryField = false; + let retryValue: number | undefined; + let firstEventId: string | undefined; + let disconnected = false; + let eventCount = 0; + + if (!response.body) { + checks.push({ + id: 'server-sse-polling-stream', + name: 'ServerSSEPollingStream', + description: 'Server provides SSE response body', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: 'Response body is null', + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ] + }); + return checks; + } + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new EventSourceParserStream()) + .getReader(); + + // Read events with timeout + const timeout = setTimeout(() => { + reader.cancel(); + }, 5000); + + try { + while (true) { + const { value: event, done } = await reader.read(); + + if (done) { + disconnected = true; + break; + } + + eventCount++; + + // Check for event ID + if (event.id) { + hasEventId = true; + if (!firstEventId) { + firstEventId = event.id; + } + + // Check if this is a priming event (empty or minimal data) + if ( + event.data === '' || + event.data === '{}' || + event.data.trim() === '' + ) { + hasPrimingEvent = true; + } + } + + // Check for retry field + if (event.retry !== undefined) { + hasRetryField = true; + retryValue = event.retry; + } + } + } finally { + clearTimeout(timeout); + } + + // Check 1: Server SHOULD send priming event with ID + checks.push({ + id: 'server-sse-priming-event', + name: 'ServerSendsPrimingEvent', + description: + 'Server SHOULD send SSE event with id and empty data to prime client for reconnection', + status: hasPrimingEvent ? 'SUCCESS' : 'WARNING', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + hasPrimingEvent, + hasEventId, + firstEventId, + eventCount + }, + errorMessage: !hasPrimingEvent + ? 'Server did not send priming event with id and empty data. This is a SHOULD requirement for SEP-1699.' + : undefined + }); + + // Check 2: Server SHOULD send retry field before disconnect + checks.push({ + id: 'server-sse-retry-field', + name: 'ServerSendsRetryField', + description: 'Server SHOULD send retry field before closing connection', + status: hasRetryField ? 'SUCCESS' : 'WARNING', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + hasRetryField, + retryValue, + disconnected + }, + errorMessage: !hasRetryField + ? 'Server did not send retry field. This is a SHOULD requirement for SEP-1699.' + : undefined + }); + + // Check 3: Server MAY close connection (informational) + checks.push({ + id: 'server-sse-disconnect', + name: 'ServerDisconnectBehavior', + description: + 'Server MAY close connection after sending event ID (informational)', + status: 'INFO', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + disconnected, + eventCount, + hasRetryField, + retryValue + } + }); + } catch (error) { + checks.push({ + id: 'server-sse-polling-error', + name: 'ServerSSEPollingTest', + description: 'Test server SSE polling behavior', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Error: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ] + }); + } + + return checks; + } +} From be3d929a1966bacaa117111ff5ae5f080a24241d Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Mon, 17 Nov 2025 14:12:29 +0000 Subject: [PATCH 02/17] fix: correct SSE retry timing test to track GET requests only The test was incorrectly measuring timing between all requests (POST and GET), but the retry field only applies to GET SSE stream reconnections. Changes: - Only track timestamps and Last-Event-ID for GET requests - Return 202 Accepted for notifications to trigger client GET stream - Add eventsource-parser dependency --- package-lock.json | 12 ++++++++++++ package.json | 1 + src/scenarios/client/sse-retry.ts | 18 ++++++++++++------ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2232591..d558004 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.22.0", "commander": "^14.0.2", + "eventsource-parser": "^3.0.6", "express": "^5.1.0", "lefthook": "^2.0.2", "zod": "^3.25.76" @@ -1710,6 +1711,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -1928,6 +1930,7 @@ "integrity": "sha512-LKDTZxo2D2U75EZ+8xw0AJmG3VEqUVjlWu43IJLdl+Hjyhh2eFmfufhXw3mwPRAF6hEy+E3ldUtglmmliN/CiA==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsgo": "bin/tsgo.js" }, @@ -2169,6 +2172,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2796,6 +2800,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3014,6 +3019,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -4132,6 +4138,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4377,6 +4384,7 @@ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.45.tgz", "integrity": "sha512-iMmuD72XXLf26Tqrv1cryNYLX6NNPLhZ3AmNkSf8+xda0H+yijjGJ+wVT9UdBUHOpKzq9RjKtQKRCWoEKQQBZQ==", "dev": true, + "peer": true, "dependencies": { "@oxc-project/types": "=0.95.0", "@rolldown/pluginutils": "1.0.0-beta.45" @@ -4929,6 +4937,7 @@ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -4975,6 +4984,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5062,6 +5072,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -5282,6 +5293,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index cedad3c..b996a7c 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.22.0", "commander": "^14.0.2", + "eventsource-parser": "^3.0.6", "express": "^5.1.0", "lefthook": "^2.0.2", "zod": "^3.25.76" diff --git a/src/scenarios/client/sse-retry.ts b/src/scenarios/client/sse-retry.ts index 7f6bd2e..0132a89 100644 --- a/src/scenarios/client/sse-retry.ts +++ b/src/scenarios/client/sse-retry.ts @@ -76,14 +76,15 @@ export class SSERetryScenario implements Scenario { req: http.IncomingMessage, res: http.ServerResponse ): void { - const timestamp = performance.now(); - this.connectionTimestamps.push(timestamp); + if (req.method === 'GET') { + // Track timing and Last-Event-ID only for GET requests + // since retry field only applies to SSE stream reconnections + const timestamp = performance.now(); + this.connectionTimestamps.push(timestamp); - // Track Last-Event-ID header - const lastEventId = req.headers['last-event-id'] as string | undefined; - this.lastEventIds.push(lastEventId); + const lastEventId = req.headers['last-event-id'] as string | undefined; + this.lastEventIds.push(lastEventId); - if (req.method === 'GET') { // Handle SSE stream request this.handleSSEStream(req, res); } else if (req.method === 'POST') { @@ -174,6 +175,11 @@ export class SSERetryScenario implements Scenario { setTimeout(() => { res.end(); }, 100); + } else if (request.id === undefined) { + // Notifications (no id) - return 202 Accepted + // This triggers the client to start a GET SSE stream + res.writeHead(202); + res.end(); } else { // For other requests, send a simple JSON response res.writeHead(200, { 'Content-Type': 'application/json' }); From de5ebab0a8ed06436f52f222a839f4dcab6da7f3 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Mon, 17 Nov 2025 14:30:07 +0000 Subject: [PATCH 03/17] fix: improve SSE conformance test validation (SEP-1699) Enhance timing validation and event ordering checks: Client test (sse-retry): - Add "very late" threshold (>2x retry value) as FAILURE - Distinguish between slightly late (WARNING) and very late (FAILURE) Server test (sse-polling): - Check that priming event is sent immediately (first event) - Warn if server disconnects without sending event ID first --- src/scenarios/client/sse-retry.ts | 21 +++++++++---- src/scenarios/server/sse-polling.ts | 47 +++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/scenarios/client/sse-retry.ts b/src/scenarios/client/sse-retry.ts index 0132a89..c7304dc 100644 --- a/src/scenarios/client/sse-retry.ts +++ b/src/scenarios/client/sse-retry.ts @@ -26,6 +26,7 @@ export class SSERetryScenario implements Scenario { // Tolerances for timing validation private readonly EARLY_TOLERANCE = 50; // Allow 50ms early for scheduler variance private readonly LATE_TOLERANCE = 200; // Allow 200ms late for network/event loop + private readonly VERY_LATE_MULTIPLIER = 2; // If >2x retry value, client is likely ignoring it async start(): Promise { return new Promise((resolve, reject) => { @@ -237,8 +238,10 @@ export class SSERetryScenario implements Scenario { const maxExpected = this.retryValue + this.LATE_TOLERANCE; const tooEarly = actualDelay < minExpected; - const tooLate = actualDelay > maxExpected; - const withinTolerance = !tooEarly && !tooLate; + const slightlyLate = actualDelay > maxExpected; + const veryLate = + actualDelay > this.retryValue * this.VERY_LATE_MULTIPLIER; + const withinTolerance = !tooEarly && !slightlyLate; let status: 'SUCCESS' | 'FAILURE' | 'WARNING' = 'SUCCESS'; let errorMessage: string | undefined; @@ -247,10 +250,14 @@ export class SSERetryScenario implements Scenario { // Client reconnected too soon - MUST violation status = 'FAILURE'; errorMessage = `Client reconnected too early (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). Client MUST respect the retry field and wait the specified time.`; - } else if (tooLate) { - // Client reconnected too late - not a spec violation but suspicious + } else if (veryLate) { + // Client reconnected way too late - likely ignoring retry field entirely + status = 'FAILURE'; + errorMessage = `Client reconnected very late (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). Client appears to be ignoring the retry field and using its own backoff strategy.`; + } else if (slightlyLate) { + // Client reconnected slightly late - not a spec violation but suspicious status = 'WARNING'; - errorMessage = `Client reconnected late (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). This is acceptable but may indicate the client is ignoring the retry field and using its own backoff.`; + errorMessage = `Client reconnected slightly late (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). This is acceptable but may indicate network delays.`; } this.checks.push({ @@ -272,11 +279,13 @@ export class SSERetryScenario implements Scenario { actualDelayMs: Math.round(actualDelay), minAcceptableMs: minExpected, maxAcceptableMs: maxExpected, + veryLateThresholdMs: this.retryValue * this.VERY_LATE_MULTIPLIER, earlyToleranceMs: this.EARLY_TOLERANCE, lateToleranceMs: this.LATE_TOLERANCE, withinTolerance, tooEarly, - tooLate, + slightlyLate, + veryLate, connectionCount: this.connectionTimestamps.length } }); diff --git a/src/scenarios/server/sse-polling.ts b/src/scenarios/server/sse-polling.ts index d73d280..74ff462 100644 --- a/src/scenarios/server/sse-polling.ts +++ b/src/scenarios/server/sse-polling.ts @@ -71,6 +71,7 @@ export class ServerSSEPollingScenario implements ClientScenario { // Parse SSE stream let hasEventId = false; let hasPrimingEvent = false; + let primingEventIsFirst = false; let hasRetryField = false; let retryValue: number | undefined; let firstEventId: string | undefined; @@ -130,6 +131,10 @@ export class ServerSSEPollingScenario implements ClientScenario { event.data.trim() === '' ) { hasPrimingEvent = true; + // Check if priming event is the first event (SEP-1699 says "immediately") + if (eventCount === 1) { + primingEventIsFirst = true; + } } } @@ -143,13 +148,26 @@ export class ServerSSEPollingScenario implements ClientScenario { clearTimeout(timeout); } - // Check 1: Server SHOULD send priming event with ID + // Check 1: Server SHOULD send priming event with ID immediately + let primingStatus: 'SUCCESS' | 'WARNING' = 'SUCCESS'; + let primingErrorMessage: string | undefined; + + if (!hasPrimingEvent) { + primingStatus = 'WARNING'; + primingErrorMessage = + 'Server did not send priming event with id and empty data. This is a SHOULD requirement for SEP-1699.'; + } else if (!primingEventIsFirst) { + primingStatus = 'WARNING'; + primingErrorMessage = + 'Priming event was not sent immediately (not the first event). SEP-1699 says server SHOULD immediately send the priming event.'; + } + checks.push({ id: 'server-sse-priming-event', name: 'ServerSendsPrimingEvent', description: - 'Server SHOULD send SSE event with id and empty data to prime client for reconnection', - status: hasPrimingEvent ? 'SUCCESS' : 'WARNING', + 'Server SHOULD immediately send SSE event with id and empty data to prime client for reconnection', + status: primingStatus, timestamp: new Date().toISOString(), specReferences: [ { @@ -159,13 +177,12 @@ export class ServerSSEPollingScenario implements ClientScenario { ], details: { hasPrimingEvent, + primingEventIsFirst, hasEventId, firstEventId, eventCount }, - errorMessage: !hasPrimingEvent - ? 'Server did not send priming event with id and empty data. This is a SHOULD requirement for SEP-1699.' - : undefined + errorMessage: primingErrorMessage }); // Check 2: Server SHOULD send retry field before disconnect @@ -191,13 +208,23 @@ export class ServerSSEPollingScenario implements ClientScenario { : undefined }); - // Check 3: Server MAY close connection (informational) + // Check 3: Server MAY close connection after sending event ID + // Per SEP-1699, server can only close "if it has sent an SSE event with an event ID" + let disconnectStatus: 'SUCCESS' | 'WARNING' | 'INFO' = 'INFO'; + let disconnectMessage: string | undefined; + + if (disconnected && !hasEventId) { + disconnectStatus = 'WARNING'; + disconnectMessage = + 'Server closed connection without sending an event ID first. SEP-1699 allows disconnect only after sending event ID.'; + } + checks.push({ id: 'server-sse-disconnect', name: 'ServerDisconnectBehavior', description: 'Server MAY close connection after sending event ID (informational)', - status: 'INFO', + status: disconnectStatus, timestamp: new Date().toISOString(), specReferences: [ { @@ -207,10 +234,12 @@ export class ServerSSEPollingScenario implements ClientScenario { ], details: { disconnected, + hasEventId, eventCount, hasRetryField, retryValue - } + }, + errorMessage: disconnectMessage }); } catch (error) { checks.push({ From 12de2ceceb1e926dff15272f8b18c7582f86e85c Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Mon, 17 Nov 2025 14:50:18 +0000 Subject: [PATCH 04/17] fix: capture SSE retry field via onRetry callback The eventsource-parser library delivers retry through onRetry callback, not as a property on EventSourceMessage objects. --- src/scenarios/server/sse-polling.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/scenarios/server/sse-polling.ts b/src/scenarios/server/sse-polling.ts index 74ff462..a427d30 100644 --- a/src/scenarios/server/sse-polling.ts +++ b/src/scenarios/server/sse-polling.ts @@ -98,7 +98,12 @@ export class ServerSSEPollingScenario implements ClientScenario { const reader = response.body .pipeThrough(new TextDecoderStream()) - .pipeThrough(new EventSourceParserStream()) + .pipeThrough(new EventSourceParserStream({ + onRetry: (retryMs: number) => { + hasRetryField = true; + retryValue = retryMs; + } + })) .getReader(); // Read events with timeout @@ -137,12 +142,6 @@ export class ServerSSEPollingScenario implements ClientScenario { } } } - - // Check for retry field - if (event.retry !== undefined) { - hasRetryField = true; - retryValue = event.retry; - } } } finally { clearTimeout(timeout); From 32fb39745d74649aa53f23b7b018c28f7fc58bfb Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Mon, 17 Nov 2025 14:52:15 +0000 Subject: [PATCH 05/17] style: fix Prettier formatting --- src/scenarios/client/sse-retry.ts | 3 +-- src/scenarios/server/sse-polling.ts | 14 ++++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/scenarios/client/sse-retry.ts b/src/scenarios/client/sse-retry.ts index c7304dc..6554163 100644 --- a/src/scenarios/client/sse-retry.ts +++ b/src/scenarios/client/sse-retry.ts @@ -239,8 +239,7 @@ export class SSERetryScenario implements Scenario { const tooEarly = actualDelay < minExpected; const slightlyLate = actualDelay > maxExpected; - const veryLate = - actualDelay > this.retryValue * this.VERY_LATE_MULTIPLIER; + const veryLate = actualDelay > this.retryValue * this.VERY_LATE_MULTIPLIER; const withinTolerance = !tooEarly && !slightlyLate; let status: 'SUCCESS' | 'FAILURE' | 'WARNING' = 'SUCCESS'; diff --git a/src/scenarios/server/sse-polling.ts b/src/scenarios/server/sse-polling.ts index a427d30..972b9c9 100644 --- a/src/scenarios/server/sse-polling.ts +++ b/src/scenarios/server/sse-polling.ts @@ -98,12 +98,14 @@ export class ServerSSEPollingScenario implements ClientScenario { const reader = response.body .pipeThrough(new TextDecoderStream()) - .pipeThrough(new EventSourceParserStream({ - onRetry: (retryMs: number) => { - hasRetryField = true; - retryValue = retryMs; - } - })) + .pipeThrough( + new EventSourceParserStream({ + onRetry: (retryMs: number) => { + hasRetryField = true; + retryValue = retryMs; + } + }) + ) .getReader(); // Read events with timeout From bc2298d9735bd7bd80ec6ca847df35f8b8c806b8 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Mon, 17 Nov 2025 14:55:02 +0000 Subject: [PATCH 06/17] fix: handle 400 response in SSE polling scenario Server may return 400 when GET request is missing session context (mcp-session-id header). Treat as INFO similar to 405. --- package-lock.json | 11 ----------- src/scenarios/server/sse-polling.ts | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index d558004..12cdd24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1711,7 +1711,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -1930,7 +1929,6 @@ "integrity": "sha512-LKDTZxo2D2U75EZ+8xw0AJmG3VEqUVjlWu43IJLdl+Hjyhh2eFmfufhXw3mwPRAF6hEy+E3ldUtglmmliN/CiA==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsgo": "bin/tsgo.js" }, @@ -2172,7 +2170,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2800,7 +2797,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3019,7 +3015,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -4138,7 +4133,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4384,7 +4378,6 @@ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.45.tgz", "integrity": "sha512-iMmuD72XXLf26Tqrv1cryNYLX6NNPLhZ3AmNkSf8+xda0H+yijjGJ+wVT9UdBUHOpKzq9RjKtQKRCWoEKQQBZQ==", "dev": true, - "peer": true, "dependencies": { "@oxc-project/types": "=0.95.0", "@rolldown/pluginutils": "1.0.0-beta.45" @@ -4937,7 +4930,6 @@ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -4984,7 +4976,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5072,7 +5063,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -5293,7 +5283,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/scenarios/server/sse-polling.ts b/src/scenarios/server/sse-polling.ts index 972b9c9..f22be89 100644 --- a/src/scenarios/server/sse-polling.ts +++ b/src/scenarios/server/sse-polling.ts @@ -50,6 +50,30 @@ export class ServerSSEPollingScenario implements ClientScenario { return checks; } + // Server may require session context for standalone SSE GET + if (response.status === 400) { + checks.push({ + id: 'server-sse-polling-endpoint', + name: 'ServerSSEPollingEndpoint', + description: 'Server supports SSE GET endpoint', + status: 'INFO', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + serverUrl, + statusCode: response.status, + message: + 'Server requires session context for standalone SSE GET endpoint (400 Bad Request)' + } + }); + return checks; + } + if (!response.ok) { checks.push({ id: 'server-sse-polling-connection', From bc7c64908f1f3521097c2e4800d62e922f404ed5 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 18 Nov 2025 16:50:17 +0000 Subject: [PATCH 07/17] feat: update SEP-1699 conformance tests for POST stream priming events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrite server tests to validate priming events on POST SSE streams - Update client tests for POST→GET reconnection flow with retry timing - Add eventStore and retryInterval to example server - All 3 server conformance checks now pass --- examples/clients/typescript/sse-retry-test.ts | 15 +- .../servers/typescript/everything-server.ts | 44 ++- src/scenarios/client/sse-retry.ts | 220 ++++++++----- src/scenarios/server/sse-polling.ts | 307 +++++++++++++----- 4 files changed, 409 insertions(+), 177 deletions(-) diff --git a/examples/clients/typescript/sse-retry-test.ts b/examples/clients/typescript/sse-retry-test.ts index a2266f5..8595668 100644 --- a/examples/clients/typescript/sse-retry-test.ts +++ b/examples/clients/typescript/sse-retry-test.ts @@ -56,17 +56,20 @@ async function main(): Promise { console.log('Connected to MCP server'); // Keep connection alive to observe reconnection behavior - // The server will disconnect and the client should reconnect + // The server will close the POST SSE stream and the client should reconnect via GET console.log('Waiting for reconnection cycle...'); console.log( - 'Server will close connection and client should wait for retry field timing' + 'Server will send priming event with retry field, then close POST SSE stream' + ); + console.log( + 'Client should wait for retry period (2000ms) then reconnect via GET with Last-Event-ID' ); // Wait long enough for: - // 1. Server to send retry field and close (100ms) - // 2. Client to wait for retry period (2000ms expected) - // 3. Client to reconnect (100ms) - // 4. Second disconnect cycle (optional) + // 1. Server to send priming event with retry field on POST SSE stream (100ms) + // 2. Server closes POST stream to trigger reconnection + // 3. Client waits for retry period (2000ms expected) + // 4. Client reconnects via GET with Last-Event-ID header await new Promise((resolve) => setTimeout(resolve, 6000)); console.log('Test duration complete'); diff --git a/examples/servers/typescript/everything-server.ts b/examples/servers/typescript/everything-server.ts index be90caf..0888732 100644 --- a/examples/servers/typescript/everything-server.ts +++ b/examples/servers/typescript/everything-server.ts @@ -12,7 +12,12 @@ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { + StreamableHTTPServerTransport, + EventStore, + EventId, + StreamId +} from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { z } from 'zod'; import express from 'express'; import cors from 'cors'; @@ -26,6 +31,41 @@ const watchedResourceContent = 'Watched resource content'; const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; const servers: { [sessionId: string]: McpServer } = {}; +// In-memory event store for SEP-1699 resumability +const eventStoreData = new Map< + string, + { eventId: string; message: any; streamId: string } +>(); + +function createEventStore(): EventStore { + return { + async storeEvent(streamId: StreamId, message: any): Promise { + const eventId = `${streamId}::${Date.now()}_${randomUUID()}`; + eventStoreData.set(eventId, { eventId, message, streamId }); + return eventId; + }, + async replayEventsAfter( + lastEventId: EventId, + { send }: { send: (eventId: EventId, message: any) => Promise } + ): Promise { + const streamId = lastEventId.split('::')[0]; + const eventsToReplay: Array<[string, { message: any }]> = []; + for (const [eventId, data] of eventStoreData.entries()) { + if (data.streamId === streamId && eventId > lastEventId) { + eventsToReplay.push([eventId, data]); + } + } + eventsToReplay.sort(([a], [b]) => a.localeCompare(b)); + for (const [eventId, { message }] of eventsToReplay) { + if (Object.keys(message).length > 0) { + await send(eventId, message); + } + } + return streamId; + } + }; +} + // Sample base64 encoded 1x1 red PNG pixel for testing const TEST_IMAGE_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=='; @@ -872,6 +912,8 @@ app.post('/mcp', async (req, res) => { transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), + eventStore: createEventStore(), + retryInterval: 5000, // 5 second retry interval for SEP-1699 onsessioninitialized: (newSessionId) => { transports[newSessionId] = transport; servers[newSessionId] = mcpServer; diff --git a/src/scenarios/client/sse-retry.ts b/src/scenarios/client/sse-retry.ts index 6554163..a982678 100644 --- a/src/scenarios/client/sse-retry.ts +++ b/src/scenarios/client/sse-retry.ts @@ -4,6 +4,7 @@ * Tests that clients properly respect the SSE retry field by: * - Waiting the specified milliseconds before reconnecting * - Sending Last-Event-ID header on reconnection + * - Treating graceful stream closure as reconnectable */ import http from 'http'; @@ -11,17 +12,22 @@ import { Scenario, ScenarioUrls, ConformanceCheck } from '../../types.js'; export class SSERetryScenario implements Scenario { name = 'sse-retry'; - description = 'Tests that client respects SSE retry field timing (SEP-1699)'; + description = + 'Tests that client respects SSE retry field timing and reconnects properly (SEP-1699)'; private server: http.Server | null = null; private checks: ConformanceCheck[] = []; private port: number = 0; // Timing tracking - private connectionTimestamps: number[] = []; + private postStreamCloseTime: number | null = null; + private getReconnectionTime: number | null = null; + private getConnectionCount: number = 0; private lastEventIds: (string | undefined)[] = []; private retryValue: number = 2000; // 2 seconds private eventIdCounter: number = 0; + private sessionId: string = `session-${Date.now()}`; + private primingEventId: string | null = null; // Tolerances for timing validation private readonly EARLY_TOLERANCE = 50; // Allow 50ms early for scheduler variance @@ -78,52 +84,50 @@ export class SSERetryScenario implements Scenario { res: http.ServerResponse ): void { if (req.method === 'GET') { - // Track timing and Last-Event-ID only for GET requests - // since retry field only applies to SSE stream reconnections - const timestamp = performance.now(); - this.connectionTimestamps.push(timestamp); + // Track GET reconnection timing and Last-Event-ID + this.getConnectionCount++; + this.getReconnectionTime = performance.now(); const lastEventId = req.headers['last-event-id'] as string | undefined; this.lastEventIds.push(lastEventId); - // Handle SSE stream request - this.handleSSEStream(req, res); + // Handle GET SSE stream request (reconnection) + this.handleGetSSEStream(req, res); } else if (req.method === 'POST') { - // Handle JSON-RPC requests (for initialization) - this.handleJSONRPC(req, res); + // Handle POST JSON-RPC requests + this.handlePostRequest(req, res); } else { res.writeHead(405); res.end('Method Not Allowed'); } } - private handleSSEStream( - req: http.IncomingMessage, + private handleGetSSEStream( + _req: http.IncomingMessage, res: http.ServerResponse ): void { // Set SSE headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - Connection: 'keep-alive' + Connection: 'keep-alive', + 'mcp-session-id': this.sessionId }); // Generate event ID this.eventIdCounter++; const eventId = `event-${this.eventIdCounter}`; - // Send priming event with ID and empty data + // Send priming event with ID and retry field res.write(`id: ${eventId}\n`); res.write(`retry: ${this.retryValue}\n`); res.write(`data: \n\n`); - // Close connection after a short delay to trigger client reconnection - setTimeout(() => { - res.end(); - }, 100); + // Keep connection open for now (don't close immediately to avoid infinite reconnection loop) + // The test will stop the server when done } - private handleJSONRPC( + private handlePostRequest( req: http.IncomingMessage, res: http.ServerResponse ): void { @@ -138,19 +142,20 @@ export class SSERetryScenario implements Scenario { const request = JSON.parse(body); if (request.method === 'initialize') { - // Respond to initialize request with SSE stream + // Respond to initialize request with SSE stream containing priming event res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - Connection: 'keep-alive' + Connection: 'keep-alive', + 'mcp-session-id': this.sessionId }); - // Generate event ID + // Generate priming event ID this.eventIdCounter++; - const eventId = `event-${this.eventIdCounter}`; + this.primingEventId = `event-${this.eventIdCounter}`; - // Send priming event - res.write(`id: ${eventId}\n`); + // Send priming event with retry field + res.write(`id: ${this.primingEventId}\n`); res.write(`retry: ${this.retryValue}\n`); res.write(`data: \n\n`); @@ -159,7 +164,7 @@ export class SSERetryScenario implements Scenario { jsonrpc: '2.0', id: request.id, result: { - protocolVersion: '2025-06-18', + protocolVersion: '2025-03-26', serverInfo: { name: 'sse-retry-test-server', version: '1.0.0' @@ -173,17 +178,21 @@ export class SSERetryScenario implements Scenario { res.write(`data: ${JSON.stringify(response)}\n\n`); // Close connection after sending response to trigger reconnection + // Record the time when we close the stream setTimeout(() => { + this.postStreamCloseTime = performance.now(); res.end(); }, 100); } else if (request.id === undefined) { // Notifications (no id) - return 202 Accepted - // This triggers the client to start a GET SSE stream res.writeHead(202); res.end(); } else { // For other requests, send a simple JSON response - res.writeHead(200, { 'Content-Type': 'application/json' }); + res.writeHead(200, { + 'Content-Type': 'application/json', + 'mcp-session-id': this.sessionId + }); res.end( JSON.stringify({ jsonrpc: '2.0', @@ -208,15 +217,16 @@ export class SSERetryScenario implements Scenario { } private generateChecks(): void { - // Check 1: Client should have reconnected - if (this.connectionTimestamps.length < 2) { + // Check 1: Client should have reconnected via GET after POST stream close + if (this.getConnectionCount < 1) { this.checks.push({ - id: 'client-sse-retry-reconnect', - name: 'ClientReconnects', - description: 'Client reconnects after server disconnect', + id: 'client-sse-graceful-reconnect', + name: 'ClientGracefulReconnect', + description: + 'Client reconnects via GET after POST SSE stream is closed gracefully', status: 'FAILURE', timestamp: new Date().toISOString(), - errorMessage: `Expected at least 2 connections, got ${this.connectionTimestamps.length}. Client may not have attempted to reconnect.`, + errorMessage: `Client did not attempt GET reconnection after POST stream closure. Client should treat graceful stream close as reconnectable.`, specReferences: [ { id: 'SEP-1699', @@ -224,49 +234,22 @@ export class SSERetryScenario implements Scenario { } ], details: { - connectionCount: this.connectionTimestamps.length, + getConnectionCount: this.getConnectionCount, + postStreamCloseTime: this.postStreamCloseTime, retryValue: this.retryValue } }); return; } - // Check 2: Client MUST respect retry field timing - const actualDelay = - this.connectionTimestamps[1] - this.connectionTimestamps[0]; - const minExpected = this.retryValue - this.EARLY_TOLERANCE; - const maxExpected = this.retryValue + this.LATE_TOLERANCE; - - const tooEarly = actualDelay < minExpected; - const slightlyLate = actualDelay > maxExpected; - const veryLate = actualDelay > this.retryValue * this.VERY_LATE_MULTIPLIER; - const withinTolerance = !tooEarly && !slightlyLate; - - let status: 'SUCCESS' | 'FAILURE' | 'WARNING' = 'SUCCESS'; - let errorMessage: string | undefined; - - if (tooEarly) { - // Client reconnected too soon - MUST violation - status = 'FAILURE'; - errorMessage = `Client reconnected too early (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). Client MUST respect the retry field and wait the specified time.`; - } else if (veryLate) { - // Client reconnected way too late - likely ignoring retry field entirely - status = 'FAILURE'; - errorMessage = `Client reconnected very late (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). Client appears to be ignoring the retry field and using its own backoff strategy.`; - } else if (slightlyLate) { - // Client reconnected slightly late - not a spec violation but suspicious - status = 'WARNING'; - errorMessage = `Client reconnected slightly late (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). This is acceptable but may indicate network delays.`; - } - + // Client did reconnect - SUCCESS for graceful reconnection this.checks.push({ - id: 'client-sse-retry-timing', - name: 'ClientRespectsRetryField', + id: 'client-sse-graceful-reconnect', + name: 'ClientGracefulReconnect', description: - 'Client MUST respect the retry field, waiting the given number of milliseconds before attempting to reconnect', - status, + 'Client reconnects via GET after POST SSE stream is closed gracefully', + status: 'SUCCESS', timestamp: new Date().toISOString(), - errorMessage, specReferences: [ { id: 'SEP-1699', @@ -274,24 +257,96 @@ export class SSERetryScenario implements Scenario { } ], details: { - expectedRetryMs: this.retryValue, - actualDelayMs: Math.round(actualDelay), - minAcceptableMs: minExpected, - maxAcceptableMs: maxExpected, - veryLateThresholdMs: this.retryValue * this.VERY_LATE_MULTIPLIER, - earlyToleranceMs: this.EARLY_TOLERANCE, - lateToleranceMs: this.LATE_TOLERANCE, - withinTolerance, - tooEarly, - slightlyLate, - veryLate, - connectionCount: this.connectionTimestamps.length + getConnectionCount: this.getConnectionCount } }); + // Check 2: Client MUST respect retry field timing + if ( + this.postStreamCloseTime !== null && + this.getReconnectionTime !== null + ) { + const actualDelay = this.getReconnectionTime - this.postStreamCloseTime; + const minExpected = this.retryValue - this.EARLY_TOLERANCE; + const maxExpected = this.retryValue + this.LATE_TOLERANCE; + + const tooEarly = actualDelay < minExpected; + const slightlyLate = actualDelay > maxExpected; + const veryLate = + actualDelay > this.retryValue * this.VERY_LATE_MULTIPLIER; + const withinTolerance = !tooEarly && !slightlyLate; + + let status: 'SUCCESS' | 'FAILURE' | 'WARNING' = 'SUCCESS'; + let errorMessage: string | undefined; + + if (tooEarly) { + // Client reconnected too soon - MUST violation + status = 'FAILURE'; + errorMessage = `Client reconnected too early (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). Client MUST respect the retry field and wait the specified time.`; + } else if (veryLate) { + // Client reconnected way too late - likely ignoring retry field entirely + status = 'FAILURE'; + errorMessage = `Client reconnected very late (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). Client appears to be ignoring the retry field and using its own backoff strategy.`; + } else if (slightlyLate) { + // Client reconnected slightly late - not a spec violation but suspicious + status = 'WARNING'; + errorMessage = `Client reconnected slightly late (${actualDelay.toFixed(0)}ms instead of ${this.retryValue}ms). This is acceptable but may indicate network delays.`; + } + + this.checks.push({ + id: 'client-sse-retry-timing', + name: 'ClientRespectsRetryField', + description: + 'Client MUST respect the retry field, waiting the given number of milliseconds before attempting to reconnect', + status, + timestamp: new Date().toISOString(), + errorMessage, + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + expectedRetryMs: this.retryValue, + actualDelayMs: Math.round(actualDelay), + minAcceptableMs: minExpected, + maxAcceptableMs: maxExpected, + veryLateThresholdMs: this.retryValue * this.VERY_LATE_MULTIPLIER, + earlyToleranceMs: this.EARLY_TOLERANCE, + lateToleranceMs: this.LATE_TOLERANCE, + withinTolerance, + tooEarly, + slightlyLate, + veryLate, + getConnectionCount: this.getConnectionCount + } + }); + } else { + this.checks.push({ + id: 'client-sse-retry-timing', + name: 'ClientRespectsRetryField', + description: 'Client MUST respect the retry field timing', + status: 'WARNING', + timestamp: new Date().toISOString(), + errorMessage: + 'Could not measure timing - POST stream close time or GET reconnection time not recorded', + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + postStreamCloseTime: this.postStreamCloseTime, + getReconnectionTime: this.getReconnectionTime + } + }); + } + // Check 3: Client SHOULD send Last-Event-ID header on reconnection const hasLastEventId = - this.lastEventIds.length > 1 && this.lastEventIds[1] !== undefined; + this.lastEventIds.length > 0 && this.lastEventIds[0] !== undefined; this.checks.push({ id: 'client-sse-last-event-id', @@ -309,7 +364,8 @@ export class SSERetryScenario implements Scenario { details: { hasLastEventId, lastEventIds: this.lastEventIds, - connectionCount: this.connectionTimestamps.length + primingEventId: this.primingEventId, + getConnectionCount: this.getConnectionCount }, errorMessage: !hasLastEventId ? 'Client did not send Last-Event-ID header on reconnection. This is a SHOULD requirement for resumability.' diff --git a/src/scenarios/server/sse-polling.ts b/src/scenarios/server/sse-polling.ts index f22be89..0b1194d 100644 --- a/src/scenarios/server/sse-polling.ts +++ b/src/scenarios/server/sse-polling.ts @@ -2,37 +2,55 @@ * SSE Polling conformance test scenarios for MCP servers (SEP-1699) * * Tests that servers properly implement SSE polling behavior including: - * - Sending priming events with event ID and empty data - * - Sending retry field before closing connection + * - Sending priming events with event ID and empty data on POST SSE streams + * - Sending retry field in priming events when configured + * - Replaying events when client reconnects with Last-Event-ID */ import { ClientScenario, ConformanceCheck } from '../../types.js'; import { EventSourceParserStream } from 'eventsource-parser/stream'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; export class ServerSSEPollingScenario implements ClientScenario { name = 'server-sse-polling'; description = - 'Test server sends SSE priming event and retry field (SEP-1699)'; + 'Test server sends SSE priming events on POST streams and supports event replay (SEP-1699)'; async run(serverUrl: string): Promise { const checks: ConformanceCheck[] = []; + let sessionId: string | undefined; + let client: Client | undefined; + let transport: StreamableHTTPClientTransport | undefined; + try { - // Make a GET request to establish SSE stream - const response = await fetch(serverUrl, { - method: 'GET', - headers: { - Accept: 'text/event-stream' + // Step 1: Initialize session with the server + client = new Client( + { + name: 'conformance-test-client', + version: '1.0.0' + }, + { + capabilities: { + sampling: {}, + elicitation: {} + } } - }); + ); - // Check if server supports SSE GET endpoint - if (response.status === 405) { + transport = new StreamableHTTPClientTransport(new URL(serverUrl)); + await client.connect(transport); + + // Extract session ID from transport (accessing internal state) + sessionId = (transport as unknown as { sessionId?: string }).sessionId; + + if (!sessionId) { checks.push({ - id: 'server-sse-polling-endpoint', - name: 'ServerSSEPollingEndpoint', - description: 'Server supports SSE GET endpoint', - status: 'INFO', + id: 'server-sse-polling-session', + name: 'ServerSSEPollingSession', + description: 'Server provides session ID for SSE polling tests', + status: 'WARNING', timestamp: new Date().toISOString(), specReferences: [ { @@ -41,68 +59,82 @@ export class ServerSSEPollingScenario implements ClientScenario { } ], details: { - serverUrl, - statusCode: response.status, message: - 'Server does not support SSE GET endpoint (405 Method Not Allowed)' + 'Server did not provide session ID - SSE polling tests may not work correctly' } }); - return checks; } - // Server may require session context for standalone SSE GET - if (response.status === 400) { + // Step 2: Make a POST request that returns SSE stream + // We need to use raw fetch to observe the priming event + const postResponse = await fetch(serverUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream, application/json', + ...(sessionId && { 'mcp-session-id': sessionId }), + 'mcp-protocol-version': '2025-03-26' + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'tools/list', + params: {} + }) + }); + + if (!postResponse.ok) { checks.push({ - id: 'server-sse-polling-endpoint', - name: 'ServerSSEPollingEndpoint', - description: 'Server supports SSE GET endpoint', - status: 'INFO', + id: 'server-sse-post-request', + name: 'ServerSSEPostRequest', + description: 'Server accepts POST request with SSE stream response', + status: 'FAILURE', timestamp: new Date().toISOString(), + errorMessage: `Server returned HTTP ${postResponse.status}`, specReferences: [ { id: 'SEP-1699', url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' } - ], - details: { - serverUrl, - statusCode: response.status, - message: - 'Server requires session context for standalone SSE GET endpoint (400 Bad Request)' - } + ] }); return checks; } - if (!response.ok) { + // Check if server returned SSE stream + const contentType = postResponse.headers.get('content-type'); + if (!contentType?.includes('text/event-stream')) { checks.push({ - id: 'server-sse-polling-connection', - name: 'ServerSSEPollingConnection', - description: 'Server accepts SSE GET request', - status: 'FAILURE', + id: 'server-sse-content-type', + name: 'ServerSSEContentType', + description: 'Server returns text/event-stream for POST request', + status: 'INFO', timestamp: new Date().toISOString(), - errorMessage: `Server returned HTTP ${response.status}`, specReferences: [ { id: 'SEP-1699', url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' } - ] + ], + details: { + contentType, + message: + 'Server returned JSON instead of SSE stream - priming event tests not applicable' + } }); return checks; } - // Parse SSE stream + // Step 3: Parse SSE stream for priming event let hasEventId = false; let hasPrimingEvent = false; let primingEventIsFirst = false; let hasRetryField = false; let retryValue: number | undefined; - let firstEventId: string | undefined; - let disconnected = false; + let primingEventId: string | undefined; let eventCount = 0; - if (!response.body) { + if (!postResponse.body) { checks.push({ id: 'server-sse-polling-stream', name: 'ServerSSEPollingStream', @@ -120,7 +152,7 @@ export class ServerSSEPollingScenario implements ClientScenario { return checks; } - const reader = response.body + const reader = postResponse.body .pipeThrough(new TextDecoderStream()) .pipeThrough( new EventSourceParserStream({ @@ -142,7 +174,6 @@ export class ServerSSEPollingScenario implements ClientScenario { const { value: event, done } = await reader.read(); if (done) { - disconnected = true; break; } @@ -151,8 +182,8 @@ export class ServerSSEPollingScenario implements ClientScenario { // Check for event ID if (event.id) { hasEventId = true; - if (!firstEventId) { - firstEventId = event.id; + if (!primingEventId) { + primingEventId = event.id; } // Check if this is a priming event (empty or minimal data) @@ -162,7 +193,7 @@ export class ServerSSEPollingScenario implements ClientScenario { event.data.trim() === '' ) { hasPrimingEvent = true; - // Check if priming event is the first event (SEP-1699 says "immediately") + // Check if priming event is the first event if (eventCount === 1) { primingEventIsFirst = true; } @@ -173,25 +204,25 @@ export class ServerSSEPollingScenario implements ClientScenario { clearTimeout(timeout); } - // Check 1: Server SHOULD send priming event with ID immediately + // Check 1: Server SHOULD send priming event with ID on POST SSE stream let primingStatus: 'SUCCESS' | 'WARNING' = 'SUCCESS'; let primingErrorMessage: string | undefined; if (!hasPrimingEvent) { primingStatus = 'WARNING'; primingErrorMessage = - 'Server did not send priming event with id and empty data. This is a SHOULD requirement for SEP-1699.'; + 'Server did not send priming event with id and empty data on POST SSE stream. This is recommended for resumability.'; } else if (!primingEventIsFirst) { primingStatus = 'WARNING'; primingErrorMessage = - 'Priming event was not sent immediately (not the first event). SEP-1699 says server SHOULD immediately send the priming event.'; + 'Priming event was not sent first. It should be sent immediately when the SSE stream is established.'; } checks.push({ id: 'server-sse-priming-event', name: 'ServerSendsPrimingEvent', description: - 'Server SHOULD immediately send SSE event with id and empty data to prime client for reconnection', + 'Server SHOULD send priming event with id and empty data on POST SSE streams', status: primingStatus, timestamp: new Date().toISOString(), specReferences: [ @@ -204,17 +235,18 @@ export class ServerSSEPollingScenario implements ClientScenario { hasPrimingEvent, primingEventIsFirst, hasEventId, - firstEventId, + primingEventId, eventCount }, errorMessage: primingErrorMessage }); - // Check 2: Server SHOULD send retry field before disconnect + // Check 2: Server SHOULD send retry field in priming event checks.push({ id: 'server-sse-retry-field', name: 'ServerSendsRetryField', - description: 'Server SHOULD send retry field before closing connection', + description: + 'Server SHOULD send retry field to control client reconnection timing', status: hasRetryField ? 'SUCCESS' : 'WARNING', timestamp: new Date().toISOString(), specReferences: [ @@ -225,47 +257,137 @@ export class ServerSSEPollingScenario implements ClientScenario { ], details: { hasRetryField, - retryValue, - disconnected + retryValue }, errorMessage: !hasRetryField - ? 'Server did not send retry field. This is a SHOULD requirement for SEP-1699.' + ? 'Server did not send retry field. This is recommended for controlling client reconnection timing.' : undefined }); - // Check 3: Server MAY close connection after sending event ID - // Per SEP-1699, server can only close "if it has sent an SSE event with an event ID" - let disconnectStatus: 'SUCCESS' | 'WARNING' | 'INFO' = 'INFO'; - let disconnectMessage: string | undefined; + // Step 4: Test event replay by reconnecting with Last-Event-ID + if (primingEventId && sessionId) { + // Make a GET request with Last-Event-ID to test replay + const getResponse = await fetch(serverUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-03-26', + 'last-event-id': primingEventId + } + }); - if (disconnected && !hasEventId) { - disconnectStatus = 'WARNING'; - disconnectMessage = - 'Server closed connection without sending an event ID first. SEP-1699 allows disconnect only after sending event ID.'; - } + if (getResponse.ok) { + // Server accepted reconnection with Last-Event-ID + let replayedEvents = 0; - checks.push({ - id: 'server-sse-disconnect', - name: 'ServerDisconnectBehavior', - description: - 'Server MAY close connection after sending event ID (informational)', - status: disconnectStatus, - timestamp: new Date().toISOString(), - specReferences: [ - { - id: 'SEP-1699', - url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + if (getResponse.body) { + const replayReader = getResponse.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new EventSourceParserStream()) + .getReader(); + + const replayTimeout = setTimeout(() => { + replayReader.cancel(); + }, 2000); + + try { + while (true) { + const { done } = await replayReader.read(); + if (done) break; + replayedEvents++; + } + } finally { + clearTimeout(replayTimeout); + } } - ], - details: { - disconnected, - hasEventId, - eventCount, - hasRetryField, - retryValue - }, - errorMessage: disconnectMessage - }); + + checks.push({ + id: 'server-sse-event-replay', + name: 'ServerReplaysEvents', + description: + 'Server replays events after Last-Event-ID on reconnection', + status: 'SUCCESS', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + lastEventIdUsed: primingEventId, + replayedEvents, + message: 'Server accepted GET request with Last-Event-ID header' + } + }); + } else { + // Check if server doesn't support standalone GET streams + if (getResponse.status === 405) { + checks.push({ + id: 'server-sse-event-replay', + name: 'ServerReplaysEvents', + description: + 'Server supports GET reconnection with Last-Event-ID', + status: 'INFO', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + statusCode: getResponse.status, + message: + 'Server does not support standalone GET SSE endpoint (405 Method Not Allowed)' + } + }); + } else { + checks.push({ + id: 'server-sse-event-replay', + name: 'ServerReplaysEvents', + description: + 'Server replays events after Last-Event-ID on reconnection', + status: 'WARNING', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + statusCode: getResponse.status, + lastEventIdUsed: primingEventId, + message: `Server returned ${getResponse.status} for GET request with Last-Event-ID` + }, + errorMessage: `Server did not accept reconnection with Last-Event-ID (HTTP ${getResponse.status})` + }); + } + } + } else { + checks.push({ + id: 'server-sse-event-replay', + name: 'ServerReplaysEvents', + description: + 'Server replays events after Last-Event-ID on reconnection', + status: 'INFO', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + primingEventId, + sessionId, + message: + 'Could not test event replay - no priming event ID or session ID available' + } + }); + } } catch (error) { checks.push({ id: 'server-sse-polling-error', @@ -281,6 +403,15 @@ export class ServerSSEPollingScenario implements ClientScenario { } ] }); + } finally { + // Clean up + if (client) { + try { + await client.close(); + } catch { + // Ignore cleanup errors + } + } } return checks; From 2b1245d40dd8c112d6b8753d9f81c661d6546a34 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 18 Nov 2025 19:20:07 +0000 Subject: [PATCH 08/17] feat: add conformance test for multiple concurrent GET SSE streams New scenario tests that servers properly support multiple concurrent GET SSE streams per SEP-1699 spec requirement. --- src/scenarios/index.ts | 2 + src/scenarios/server/sse-multiple-streams.ts | 212 +++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 src/scenarios/server/sse-multiple-streams.ts diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index 03dcddf..1ab72ed 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -29,6 +29,7 @@ import { import { ElicitationDefaultsScenario } from './server/elicitation-defaults.js'; import { ElicitationEnumsScenario } from './server/elicitation-enums.js'; import { ServerSSEPollingScenario } from './server/sse-polling.js'; +import { ServerSSEMultipleStreamsScenario } from './server/sse-multiple-streams.js'; import { ResourcesListScenario, @@ -82,6 +83,7 @@ const allClientScenariosList: ClientScenario[] = [ // SSE Polling scenarios (SEP-1699) new ServerSSEPollingScenario(), + new ServerSSEMultipleStreamsScenario(), // Elicitation scenarios (SEP-1330) - pending ...pendingClientScenariosList, diff --git a/src/scenarios/server/sse-multiple-streams.ts b/src/scenarios/server/sse-multiple-streams.ts new file mode 100644 index 0000000..d783ac4 --- /dev/null +++ b/src/scenarios/server/sse-multiple-streams.ts @@ -0,0 +1,212 @@ +/** + * SSE Multiple Streams conformance test scenarios for MCP servers (SEP-1699) + * + * Tests that servers properly support multiple concurrent SSE streams: + * - Accepting multiple GET SSE streams for the same session + * - Isolating events between different streams + */ + +import { ClientScenario, ConformanceCheck } from '../../types.js'; +import { EventSourceParserStream } from 'eventsource-parser/stream'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; + +export class ServerSSEMultipleStreamsScenario implements ClientScenario { + name = 'server-sse-multiple-streams'; + description = + 'Test server supports multiple concurrent GET SSE streams (SEP-1699)'; + + async run(serverUrl: string): Promise { + const checks: ConformanceCheck[] = []; + + let sessionId: string | undefined; + let client: Client | undefined; + let transport: StreamableHTTPClientTransport | undefined; + + try { + // Step 1: Initialize session with the server + client = new Client( + { + name: 'conformance-test-client', + version: '1.0.0' + }, + { + capabilities: { + sampling: {}, + elicitation: {} + } + } + ); + + transport = new StreamableHTTPClientTransport(new URL(serverUrl)); + await client.connect(transport); + + // Extract session ID from transport + sessionId = (transport as unknown as { sessionId?: string }).sessionId; + + if (!sessionId) { + checks.push({ + id: 'server-sse-multiple-streams-session', + name: 'ServerSSEMultipleStreamsSession', + description: 'Server provides session ID for multiple streams test', + status: 'WARNING', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + message: + 'Server did not provide session ID - multiple streams test may not work correctly' + } + }); + return checks; + } + + // Step 2: Open multiple GET SSE streams concurrently + // Spec says: "The client MAY remain connected to multiple SSE streams simultaneously" + const streamResponses: Response[] = []; + const numStreams = 3; + + for (let i = 0; i < numStreams; i++) { + const response = await fetch(serverUrl, { + method: 'GET', + headers: { + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-03-26' + } + }); + streamResponses.push(response); + } + + // Check that all streams were accepted + const allAccepted = streamResponses.every((r) => r.ok); + const statuses = streamResponses.map((r) => r.status); + + checks.push({ + id: 'server-accepts-multiple-get-streams', + name: 'ServerAcceptsMultipleGetStreams', + description: + 'Server MUST allow multiple concurrent GET SSE streams (no 409)', + status: allAccepted ? 'SUCCESS' : 'FAILURE', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + numStreamsAttempted: numStreams, + numStreamsAccepted: statuses.filter((s) => s === 200).length, + statuses + }, + errorMessage: !allAccepted + ? `Server rejected some streams. Statuses: ${statuses.join(', ')}` + : undefined + }); + + // Step 3: Test event isolation between streams + // Make POST requests to generate events that should go to different streams + // Note: This is harder to test properly because the spec says events are + // NOT broadcast across multiple streams - only one stream receives each event + + // Get the first event from each stream to verify they're working + const eventResults = await Promise.all( + streamResponses.map(async (response, index) => { + if (!response.ok || !response.body) { + return { index, error: 'Stream not available' }; + } + + try { + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new EventSourceParserStream()) + .getReader(); + + // Wait for one event with timeout + const timeoutPromise = new Promise((resolve) => + setTimeout(() => resolve(null), 2000) + ); + + const eventPromise = reader.read().then(({ value }) => value); + + const event = await Promise.race([eventPromise, timeoutPromise]); + + // Cancel reader + await reader.cancel(); + + return { index, event }; + } catch (error) { + return { + index, + error: error instanceof Error ? error.message : String(error) + }; + } + }) + ); + + // All streams should be functional (either receive events or timeout waiting) + const streamsFunctional = eventResults.filter( + (r) => !('error' in r) + ).length; + + checks.push({ + id: 'server-sse-streams-functional', + name: 'ServerSSEStreamsFunctional', + description: 'Multiple SSE streams should be functional', + status: + streamsFunctional === numStreams + ? 'SUCCESS' + : streamsFunctional > 0 + ? 'WARNING' + : 'FAILURE', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + numStreams, + streamsFunctional, + results: eventResults + }, + errorMessage: + streamsFunctional < numStreams + ? `Only ${streamsFunctional}/${numStreams} streams were functional` + : undefined + }); + } catch (error) { + checks.push({ + id: 'server-sse-multiple-streams-error', + name: 'ServerSSEMultipleStreamsTest', + description: 'Test server multiple SSE streams behavior', + status: 'FAILURE', + timestamp: new Date().toISOString(), + errorMessage: `Error: ${error instanceof Error ? error.message : String(error)}`, + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ] + }); + } finally { + // Clean up + if (client) { + try { + await client.close(); + } catch { + // Ignore cleanup errors + } + } + } + + return checks; + } +} From e022392de8eb857fb38cc52a0cdc6d93cf380247 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 20 Nov 2025 18:12:55 +0000 Subject: [PATCH 09/17] fix: test POST-initiated concurrent streams instead of standalone GET The multiple-streams test was incorrectly testing standalone GET streams, which are limited to one per session by SDK design. The spec's "multiple concurrent streams" refers to POST-initiated SSE streams, where each POST request gets its own unique streamId. Changes: - Rewrite server-sse-multiple-streams to use concurrent POST requests - Each POST request may return SSE or JSON (both valid per spec) - Update test expectations to allow INFO status for JSON responses - Add documentation clarifying standalone GET vs POST stream behavior --- src/scenarios/server/all-scenarios.test.ts | 6 +- src/scenarios/server/sse-multiple-streams.ts | 158 +++++++++++++------ 2 files changed, 110 insertions(+), 54 deletions(-) diff --git a/src/scenarios/server/all-scenarios.test.ts b/src/scenarios/server/all-scenarios.test.ts index 6eab252..0a9935e 100644 --- a/src/scenarios/server/all-scenarios.test.ts +++ b/src/scenarios/server/all-scenarios.test.ts @@ -125,9 +125,9 @@ describe('Server Scenarios', () => { throw new Error(`Scenario failed with checks:\n ${failureMessages}`); } - // All checks should be SUCCESS - const successes = checks.filter((c) => c.status === 'SUCCESS'); - expect(successes.length).toBe(checks.length); + // All checks should be non-FAILURE (SUCCESS, WARNING, or INFO are acceptable) + const nonFailures = checks.filter((c) => c.status !== 'FAILURE'); + expect(nonFailures.length).toBe(checks.length); }, 10000); // 10 second timeout per scenario } }); diff --git a/src/scenarios/server/sse-multiple-streams.ts b/src/scenarios/server/sse-multiple-streams.ts index d783ac4..b676f59 100644 --- a/src/scenarios/server/sse-multiple-streams.ts +++ b/src/scenarios/server/sse-multiple-streams.ts @@ -2,8 +2,11 @@ * SSE Multiple Streams conformance test scenarios for MCP servers (SEP-1699) * * Tests that servers properly support multiple concurrent SSE streams: - * - Accepting multiple GET SSE streams for the same session - * - Isolating events between different streams + * - Accepting multiple POST requests that return SSE streams simultaneously + * - Each POST request gets its own stream with unique stream ID + * + * Note: The standalone GET stream (without Last-Event-ID) is limited to one per session. + * Multiple concurrent streams are achieved via POST requests, each getting their own stream. */ import { ClientScenario, ConformanceCheck } from '../../types.js'; @@ -14,7 +17,7 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/ export class ServerSSEMultipleStreamsScenario implements ClientScenario { name = 'server-sse-multiple-streams'; description = - 'Test server supports multiple concurrent GET SSE streams (SEP-1699)'; + 'Test server supports multiple concurrent POST SSE streams (SEP-1699)'; async run(serverUrl: string): Promise { const checks: ConformanceCheck[] = []; @@ -65,32 +68,54 @@ export class ServerSSEMultipleStreamsScenario implements ClientScenario { return checks; } - // Step 2: Open multiple GET SSE streams concurrently + // Step 2: Open multiple POST SSE streams concurrently + // Each POST request gets its own stream with unique streamId // Spec says: "The client MAY remain connected to multiple SSE streams simultaneously" const streamResponses: Response[] = []; const numStreams = 3; + // Launch all POST requests concurrently + const postPromises = []; for (let i = 0; i < numStreams; i++) { - const response = await fetch(serverUrl, { - method: 'GET', + const promise = fetch(serverUrl, { + method: 'POST', headers: { - Accept: 'text/event-stream', + 'Content-Type': 'application/json', + Accept: 'text/event-stream, application/json', 'mcp-session-id': sessionId, 'mcp-protocol-version': '2025-03-26' - } + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1000 + i, // Different request IDs for each stream + method: 'tools/list', + params: {} + }) }); - streamResponses.push(response); + postPromises.push(promise); } - // Check that all streams were accepted + // Wait for all responses + const responses = await Promise.all(postPromises); + streamResponses.push(...responses); + + // Check that all streams were accepted (HTTP 200) const allAccepted = streamResponses.every((r) => r.ok); const statuses = streamResponses.map((r) => r.status); + const contentTypes = streamResponses.map((r) => + r.headers.get('content-type') + ); + + // Count how many returned SSE streams vs JSON + const sseStreams = contentTypes.filter( + (ct) => ct?.includes('text/event-stream') + ).length; checks.push({ - id: 'server-accepts-multiple-get-streams', - name: 'ServerAcceptsMultipleGetStreams', + id: 'server-accepts-multiple-post-streams', + name: 'ServerAcceptsMultiplePostStreams', description: - 'Server MUST allow multiple concurrent GET SSE streams (no 409)', + 'Server allows multiple concurrent POST requests (each may return SSE or JSON)', status: allAccepted ? 'SUCCESS' : 'FAILURE', timestamp: new Date().toISOString(), specReferences: [ @@ -102,23 +127,28 @@ export class ServerSSEMultipleStreamsScenario implements ClientScenario { details: { numStreamsAttempted: numStreams, numStreamsAccepted: statuses.filter((s) => s === 200).length, - statuses + numSseStreams: sseStreams, + statuses, + contentTypes }, errorMessage: !allAccepted - ? `Server rejected some streams. Statuses: ${statuses.join(', ')}` + ? `Server rejected some requests. Statuses: ${statuses.join(', ')}` : undefined }); - // Step 3: Test event isolation between streams - // Make POST requests to generate events that should go to different streams - // Note: This is harder to test properly because the spec says events are - // NOT broadcast across multiple streams - only one stream receives each event - - // Get the first event from each stream to verify they're working + // Step 3: Verify SSE streams are functional by reading events + // Only test streams that returned SSE content-type const eventResults = await Promise.all( streamResponses.map(async (response, index) => { + const contentType = response.headers.get('content-type'); + + // Skip non-SSE responses (JSON responses are also valid) + if (!contentType?.includes('text/event-stream')) { + return { index, type: 'json', skipped: true }; + } + if (!response.ok || !response.body) { - return { index, error: 'Stream not available' }; + return { index, type: 'sse', error: 'Stream not available' }; } try { @@ -139,48 +169,74 @@ export class ServerSSEMultipleStreamsScenario implements ClientScenario { // Cancel reader await reader.cancel(); - return { index, event }; + return { index, type: 'sse', event }; } catch (error) { return { index, + type: 'sse', error: error instanceof Error ? error.message : String(error) }; } }) ); - // All streams should be functional (either receive events or timeout waiting) - const streamsFunctional = eventResults.filter( + // Count functional SSE streams (received event or timed out waiting - both are valid) + const sseResults = eventResults.filter((r) => r.type === 'sse'); + const functionalSseStreams = sseResults.filter( (r) => !('error' in r) ).length; - checks.push({ - id: 'server-sse-streams-functional', - name: 'ServerSSEStreamsFunctional', - description: 'Multiple SSE streams should be functional', - status: - streamsFunctional === numStreams - ? 'SUCCESS' - : streamsFunctional > 0 - ? 'WARNING' - : 'FAILURE', - timestamp: new Date().toISOString(), - specReferences: [ - { - id: 'SEP-1699', - url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + // If server returned SSE streams, they should be functional + if (sseStreams > 0) { + checks.push({ + id: 'server-sse-streams-functional', + name: 'ServerSSEStreamsFunctional', + description: 'Multiple POST SSE streams should be functional', + status: + functionalSseStreams === sseStreams + ? 'SUCCESS' + : functionalSseStreams > 0 + ? 'WARNING' + : 'FAILURE', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + numSseStreams: sseStreams, + functionalSseStreams, + results: eventResults + }, + errorMessage: + functionalSseStreams < sseStreams + ? `Only ${functionalSseStreams}/${sseStreams} SSE streams were functional` + : undefined + }); + } else { + // Server returned JSON for all requests - this is valid but worth noting + checks.push({ + id: 'server-sse-streams-functional', + name: 'ServerSSEStreamsFunctional', + description: 'Server returned JSON responses (SSE streams optional)', + status: 'INFO', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + numSseStreams: 0, + message: + 'Server returned JSON for all requests - SSE streaming is optional', + results: eventResults } - ], - details: { - numStreams, - streamsFunctional, - results: eventResults - }, - errorMessage: - streamsFunctional < numStreams - ? `Only ${streamsFunctional}/${numStreams} streams were functional` - : undefined - }); + }); + } } catch (error) { checks.push({ id: 'server-sse-multiple-streams-error', From 9cbf0fa810e1729d4dc0a0ff51ea74ee2878b19b Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 20 Nov 2025 20:31:23 +0000 Subject: [PATCH 10/17] style: apply prettier formatting --- src/scenarios/server/sse-multiple-streams.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scenarios/server/sse-multiple-streams.ts b/src/scenarios/server/sse-multiple-streams.ts index b676f59..7cda3f2 100644 --- a/src/scenarios/server/sse-multiple-streams.ts +++ b/src/scenarios/server/sse-multiple-streams.ts @@ -107,8 +107,8 @@ export class ServerSSEMultipleStreamsScenario implements ClientScenario { ); // Count how many returned SSE streams vs JSON - const sseStreams = contentTypes.filter( - (ct) => ct?.includes('text/event-stream') + const sseStreams = contentTypes.filter((ct) => + ct?.includes('text/event-stream') ).length; checks.push({ From 608793b8f6f26228d08078a0d2984906cbc21b72 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 24 Nov 2025 13:29:41 +0000 Subject: [PATCH 11/17] some logging things, and fix for warning check for bare GET --- src/scenarios/client/sse-retry.ts | 105 +++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 10 deletions(-) diff --git a/src/scenarios/client/sse-retry.ts b/src/scenarios/client/sse-retry.ts index a982678..25ca7b2 100644 --- a/src/scenarios/client/sse-retry.ts +++ b/src/scenarios/client/sse-retry.ts @@ -89,7 +89,26 @@ export class SSERetryScenario implements Scenario { this.getReconnectionTime = performance.now(); const lastEventId = req.headers['last-event-id'] as string | undefined; - this.lastEventIds.push(lastEventId); + const description = lastEventId + ? `Received GET request for ${req.url} (Last-Event-ID: ${lastEventId})` + : `Received GET request for ${req.url}`; + this.checks.push({ + id: 'incoming-request', + name: 'IncomingRequest', + description, + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + method: 'GET', + url: req.url, + headers: req.headers, + connectionCount: this.getConnectionCount + } + }); + + if (lastEventId) { + this.lastEventIds.push(lastEventId); + } // Handle GET SSE stream request (reconnection) this.handleGetSSEStream(req, res); @@ -119,9 +138,22 @@ export class SSERetryScenario implements Scenario { const eventId = `event-${this.eventIdCounter}`; // Send priming event with ID and retry field - res.write(`id: ${eventId}\n`); - res.write(`retry: ${this.retryValue}\n`); - res.write(`data: \n\n`); + const primingContent = `id: ${eventId}\nretry: ${this.retryValue}\ndata: \n\n`; + res.write(primingContent); + + this.checks.push({ + id: 'outgoing-sse-event', + name: 'OutgoingSseEvent', + description: `Sent SSE priming event on GET stream (id: ${eventId}, retry: ${this.retryValue}ms)`, + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + eventId, + retryMs: this.retryValue, + eventType: 'priming', + raw: primingContent + } + }); // Keep connection open for now (don't close immediately to avoid infinite reconnection loop) // The test will stop the server when done @@ -141,6 +173,20 @@ export class SSERetryScenario implements Scenario { try { const request = JSON.parse(body); + this.checks.push({ + id: 'incoming-request', + name: 'IncomingRequest', + description: `Received POST request for ${req.url} (method: ${request.method})`, + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + method: 'POST', + url: req.url, + jsonrpcMethod: request.method, + jsonrpcId: request.id + } + }); + if (request.method === 'initialize') { // Respond to initialize request with SSE stream containing priming event res.writeHead(200, { @@ -155,9 +201,22 @@ export class SSERetryScenario implements Scenario { this.primingEventId = `event-${this.eventIdCounter}`; // Send priming event with retry field - res.write(`id: ${this.primingEventId}\n`); - res.write(`retry: ${this.retryValue}\n`); - res.write(`data: \n\n`); + const postPrimingContent = `id: ${this.primingEventId}\nretry: ${this.retryValue}\ndata: \n\n`; + res.write(postPrimingContent); + + this.checks.push({ + id: 'outgoing-sse-event', + name: 'OutgoingSseEvent', + description: `Sent SSE priming event (id: ${this.primingEventId}, retry: ${this.retryValue}ms)`, + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + eventId: this.primingEventId, + retryMs: this.retryValue, + eventType: 'priming', + raw: postPrimingContent + } + }); // Send initialize response const response = { @@ -173,14 +232,40 @@ export class SSERetryScenario implements Scenario { } }; - res.write(`event: message\n`); - res.write(`id: event-${++this.eventIdCounter}\n`); - res.write(`data: ${JSON.stringify(response)}\n\n`); + const messageEventId = `event-${++this.eventIdCounter}`; + const messageContent = `event: message\nid: ${messageEventId}\ndata: ${JSON.stringify(response)}\n\n`; + res.write(messageContent); + + this.checks.push({ + id: 'outgoing-sse-event', + name: 'OutgoingSseEvent', + description: `Sent SSE message event (id: ${messageEventId}, method: initialize response)`, + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + eventId: messageEventId, + eventType: 'message', + jsonrpcId: request.id, + body: response, + raw: messageContent + } + }); // Close connection after sending response to trigger reconnection // Record the time when we close the stream setTimeout(() => { this.postStreamCloseTime = performance.now(); + this.checks.push({ + id: 'outgoing-stream-close', + name: 'OutgoingStreamClose', + description: + 'Closed POST SSE stream to trigger client reconnection', + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + retryMs: this.retryValue + } + }); res.end(); }, 100); } else if (request.id === undefined) { From f58370afeeb9278d5f4f43baf2ebb3e23f6c0138 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 24 Nov 2025 14:25:06 +0000 Subject: [PATCH 12/17] switch to tool call to make more representative --- examples/clients/typescript/sse-retry-test.ts | 33 +- src/scenarios/client/sse-retry.ts | 285 ++++++++++++------ 2 files changed, 206 insertions(+), 112 deletions(-) diff --git a/examples/clients/typescript/sse-retry-test.ts b/examples/clients/typescript/sse-retry-test.ts index 8595668..8c16ca2 100644 --- a/examples/clients/typescript/sse-retry-test.ts +++ b/examples/clients/typescript/sse-retry-test.ts @@ -4,12 +4,13 @@ * SSE Retry Test Client * * Tests that the MCP client respects the SSE retry field when reconnecting. - * This client connects to a test server that sends retry: field and closes - * the connection, then validates that the client waits the appropriate time. + * This client connects to a test server that closes the SSE stream mid-tool-call, + * then waits for the client to reconnect and sends the tool result. */ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; async function main(): Promise { const serverUrl = process.argv[2]; @@ -42,7 +43,6 @@ async function main(): Promise { } }); - // Track reconnection events transport.onerror = (error) => { console.log(`Transport error: ${error.message}`); }; @@ -55,24 +55,23 @@ async function main(): Promise { await client.connect(transport); console.log('Connected to MCP server'); - // Keep connection alive to observe reconnection behavior - // The server will close the POST SSE stream and the client should reconnect via GET - console.log('Waiting for reconnection cycle...'); + console.log('Calling test_reconnection tool...'); console.log( - 'Server will send priming event with retry field, then close POST SSE stream' - ); - console.log( - 'Client should wait for retry period (2000ms) then reconnect via GET with Last-Event-ID' + 'Server will close SSE stream mid-call and send result after reconnection' ); - // Wait long enough for: - // 1. Server to send priming event with retry field on POST SSE stream (100ms) - // 2. Server closes POST stream to trigger reconnection - // 3. Client waits for retry period (2000ms expected) - // 4. Client reconnects via GET with Last-Event-ID header - await new Promise((resolve) => setTimeout(resolve, 6000)); + const result = await client.request( + { + method: 'tools/call', + params: { + name: 'test_reconnection', + arguments: {} + } + }, + CallToolResultSchema + ); - console.log('Test duration complete'); + console.log('Tool call completed:', JSON.stringify(result, null, 2)); await transport.close(); console.log('Connection closed successfully'); diff --git a/src/scenarios/client/sse-retry.ts b/src/scenarios/client/sse-retry.ts index 25ca7b2..6e44a06 100644 --- a/src/scenarios/client/sse-retry.ts +++ b/src/scenarios/client/sse-retry.ts @@ -20,14 +20,17 @@ export class SSERetryScenario implements Scenario { private port: number = 0; // Timing tracking - private postStreamCloseTime: number | null = null; + private toolStreamCloseTime: number | null = null; private getReconnectionTime: number | null = null; private getConnectionCount: number = 0; private lastEventIds: (string | undefined)[] = []; - private retryValue: number = 2000; // 2 seconds + private retryValue: number = 500; // 500ms private eventIdCounter: number = 0; private sessionId: string = `session-${Date.now()}`; - private primingEventId: string | null = null; + + // Pending tool call to respond to after reconnection + private pendingToolCallId: number | string | null = null; + private getResponseStream: http.ServerResponse | null = null; // Tolerances for timing validation private readonly EARLY_TOLERANCE = 50; // Allow 50ms early for scheduler variance @@ -155,8 +158,45 @@ export class SSERetryScenario implements Scenario { } }); - // Keep connection open for now (don't close immediately to avoid infinite reconnection loop) - // The test will stop the server when done + // Store the GET stream to send pending tool response + this.getResponseStream = res; + + // If we have a pending tool call, send the response now + if (this.pendingToolCallId !== null) { + const toolResponse = { + jsonrpc: '2.0', + id: this.pendingToolCallId, + result: { + content: [ + { + type: 'text', + text: 'Reconnection test completed successfully' + } + ] + } + }; + + const responseEventId = `event-${++this.eventIdCounter}`; + const responseContent = `event: message\nid: ${responseEventId}\ndata: ${JSON.stringify(toolResponse)}\n\n`; + res.write(responseContent); + + this.checks.push({ + id: 'outgoing-sse-event', + name: 'OutgoingSseEvent', + description: `Sent tool response on GET stream after reconnection (id: ${responseEventId})`, + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + eventId: responseEventId, + eventType: 'message', + jsonrpcId: this.pendingToolCallId, + body: toolResponse, + raw: responseContent + } + }); + + this.pendingToolCallId = null; + } } private handlePostRequest( @@ -188,86 +228,11 @@ export class SSERetryScenario implements Scenario { }); if (request.method === 'initialize') { - // Respond to initialize request with SSE stream containing priming event - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - Connection: 'keep-alive', - 'mcp-session-id': this.sessionId - }); - - // Generate priming event ID - this.eventIdCounter++; - this.primingEventId = `event-${this.eventIdCounter}`; - - // Send priming event with retry field - const postPrimingContent = `id: ${this.primingEventId}\nretry: ${this.retryValue}\ndata: \n\n`; - res.write(postPrimingContent); - - this.checks.push({ - id: 'outgoing-sse-event', - name: 'OutgoingSseEvent', - description: `Sent SSE priming event (id: ${this.primingEventId}, retry: ${this.retryValue}ms)`, - status: 'INFO', - timestamp: new Date().toISOString(), - details: { - eventId: this.primingEventId, - retryMs: this.retryValue, - eventType: 'priming', - raw: postPrimingContent - } - }); - - // Send initialize response - const response = { - jsonrpc: '2.0', - id: request.id, - result: { - protocolVersion: '2025-03-26', - serverInfo: { - name: 'sse-retry-test-server', - version: '1.0.0' - }, - capabilities: {} - } - }; - - const messageEventId = `event-${++this.eventIdCounter}`; - const messageContent = `event: message\nid: ${messageEventId}\ndata: ${JSON.stringify(response)}\n\n`; - res.write(messageContent); - - this.checks.push({ - id: 'outgoing-sse-event', - name: 'OutgoingSseEvent', - description: `Sent SSE message event (id: ${messageEventId}, method: initialize response)`, - status: 'INFO', - timestamp: new Date().toISOString(), - details: { - eventId: messageEventId, - eventType: 'message', - jsonrpcId: request.id, - body: response, - raw: messageContent - } - }); - - // Close connection after sending response to trigger reconnection - // Record the time when we close the stream - setTimeout(() => { - this.postStreamCloseTime = performance.now(); - this.checks.push({ - id: 'outgoing-stream-close', - name: 'OutgoingStreamClose', - description: - 'Closed POST SSE stream to trigger client reconnection', - status: 'INFO', - timestamp: new Date().toISOString(), - details: { - retryMs: this.retryValue - } - }); - res.end(); - }, 100); + this.handleInitialize(req, res, request); + } else if (request.method === 'tools/list') { + this.handleToolsList(res, request); + } else if (request.method === 'tools/call') { + this.handleToolsCall(res, request); } else if (request.id === undefined) { // Notifications (no id) - return 202 Accepted res.writeHead(202); @@ -301,17 +266,148 @@ export class SSERetryScenario implements Scenario { }); } + private handleInitialize( + _req: http.IncomingMessage, + res: http.ServerResponse, + request: any + ): void { + res.writeHead(200, { + 'Content-Type': 'application/json', + 'mcp-session-id': this.sessionId + }); + + const response = { + jsonrpc: '2.0', + id: request.id, + result: { + protocolVersion: '2025-03-26', + serverInfo: { + name: 'sse-retry-test-server', + version: '1.0.0' + }, + capabilities: { + tools: {} + } + } + }; + + res.end(JSON.stringify(response)); + + this.checks.push({ + id: 'outgoing-response', + name: 'OutgoingResponse', + description: `Sent initialize response`, + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + jsonrpcId: request.id, + body: response + } + }); + } + + private handleToolsList(res: http.ServerResponse, request: any): void { + res.writeHead(200, { + 'Content-Type': 'application/json', + 'mcp-session-id': this.sessionId + }); + + const response = { + jsonrpc: '2.0', + id: request.id, + result: { + tools: [ + { + name: 'test_reconnection', + description: + 'A tool that triggers SSE stream closure to test client reconnection behavior', + inputSchema: { + type: 'object', + properties: {}, + required: [] + } + } + ] + } + }; + + res.end(JSON.stringify(response)); + + this.checks.push({ + id: 'outgoing-response', + name: 'OutgoingResponse', + description: `Sent tools/list response`, + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + jsonrpcId: request.id, + body: response + } + }); + } + + private handleToolsCall(res: http.ServerResponse, request: any): void { + // Store the request ID so we can respond after reconnection + this.pendingToolCallId = request.id; + + // Start SSE stream + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'mcp-session-id': this.sessionId + }); + + // Send priming event with retry field + this.eventIdCounter++; + const primingEventId = `event-${this.eventIdCounter}`; + const primingContent = `id: ${primingEventId}\nretry: ${this.retryValue}\ndata: \n\n`; + res.write(primingContent); + + this.checks.push({ + id: 'outgoing-sse-event', + name: 'OutgoingSseEvent', + description: `Sent SSE priming event for tools/call (id: ${primingEventId}, retry: ${this.retryValue}ms)`, + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + eventId: primingEventId, + retryMs: this.retryValue, + eventType: 'priming', + raw: primingContent + } + }); + + // Close the stream after a short delay to trigger reconnection + setTimeout(() => { + this.toolStreamCloseTime = performance.now(); + this.checks.push({ + id: 'outgoing-stream-close', + name: 'OutgoingStreamClose', + description: + 'Closed tools/call SSE stream to trigger client reconnection', + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + retryMs: this.retryValue, + pendingToolCallId: this.pendingToolCallId + } + }); + res.end(); + }, 50); + } + private generateChecks(): void { - // Check 1: Client should have reconnected via GET after POST stream close + // Check 1: Client should have reconnected via GET after tool call stream close if (this.getConnectionCount < 1) { this.checks.push({ id: 'client-sse-graceful-reconnect', name: 'ClientGracefulReconnect', description: - 'Client reconnects via GET after POST SSE stream is closed gracefully', + 'Client reconnects via GET after SSE stream is closed gracefully', status: 'FAILURE', timestamp: new Date().toISOString(), - errorMessage: `Client did not attempt GET reconnection after POST stream closure. Client should treat graceful stream close as reconnectable.`, + errorMessage: `Client did not attempt GET reconnection after stream closure. Client should treat graceful stream close as reconnectable.`, specReferences: [ { id: 'SEP-1699', @@ -320,7 +416,7 @@ export class SSERetryScenario implements Scenario { ], details: { getConnectionCount: this.getConnectionCount, - postStreamCloseTime: this.postStreamCloseTime, + toolStreamCloseTime: this.toolStreamCloseTime, retryValue: this.retryValue } }); @@ -332,7 +428,7 @@ export class SSERetryScenario implements Scenario { id: 'client-sse-graceful-reconnect', name: 'ClientGracefulReconnect', description: - 'Client reconnects via GET after POST SSE stream is closed gracefully', + 'Client reconnects via GET after SSE stream is closed gracefully', status: 'SUCCESS', timestamp: new Date().toISOString(), specReferences: [ @@ -348,10 +444,10 @@ export class SSERetryScenario implements Scenario { // Check 2: Client MUST respect retry field timing if ( - this.postStreamCloseTime !== null && + this.toolStreamCloseTime !== null && this.getReconnectionTime !== null ) { - const actualDelay = this.getReconnectionTime - this.postStreamCloseTime; + const actualDelay = this.getReconnectionTime - this.toolStreamCloseTime; const minExpected = this.retryValue - this.EARLY_TOLERANCE; const maxExpected = this.retryValue + this.LATE_TOLERANCE; @@ -415,7 +511,7 @@ export class SSERetryScenario implements Scenario { status: 'WARNING', timestamp: new Date().toISOString(), errorMessage: - 'Could not measure timing - POST stream close time or GET reconnection time not recorded', + 'Could not measure timing - tool stream close time or GET reconnection time not recorded', specReferences: [ { id: 'SEP-1699', @@ -423,7 +519,7 @@ export class SSERetryScenario implements Scenario { } ], details: { - postStreamCloseTime: this.postStreamCloseTime, + toolStreamCloseTime: this.toolStreamCloseTime, getReconnectionTime: this.getReconnectionTime } }); @@ -449,7 +545,6 @@ export class SSERetryScenario implements Scenario { details: { hasLastEventId, lastEventIds: this.lastEventIds, - primingEventId: this.primingEventId, getConnectionCount: this.getConnectionCount }, errorMessage: !hasLastEventId From c385462e324d5cc7c1bd6a3f6123e2060bb41715 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 24 Nov 2025 16:19:16 +0000 Subject: [PATCH 13/17] server pretty printing, add server tool, adjust server test scenario --- .../servers/typescript/everything-server.ts | 40 +++ src/index.ts | 6 +- src/runner/server.ts | 11 +- src/scenarios/server/sse-polling.ts | 300 ++++++++++++++---- 4 files changed, 293 insertions(+), 64 deletions(-) diff --git a/examples/servers/typescript/everything-server.ts b/examples/servers/typescript/everything-server.ts index a9c7f77..9dd382a 100644 --- a/examples/servers/typescript/everything-server.ts +++ b/examples/servers/typescript/everything-server.ts @@ -349,6 +349,46 @@ function createMcpServer() { } ); + // SEP-1699: Reconnection test tool - closes SSE stream mid-call to test client reconnection + mcpServer.registerTool( + 'test_reconnection', + { + description: + 'Tests SSE stream disconnection and client reconnection (SEP-1699). Server will close the stream mid-call and send the result after client reconnects.', + inputSchema: {} + }, + async (_args, { sessionId, requestId }) => { + const sleep = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + + console.log(`[${sessionId}] Starting test_reconnection tool...`); + + // Get the transport for this session + const transport = sessionId ? transports[sessionId] : undefined; + if (transport && requestId) { + // Close the SSE stream to trigger client reconnection + console.log( + `[${sessionId}] Closing SSE stream to trigger client polling...` + ); + transport.closeSSEStream(requestId); + } + + // Wait for client to reconnect (should respect retry field) + await sleep(100); + + console.log(`[${sessionId}] test_reconnection tool complete`); + + return { + content: [ + { + type: 'text', + text: 'Reconnection test completed successfully. If you received this, the client properly reconnected after stream closure.' + } + ] + }; + } + ); + // Sampling tool - requests LLM completion from client mcpServer.registerTool( 'test_sampling', diff --git a/src/index.ts b/src/index.ts index a65e79f..6128a16 100644 --- a/src/index.ts +++ b/src/index.ts @@ -204,11 +204,14 @@ program 'Suite to run: "active" (default, excludes pending), "all", or "pending"', 'active' ) + .option('--verbose', 'Show verbose output (JSON instead of pretty print)') .action(async (options) => { try { // Validate options with Zod const validated = ServerOptionsSchema.parse(options); + const verbose = options.verbose ?? false; + // If a single scenario is specified, run just that one if (validated.scenario) { const result = await runServerConformanceTest( @@ -218,7 +221,8 @@ program const { failed } = printServerResults( result.checks, - result.scenarioDescription + result.scenarioDescription, + verbose ); process.exit(failed > 0 ? 1 : 0); } else { diff --git a/src/runner/server.ts b/src/runner/server.ts index 46be558..18c8254 100644 --- a/src/runner/server.ts +++ b/src/runner/server.ts @@ -2,7 +2,7 @@ import { promises as fs } from 'fs'; import path from 'path'; import { ConformanceCheck } from '../types'; import { getClientScenario } from '../scenarios'; -import { ensureResultsDir, createResultDir } from './utils'; +import { ensureResultsDir, createResultDir, formatPrettyChecks } from './utils'; /** * Format markdown-style text for terminal output using ANSI codes @@ -54,7 +54,8 @@ export async function runServerConformanceTest( export function printServerResults( checks: ConformanceCheck[], - scenarioDescription: string + scenarioDescription: string, + verbose: boolean = false ): { passed: number; failed: number; @@ -68,7 +69,11 @@ export function printServerResults( const failed = checks.filter((c) => c.status === 'FAILURE').length; const warnings = checks.filter((c) => c.status === 'WARNING').length; - console.log(`Checks:\n${JSON.stringify(checks, null, 2)}`); + if (verbose) { + console.log(JSON.stringify(checks, null, 2)); + } else { + console.log(`Checks:\n${formatPrettyChecks(checks)}`); + } console.log(`\nTest Results:`); console.log( diff --git a/src/scenarios/server/sse-polling.ts b/src/scenarios/server/sse-polling.ts index 0b1194d..9735bb2 100644 --- a/src/scenarios/server/sse-polling.ts +++ b/src/scenarios/server/sse-polling.ts @@ -4,6 +4,7 @@ * Tests that servers properly implement SSE polling behavior including: * - Sending priming events with event ID and empty data on POST SSE streams * - Sending retry field in priming events when configured + * - Closing SSE stream mid-operation and resuming after client reconnects * - Replaying events when client reconnects with Last-Event-ID */ @@ -12,10 +13,62 @@ import { EventSourceParserStream } from 'eventsource-parser/stream'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +function createLoggingFetch(checks: ConformanceCheck[]) { + return async (url: string, options: RequestInit): Promise => { + const method = options.method || 'GET'; + let description = `Sending ${method} request`; + if (options.body) { + try { + const body = JSON.parse(options.body as string); + if (body.method) { + description = `Sending ${method} ${body.method}`; + } + } catch { + // Not JSON + } + } + + checks.push({ + id: 'outgoing-request', + name: 'OutgoingRequest', + description, + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + method, + url, + headers: options.headers, + body: options.body ? JSON.parse(options.body as string) : undefined + } + }); + + const response = await fetch(url, options); + + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + + checks.push({ + id: 'incoming-response', + name: 'IncomingResponse', + description: `Received ${response.status} response for ${method}`, + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + statusCode: response.status, + headers: responseHeaders + } + }); + + return response; + }; +} + export class ServerSSEPollingScenario implements ClientScenario { name = 'server-sse-polling'; description = - 'Test server sends SSE priming events on POST streams and supports event replay (SEP-1699)'; + 'Test server SSE polling via test_reconnection tool that closes stream mid-call (SEP-1699)'; async run(serverUrl: string): Promise { const checks: ConformanceCheck[] = []; @@ -65,9 +118,11 @@ export class ServerSSEPollingScenario implements ClientScenario { }); } - // Step 2: Make a POST request that returns SSE stream - // We need to use raw fetch to observe the priming event - const postResponse = await fetch(serverUrl, { + // Step 2: Call test_reconnection tool via raw fetch to observe SSE behavior + // This tool should close the stream mid-call, requiring reconnection + const loggingFetch = createLoggingFetch(checks); + + const postResponse = await loggingFetch(serverUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -78,12 +133,35 @@ export class ServerSSEPollingScenario implements ClientScenario { body: JSON.stringify({ jsonrpc: '2.0', id: 1, - method: 'tools/list', - params: {} + method: 'tools/call', + params: { + name: 'test_reconnection', + arguments: {} + } }) }); if (!postResponse.ok) { + // Check if tool doesn't exist (method not found or similar) + if (postResponse.status === 400 || postResponse.status === 404) { + checks.push({ + id: 'server-sse-test-reconnection-tool', + name: 'ServerTestReconnectionTool', + description: + 'Server implements test_reconnection tool for SSE polling tests', + status: 'WARNING', + timestamp: new Date().toISOString(), + errorMessage: `Server does not implement test_reconnection tool (HTTP ${postResponse.status}). This tool is recommended for testing SSE polling behavior.`, + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ] + }); + return checks; + } + checks.push({ id: 'server-sse-post-request', name: 'ServerSSEPostRequest', @@ -125,14 +203,15 @@ export class ServerSSEPollingScenario implements ClientScenario { return checks; } - // Step 3: Parse SSE stream for priming event + // Step 3: Parse SSE stream for priming event and tool response let hasEventId = false; let hasPrimingEvent = false; let primingEventIsFirst = false; let hasRetryField = false; let retryValue: number | undefined; - let primingEventId: string | undefined; + let lastEventId: string | undefined; let eventCount = 0; + let receivedToolResponse = false; if (!postResponse.body) { checks.push({ @@ -164,10 +243,10 @@ export class ServerSSEPollingScenario implements ClientScenario { ) .getReader(); - // Read events with timeout + // Read events with timeout - expect stream to close before we get the response const timeout = setTimeout(() => { reader.cancel(); - }, 5000); + }, 10000); try { while (true) { @@ -179,31 +258,85 @@ export class ServerSSEPollingScenario implements ClientScenario { eventCount++; - // Check for event ID + // Track the last event ID for reconnection if (event.id) { hasEventId = true; - if (!primingEventId) { - primingEventId = event.id; - } + lastEventId = event.id; // Check if this is a priming event (empty or minimal data) - if ( + const isPriming = event.data === '' || event.data === '{}' || - event.data.trim() === '' - ) { + event.data.trim() === ''; + if (isPriming) { hasPrimingEvent = true; // Check if priming event is the first event if (eventCount === 1) { primingEventIsFirst = true; } } + + // Log the SSE event + checks.push({ + id: 'incoming-sse-event', + name: 'IncomingSseEvent', + description: isPriming + ? `Received SSE priming event (id: ${event.id})` + : `Received SSE event (id: ${event.id})`, + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + eventId: event.id, + eventType: event.event || 'message', + isPriming, + hasRetryField, + retryValue, + data: event.data + } + }); + } + + // Check if this is the tool response + if (event.data) { + try { + const parsed = JSON.parse(event.data); + if (parsed.id === 1 && parsed.result) { + receivedToolResponse = true; + checks.push({ + id: 'incoming-sse-event', + name: 'IncomingSseEvent', + description: `Received tool response on POST stream`, + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + eventId: event.id, + body: parsed + } + }); + } + } catch { + // Not JSON, ignore + } } } } finally { clearTimeout(timeout); } + // Log stream closure + checks.push({ + id: 'stream-closed', + name: 'StreamClosed', + description: `POST SSE stream closed after ${eventCount} event(s)`, + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + eventCount, + lastEventId, + receivedToolResponse + } + }); + // Check 1: Server SHOULD send priming event with ID on POST SSE stream let primingStatus: 'SUCCESS' | 'WARNING' = 'SUCCESS'; let primingErrorMessage: string | undefined; @@ -235,7 +368,7 @@ export class ServerSSEPollingScenario implements ClientScenario { hasPrimingEvent, primingEventIsFirst, hasEventId, - primingEventId, + lastEventId, eventCount }, errorMessage: primingErrorMessage @@ -264,50 +397,71 @@ export class ServerSSEPollingScenario implements ClientScenario { : undefined }); - // Step 4: Test event replay by reconnecting with Last-Event-ID - if (primingEventId && sessionId) { - // Make a GET request with Last-Event-ID to test replay - const getResponse = await fetch(serverUrl, { + // Step 4: If tool response wasn't received, reconnect with Last-Event-ID + if (!receivedToolResponse && lastEventId && sessionId) { + // Make a GET request with Last-Event-ID to get the tool response + const getResponse = await loggingFetch(serverUrl, { method: 'GET', headers: { Accept: 'text/event-stream', 'mcp-session-id': sessionId, 'mcp-protocol-version': '2025-03-26', - 'last-event-id': primingEventId + 'last-event-id': lastEventId } }); - if (getResponse.ok) { - // Server accepted reconnection with Last-Event-ID - let replayedEvents = 0; + if (getResponse.ok && getResponse.body) { + const reconnectReader = getResponse.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new EventSourceParserStream()) + .getReader(); - if (getResponse.body) { - const replayReader = getResponse.body - .pipeThrough(new TextDecoderStream()) - .pipeThrough(new EventSourceParserStream()) - .getReader(); + const reconnectTimeout = setTimeout(() => { + reconnectReader.cancel(); + }, 5000); - const replayTimeout = setTimeout(() => { - replayReader.cancel(); - }, 2000); + try { + while (true) { + const { value: event, done } = await reconnectReader.read(); + if (done) break; - try { - while (true) { - const { done } = await replayReader.read(); - if (done) break; - replayedEvents++; + // Log each event received on GET stream + checks.push({ + id: 'incoming-sse-event', + name: 'IncomingSseEvent', + description: `Received SSE event on GET reconnection stream (id: ${event.id || 'none'})`, + status: 'INFO', + timestamp: new Date().toISOString(), + details: { + eventId: event.id, + eventType: event.event || 'message', + data: event.data + } + }); + + // Check if this is the tool response + if (event.data) { + try { + const parsed = JSON.parse(event.data); + if (parsed.id === 1 && parsed.result) { + receivedToolResponse = true; + break; + } + } catch { + // Not JSON, ignore + } } - } finally { - clearTimeout(replayTimeout); } + } finally { + clearTimeout(reconnectTimeout); } checks.push({ - id: 'server-sse-event-replay', - name: 'ServerReplaysEvents', + id: 'server-sse-disconnect-resume', + name: 'ServerDisconnectResume', description: - 'Server replays events after Last-Event-ID on reconnection', - status: 'SUCCESS', + 'Server closes SSE stream mid-call and resumes after client reconnects with Last-Event-ID', + status: receivedToolResponse ? 'SUCCESS' : 'WARNING', timestamp: new Date().toISOString(), specReferences: [ { @@ -316,17 +470,22 @@ export class ServerSSEPollingScenario implements ClientScenario { } ], details: { - lastEventIdUsed: primingEventId, - replayedEvents, - message: 'Server accepted GET request with Last-Event-ID header' - } + lastEventIdUsed: lastEventId, + receivedToolResponse, + message: receivedToolResponse + ? 'Successfully received tool response after reconnection' + : 'Tool response not received after reconnection' + }, + errorMessage: !receivedToolResponse + ? 'Server did not send tool response after client reconnected with Last-Event-ID' + : undefined }); } else { // Check if server doesn't support standalone GET streams if (getResponse.status === 405) { checks.push({ - id: 'server-sse-event-replay', - name: 'ServerReplaysEvents', + id: 'server-sse-disconnect-resume', + name: 'ServerDisconnectResume', description: 'Server supports GET reconnection with Last-Event-ID', status: 'INFO', @@ -345,10 +504,10 @@ export class ServerSSEPollingScenario implements ClientScenario { }); } else { checks.push({ - id: 'server-sse-event-replay', - name: 'ServerReplaysEvents', + id: 'server-sse-disconnect-resume', + name: 'ServerDisconnectResume', description: - 'Server replays events after Last-Event-ID on reconnection', + 'Server supports GET reconnection with Last-Event-ID', status: 'WARNING', timestamp: new Date().toISOString(), specReferences: [ @@ -359,19 +518,40 @@ export class ServerSSEPollingScenario implements ClientScenario { ], details: { statusCode: getResponse.status, - lastEventIdUsed: primingEventId, + lastEventIdUsed: lastEventId, message: `Server returned ${getResponse.status} for GET request with Last-Event-ID` }, errorMessage: `Server did not accept reconnection with Last-Event-ID (HTTP ${getResponse.status})` }); } } + } else if (receivedToolResponse) { + // Tool response was received on the initial POST stream (server didn't disconnect) + checks.push({ + id: 'server-sse-disconnect-resume', + name: 'ServerDisconnectResume', + description: + 'Server closes SSE stream mid-call and resumes after reconnection', + status: 'INFO', + timestamp: new Date().toISOString(), + specReferences: [ + { + id: 'SEP-1699', + url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699' + } + ], + details: { + receivedToolResponse: true, + message: + 'Tool response received on initial POST stream - server did not disconnect mid-call. The test_reconnection tool should close the stream before sending the result.' + } + }); } else { checks.push({ - id: 'server-sse-event-replay', - name: 'ServerReplaysEvents', + id: 'server-sse-disconnect-resume', + name: 'ServerDisconnectResume', description: - 'Server replays events after Last-Event-ID on reconnection', + 'Server closes SSE stream mid-call and resumes after reconnection', status: 'INFO', timestamp: new Date().toISOString(), specReferences: [ @@ -381,10 +561,10 @@ export class ServerSSEPollingScenario implements ClientScenario { } ], details: { - primingEventId, + lastEventId, sessionId, message: - 'Could not test event replay - no priming event ID or session ID available' + 'Could not test disconnect/resume - no last event ID or session ID available' } }); } From 8c14b8087a0095bcda41b0d12a2191d0e999bacd Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 24 Nov 2025 17:44:59 +0000 Subject: [PATCH 14/17] fix nested package-lock --- examples/servers/typescript/package-lock.json | 631 +++++++++--------- 1 file changed, 306 insertions(+), 325 deletions(-) diff --git a/examples/servers/typescript/package-lock.json b/examples/servers/typescript/package-lock.json index b968361..8a51109 100644 --- a/examples/servers/typescript/package-lock.json +++ b/examples/servers/typescript/package-lock.json @@ -19,9 +19,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", - "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -36,9 +36,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/android-arm/-/android-arm-0.25.11.tgz", - "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -53,9 +53,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", - "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -70,9 +70,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/android-x64/-/android-x64-0.25.11.tgz", - "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -87,9 +87,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", - "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -104,9 +104,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", - "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -121,9 +121,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", - "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -138,9 +138,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", - "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -155,9 +155,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", - "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -172,9 +172,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", - "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -189,9 +189,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", - "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -206,9 +206,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", - "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -223,9 +223,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", - "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -240,9 +240,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", - "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -257,9 +257,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", - "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -274,9 +274,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", - "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -291,9 +291,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", - "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -308,9 +308,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", - "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -325,9 +325,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", - "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -342,9 +342,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", - "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -359,9 +359,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", - "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -376,9 +376,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", - "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -393,9 +393,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", - "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -410,9 +410,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", - "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -427,9 +427,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", - "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -444,9 +444,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", - "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -461,12 +461,13 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.20.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@modelcontextprotocol/sdk/-/sdk-1.20.1.tgz", - "integrity": "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.22.0.tgz", + "integrity": "sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==", "license": "MIT", "dependencies": { - "ajv": "^6.12.6", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", @@ -481,11 +482,19 @@ }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + } } }, "node_modules/@types/cors": { "version": "2.8.19", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@types/cors/-/cors-2.8.19.tgz", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", "license": "MIT", "dependencies": { @@ -493,17 +502,17 @@ } }, "node_modules/@types/node": { - "version": "24.8.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@types/node/-/node-24.8.0.tgz", - "integrity": "sha512-5x08bUtU8hfboMTrJ7mEO4CpepS9yBwAqcL52y86SWNmbPX8LVbNs3EP4cNrIZgdjk2NAlP2ahNihozpoZIxSg==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "license": "MIT", "dependencies": { - "undici-types": "~7.14.0" + "undici-types": "~7.16.0" } }, "node_modules/accepts": { "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/accepts/-/accepts-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { @@ -515,42 +524,41 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/async-generator-function": { - "version": "1.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/async-generator-function/-/async-generator-function-1.0.0.tgz", - "integrity": "sha512-+NAXNqgCrB95ya4Sr66i1CL2hqLVckAk7xwRYWdcm39/ELQ6YNn1aw5r0bdQtqNZgQpEWzc5yc/igXc7aL5SLA==", + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, "node_modules/body-parser": { "version": "2.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/body-parser/-/body-parser-2.2.0.tgz", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "license": "MIT", "dependencies": { @@ -570,7 +578,7 @@ }, "node_modules/bytes": { "version": "3.1.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/bytes/-/bytes-3.1.2.tgz", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", "engines": { @@ -579,7 +587,7 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { @@ -592,7 +600,7 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/call-bound/-/call-bound-1.0.4.tgz", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { @@ -607,20 +615,21 @@ } }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { "version": "1.0.5", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/content-type/-/content-type-1.0.5.tgz", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", "engines": { @@ -629,7 +638,7 @@ }, "node_modules/cookie": { "version": "0.7.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cookie/-/cookie-0.7.2.tgz", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { @@ -638,7 +647,7 @@ }, "node_modules/cookie-signature": { "version": "1.2.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cookie-signature/-/cookie-signature-1.2.2.tgz", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", "engines": { @@ -647,7 +656,7 @@ }, "node_modules/cors": { "version": "2.8.5", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cors/-/cors-2.8.5.tgz", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "license": "MIT", "dependencies": { @@ -660,7 +669,7 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/cross-spawn/-/cross-spawn-7.0.6.tgz", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { @@ -674,7 +683,7 @@ }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/debug/-/debug-4.4.3.tgz", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { @@ -691,7 +700,7 @@ }, "node_modules/depd": { "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/depd/-/depd-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", "engines": { @@ -700,7 +709,7 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/dunder-proto/-/dunder-proto-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { @@ -714,13 +723,13 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ee-first/-/ee-first-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/encodeurl/-/encodeurl-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", "engines": { @@ -729,7 +738,7 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-define-property/-/es-define-property-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { @@ -738,7 +747,7 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-errors/-/es-errors-1.3.0.tgz", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { @@ -747,7 +756,7 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { @@ -758,9 +767,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.11", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/esbuild/-/esbuild-0.25.11.tgz", - "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -771,43 +780,43 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.11", - "@esbuild/android-arm": "0.25.11", - "@esbuild/android-arm64": "0.25.11", - "@esbuild/android-x64": "0.25.11", - "@esbuild/darwin-arm64": "0.25.11", - "@esbuild/darwin-x64": "0.25.11", - "@esbuild/freebsd-arm64": "0.25.11", - "@esbuild/freebsd-x64": "0.25.11", - "@esbuild/linux-arm": "0.25.11", - "@esbuild/linux-arm64": "0.25.11", - "@esbuild/linux-ia32": "0.25.11", - "@esbuild/linux-loong64": "0.25.11", - "@esbuild/linux-mips64el": "0.25.11", - "@esbuild/linux-ppc64": "0.25.11", - "@esbuild/linux-riscv64": "0.25.11", - "@esbuild/linux-s390x": "0.25.11", - "@esbuild/linux-x64": "0.25.11", - "@esbuild/netbsd-arm64": "0.25.11", - "@esbuild/netbsd-x64": "0.25.11", - "@esbuild/openbsd-arm64": "0.25.11", - "@esbuild/openbsd-x64": "0.25.11", - "@esbuild/openharmony-arm64": "0.25.11", - "@esbuild/sunos-x64": "0.25.11", - "@esbuild/win32-arm64": "0.25.11", - "@esbuild/win32-ia32": "0.25.11", - "@esbuild/win32-x64": "0.25.11" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escape-html": { "version": "1.0.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/escape-html/-/escape-html-1.0.3.tgz", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, "node_modules/etag": { "version": "1.8.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/etag/-/etag-1.8.1.tgz", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", "engines": { @@ -816,7 +825,7 @@ }, "node_modules/eventsource": { "version": "3.0.7", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/eventsource/-/eventsource-3.0.7.tgz", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "license": "MIT", "dependencies": { @@ -828,7 +837,7 @@ }, "node_modules/eventsource-parser": { "version": "3.0.6", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", "license": "MIT", "engines": { @@ -837,7 +846,7 @@ }, "node_modules/express": { "version": "5.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/express/-/express-5.1.0.tgz", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", "dependencies": { @@ -879,7 +888,7 @@ }, "node_modules/express-rate-limit": { "version": "7.5.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", "license": "MIT", "engines": { @@ -894,19 +903,29 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, "node_modules/finalhandler": { "version": "2.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/finalhandler/-/finalhandler-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "license": "MIT", "dependencies": { @@ -923,7 +942,7 @@ }, "node_modules/forwarded": { "version": "0.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/forwarded/-/forwarded-0.2.0.tgz", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", "engines": { @@ -932,7 +951,7 @@ }, "node_modules/fresh": { "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fresh/-/fresh-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "engines": { @@ -941,9 +960,10 @@ }, "node_modules/fsevents": { "version": "2.3.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/fsevents/-/fsevents-2.3.3.tgz", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -955,36 +975,24 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/function-bind/-/function-bind-1.1.2.tgz", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-intrinsic": { - "version": "1.3.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/get-intrinsic/-/get-intrinsic-1.3.1.tgz", - "integrity": "sha512-fk1ZVEeOX9hVZ6QzoBNEC55+Ucqg4sTVwrVuigZhuRPESVFpMyXnd3sbXvPOwp7Y9riVyANiqhEuRF0G1aVSeQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "async-function": "^1.0.0", - "async-generator-function": "^1.0.0", "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "generator-function": "^2.0.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", @@ -1000,7 +1008,7 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/get-proto/-/get-proto-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { @@ -1012,9 +1020,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.12.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/get-tsconfig/-/get-tsconfig-4.12.0.tgz", - "integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1026,7 +1034,7 @@ }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/gopd/-/gopd-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { @@ -1038,8 +1046,9 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/has-symbols/-/has-symbols-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1049,7 +1058,7 @@ }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/hasown/-/hasown-2.0.2.tgz", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { @@ -1060,33 +1069,28 @@ } }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/iconv-lite": { "version": "0.6.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/iconv-lite/-/iconv-lite-0.6.3.tgz", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { @@ -1098,13 +1102,13 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/inherits/-/inherits-2.0.4.tgz", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ipaddr.js": { "version": "1.9.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", "engines": { @@ -1113,25 +1117,25 @@ }, "node_modules/is-promise": { "version": "4.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/is-promise/-/is-promise-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/isexe/-/isexe-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { @@ -1140,7 +1144,7 @@ }, "node_modules/media-typer": { "version": "1.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/media-typer/-/media-typer-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { @@ -1149,7 +1153,7 @@ }, "node_modules/merge-descriptors": { "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", "engines": { @@ -1161,7 +1165,7 @@ }, "node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/mime-db/-/mime-db-1.54.0.tgz", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { @@ -1169,26 +1173,30 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ms/-/ms-2.1.3.tgz", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/negotiator": { "version": "1.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/negotiator/-/negotiator-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { @@ -1197,7 +1205,7 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/object-assign/-/object-assign-4.1.1.tgz", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { @@ -1206,7 +1214,7 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/object-inspect/-/object-inspect-1.13.4.tgz", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { @@ -1218,7 +1226,7 @@ }, "node_modules/on-finished": { "version": "2.4.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/on-finished/-/on-finished-2.4.1.tgz", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { @@ -1230,7 +1238,7 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/once/-/once-1.4.0.tgz", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", "dependencies": { @@ -1239,7 +1247,7 @@ }, "node_modules/parseurl": { "version": "1.3.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/parseurl/-/parseurl-1.3.3.tgz", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", "engines": { @@ -1248,7 +1256,7 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/path-key/-/path-key-3.1.1.tgz", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { @@ -1257,7 +1265,7 @@ }, "node_modules/path-to-regexp": { "version": "8.3.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "license": "MIT", "funding": { @@ -1266,9 +1274,9 @@ } }, "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "license": "MIT", "engines": { "node": ">=16.20.0" @@ -1276,7 +1284,7 @@ }, "node_modules/proxy-addr": { "version": "2.0.7", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/proxy-addr/-/proxy-addr-2.0.7.tgz", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", "dependencies": { @@ -1287,18 +1295,9 @@ "node": ">= 0.10" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { "version": "6.14.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/qs/-/qs-6.14.0.tgz", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { @@ -1313,7 +1312,7 @@ }, "node_modules/range-parser": { "version": "1.2.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/range-parser/-/range-parser-1.2.1.tgz", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", "engines": { @@ -1321,15 +1320,15 @@ } }, "node_modules/raw-body": { - "version": "3.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/raw-body/-/raw-body-3.0.1.tgz", - "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.7.0", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.10" @@ -1337,7 +1336,7 @@ }, "node_modules/raw-body/node_modules/iconv-lite": { "version": "0.7.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/iconv-lite/-/iconv-lite-0.7.0.tgz", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { @@ -1351,9 +1350,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, "license": "MIT", @@ -1363,7 +1371,7 @@ }, "node_modules/router": { "version": "2.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/router/-/router-2.2.0.tgz", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", "dependencies": { @@ -1377,35 +1385,15 @@ "node": ">= 18" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/safer-buffer/-/safer-buffer-2.1.2.tgz", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, "node_modules/send": { "version": "1.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/send/-/send-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", "dependencies": { @@ -1427,7 +1415,7 @@ }, "node_modules/serve-static": { "version": "2.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/serve-static/-/serve-static-2.2.0.tgz", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", "dependencies": { @@ -1442,13 +1430,13 @@ }, "node_modules/setprototypeof": { "version": "1.2.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/setprototypeof/-/setprototypeof-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/shebang-command/-/shebang-command-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", "dependencies": { @@ -1460,7 +1448,7 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/shebang-regex/-/shebang-regex-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { @@ -1469,7 +1457,7 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel/-/side-channel-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { @@ -1488,7 +1476,7 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-list/-/side-channel-list-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { @@ -1504,7 +1492,7 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-map/-/side-channel-map-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", "dependencies": { @@ -1522,7 +1510,7 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { @@ -1541,7 +1529,7 @@ }, "node_modules/statuses": { "version": "2.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/statuses/-/statuses-2.0.2.tgz", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { @@ -1550,7 +1538,7 @@ }, "node_modules/toidentifier": { "version": "1.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/toidentifier/-/toidentifier-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", "engines": { @@ -1559,7 +1547,7 @@ }, "node_modules/tsx": { "version": "4.20.6", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/tsx/-/tsx-4.20.6.tgz", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", @@ -1579,7 +1567,7 @@ }, "node_modules/type-is": { "version": "2.0.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/type-is/-/type-is-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { @@ -1593,7 +1581,7 @@ }, "node_modules/typescript": { "version": "5.9.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/typescript/-/typescript-5.9.3.tgz", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", @@ -1606,31 +1594,23 @@ } }, "node_modules/undici-types": { - "version": "7.14.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/undici-types/-/undici-types-7.14.0.tgz", - "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, "node_modules/unpipe": { "version": "1.0.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/unpipe/-/unpipe-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/vary": { "version": "1.1.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/vary/-/vary-1.1.2.tgz", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", "engines": { @@ -1639,7 +1619,7 @@ }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/which/-/which-2.0.2.tgz", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { @@ -1654,12 +1634,13 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/wrappy/1.0.2/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" }, "node_modules/zod": { "version": "3.25.76", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/zod/-/zod-3.25.76.tgz", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { @@ -1667,12 +1648,12 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", "license": "ISC", "peerDependencies": { - "zod": "^3.24.1" + "zod": "^3.25 || ^4" } } } From e6898e7d6774f758c0ed47a5fb52efdac9183b01 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 24 Nov 2025 18:18:50 +0000 Subject: [PATCH 15/17] return error if tool call fails, and update package locks --- examples/servers/typescript/package-lock.json | 3 ++- examples/servers/typescript/package.json | 3 ++- src/scenarios/server/sse-polling.ts | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/servers/typescript/package-lock.json b/examples/servers/typescript/package-lock.json index 8a51109..4db8198 100644 --- a/examples/servers/typescript/package-lock.json +++ b/examples/servers/typescript/package-lock.json @@ -11,7 +11,8 @@ "@modelcontextprotocol/sdk": "^1.20.1", "@types/cors": "^2.8.19", "cors": "^2.8.5", - "express": "^5.1.0" + "express": "^5.1.0", + "zod-to-json-schema": "^3.25.0" }, "devDependencies": { "tsx": "^4.7.0", diff --git a/examples/servers/typescript/package.json b/examples/servers/typescript/package.json index 83e462e..5e12580 100644 --- a/examples/servers/typescript/package.json +++ b/examples/servers/typescript/package.json @@ -18,7 +18,8 @@ "@modelcontextprotocol/sdk": "^1.20.1", "@types/cors": "^2.8.19", "cors": "^2.8.5", - "express": "^5.1.0" + "express": "^5.1.0", + "zod-to-json-schema": "^3.25.0" }, "devDependencies": { "tsx": "^4.7.0", diff --git a/src/scenarios/server/sse-polling.ts b/src/scenarios/server/sse-polling.ts index 9735bb2..5a3a240 100644 --- a/src/scenarios/server/sse-polling.ts +++ b/src/scenarios/server/sse-polling.ts @@ -302,16 +302,18 @@ export class ServerSSEPollingScenario implements ClientScenario { const parsed = JSON.parse(event.data); if (parsed.id === 1 && parsed.result) { receivedToolResponse = true; + const isError = parsed.result?.isError === true; checks.push({ id: 'incoming-sse-event', name: 'IncomingSseEvent', description: `Received tool response on POST stream`, - status: 'INFO', + status: isError ? 'FAILURE' : 'INFO', timestamp: new Date().toISOString(), details: { eventId: event.id, body: parsed - } + }, + ...(isError && { errorMessage: `Tool call failed` }) }); } } catch { From 4982c12166fe14ca197354498b03a3d6833de349 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 24 Nov 2025 18:27:50 +0000 Subject: [PATCH 16/17] mark polling as pending --- src/scenarios/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/scenarios/index.ts b/src/scenarios/index.ts index 89ed047..de76cd6 100644 --- a/src/scenarios/index.ts +++ b/src/scenarios/index.ts @@ -66,7 +66,11 @@ const pendingClientScenariosList: ClientScenario[] = [ // On hold until elicitation schema types are fixed // https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1863 new ToolsCallElicitationScenario(), - new ElicitationDefaultsScenario() + new ElicitationDefaultsScenario(), + + // On hold until server-side SSE improvements are made + // https://github.com/modelcontextprotocol/typescript-sdk/pull/1129 + new ServerSSEPollingScenario() ]; // All client scenarios From f9f863d568c1c808a5408c9ad61df80a120d6cf5 Mon Sep 17 00:00:00 2001 From: Paul Carleton Date: Mon, 24 Nov 2025 18:32:26 +0000 Subject: [PATCH 17/17] fix: exclude nested node_modules from vitest tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit đŸ¤– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- vitest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vitest.config.ts b/vitest.config.ts index 436dd00..93242b5 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ globals: true, environment: 'node', include: ['**/*.test.ts'], - exclude: ['node_modules', 'dist'], + exclude: ['**/node_modules/**', 'dist'], // Run test files sequentially to avoid port conflicts fileParallelism: false, // Increase timeout for server tests in CI