Skip to content

Commit 64653f5

Browse files
authored
Merge pull request #351 from modelcontextprotocol/ihrpr/clean-up-examples
StreamableHttp - Update examples and README
2 parents 12d8ac9 + f10d1ff commit 64653f5

11 files changed

+565
-381
lines changed

Diff for: README.md

+222-28
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
- [Prompts](#prompts)
1313
- [Running Your Server](#running-your-server)
1414
- [stdio](#stdio)
15-
- [HTTP with SSE](#http-with-sse)
15+
- [Streamable HTTP](#streamable-http)
1616
- [Testing and Debugging](#testing-and-debugging)
1717
- [Examples](#examples)
1818
- [Echo Server](#echo-server)
@@ -22,14 +22,15 @@
2222
- [Writing MCP Clients](#writing-mcp-clients)
2323
- [Server Capabilities](#server-capabilities)
2424
- [Proxy OAuth Server](#proxy-authorization-requests-upstream)
25+
- [Backwards Compatibility](#backwards-compatibility)
2526

2627
## Overview
2728

2829
The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This TypeScript SDK implements the full MCP specification, making it easy to:
2930

3031
- Build MCP clients that can connect to any MCP server
3132
- Create MCP servers that expose resources, prompts and tools
32-
- Use standard transports like stdio and SSE
33+
- Use standard transports like stdio and Streamable HTTP
3334
- Handle all MCP protocol messages and lifecycle events
3435

3536
## Installation
@@ -207,50 +208,143 @@ const transport = new StdioServerTransport();
207208
await server.connect(transport);
208209
```
209210

210-
### HTTP with SSE
211+
### Streamable HTTP
211212

212-
For remote servers, start a web server with a Server-Sent Events (SSE) endpoint, and a separate endpoint for the client to send its messages to:
213+
For remote servers, set up a Streamable HTTP transport that handles both client requests and server-to-client notifications.
214+
215+
#### With Session Management
216+
217+
In some cases, servers need to be stateful. This is achieved by [session management](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management).
213218

214219
```typescript
215-
import express, { Request, Response } from "express";
220+
import express from "express";
221+
import { randomUUID } from "node:crypto";
216222
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
217-
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
223+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
224+
import { InMemoryEventStore } from "@modelcontextprotocol/sdk/inMemory.js";
225+
226+
227+
const app = express();
228+
app.use(express.json());
229+
230+
// Map to store transports by session ID
231+
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
232+
233+
// Handle POST requests for client-to-server communication
234+
app.post('/mcp', async (req, res) => {
235+
// Check for existing session ID
236+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
237+
let transport: StreamableHTTPServerTransport;
238+
239+
if (sessionId && transports[sessionId]) {
240+
// Reuse existing transport
241+
transport = transports[sessionId];
242+
} else if (!sessionId && isInitializeRequest(req.body)) {
243+
// New initialization request
244+
const eventStore = new InMemoryEventStore();
245+
transport = new StreamableHTTPServerTransport({
246+
sessionIdGenerator: () => randomUUID(),
247+
eventStore, // Enable resumability
248+
onsessioninitialized: (sessionId) => {
249+
// Store the transport by session ID
250+
transports[sessionId] = transport;
251+
}
252+
});
253+
254+
// Clean up transport when closed
255+
transport.onclose = () => {
256+
if (transport.sessionId) {
257+
delete transports[transport.sessionId];
258+
}
259+
};
260+
const server = new McpServer({
261+
name: "example-server",
262+
version: "1.0.0"
263+
});
264+
265+
// ... set up server resources, tools, and prompts ...
266+
267+
// Connect to the MCP server
268+
await server.connect(transport);
269+
} else {
270+
// Invalid request
271+
res.status(400).json({
272+
jsonrpc: '2.0',
273+
error: {
274+
code: -32000,
275+
message: 'Bad Request: No valid session ID provided',
276+
},
277+
id: null,
278+
});
279+
return;
280+
}
281+
282+
// Handle the request
283+
await transport.handleRequest(req, res, req.body);
284+
});
285+
286+
// Reusable handler for GET and DELETE requests
287+
const handleSessionRequest = async (req: express.Request, res: express.Response) => {
288+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
289+
if (!sessionId || !transports[sessionId]) {
290+
res.status(400).send('Invalid or missing session ID');
291+
return;
292+
}
293+
294+
const transport = transports[sessionId];
295+
await transport.handleRequest(req, res);
296+
};
297+
298+
// Handle GET requests for server-to-client notifications via SSE
299+
app.get('/mcp', handleSessionRequest);
300+
301+
// Handle DELETE requests for session termination
302+
app.delete('/mcp', handleSessionRequest);
303+
304+
app.listen(3000);
305+
```
306+
307+
#### Without Session Management (Stateless)
308+
309+
For simpler use cases where session management isn't needed:
310+
311+
```typescript
312+
import express from "express";
313+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
314+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
218315

219316
const server = new McpServer({
220-
name: "example-server",
317+
name: "stateless-server",
221318
version: "1.0.0"
222319
});
223320

224321
// ... set up server resources, tools, and prompts ...
225322

226323
const app = express();
227-
228-
// to support multiple simultaneous connections we have a lookup object from
229-
// sessionId to transport
230-
const transports: {[sessionId: string]: SSEServerTransport} = {};
231-
232-
app.get("/sse", async (_: Request, res: Response) => {
233-
const transport = new SSEServerTransport('/messages', res);
234-
transports[transport.sessionId] = transport;
235-
res.on("close", () => {
236-
delete transports[transport.sessionId];
324+
app.use(express.json());
325+
326+
// Handle all MCP requests (GET, POST, DELETE) at a single endpoint
327+
app.all('/mcp', async (req, res) => {
328+
// Disable session tracking by setting sessionIdGenerator to undefined
329+
const transport = new StreamableHTTPServerTransport({
330+
sessionIdGenerator: undefined,
331+
req,
332+
res
237333
});
334+
335+
// Connect to server and handle the request
238336
await server.connect(transport);
337+
await transport.handleRequest(req, res);
239338
});
240339

241-
app.post("/messages", async (req: Request, res: Response) => {
242-
const sessionId = req.query.sessionId as string;
243-
const transport = transports[sessionId];
244-
if (transport) {
245-
await transport.handlePostMessage(req, res);
246-
} else {
247-
res.status(400).send('No transport found for sessionId');
248-
}
249-
});
250-
251-
app.listen(3001);
340+
app.listen(3000);
252341
```
253342

343+
This stateless approach is useful for:
344+
- Simple API wrappers
345+
- RESTful scenarios where each request is independent
346+
- Horizontally scaled deployments without shared session state
347+
254348
### Testing and Debugging
255349

256350
To test your server, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). See its README for more information.
@@ -596,6 +690,106 @@ This setup allows you to:
596690
- Provide custom documentation URLs
597691
- Maintain control over the OAuth flow while delegating to an external provider
598692

693+
### Backwards Compatibility
694+
695+
Clients and servers with StreamableHttp tranport can maintain [backwards compatibility](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#backwards-compatibility) with the deprecated HTTP+SSE transport (from protocol version 2024-11-05) as follows
696+
697+
#### Client-Side Compatibility
698+
699+
For clients that need to work with both Streamable HTTP and older SSE servers:
700+
701+
```typescript
702+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
703+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
704+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
705+
let client: Client|undefined = undefined
706+
const baseUrl = new URL(url);
707+
try {
708+
client = new Client({
709+
name: 'streamable-http-client',
710+
version: '1.0.0'
711+
});
712+
const transport = new StreamableHTTPClientTransport(
713+
new URL(baseUrl)
714+
);
715+
await client.connect(transport);
716+
console.log("Connected using Streamable HTTP transport");
717+
} catch (error) {
718+
// If that fails with a 4xx error, try the older SSE transport
719+
console.log("Streamable HTTP connection failed, falling back to SSE transport");
720+
client = new Client({
721+
name: 'sse-client',
722+
version: '1.0.0'
723+
});
724+
const sseTransport = new SSEClientTransport(baseUrl);
725+
await client.connect(sseTransport);
726+
console.log("Connected using SSE transport");
727+
}
728+
```
729+
730+
#### Server-Side Compatibility
731+
732+
For servers that need to support both Streamable HTTP and older clients:
733+
734+
```typescript
735+
import express from "express";
736+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
737+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
738+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
739+
import { InMemoryEventStore } from "@modelcontextprotocol/sdk/inMemory.js";
740+
741+
const server = new McpServer({
742+
name: "backwards-compatible-server",
743+
version: "1.0.0"
744+
});
745+
746+
// ... set up server resources, tools, and prompts ...
747+
748+
const app = express();
749+
app.use(express.json());
750+
751+
// Store transports for each session type
752+
const transports = {
753+
streamable: {} as Record<string, StreamableHTTPServerTransport>,
754+
sse: {} as Record<string, SSEServerTransport>
755+
};
756+
757+
// Modern Streamable HTTP endpoint
758+
app.all('/mcp', async (req, res) => {
759+
// Handle Streamable HTTP transport for modern clients
760+
// Implementation as shown in the "With Session Management" example
761+
// ...
762+
});
763+
764+
// Legacy SSE endpoint for older clients
765+
app.get('/sse', async (req, res) => {
766+
// Create SSE transport for legacy clients
767+
const transport = new SSEServerTransport('/messages', res);
768+
transports.sse[transport.sessionId] = transport;
769+
770+
res.on("close", () => {
771+
delete transports.sse[transport.sessionId];
772+
});
773+
774+
await server.connect(transport);
775+
});
776+
777+
// Legacy message endpoint for older clients
778+
app.post('/messages', async (req, res) => {
779+
const sessionId = req.query.sessionId as string;
780+
const transport = transports.sse[sessionId];
781+
if (transport) {
782+
await transport.handlePostMessage(req, res);
783+
} else {
784+
res.status(400).send('No transport found for sessionId');
785+
}
786+
});
787+
788+
app.listen(3000);
789+
```
790+
791+
**Note**: The SSE transport is now deprecated in favor of Streamable HTTP. New implementations should use Streamable HTTP, and existing SSE implementations should plan to migrate.
792+
599793
## Documentation
600794

601795
- [Model Context Protocol documentation](https://modelcontextprotocol.io)

0 commit comments

Comments
 (0)