Skip to content

Commit 864aa21

Browse files
committed
update readme with StreamableHttp
1 parent 8806c0e commit 864aa21

File tree

1 file changed

+218
-23
lines changed

1 file changed

+218
-23
lines changed

README.md

Lines changed: 218 additions & 23 deletions
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,14 +208,18 @@ 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
213216

214217
```typescript
215-
import express, { Request, Response } from "express";
218+
import express from "express";
219+
import { randomUUID } from "node:crypto";
216220
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
217-
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
221+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
222+
import { InMemoryEventStore } from "@modelcontextprotocol/sdk/inMemory.js";
218223

219224
const server = new McpServer({
220225
name: "example-server",
@@ -224,33 +229,128 @@ const server = new McpServer({
224229
// ... set up server resources, tools, and prompts ...
225230

226231
const app = express();
232+
app.use(express.json());
233+
234+
// Map to store transports by session ID
235+
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
236+
237+
// Handle POST requests for client-to-server communication
238+
app.post('/mcp', async (req, res) => {
239+
// Check for existing session ID
240+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
241+
let transport: StreamableHTTPServerTransport;
242+
243+
if (sessionId && transports[sessionId]) {
244+
// Reuse existing transport
245+
transport = transports[sessionId];
246+
} else if (!sessionId && isInitializeRequest(req.body)) {
247+
// New initialization request
248+
const eventStore = new InMemoryEventStore();
249+
transport = new StreamableHTTPServerTransport({
250+
sessionIdGenerator: () => randomUUID(),
251+
eventStore, // Enable resumability
252+
onsessioninitialized: (sessionId) => {
253+
// Store the transport by session ID
254+
transports[sessionId] = transport;
255+
}
256+
});
227257

228-
// to support multiple simultaneous connections we have a lookup object from
229-
// sessionId to transport
230-
const transports: {[sessionId: string]: SSEServerTransport} = {};
258+
// Clean up transport when closed
259+
transport.onclose = () => {
260+
if (transport.sessionId) {
261+
delete transports[transport.sessionId];
262+
}
263+
};
231264

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];
237-
});
238-
await server.connect(transport);
265+
// Connect to the MCP server
266+
await server.connect(transport);
267+
} else {
268+
// Invalid request
269+
res.status(400).json({
270+
jsonrpc: '2.0',
271+
error: {
272+
code: -32000,
273+
message: 'Bad Request: No valid session ID provided',
274+
},
275+
id: null,
276+
});
277+
return;
278+
}
279+
280+
// Handle the request
281+
await transport.handleRequest(req, res, req.body);
239282
});
240283

241-
app.post("/messages", async (req: Request, res: Response) => {
242-
const sessionId = req.query.sessionId as string;
284+
// Handle GET requests for server-to-client notifications via SSE
285+
app.get('/mcp', async (req, res) => {
286+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
287+
if (!sessionId || !transports[sessionId]) {
288+
res.status(400).send('Invalid or missing session ID');
289+
return;
290+
}
291+
292+
// Support resumability with Last-Event-ID header
243293
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');
294+
await transport.handleRequest(req, res);
295+
});
296+
297+
// Handle DELETE requests for session termination
298+
app.delete('/mcp', async (req, res) => {
299+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
300+
if (!sessionId || !transports[sessionId]) {
301+
res.status(400).send('Invalid or missing session ID');
302+
return;
248303
}
304+
305+
const transport = transports[sessionId];
306+
await transport.handleRequest(req, res);
249307
});
250308

251-
app.listen(3001);
309+
app.listen(3000);
252310
```
253311

312+
#### Without Session Management (Stateless)
313+
314+
For simpler use cases where session management isn't needed:
315+
316+
```typescript
317+
import express from "express";
318+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
319+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
320+
321+
const server = new McpServer({
322+
name: "stateless-server",
323+
version: "1.0.0"
324+
});
325+
326+
// ... set up server resources, tools, and prompts ...
327+
328+
const app = express();
329+
app.use(express.json());
330+
331+
// Handle all MCP requests (GET, POST, DELETE) at a single endpoint
332+
app.all('/mcp', async (req, res) => {
333+
// Create a transport with sessionIdGenerator set to return undefined
334+
// This disables session tracking completely
335+
const transport = new StreamableHTTPServerTransport({
336+
sessionIdGenerator: () => undefined,
337+
req,
338+
res
339+
});
340+
341+
// Connect to server and handle the request
342+
await server.connect(transport);
343+
await transport.handleRequest(req, res);
344+
});
345+
346+
app.listen(3000);
347+
```
348+
349+
This stateless approach is useful for:
350+
- Simple API wrappers
351+
- RESTful scenarios where each request is independent
352+
- Horizontally scaled deployments without shared session state
353+
254354
### Testing and Debugging
255355

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

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

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

0 commit comments

Comments
 (0)