| "use strict"; |
| var __importDefault = (this && this.__importDefault) || function (mod) { |
| return (mod && mod.__esModule) ? mod : { "default": mod }; |
| }; |
| Object.defineProperty(exports, "__esModule", { value: true }); |
| exports.StreamableHTTPServerTransport = void 0; |
| const types_js_1 = require("../types.js"); |
| const raw_body_1 = __importDefault(require("raw-body")); |
| const content_type_1 = __importDefault(require("content-type")); |
| const node_crypto_1 = require("node:crypto"); |
| const MAXIMUM_MESSAGE_SIZE = "4mb"; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| class StreamableHTTPServerTransport { |
| constructor(options) { |
| var _a; |
| this._started = false; |
| this._streamMapping = new Map(); |
| this._requestToStreamMapping = new Map(); |
| this._requestResponseMap = new Map(); |
| this._initialized = false; |
| this._enableJsonResponse = false; |
| this._standaloneSseStreamId = '_GET_stream'; |
| this.sessionIdGenerator = options.sessionIdGenerator; |
| this._enableJsonResponse = (_a = options.enableJsonResponse) !== null && _a !== void 0 ? _a : false; |
| this._eventStore = options.eventStore; |
| this._onsessioninitialized = options.onsessioninitialized; |
| } |
| |
| |
| |
| |
| async start() { |
| if (this._started) { |
| throw new Error("Transport already started"); |
| } |
| this._started = true; |
| } |
| |
| |
| |
| async handleRequest(req, res, parsedBody) { |
| if (req.method === "POST") { |
| await this.handlePostRequest(req, res, parsedBody); |
| } |
| else if (req.method === "GET") { |
| await this.handleGetRequest(req, res); |
| } |
| else if (req.method === "DELETE") { |
| await this.handleDeleteRequest(req, res); |
| } |
| else { |
| await this.handleUnsupportedRequest(res); |
| } |
| } |
| |
| |
| |
| async handleGetRequest(req, res) { |
| |
| const acceptHeader = req.headers.accept; |
| if (!(acceptHeader === null || acceptHeader === void 0 ? void 0 : acceptHeader.includes("text/event-stream"))) { |
| res.writeHead(406).end(JSON.stringify({ |
| jsonrpc: "2.0", |
| error: { |
| code: -32000, |
| message: "Not Acceptable: Client must accept text/event-stream" |
| }, |
| id: null |
| })); |
| return; |
| } |
| |
| |
| |
| if (!this.validateSession(req, res)) { |
| return; |
| } |
| |
| if (this._eventStore) { |
| const lastEventId = req.headers['last-event-id']; |
| if (lastEventId) { |
| await this.replayEvents(lastEventId, res); |
| return; |
| } |
| } |
| |
| |
| const headers = { |
| "Content-Type": "text/event-stream", |
| "Cache-Control": "no-cache, no-transform", |
| Connection: "keep-alive", |
| }; |
| |
| if (this.sessionId !== undefined) { |
| headers["mcp-session-id"] = this.sessionId; |
| } |
| |
| if (this._streamMapping.get(this._standaloneSseStreamId) !== undefined) { |
| |
| res.writeHead(409).end(JSON.stringify({ |
| jsonrpc: "2.0", |
| error: { |
| code: -32000, |
| message: "Conflict: Only one SSE stream is allowed per session" |
| }, |
| id: null |
| })); |
| return; |
| } |
| |
| |
| res.writeHead(200, headers).flushHeaders(); |
| |
| this._streamMapping.set(this._standaloneSseStreamId, res); |
| |
| res.on("close", () => { |
| this._streamMapping.delete(this._standaloneSseStreamId); |
| }); |
| } |
| |
| |
| |
| |
| async replayEvents(lastEventId, res) { |
| var _a, _b; |
| if (!this._eventStore) { |
| return; |
| } |
| try { |
| const headers = { |
| "Content-Type": "text/event-stream", |
| "Cache-Control": "no-cache, no-transform", |
| Connection: "keep-alive", |
| }; |
| if (this.sessionId !== undefined) { |
| headers["mcp-session-id"] = this.sessionId; |
| } |
| res.writeHead(200, headers).flushHeaders(); |
| const streamId = await ((_a = this._eventStore) === null || _a === void 0 ? void 0 : _a.replayEventsAfter(lastEventId, { |
| send: async (eventId, message) => { |
| var _a; |
| if (!this.writeSSEEvent(res, message, eventId)) { |
| (_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, new Error("Failed replay events")); |
| res.end(); |
| } |
| } |
| })); |
| this._streamMapping.set(streamId, res); |
| } |
| catch (error) { |
| (_b = this.onerror) === null || _b === void 0 ? void 0 : _b.call(this, error); |
| } |
| } |
| |
| |
| |
| writeSSEEvent(res, message, eventId) { |
| let eventData = `event: message\n`; |
| |
| if (eventId) { |
| eventData += `id: ${eventId}\n`; |
| } |
| eventData += `data: ${JSON.stringify(message)}\n\n`; |
| return res.write(eventData); |
| } |
| |
| |
| |
| async handleUnsupportedRequest(res) { |
| res.writeHead(405, { |
| "Allow": "GET, POST, DELETE" |
| }).end(JSON.stringify({ |
| jsonrpc: "2.0", |
| error: { |
| code: -32000, |
| message: "Method not allowed." |
| }, |
| id: null |
| })); |
| } |
| |
| |
| |
| async handlePostRequest(req, res, parsedBody) { |
| var _a, _b, _c, _d, _e; |
| try { |
| |
| const acceptHeader = req.headers.accept; |
| |
| if (!(acceptHeader === null || acceptHeader === void 0 ? void 0 : acceptHeader.includes("application/json")) || !acceptHeader.includes("text/event-stream")) { |
| res.writeHead(406).end(JSON.stringify({ |
| jsonrpc: "2.0", |
| error: { |
| code: -32000, |
| message: "Not Acceptable: Client must accept both application/json and text/event-stream" |
| }, |
| id: null |
| })); |
| return; |
| } |
| const ct = req.headers["content-type"]; |
| if (!ct || !ct.includes("application/json")) { |
| res.writeHead(415).end(JSON.stringify({ |
| jsonrpc: "2.0", |
| error: { |
| code: -32000, |
| message: "Unsupported Media Type: Content-Type must be application/json" |
| }, |
| id: null |
| })); |
| return; |
| } |
| let rawMessage; |
| if (parsedBody !== undefined) { |
| rawMessage = parsedBody; |
| } |
| else { |
| const parsedCt = content_type_1.default.parse(ct); |
| const body = await (0, raw_body_1.default)(req, { |
| limit: MAXIMUM_MESSAGE_SIZE, |
| encoding: (_a = parsedCt.parameters.charset) !== null && _a !== void 0 ? _a : "utf-8", |
| }); |
| rawMessage = JSON.parse(body.toString()); |
| } |
| let messages; |
| |
| if (Array.isArray(rawMessage)) { |
| messages = rawMessage.map(msg => types_js_1.JSONRPCMessageSchema.parse(msg)); |
| } |
| else { |
| messages = [types_js_1.JSONRPCMessageSchema.parse(rawMessage)]; |
| } |
| |
| |
| const isInitializationRequest = messages.some(types_js_1.isInitializeRequest); |
| if (isInitializationRequest) { |
| |
| |
| if (this._initialized && this.sessionId !== undefined) { |
| res.writeHead(400).end(JSON.stringify({ |
| jsonrpc: "2.0", |
| error: { |
| code: -32600, |
| message: "Invalid Request: Server already initialized" |
| }, |
| id: null |
| })); |
| return; |
| } |
| if (messages.length > 1) { |
| res.writeHead(400).end(JSON.stringify({ |
| jsonrpc: "2.0", |
| error: { |
| code: -32600, |
| message: "Invalid Request: Only one initialization request is allowed" |
| }, |
| id: null |
| })); |
| return; |
| } |
| this.sessionId = (_b = this.sessionIdGenerator) === null || _b === void 0 ? void 0 : _b.call(this); |
| this._initialized = true; |
| |
| |
| if (this.sessionId && this._onsessioninitialized) { |
| this._onsessioninitialized(this.sessionId); |
| } |
| } |
| |
| |
| |
| if (!isInitializationRequest && !this.validateSession(req, res)) { |
| return; |
| } |
| |
| const hasRequests = messages.some(types_js_1.isJSONRPCRequest); |
| if (!hasRequests) { |
| |
| res.writeHead(202).end(); |
| |
| for (const message of messages) { |
| (_c = this.onmessage) === null || _c === void 0 ? void 0 : _c.call(this, message); |
| } |
| } |
| else if (hasRequests) { |
| |
| |
| const streamId = (0, node_crypto_1.randomUUID)(); |
| if (!this._enableJsonResponse) { |
| const headers = { |
| "Content-Type": "text/event-stream", |
| "Cache-Control": "no-cache", |
| Connection: "keep-alive", |
| }; |
| |
| if (this.sessionId !== undefined) { |
| headers["mcp-session-id"] = this.sessionId; |
| } |
| res.writeHead(200, headers); |
| } |
| |
| |
| for (const message of messages) { |
| if ((0, types_js_1.isJSONRPCRequest)(message)) { |
| this._streamMapping.set(streamId, res); |
| this._requestToStreamMapping.set(message.id, streamId); |
| } |
| } |
| |
| res.on("close", () => { |
| this._streamMapping.delete(streamId); |
| }); |
| |
| for (const message of messages) { |
| (_d = this.onmessage) === null || _d === void 0 ? void 0 : _d.call(this, message); |
| } |
| |
| |
| } |
| } |
| catch (error) { |
| |
| res.writeHead(400).end(JSON.stringify({ |
| jsonrpc: "2.0", |
| error: { |
| code: -32700, |
| message: "Parse error", |
| data: String(error) |
| }, |
| id: null |
| })); |
| (_e = this.onerror) === null || _e === void 0 ? void 0 : _e.call(this, error); |
| } |
| } |
| |
| |
| |
| async handleDeleteRequest(req, res) { |
| if (!this.validateSession(req, res)) { |
| return; |
| } |
| await this.close(); |
| res.writeHead(200).end(); |
| } |
| |
| |
| |
| |
| validateSession(req, res) { |
| if (this.sessionIdGenerator === undefined) { |
| |
| |
| return true; |
| } |
| if (!this._initialized) { |
| |
| res.writeHead(400).end(JSON.stringify({ |
| jsonrpc: "2.0", |
| error: { |
| code: -32000, |
| message: "Bad Request: Server not initialized" |
| }, |
| id: null |
| })); |
| return false; |
| } |
| const sessionId = req.headers["mcp-session-id"]; |
| if (!sessionId) { |
| |
| res.writeHead(400).end(JSON.stringify({ |
| jsonrpc: "2.0", |
| error: { |
| code: -32000, |
| message: "Bad Request: Mcp-Session-Id header is required" |
| }, |
| id: null |
| })); |
| return false; |
| } |
| else if (Array.isArray(sessionId)) { |
| res.writeHead(400).end(JSON.stringify({ |
| jsonrpc: "2.0", |
| error: { |
| code: -32000, |
| message: "Bad Request: Mcp-Session-Id header must be a single value" |
| }, |
| id: null |
| })); |
| return false; |
| } |
| else if (sessionId !== this.sessionId) { |
| |
| res.writeHead(404).end(JSON.stringify({ |
| jsonrpc: "2.0", |
| error: { |
| code: -32001, |
| message: "Session not found" |
| }, |
| id: null |
| })); |
| return false; |
| } |
| return true; |
| } |
| async close() { |
| var _a; |
| |
| this._streamMapping.forEach((response) => { |
| response.end(); |
| }); |
| this._streamMapping.clear(); |
| |
| this._requestResponseMap.clear(); |
| (_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this); |
| } |
| async send(message, options) { |
| let requestId = options === null || options === void 0 ? void 0 : options.relatedRequestId; |
| if ((0, types_js_1.isJSONRPCResponse)(message) || (0, types_js_1.isJSONRPCError)(message)) { |
| |
| requestId = message.id; |
| } |
| |
| |
| |
| if (requestId === undefined) { |
| |
| if ((0, types_js_1.isJSONRPCResponse)(message) || (0, types_js_1.isJSONRPCError)(message)) { |
| throw new Error("Cannot send a response on a standalone SSE stream unless resuming a previous client request"); |
| } |
| const standaloneSse = this._streamMapping.get(this._standaloneSseStreamId); |
| if (standaloneSse === undefined) { |
| |
| return; |
| } |
| |
| let eventId; |
| if (this._eventStore) { |
| |
| eventId = await this._eventStore.storeEvent(this._standaloneSseStreamId, message); |
| } |
| |
| this.writeSSEEvent(standaloneSse, message, eventId); |
| return; |
| } |
| |
| const streamId = this._requestToStreamMapping.get(requestId); |
| const response = this._streamMapping.get(streamId); |
| if (!streamId) { |
| throw new Error(`No connection established for request ID: ${String(requestId)}`); |
| } |
| if (!this._enableJsonResponse) { |
| |
| let eventId; |
| if (this._eventStore) { |
| eventId = await this._eventStore.storeEvent(streamId, message); |
| } |
| if (response) { |
| |
| this.writeSSEEvent(response, message, eventId); |
| } |
| } |
| if ((0, types_js_1.isJSONRPCResponse)(message) || (0, types_js_1.isJSONRPCError)(message)) { |
| this._requestResponseMap.set(requestId, message); |
| const relatedIds = Array.from(this._requestToStreamMapping.entries()) |
| .filter(([_, streamId]) => this._streamMapping.get(streamId) === response) |
| .map(([id]) => id); |
| |
| const allResponsesReady = relatedIds.every(id => this._requestResponseMap.has(id)); |
| if (allResponsesReady) { |
| if (!response) { |
| throw new Error(`No connection established for request ID: ${String(requestId)}`); |
| } |
| if (this._enableJsonResponse) { |
| |
| const headers = { |
| 'Content-Type': 'application/json', |
| }; |
| if (this.sessionId !== undefined) { |
| headers['mcp-session-id'] = this.sessionId; |
| } |
| const responses = relatedIds |
| .map(id => this._requestResponseMap.get(id)); |
| response.writeHead(200, headers); |
| if (responses.length === 1) { |
| response.end(JSON.stringify(responses[0])); |
| } |
| else { |
| response.end(JSON.stringify(responses)); |
| } |
| } |
| else { |
| |
| response.end(); |
| } |
| |
| for (const id of relatedIds) { |
| this._requestResponseMap.delete(id); |
| this._requestToStreamMapping.delete(id); |
| } |
| } |
| } |
| } |
| } |
| exports.StreamableHTTPServerTransport = StreamableHTTPServerTransport; |
| |