UNPKG

@modelcontextprotocol/server-everything

Version:

MCP server that exercises all the features of the MCP protocol

125 lines (124 loc) • 5.63 kB
// The HTTP+SSE transport manager (deprecated transport; kept working): one // server per `GET /sse` stream, with client messages posted to `/message`. // // HTTP+SSE is a 2025-era transport, deprecated since 2025-03-26 and absent // from 2026-07-28. The v2 SDK serves it only through the frozen copy in // `@modelcontextprotocol/server-legacy/sse` (itself deprecated). It is kept so // existing SSE clients keep working, serves the 2025 era only, and is never // the default: Streamable HTTP serves both eras (#4852). The launcher warns // on start. // // `createApp()` builds the Express app without binding a port (#4854), so a // test can serve it on a free port and drive it with a real client, and each // app has its own session map. `startSseServer()` is what the launcher runs. import { SSEServerTransport } from "@modelcontextprotocol/server-legacy/sse"; import { localhostOriginValidation } from "@modelcontextprotocol/node"; import express from "express"; import { createServer } from "../server/index.js"; import cors from "cors"; import { bindHost, hostAndPort, listenOrExit } from "./listen.js"; /** * Answer a request the transport cannot serve with a JSON-RPC error body, as * the Streamable HTTP transport does, so the client is not left waiting. */ function sendError(res, status, message) { res.status(status).json({ jsonrpc: "2.0", error: { code: -32000, message }, id: null, }); } /** * Build the SSE transport's Express app: Origin validation, CORS, `GET /sse` * and `POST /message`, and the map from session id to transport that they * share. */ export function createApp() { const app = express(); // A browser page may reach this server only from a localhost origin. The // SDK's guard answers any other Origin with a 403 before a route or the // CORS middleware sees it, preflights included; a request with no Origin // (a non-browser client) passes. CORS then grants the origin that passed, // so the Inspector's direct connect mode keeps working from localhost. const allowOrigin = localhostOriginValidation(); app.use((req, res, next) => { if (allowOrigin(req, res)) next(); }); app.use(cors({ origin: true, methods: "GET,POST", preflightContinue: false, optionsSuccessStatus: 204, })); // Map sessionId to transport for each client const transports = new Map(); // Handle GET requests for new SSE streams app.get("/sse", async (req, res) => { // Session Id should not exist for GET /sse requests: each stream is a new // session. Answer one that names a session rather than leaving it hanging // (#4981): 409 if that session already has its stream, 404 if it is unknown. if (req?.query?.sessionId) { const sessionId = String(req.query.sessionId); if (transports.has(sessionId)) { console.error("Client Reconnecting? This shouldn't happen; when client has a sessionId, GET /sse should not be called again.", sessionId); sendError(res, 409, "Conflict: this session already has an SSE stream"); } else { console.error(`No transport found for sessionId ${sessionId}`); sendError(res, 404, "Session not found"); } return; } // Create and store transport for the new session const { server, cleanup } = createServer(); const transport = new SSEServerTransport("/message", res); transports.set(transport.sessionId, transport); // Connect server to transport await server.connect(transport); const sessionId = transport.sessionId; console.error("Client Connected: ", sessionId); // Handle close of connection server.server.onclose = async () => { const sessionId = transport.sessionId; console.error("Client Disconnected: ", sessionId); transports.delete(sessionId); cleanup(); }; }); // Handle POST requests for client messages app.post("/message", async (req, res) => { // Session Id should exist for POST /message requests const sessionId = req?.query?.sessionId; // Get the transport for this session and use it to handle the request const transport = transports.get(sessionId); if (transport) { console.error("Client Message from", sessionId); await transport.handlePostMessage(req, res); } else if (!sessionId) { // Answer rather than leave the request hanging (#4981) console.error("No sessionId in POST /message"); sendError(res, 400, "Bad Request: sessionId query parameter is required"); } else { console.error(`No transport found for sessionId ${sessionId}`); sendError(res, 404, "Session not found"); } }); return app; } /** * Start the SSE server as the launcher does: build the app and bind it to * `PORT` (default 3001) on `HOST` (default loopback). */ export function startSseServer() { console.error("Starting SSE server..."); console.error("Warning: the HTTP+SSE transport is deprecated and serves 2025-era clients only. " + "Use streamableHttp, which serves both 2025-era and 2026-07-28 clients."); const app = createApp(); // Start the express server const PORT = process.env.PORT || 3001; const HOST = bindHost(); return listenOrExit(app, PORT, HOST, `Server is running on ${hostAndPort(HOST, PORT)}`); }