Skip to content

Commit dec31b7

Browse files
authored
Merge branch 'main' into main
2 parents f152a4d + 1317767 commit dec31b7

30 files changed

+5076
-2017
lines changed

README.md

+257-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,179 @@ 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 { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"
218225

219-
const server = new McpServer({
220-
name: "example-server",
221-
version: "1.0.0"
222-
});
223226

224-
// ... set up server resources, tools, and prompts ...
225227

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

228-
// to support multiple simultaneous connections we have a lookup object from
229-
// sessionId to transport
230-
const transports: {[sessionId: string]: SSEServerTransport} = {};
253+
// Clean up transport when closed
254+
transport.onclose = () => {
255+
if (transport.sessionId) {
256+
delete transports[transport.sessionId];
257+
}
258+
};
259+
const server = new McpServer({
260+
name: "example-server",
261+
version: "1.0.0"
262+
});
231263

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

241-
app.post("/messages", async (req: Request, res: Response) => {
242-
const sessionId = req.query.sessionId as string;
285+
// Reusable handler for GET and DELETE requests
286+
const handleSessionRequest = async (req: express.Request, res: express.Response) => {
287+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
288+
if (!sessionId || !transports[sessionId]) {
289+
res.status(400).send('Invalid or missing session ID');
290+
return;
291+
}
292+
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 GET requests for server-to-client notifications via SSE
298+
app.get('/mcp', handleSessionRequest);
299+
300+
// Handle DELETE requests for session termination
301+
app.delete('/mcp', handleSessionRequest);
302+
303+
app.listen(3000);
304+
```
305+
306+
#### Without Session Management (Stateless)
307+
308+
For simpler use cases where session management isn't needed:
309+
310+
```typescript
311+
const app = express();
312+
app.use(express.json());
313+
314+
app.post('/mcp', async (req: Request, res: Response) => {
315+
// In stateless mode, create a new instance of transport and server for each request
316+
// to ensure complete isolation. A single instance would cause request ID collisions
317+
// when multiple clients connect concurrently.
318+
319+
try {
320+
const server = getServer();
321+
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
322+
sessionIdGenerator: undefined,
323+
});
324+
await server.connect(transport);
325+
await transport.handleRequest(req, res, req.body);
326+
res.on('close', () => {
327+
console.log('Request closed');
328+
transport.close();
329+
server.close();
330+
});
331+
} catch (error) {
332+
console.error('Error handling MCP request:', error);
333+
if (!res.headersSent) {
334+
res.status(500).json({
335+
jsonrpc: '2.0',
336+
error: {
337+
code: -32603,
338+
message: 'Internal server error',
339+
},
340+
id: null,
341+
});
342+
}
248343
}
249344
});
250345

251-
app.listen(3001);
346+
app.get('/mcp', async (req: Request, res: Response) => {
347+
console.log('Received GET MCP request');
348+
res.writeHead(405).end(JSON.stringify({
349+
jsonrpc: "2.0",
350+
error: {
351+
code: -32000,
352+
message: "Method not allowed."
353+
},
354+
id: null
355+
}));
356+
});
357+
358+
app.delete('/mcp', async (req: Request, res: Response) => {
359+
console.log('Received DELETE MCP request');
360+
res.writeHead(405).end(JSON.stringify({
361+
jsonrpc: "2.0",
362+
error: {
363+
code: -32000,
364+
message: "Method not allowed."
365+
},
366+
id: null
367+
}));
368+
});
369+
370+
371+
// Start the server
372+
const PORT = 3000;
373+
app.listen(PORT, () => {
374+
console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`);
375+
});
376+
252377
```
253378

379+
This stateless approach is useful for:
380+
- Simple API wrappers
381+
- RESTful scenarios where each request is independent
382+
- Horizontally scaled deployments without shared session state
383+
254384
### Testing and Debugging
255385

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

729+
### Backwards Compatibility
730+
731+
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
732+
733+
#### Client-Side Compatibility
734+
735+
For clients that need to work with both Streamable HTTP and older SSE servers:
736+
737+
```typescript
738+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
739+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
740+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
741+
let client: Client|undefined = undefined
742+
const baseUrl = new URL(url);
743+
try {
744+
client = new Client({
745+
name: 'streamable-http-client',
746+
version: '1.0.0'
747+
});
748+
const transport = new StreamableHTTPClientTransport(
749+
new URL(baseUrl)
750+
);
751+
await client.connect(transport);
752+
console.log("Connected using Streamable HTTP transport");
753+
} catch (error) {
754+
// If that fails with a 4xx error, try the older SSE transport
755+
console.log("Streamable HTTP connection failed, falling back to SSE transport");
756+
client = new Client({
757+
name: 'sse-client',
758+
version: '1.0.0'
759+
});
760+
const sseTransport = new SSEClientTransport(baseUrl);
761+
await client.connect(sseTransport);
762+
console.log("Connected using SSE transport");
763+
}
764+
```
765+
766+
#### Server-Side Compatibility
767+
768+
For servers that need to support both Streamable HTTP and older clients:
769+
770+
```typescript
771+
import express from "express";
772+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
773+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
774+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
775+
776+
const server = new McpServer({
777+
name: "backwards-compatible-server",
778+
version: "1.0.0"
779+
});
780+
781+
// ... set up server resources, tools, and prompts ...
782+
783+
const app = express();
784+
app.use(express.json());
785+
786+
// Store transports for each session type
787+
const transports = {
788+
streamable: {} as Record<string, StreamableHTTPServerTransport>,
789+
sse: {} as Record<string, SSEServerTransport>
790+
};
791+
792+
// Modern Streamable HTTP endpoint
793+
app.all('/mcp', async (req, res) => {
794+
// Handle Streamable HTTP transport for modern clients
795+
// Implementation as shown in the "With Session Management" example
796+
// ...
797+
});
798+
799+
// Legacy SSE endpoint for older clients
800+
app.get('/sse', async (req, res) => {
801+
// Create SSE transport for legacy clients
802+
const transport = new SSEServerTransport('/messages', res);
803+
transports.sse[transport.sessionId] = transport;
804+
805+
res.on("close", () => {
806+
delete transports.sse[transport.sessionId];
807+
});
808+
809+
await server.connect(transport);
810+
});
811+
812+
// Legacy message endpoint for older clients
813+
app.post('/messages', async (req, res) => {
814+
const sessionId = req.query.sessionId as string;
815+
const transport = transports.sse[sessionId];
816+
if (transport) {
817+
await transport.handlePostMessage(req, res, req.body);
818+
} else {
819+
res.status(400).send('No transport found for sessionId');
820+
}
821+
});
822+
823+
app.listen(3000);
824+
```
825+
826+
**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.
827+
599828
## Documentation
600829

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

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/sdk",
3-
"version": "1.10.0",
3+
"version": "1.10.2",
44
"description": "Model Context Protocol implementation for TypeScript",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",

src/client/index.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,11 @@ export class Client<
126126

127127
override async connect(transport: Transport, options?: RequestOptions): Promise<void> {
128128
await super.connect(transport);
129-
129+
// When transport sessionId is already set this means we are trying to reconnect.
130+
// In this case we don't need to initialize again.
131+
if (transport.sessionId !== undefined) {
132+
return;
133+
}
130134
try {
131135
const result = await this.request(
132136
{

0 commit comments

Comments
 (0)