Skip to content

Commit 56b0427

Browse files
authored
Merge pull request #304 from modelcontextprotocol/get-sse
StreamableHTTP - listening for messages from the server trough GET established SSE
2 parents c3adee2 + 4433044 commit 56b0427

9 files changed

+517
-144
lines changed

Diff for: src/client/streamableHttp.test.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,7 @@ describe("StreamableHTTPClientTransport", () => {
164164
// We expect the 405 error to be caught and handled gracefully
165165
// This should not throw an error that breaks the transport
166166
await transport.start();
167-
await expect(transport.openSseStream()).rejects.toThrow("Failed to open SSE stream: Method Not Allowed");
168-
167+
await expect(transport["_startOrAuthStandaloneSSE"]()).resolves.not.toThrow("Failed to open SSE stream: Method Not Allowed");
169168
// Check that GET was attempted
170169
expect(global.fetch).toHaveBeenCalledWith(
171170
expect.anything(),
@@ -209,7 +208,7 @@ describe("StreamableHTTPClientTransport", () => {
209208
transport.onmessage = messageSpy;
210209

211210
await transport.start();
212-
await transport.openSseStream();
211+
await transport["_startOrAuthStandaloneSSE"]();
213212

214213
// Give time for the SSE event to be processed
215214
await new Promise(resolve => setTimeout(resolve, 50));
@@ -295,7 +294,7 @@ describe("StreamableHTTPClientTransport", () => {
295294
});
296295

297296
await transport.start();
298-
await transport.openSseStream();
297+
await transport["_startOrAuthStandaloneSSE"]();
299298
await new Promise(resolve => setTimeout(resolve, 50));
300299

301300
// Now simulate attempting to reconnect
@@ -306,7 +305,7 @@ describe("StreamableHTTPClientTransport", () => {
306305
body: null
307306
});
308307

309-
await transport.openSseStream();
308+
await transport["_startOrAuthStandaloneSSE"]();
310309

311310
// Check that Last-Event-ID was included
312311
const calls = (global.fetch as jest.Mock).mock.calls;
@@ -366,7 +365,7 @@ describe("StreamableHTTPClientTransport", () => {
366365

367366
await transport.start();
368367

369-
await transport.openSseStream();
368+
await transport["_startOrAuthStandaloneSSE"]();
370369
expect((actualReqInit.headers as Headers).get("x-custom-header")).toBe("CustomValue");
371370

372371
requestInit.headers["X-Custom-Header"] = "SecondCustomValue";

Diff for: src/client/streamableHttp.ts

+13-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Transport } from "../shared/transport.js";
2-
import { JSONRPCMessage, JSONRPCMessageSchema } from "../types.js";
2+
import { isJSONRPCNotification, JSONRPCMessage, JSONRPCMessageSchema } from "../types.js";
33
import { auth, AuthResult, OAuthClientProvider, UnauthorizedError } from "./auth.js";
44
import { EventSourceParserStream } from "eventsource-parser/stream";
55

@@ -126,12 +126,17 @@ export class StreamableHTTPClientTransport implements Transport {
126126
return await this._authThenStart();
127127
}
128128

129+
// 405 indicates that the server does not offer an SSE stream at GET endpoint
130+
// This is an expected case that should not trigger an error
131+
if (response.status === 405) {
132+
return;
133+
}
134+
129135
throw new StreamableHTTPError(
130136
response.status,
131137
`Failed to open SSE stream: ${response.statusText}`,
132138
);
133139
}
134-
135140
// Successful connection, handle the SSE stream as a standalone listener
136141
this._handleSseStream(response.body);
137142
} catch (error) {
@@ -244,6 +249,12 @@ export class StreamableHTTPClientTransport implements Transport {
244249

245250
// If the response is 202 Accepted, there's no body to process
246251
if (response.status === 202) {
252+
// if the accepted notification is initialized, we start the SSE stream
253+
// if it's supported by the server
254+
if (isJSONRPCNotification(message) && message.method === "notifications/initialized") {
255+
// We don't need to handle 405 here anymore as it's handled in _startOrAuthStandaloneSSE
256+
this._startOrAuthStandaloneSSE().catch(err => this.onerror?.(err));
257+
}
247258
return;
248259
}
249260

@@ -280,20 +291,4 @@ export class StreamableHTTPClientTransport implements Transport {
280291
throw error;
281292
}
282293
}
283-
284-
/**
285-
* Opens SSE stream to receive messages from the server.
286-
*
287-
* This allows the server to push messages to the client without requiring the client
288-
* to first send a request via HTTP POST. Some servers may not support this feature.
289-
* If authentication is required but fails, this method will throw an UnauthorizedError.
290-
*/
291-
async openSseStream(): Promise<void> {
292-
if (!this._abortController) {
293-
throw new Error(
294-
"StreamableHTTPClientTransport not started! Call connect() before openSseStream().",
295-
);
296-
}
297-
await this._startOrAuthStandaloneSSE();
298-
}
299294
}

Diff for: src/examples/README.md

+77-70
Original file line numberDiff line numberDiff line change
@@ -2,81 +2,82 @@
22

33
This directory contains example implementations of MCP clients and servers using the TypeScript SDK.
44

5+
## Table of Contents
6+
7+
- [Streamable HTTP Servers - Single Node Deployment](#streamable-http---single-node-deployment-with-basic-session-state-management)
8+
- [Simple Server with Streamable HTTP](#simple-server-with-streamable-http-transport-serversimplestreamablehttpts)
9+
- [Server Supporting SSE via GET](#server-supporting-with-sse-via-get-serverstandalonessewithgetstreamablehttpts)
10+
- [Server with JSON Response Mode](#server-with-json-response-mode-serverjsonresponsestreamablehttpts)
11+
- [Client Example - Streamable HTTP](#client-clientsimplestreamablehttpts)
12+
- [Useful bash commands for testing](#useful-commands-for-testing)
13+
514
## Streamable HTTP - single node deployment with basic session state management
615

7-
Multi node with stete management example will be added soon after we add support.
16+
Multi node with state management example will be added soon after we add support.
817

9-
### Server with JSON response mode (`server/jsonResponseStreamableHttp.ts`)
1018

11-
A simple MCP server that uses the Streamable HTTP transport with JSON response mode enabled, implemented with Express. The server provides a simple `greet` tool that returns a greeting for a name.
19+
### Simple server with Streamable HTTP transport (`server/simpleStreamableHttp.ts`)
20+
21+
A simple MCP server that uses the Streamable HTTP transport, implemented with Express. The server provides:
22+
23+
- A simple `greet` tool that returns a greeting for a name
24+
- A `greeting-template` prompt that generates a greeting template
25+
- A static `greeting-resource` resource
1226

1327
#### Running the server
1428

1529
```bash
16-
npx tsx src/examples/server/jsonResponseStreamableHttp.ts
30+
npx tsx src/examples/server/simpleStreamableHttp.ts
1731
```
1832

19-
The server will start on port 3000. You can test the initialization and tool calling:
33+
The server will start on port 3000. You can test the initialization and tool listing:
2034

21-
```bash
22-
# Initialize the server and get the session ID from headers
23-
SESSION_ID=$(curl -X POST \
24-
-H "Content-Type: application/json" \
25-
-H "Accept: application/json" \
26-
-H "Accept: text/event-stream" \
27-
-d '{
28-
"jsonrpc": "2.0",
29-
"method": "initialize",
30-
"params": {
31-
"capabilities": {},
32-
"protocolVersion": "2025-03-26",
33-
"clientInfo": {
34-
"name": "test",
35-
"version": "1.0.0"
36-
}
37-
},
38-
"id": "1"
39-
}' \
40-
-i http://localhost:3000/mcp 2>&1 | grep -i "mcp-session-id" | cut -d' ' -f2 | tr -d '\r')
41-
echo "Session ID: $SESSION_ID"
35+
### Server supporting SSE via GET (`server/standaloneSseWithGetStreamableHttp.ts`)
4236

43-
# Call the greet tool using the saved session ID
44-
curl -X POST \
45-
-H "Content-Type: application/json" \
46-
-H "Accept: application/json" \
47-
-H "Accept: text/event-stream" \
48-
-H "mcp-session-id: $SESSION_ID" \
49-
-d '{
50-
"jsonrpc": "2.0",
51-
"method": "tools/call",
52-
"params": {
53-
"name": "greet",
54-
"arguments": {
55-
"name": "World"
56-
}
57-
},
58-
"id": "2"
59-
}' \
60-
http://localhost:3000/mcp
37+
An MCP server that demonstrates how to support SSE notifications via GET requests using the Streamable HTTP transport with Express. The server dynamically adds resources at regular intervals and supports notifications for resource list changes (server notifications are available through the standalone SSE connection established by GET request).
38+
39+
#### Running the server
40+
41+
```bash
42+
npx tsx src/examples/server/standaloneSseWithGetStreamableHttp.ts
6143
```
6244

63-
Note that in this example, we're using plain JSON response mode by setting `Accept: application/json` header.
45+
The server will start on port 3000 and automatically create new resources every 5 seconds.
6446

65-
### Server (`server/simpleStreamableHttp.ts`)
47+
### Server with JSON response mode (`server/jsonResponseStreamableHttp.ts`)
6648

67-
A simple MCP server that uses the Streamable HTTP transport, implemented with Express. The server provides:
49+
A simple MCP server that uses the Streamable HTTP transport with JSON response mode enabled, implemented with Express. The server provides a simple `greet` tool that returns a greeting for a name.
6850

69-
- A simple `greet` tool that returns a greeting for a name
70-
- A `greeting-template` prompt that generates a greeting template
71-
- A static `greeting-resource` resource
51+
_NOTE: This demonstrates a server that does not use SSE at all. Note that this limits its support for MCP features; for example, it cannot provide logging and progress notifications for tool execution._
7252

7353
#### Running the server
7454

7555
```bash
76-
npx tsx src/examples/server/simpleStreamableHttp.ts
56+
npx tsx src/examples/server/jsonResponseStreamableHttp.ts
7757
```
7858

79-
The server will start on port 3000. You can test the initialization and tool listing:
59+
60+
### Client (`client/simpleStreamableHttp.ts`)
61+
62+
A client that connects to the server, initializes it, and demonstrates how to:
63+
64+
- List available tools and call the `greet` tool
65+
- List available prompts and get the `greeting-template` prompt
66+
- List available resources
67+
68+
#### Running the client
69+
70+
```bash
71+
npx tsx src/examples/client/simpleStreamableHttp.ts
72+
```
73+
74+
Make sure the server is running before starting the client.
75+
76+
77+
### Useful commands for testing
78+
79+
#### Initialize
80+
Streamable HTTP transport requires to do the initialization first.
8081

8182
```bash
8283
# First initialize the server and save the session ID to a variable
@@ -100,31 +101,37 @@ SESSION_ID=$(curl -X POST \
100101
-i http://localhost:3000/mcp 2>&1 | grep -i "mcp-session-id" | cut -d' ' -f2 | tr -d '\r')
101102
echo "Session ID: $SESSION_ID
102103
104+
```
105+
Once a session is established, we can send POST requests:
106+
107+
#### List tools
108+
```bash
103109
# Then list tools using the saved session ID
104110
curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
105111
-H "mcp-session-id: $SESSION_ID" \
106112
-d '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":"2"}' \
107113
http://localhost:3000/mcp
108114
```
109115
110-
### Client (`client/simpleStreamableHttp.ts`)
111-
112-
A client that connects to the server, initializes it, and demonstrates how to:
113-
114-
- List available tools and call the `greet` tool
115-
- List available prompts and get the `greeting-template` prompt
116-
- List available resources
117-
118-
#### Running the client
116+
#### Call tool
119117
120118
```bash
121-
npx tsx src/examples/client/simpleStreamableHttp.ts
119+
# Call the greet tool using the saved session ID
120+
curl -X POST \
121+
-H "Content-Type: application/json" \
122+
-H "Accept: application/json" \
123+
-H "Accept: text/event-stream" \
124+
-H "mcp-session-id: $SESSION_ID" \
125+
-d '{
126+
"jsonrpc": "2.0",
127+
"method": "tools/call",
128+
"params": {
129+
"name": "greet",
130+
"arguments": {
131+
"name": "World"
132+
}
133+
},
134+
"id": "2"
135+
}' \
136+
http://localhost:3000/mcp
122137
```
123-
124-
Make sure the server is running before starting the client.
125-
126-
## Notes
127-
128-
- These examples demonstrate the basic usage of the Streamable HTTP transport
129-
- The server manages sessions between the calls
130-
- The client handles both direct HTTP responses and SSE streaming responses

0 commit comments

Comments
 (0)