Skip to content

StreamableHTTP - listening for messages from the server trough GET established SSE #304

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 11, 2025
1 change: 0 additions & 1 deletion src/client/streamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ export class StreamableHTTPClientTransport implements Transport {
`Failed to open SSE stream: ${response.statusText}`,
);
}

// Successful connection, handle the SSE stream as a standalone listener
this._handleSseStream(response.body);
} catch (error) {
Expand Down
147 changes: 77 additions & 70 deletions src/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,81 +2,81 @@

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

## Table of Contents

- [Streamable HTTP Servers - Single Node Deployment](#streamable-http---single-node-deployment-with-basic-session-state-management)
- [Simple Server with Streamable HTTP](#simple-server-with-streamable-http-transport-serversimplestreamablehttpts)
- [Server Supporting SSE via GET](#server-supporting-with-sse-via-get-serverstandalonessewithgetstreamablehttpts)
- [Server with JSON Response Mode](#server-with-json-response-mode-serverjsonresponsestreamablehttpts)
- [Client Example - Streamable HTTP](#client-clientsimplestreamablehttpts)
- [Useful bash commands for testing](#useful-commands-for-testing)

## Streamable HTTP - single node deployment with basic session state management

Multi node with stete management example will be added soon after we add support.
Multi node with state management example will be added soon after we add support.

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

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.
### Simple Server with Streamable Http transport (`server/simpleStreamableHttp.ts`)

A simple MCP server that uses the Streamable HTTP transport, implemented with Express. The server provides:

- A simple `greet` tool that returns a greeting for a name
- A `greeting-template` prompt that generates a greeting template
- A static `greeting-resource` resource

#### Running the server

```bash
npx tsx src/examples/server/jsonResponseStreamableHttp.ts
npx tsx src/examples/server/simpleStreamableHttp.ts
```

The server will start on port 3000. You can test the initialization and tool calling:
The server will start on port 3000. You can test the initialization and tool listing:

### Server supporting with SSE via GET (`server/standaloneSseWithGetStreamableHttp.ts`)

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).

#### Running the server

```bash
# Initialize the server and get the session ID from headers
SESSION_ID=$(curl -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Accept: text/event-stream" \
-d '{
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"capabilities": {},
"protocolVersion": "2025-03-26",
"clientInfo": {
"name": "test",
"version": "1.0.0"
}
},
"id": "1"
}' \
-i http://localhost:3000/mcp 2>&1 | grep -i "mcp-session-id" | cut -d' ' -f2 | tr -d '\r')
echo "Session ID: $SESSION_ID"
npx tsx src/examples/server/standaloneSseWithGetStreamableHttp.ts
```

# Call the greet tool using the saved session ID
curl -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Accept: text/event-stream" \
-H "mcp-session-id: $SESSION_ID" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "greet",
"arguments": {
"name": "World"
}
},
"id": "2"
}' \
http://localhost:3000/mcp
The server will start on port 3000 and automatically create new resources every 5 seconds.

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

This is not recommented way to use the transport, as its quite limiting and not supporting features like logging and progress notifications on tool execution.
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.

#### Running the server

```bash
npx tsx src/examples/server/jsonResponseStreamableHttp.ts
```

Note that in this example, we're using plain JSON response mode by setting `Accept: application/json` header.

### Server (`server/simpleStreamableHttp.ts`)
### Client (`client/simpleStreamableHttp.ts`)

A simple MCP server that uses the Streamable HTTP transport, implemented with Express. The server provides:
A client that connects to the server, initializes it, and demonstrates how to:

- A simple `greet` tool that returns a greeting for a name
- A `greeting-template` prompt that generates a greeting template
- A static `greeting-resource` resource
- List available tools and call the `greet` tool
- List available prompts and get the `greeting-template` prompt
- List available resources

#### Running the server
#### Running the client

```bash
npx tsx src/examples/server/simpleStreamableHttp.ts
npx tsx src/examples/client/simpleStreamableHttp.ts
```

The server will start on port 3000. You can test the initialization and tool listing:
Make sure the server is running before starting the client.


### Useful commands for testing

#### Initialize
Streamable HTTP transport requires to do the initialization first.

```bash
# First initialize the server and save the session ID to a variable
Expand All @@ -100,31 +100,38 @@ SESSION_ID=$(curl -X POST \
-i http://localhost:3000/mcp 2>&1 | grep -i "mcp-session-id" | cut -d' ' -f2 | tr -d '\r')
echo "Session ID: $SESSION_ID

```

Once thre is a session we can send POST requests

#### List tools
```bash
# Then list tools using the saved session ID
curl -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-H "mcp-session-id: $SESSION_ID" \
-d '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":"2"}' \
http://localhost:3000/mcp
```

### Client (`client/simpleStreamableHttp.ts`)

A client that connects to the server, initializes it, and demonstrates how to:

- List available tools and call the `greet` tool
- List available prompts and get the `greeting-template` prompt
- List available resources

#### Running the client
#### Call tool

```bash
npx tsx src/examples/client/simpleStreamableHttp.ts
# Call the greet tool using the saved session ID
curl -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Accept: text/event-stream" \
-H "mcp-session-id: $SESSION_ID" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "greet",
"arguments": {
"name": "World"
}
},
"id": "2"
}' \
http://localhost:3000/mcp
```

Make sure the server is running before starting the client.

## Notes

- These examples demonstrate the basic usage of the Streamable HTTP transport
- The server manages sessions between the calls
- The client handles both direct HTTP responses and SSE streaming responses
104 changes: 68 additions & 36 deletions src/examples/client/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
GetPromptResultSchema,
ListResourcesRequest,
ListResourcesResultSchema,
LoggingMessageNotificationSchema
LoggingMessageNotificationSchema,
ResourceListChangedNotificationSchema
} from '../../types.js';

async function main(): Promise<void> {
Expand All @@ -24,50 +25,79 @@ async function main(): Promise<void> {
const transport = new StreamableHTTPClientTransport(
new URL('http://localhost:3000/mcp')
);
let supportsStandaloneSse = false;

// Connect the client using the transport and initialize the server
await client.connect(transport);
console.log('Connected to MCP server');
// Open a standalone SSE stream to receive server-initiated messages
console.log('Opening SSE stream to receive server notifications...');
try {
await transport.openSseStream();
supportsStandaloneSse = true;
console.log('SSE stream established successfully. Waiting for notifications...');
}
catch (error) {
console.error('Failed to open SSE stream:', error);
}

// Set up notification handlers for server-initiated messages
client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
console.log(`Notification received: ${notification.params.level} - ${notification.params.data}`);
});
client.setNotificationHandler(ResourceListChangedNotificationSchema, async (_) => {
console.log(`Resource list changed notification received!`);
const resourcesRequest: ListResourcesRequest = {
method: 'resources/list',
params: {}
};
const resourcesResult = await client.request(resourcesRequest, ListResourcesResultSchema);
console.log('Available resources count:', resourcesResult.resources.length);
});


console.log('Connected to MCP server');
// List available tools
const toolsRequest: ListToolsRequest = {
method: 'tools/list',
params: {}
};
const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);
console.log('Available tools:', toolsResult.tools);
try {
const toolsRequest: ListToolsRequest = {
method: 'tools/list',
params: {}
};
const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);
console.log('Available tools:', toolsResult.tools);

// Call the 'greet' tool
const greetRequest: CallToolRequest = {
method: 'tools/call',
params: {
name: 'greet',
arguments: { name: 'MCP User' }
}
};
const greetResult = await client.request(greetRequest, CallToolResultSchema);
console.log('Greeting result:', greetResult.content[0].text);
if (toolsResult.tools.length === 0) {
console.log('No tools available from the server');
} else {
// Call the 'greet' tool
const greetRequest: CallToolRequest = {
method: 'tools/call',
params: {
name: 'greet',
arguments: { name: 'MCP User' }
}
};
const greetResult = await client.request(greetRequest, CallToolResultSchema);
console.log('Greeting result:', greetResult.content[0].text);

// Call the new 'multi-greet' tool
console.log('\nCalling multi-greet tool (with notifications)...');
const multiGreetRequest: CallToolRequest = {
method: 'tools/call',
params: {
name: 'multi-greet',
arguments: { name: 'MCP User' }
// Call the new 'multi-greet' tool
console.log('\nCalling multi-greet tool (with notifications)...');
const multiGreetRequest: CallToolRequest = {
method: 'tools/call',
params: {
name: 'multi-greet',
arguments: { name: 'MCP User' }
}
};
const multiGreetResult = await client.request(multiGreetRequest, CallToolResultSchema);
console.log('Multi-greet results:');
multiGreetResult.content.forEach(item => {
if (item.type === 'text') {
console.log(`- ${item.text}`);
}
});
}
};
const multiGreetResult = await client.request(multiGreetRequest, CallToolResultSchema);
console.log('Multi-greet results:');
multiGreetResult.content.forEach(item => {
if (item.type === 'text') {
console.log(`- ${item.text}`);
}
});
} catch (error) {
console.log(`Tools not supported by this server (${error})`);
}

// List available prompts
try {
Expand Down Expand Up @@ -107,9 +137,11 @@ async function main(): Promise<void> {
} catch (error) {
console.log(`Resources not supported by this server (${error})`);
}
if (supportsStandaloneSse) {
// Instead of closing immediately, keep the connection open to receive notifications
console.log('\nKeeping connection open to receive notifications. Press Ctrl+C to exit.');
}

// Close the connection
await client.close();
}

main().catch((error: unknown) => {
Expand Down
7 changes: 7 additions & 0 deletions src/examples/server/jsonResponseStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@ app.post('/mcp', async (req: Request, res: Response) => {
}
});

// Handle GET requests for SSE streams according to spec
app.get('/mcp', async (req: Request, res: Response) => {
// Since this is a very simple example, we don't support GET requests for this server
// The spec requires returning 405 Method Not Allowed in this case
res.status(405).set('Allow', 'POST').send('Method Not Allowed');
});

// Helper function to detect initialize requests
function isInitializeRequest(body: unknown): boolean {
if (Array.isArray(body)) {
Expand Down
13 changes: 13 additions & 0 deletions src/examples/server/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,19 @@ app.post('/mcp', async (req: Request, res: Response) => {
}
});

// Handle GET requests for SSE streams (now using built-in support from StreamableHTTP)
app.get('/mcp', async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID');
return;
}

console.log(`Establishing SSE stream for session ${sessionId}`);
const transport = transports[sessionId];
await transport.handleRequest(req, res);
});

// Helper function to detect initialize requests
function isInitializeRequest(body: unknown): boolean {
if (Array.isArray(body)) {
Expand Down
Loading