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,14 +208,18 @@ 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
213
216
214
217
``` typescript
215
- import express , { Request , Response } from " express" ;
218
+ import express from " express" ;
219
+ import { randomUUID } from " node:crypto" ;
216
220
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" ;
218
223
219
224
const server = new McpServer ({
220
225
name: " example-server" ,
@@ -224,33 +229,128 @@ const server = new McpServer({
224
229
// ... set up server resources, tools, and prompts ...
225
230
226
231
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
+ });
227
257
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
+ };
231
264
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 );
239
282
});
240
283
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
243
293
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 ;
248
303
}
304
+
305
+ const transport = transports [sessionId ];
306
+ await transport .handleRequest (req , res );
249
307
});
250
308
251
- app .listen (3001 );
309
+ app .listen (3000 );
252
310
```
253
311
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
+
254
354
### Testing and Debugging
255
355
256
356
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:
596
696
- Provide custom documentation URLs
597
697
- Maintain control over the OAuth flow while delegating to an external provider
598
698
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
+
599
794
## Documentation
600
795
601
796
- [ Model Context Protocol documentation] ( https://modelcontextprotocol.io )
0 commit comments