|
12 | 12 | - [Prompts](#prompts)
|
13 | 13 | - [Running Your Server](#running-your-server)
|
14 | 14 | - [stdio](#stdio)
|
15 |
| - - [HTTP with SSE](#http-with-sse) |
| 15 | + - [Streamable HTTP](#streamable-http) |
16 | 16 | - [Testing and Debugging](#testing-and-debugging)
|
17 | 17 | - [Examples](#examples)
|
18 | 18 | - [Echo Server](#echo-server)
|
|
22 | 22 | - [Writing MCP Clients](#writing-mcp-clients)
|
23 | 23 | - [Server Capabilities](#server-capabilities)
|
24 | 24 | - [Proxy OAuth Server](#proxy-authorization-requests-upstream)
|
| 25 | + - [Backwards Compatibility](#backwards-compatibility) |
25 | 26 |
|
26 | 27 | ## Overview
|
27 | 28 |
|
28 | 29 | 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:
|
29 | 30 |
|
30 | 31 | - Build MCP clients that can connect to any MCP server
|
31 | 32 | - 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 |
33 | 34 | - Handle all MCP protocol messages and lifecycle events
|
34 | 35 |
|
35 | 36 | ## Installation
|
@@ -207,50 +208,143 @@ const transport = new StdioServerTransport();
|
207 | 208 | await server.connect(transport);
|
208 | 209 | ```
|
209 | 210 |
|
210 |
| -### HTTP with SSE |
| 211 | +### Streamable HTTP |
211 | 212 |
|
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). |
213 | 218 |
|
214 | 219 | ```typescript
|
215 |
| -import express, { Request, Response } from "express"; |
| 220 | +import express from "express"; |
| 221 | +import { randomUUID } from "node:crypto"; |
216 | 222 | 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"; |
218 | 315 |
|
219 | 316 | const server = new McpServer({
|
220 |
| - name: "example-server", |
| 317 | + name: "stateless-server", |
221 | 318 | version: "1.0.0"
|
222 | 319 | });
|
223 | 320 |
|
224 | 321 | // ... set up server resources, tools, and prompts ...
|
225 | 322 |
|
226 | 323 | 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 |
237 | 333 | });
|
| 334 | + |
| 335 | + // Connect to server and handle the request |
238 | 336 | await server.connect(transport);
|
| 337 | + await transport.handleRequest(req, res); |
239 | 338 | });
|
240 | 339 |
|
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); |
252 | 341 | ```
|
253 | 342 |
|
| 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 | + |
254 | 348 | ### Testing and Debugging
|
255 | 349 |
|
256 | 350 | 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:
|
596 | 690 | - Provide custom documentation URLs
|
597 | 691 | - Maintain control over the OAuth flow while delegating to an external provider
|
598 | 692 |
|
| 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 | + |
599 | 793 | ## Documentation
|
600 | 794 |
|
601 | 795 | - [Model Context Protocol documentation](https://modelcontextprotocol.io)
|
|
0 commit comments