Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8a4601d
Add conformance tests for SEP-1699 SSE polling
felixweinberger Nov 14, 2025
be3d929
fix: correct SSE retry timing test to track GET requests only
felixweinberger Nov 17, 2025
de5ebab
fix: improve SSE conformance test validation (SEP-1699)
felixweinberger Nov 17, 2025
12de2ce
fix: capture SSE retry field via onRetry callback
felixweinberger Nov 17, 2025
32fb397
style: fix Prettier formatting
felixweinberger Nov 17, 2025
bc2298d
fix: handle 400 response in SSE polling scenario
felixweinberger Nov 17, 2025
bc7c649
feat: update SEP-1699 conformance tests for POST stream priming events
felixweinberger Nov 18, 2025
2b1245d
feat: add conformance test for multiple concurrent GET SSE streams
felixweinberger Nov 18, 2025
e022392
fix: test POST-initiated concurrent streams instead of standalone GET
felixweinberger Nov 20, 2025
9cbf0fa
style: apply prettier formatting
felixweinberger Nov 20, 2025
6c2646b
Merge main and resolve conflict in everything-server.ts
felixweinberger Nov 20, 2025
03a4c0f
Merge main (CIMD conformance test) and resolve conflict
felixweinberger Nov 20, 2025
acb22cf
Merge branch 'main' into fweinberger/sep-1699-test
felixweinberger Nov 20, 2025
2af3a13
Merge branch 'main' into fweinberger/sep-1699-test
felixweinberger Nov 21, 2025
d305d3a
Merge branch 'main' into fweinberger/sep-1699-test
felixweinberger Nov 23, 2025
608793b
some logging things, and fix for warning check for bare GET
pcarleton Nov 24, 2025
f58370a
switch to tool call to make more representative
pcarleton Nov 24, 2025
c385462
server pretty printing, add server tool, adjust server test scenario
pcarleton Nov 24, 2025
8c14b80
fix nested package-lock
pcarleton Nov 24, 2025
e6898e7
return error if tool call fails, and update package locks
pcarleton Nov 24, 2025
4982c12
mark polling as pending
pcarleton Nov 24, 2025
f9f863d
fix: exclude nested node_modules from vitest tests
pcarleton Nov 24, 2025
085b9e0
Merge branch 'main' into fweinberger/sep-1699-test
felixweinberger Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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.
  • Loading branch information
felixweinberger committed Nov 18, 2025
commit 2b1245d40dd8c112d6b8753d9f81c661d6546a34
2 changes: 2 additions & 0 deletions src/scenarios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -82,6 +83,7 @@ const allClientScenariosList: ClientScenario[] = [

// SSE Polling scenarios (SEP-1699)
new ServerSSEPollingScenario(),
new ServerSSEMultipleStreamsScenario(),

// Elicitation scenarios (SEP-1330) - pending
...pendingClientScenariosList,
Expand Down
212 changes: 212 additions & 0 deletions src/scenarios/server/sse-multiple-streams.ts
Original file line number Diff line number Diff line change
@@ -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<ConformanceCheck[]> {
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<null>((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;
}
}