update readme with StreamableHttp

This commit is contained in:
ihrpr 2025-04-16 18:22:55 +01:00
commit 864aa2155c
No known key found for this signature in database

243
README.md
View file

@ -12,7 +12,7 @@
- [Prompts](#prompts)
- [Running Your Server](#running-your-server)
- [stdio](#stdio)
- [HTTP with SSE](#http-with-sse)
- [Streamable HTTP](#streamable-http)
- [Testing and Debugging](#testing-and-debugging)
- [Examples](#examples)
- [Echo Server](#echo-server)
@ -22,6 +22,7 @@
- [Writing MCP Clients](#writing-mcp-clients)
- [Server Capabilities](#server-capabilities)
- [Proxy OAuth Server](#proxy-authorization-requests-upstream)
- [Backwards Compatibility](#backwards-compatibility)
## Overview
@ -29,7 +30,7 @@ The Model Context Protocol allows applications to provide context for LLMs in a
- Build MCP clients that can connect to any MCP server
- Create MCP servers that expose resources, prompts and tools
- Use standard transports like stdio and SSE
- Use standard transports like stdio and Streamable HTTP
- Handle all MCP protocol messages and lifecycle events
## Installation
@ -207,14 +208,18 @@ const transport = new StdioServerTransport();
await server.connect(transport);
```
### HTTP with SSE
### Streamable HTTP
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:
For remote servers, set up a Streamable HTTP transport that handles both client requests and server-to-client notifications.
#### With Session Management
```typescript
import express, { Request, Response } from "express";
import express from "express";
import { randomUUID } from "node:crypto";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { InMemoryEventStore } from "@modelcontextprotocol/sdk/inMemory.js";
const server = new McpServer({
name: "example-server",
@ -224,33 +229,128 @@ const server = new McpServer({
// ... set up server resources, tools, and prompts ...
const app = express();
app.use(express.json());
// to support multiple simultaneous connections we have a lookup object from
// sessionId to transport
const transports: {[sessionId: string]: SSEServerTransport} = {};
// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
app.get("/sse", async (_: Request, res: Response) => {
const transport = new SSEServerTransport('/messages', res);
transports[transport.sessionId] = transport;
res.on("close", () => {
delete transports[transport.sessionId];
});
await server.connect(transport);
});
// Handle POST requests for client-to-server communication
app.post('/mcp', async (req, res) => {
// Check for existing session ID
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;
app.post("/messages", async (req: Request, res: Response) => {
const sessionId = req.query.sessionId as string;
const transport = transports[sessionId];
if (transport) {
await transport.handlePostMessage(req, res);
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request
const eventStore = new InMemoryEventStore();
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
eventStore, // Enable resumability
onsessioninitialized: (sessionId) => {
// Store the transport by session ID
transports[sessionId] = transport;
}
});
// Clean up transport when closed
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId];
}
};
// Connect to the MCP server
await server.connect(transport);
} else {
res.status(400).send('No transport found for sessionId');
// Invalid request
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
});
return;
}
// Handle the request
await transport.handleRequest(req, res, req.body);
});
app.listen(3001);
// Handle GET requests for server-to-client notifications via SSE
app.get('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID');
return;
}
// Support resumability with Last-Event-ID header
const transport = transports[sessionId];
await transport.handleRequest(req, res);
});
// Handle DELETE requests for session termination
app.delete('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID');
return;
}
const transport = transports[sessionId];
await transport.handleRequest(req, res);
});
app.listen(3000);
```
#### Without Session Management (Stateless)
For simpler use cases where session management isn't needed:
```typescript
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const server = new McpServer({
name: "stateless-server",
version: "1.0.0"
});
// ... set up server resources, tools, and prompts ...
const app = express();
app.use(express.json());
// Handle all MCP requests (GET, POST, DELETE) at a single endpoint
app.all('/mcp', async (req, res) => {
// Create a transport with sessionIdGenerator set to return undefined
// This disables session tracking completely
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => undefined,
req,
res
});
// Connect to server and handle the request
await server.connect(transport);
await transport.handleRequest(req, res);
});
app.listen(3000);
```
This stateless approach is useful for:
- Simple API wrappers
- RESTful scenarios where each request is independent
- Horizontally scaled deployments without shared session state
### Testing and Debugging
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:
- Provide custom documentation URLs
- Maintain control over the OAuth flow while delegating to an external provider
### Backwards Compatibility
The SDK provides support for backwards compatibility between different protocol versions:
#### Client-Side Compatibility
For clients that need to work with both Streamable HTTP and older SSE servers:
```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
// First try connecting with Streamable HTTP transport
try {
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:3000/mcp")
);
await client.connect(transport);
console.log("Connected using Streamable HTTP transport");
} catch (error) {
// If that fails with a 4xx error, try the older SSE transport
console.log("Streamable HTTP connection failed, falling back to SSE transport");
const sseTransport = new SSEClientTransport({
sseUrl: new URL("http://localhost:3000/sse"),
postUrl: new URL("http://localhost:3000/messages")
});
await client.connect(sseTransport);
console.log("Connected using SSE transport");
}
```
#### Server-Side Compatibility
For servers that need to support both Streamable HTTP and older clients:
```typescript
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { InMemoryEventStore } from "@modelcontextprotocol/sdk/inMemory.js";
const server = new McpServer({
name: "backwards-compatible-server",
version: "1.0.0"
});
// ... set up server resources, tools, and prompts ...
const app = express();
app.use(express.json());
// Store transports for each session type
const transports = {
streamable: {} as Record<string, StreamableHTTPServerTransport>,
sse: {} as Record<string, SSEServerTransport>
};
// Modern Streamable HTTP endpoint
app.all('/mcp', async (req, res) => {
// Handle Streamable HTTP transport for modern clients
// Implementation as shown in the "With Session Management" example
// ...
});
// Legacy SSE endpoint for older clients
app.get('/sse', async (req, res) => {
// Create SSE transport for legacy clients
const transport = new SSEServerTransport('/messages', res);
transports.sse[transport.sessionId] = transport;
res.on("close", () => {
delete transports.sse[transport.sessionId];
});
await server.connect(transport);
});
// Legacy message endpoint for older clients
app.post('/messages', async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.sse[sessionId];
if (transport) {
await transport.handlePostMessage(req, res);
} else {
res.status(400).send('No transport found for sessionId');
}
});
app.listen(3000);
```
**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.
## Documentation
- [Model Context Protocol documentation](https://modelcontextprotocol.io)