update readme with StreamableHttp
This commit is contained in:
parent
8806c0ecb4
commit
864aa2155c
1 changed files with 218 additions and 23 deletions
243
README.md
243
README.md
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue