| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755 |
- "use strict";
- /**
- * Web Standards Streamable HTTP Server Transport
- *
- * This is the core transport implementation using Web Standard APIs (Request, Response, ReadableStream).
- * It can run on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc.
- *
- * For Node.js Express/HTTP compatibility, use `StreamableHTTPServerTransport` which wraps this transport.
- */
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.WebStandardStreamableHTTPServerTransport = void 0;
- const types_js_1 = require("../types.js");
- /**
- * Server transport for Web Standards Streamable HTTP: this implements the MCP Streamable HTTP transport specification
- * using Web Standard APIs (Request, Response, ReadableStream).
- *
- * This transport works on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc.
- *
- * Usage example:
- *
- * ```typescript
- * // Stateful mode - server sets the session ID
- * const statefulTransport = new WebStandardStreamableHTTPServerTransport({
- * sessionIdGenerator: () => crypto.randomUUID(),
- * });
- *
- * // Stateless mode - explicitly set session ID to undefined
- * const statelessTransport = new WebStandardStreamableHTTPServerTransport({
- * sessionIdGenerator: undefined,
- * });
- *
- * // Hono.js usage
- * app.all('/mcp', async (c) => {
- * return transport.handleRequest(c.req.raw);
- * });
- *
- * // Cloudflare Workers usage
- * export default {
- * async fetch(request: Request): Promise<Response> {
- * return transport.handleRequest(request);
- * }
- * };
- * ```
- *
- * In stateful mode:
- * - Session ID is generated and included in response headers
- * - Session ID is always included in initialization responses
- * - Requests with invalid session IDs are rejected with 404 Not Found
- * - Non-initialization requests without a session ID are rejected with 400 Bad Request
- * - State is maintained in-memory (connections, message history)
- *
- * In stateless mode:
- * - No Session ID is included in any responses
- * - No session validation is performed
- */
- class WebStandardStreamableHTTPServerTransport {
- constructor(options = {}) {
- this._started = false;
- this._hasHandledRequest = 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 = options.enableJsonResponse ?? false;
- this._eventStore = options.eventStore;
- this._onsessioninitialized = options.onsessioninitialized;
- this._onsessionclosed = options.onsessionclosed;
- this._allowedHosts = options.allowedHosts;
- this._allowedOrigins = options.allowedOrigins;
- this._enableDnsRebindingProtection = options.enableDnsRebindingProtection ?? false;
- this._retryInterval = options.retryInterval;
- }
- /**
- * Starts the transport. This is required by the Transport interface but is a no-op
- * for the Streamable HTTP transport as connections are managed per-request.
- */
- async start() {
- if (this._started) {
- throw new Error('Transport already started');
- }
- this._started = true;
- }
- /**
- * Helper to create a JSON error response
- */
- createJsonErrorResponse(status, code, message, options) {
- const error = { code, message };
- if (options?.data !== undefined) {
- error.data = options.data;
- }
- return new Response(JSON.stringify({
- jsonrpc: '2.0',
- error,
- id: null
- }), {
- status,
- headers: {
- 'Content-Type': 'application/json',
- ...options?.headers
- }
- });
- }
- /**
- * Validates request headers for DNS rebinding protection.
- * @returns Error response if validation fails, undefined if validation passes.
- */
- validateRequestHeaders(req) {
- // Skip validation if protection is not enabled
- if (!this._enableDnsRebindingProtection) {
- return undefined;
- }
- // Validate Host header if allowedHosts is configured
- if (this._allowedHosts && this._allowedHosts.length > 0) {
- const hostHeader = req.headers.get('host');
- if (!hostHeader || !this._allowedHosts.includes(hostHeader)) {
- const error = `Invalid Host header: ${hostHeader}`;
- this.onerror?.(new Error(error));
- return this.createJsonErrorResponse(403, -32000, error);
- }
- }
- // Validate Origin header if allowedOrigins is configured
- if (this._allowedOrigins && this._allowedOrigins.length > 0) {
- const originHeader = req.headers.get('origin');
- if (originHeader && !this._allowedOrigins.includes(originHeader)) {
- const error = `Invalid Origin header: ${originHeader}`;
- this.onerror?.(new Error(error));
- return this.createJsonErrorResponse(403, -32000, error);
- }
- }
- return undefined;
- }
- /**
- * Handles an incoming HTTP request, whether GET, POST, or DELETE
- * Returns a Response object (Web Standard)
- */
- async handleRequest(req, options) {
- // In stateless mode (no sessionIdGenerator), each request must use a fresh transport.
- // Reusing a stateless transport causes message ID collisions between clients.
- if (!this.sessionIdGenerator && this._hasHandledRequest) {
- throw new Error('Stateless transport cannot be reused across requests. Create a new transport per request.');
- }
- this._hasHandledRequest = true;
- // Validate request headers for DNS rebinding protection
- const validationError = this.validateRequestHeaders(req);
- if (validationError) {
- return validationError;
- }
- switch (req.method) {
- case 'POST':
- return this.handlePostRequest(req, options);
- case 'GET':
- return this.handleGetRequest(req);
- case 'DELETE':
- return this.handleDeleteRequest(req);
- default:
- return this.handleUnsupportedRequest();
- }
- }
- /**
- * Writes a priming event to establish resumption capability.
- * Only sends if eventStore is configured (opt-in for resumability) and
- * the client's protocol version supports empty SSE data (>= 2025-11-25).
- */
- async writePrimingEvent(controller, encoder, streamId, protocolVersion) {
- if (!this._eventStore) {
- return;
- }
- // Priming events have empty data which older clients cannot handle.
- // Only send priming events to clients with protocol version >= 2025-11-25
- // which includes the fix for handling empty SSE data.
- if (protocolVersion < '2025-11-25') {
- return;
- }
- const primingEventId = await this._eventStore.storeEvent(streamId, {});
- let primingEvent = `id: ${primingEventId}\ndata: \n\n`;
- if (this._retryInterval !== undefined) {
- primingEvent = `id: ${primingEventId}\nretry: ${this._retryInterval}\ndata: \n\n`;
- }
- controller.enqueue(encoder.encode(primingEvent));
- }
- /**
- * Handles GET requests for SSE stream
- */
- async handleGetRequest(req) {
- // The client MUST include an Accept header, listing text/event-stream as a supported content type.
- const acceptHeader = req.headers.get('accept');
- if (!acceptHeader?.includes('text/event-stream')) {
- this.onerror?.(new Error('Not Acceptable: Client must accept text/event-stream'));
- return this.createJsonErrorResponse(406, -32000, 'Not Acceptable: Client must accept text/event-stream');
- }
- // If an Mcp-Session-Id is returned by the server during initialization,
- // clients using the Streamable HTTP transport MUST include it
- // in the Mcp-Session-Id header on all of their subsequent HTTP requests.
- const sessionError = this.validateSession(req);
- if (sessionError) {
- return sessionError;
- }
- const protocolError = this.validateProtocolVersion(req);
- if (protocolError) {
- return protocolError;
- }
- // Handle resumability: check for Last-Event-ID header
- if (this._eventStore) {
- const lastEventId = req.headers.get('last-event-id');
- if (lastEventId) {
- return this.replayEvents(lastEventId);
- }
- }
- // Check if there's already an active standalone SSE stream for this session
- if (this._streamMapping.get(this._standaloneSseStreamId) !== undefined) {
- // Only one GET SSE stream is allowed per session
- this.onerror?.(new Error('Conflict: Only one SSE stream is allowed per session'));
- return this.createJsonErrorResponse(409, -32000, 'Conflict: Only one SSE stream is allowed per session');
- }
- const encoder = new TextEncoder();
- let streamController;
- // Create a ReadableStream with a controller we can use to push SSE events
- const readable = new ReadableStream({
- start: controller => {
- streamController = controller;
- },
- cancel: () => {
- // Stream was cancelled by client
- this._streamMapping.delete(this._standaloneSseStreamId);
- }
- });
- const headers = {
- 'Content-Type': 'text/event-stream',
- 'Cache-Control': 'no-cache, no-transform',
- Connection: 'keep-alive'
- };
- // After initialization, always include the session ID if we have one
- if (this.sessionId !== undefined) {
- headers['mcp-session-id'] = this.sessionId;
- }
- // Store the stream mapping with the controller for pushing data
- this._streamMapping.set(this._standaloneSseStreamId, {
- controller: streamController,
- encoder,
- cleanup: () => {
- this._streamMapping.delete(this._standaloneSseStreamId);
- try {
- streamController.close();
- }
- catch {
- // Controller might already be closed
- }
- }
- });
- return new Response(readable, { headers });
- }
- /**
- * Replays events that would have been sent after the specified event ID
- * Only used when resumability is enabled
- */
- async replayEvents(lastEventId) {
- if (!this._eventStore) {
- this.onerror?.(new Error('Event store not configured'));
- return this.createJsonErrorResponse(400, -32000, 'Event store not configured');
- }
- try {
- // If getStreamIdForEventId is available, use it for conflict checking
- let streamId;
- if (this._eventStore.getStreamIdForEventId) {
- streamId = await this._eventStore.getStreamIdForEventId(lastEventId);
- if (!streamId) {
- this.onerror?.(new Error('Invalid event ID format'));
- return this.createJsonErrorResponse(400, -32000, 'Invalid event ID format');
- }
- // Check conflict with the SAME streamId we'll use for mapping
- if (this._streamMapping.get(streamId) !== undefined) {
- this.onerror?.(new Error('Conflict: Stream already has an active connection'));
- return this.createJsonErrorResponse(409, -32000, 'Conflict: Stream already has an active connection');
- }
- }
- 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;
- }
- // Create a ReadableStream with controller for SSE
- const encoder = new TextEncoder();
- let streamController;
- const readable = new ReadableStream({
- start: controller => {
- streamController = controller;
- },
- cancel: () => {
- // Stream was cancelled by client
- // Cleanup will be handled by the mapping
- }
- });
- // Replay events - returns the streamId for backwards compatibility
- const replayedStreamId = await this._eventStore.replayEventsAfter(lastEventId, {
- send: async (eventId, message) => {
- const success = this.writeSSEEvent(streamController, encoder, message, eventId);
- if (!success) {
- this.onerror?.(new Error('Failed replay events'));
- try {
- streamController.close();
- }
- catch {
- // Controller might already be closed
- }
- }
- }
- });
- this._streamMapping.set(replayedStreamId, {
- controller: streamController,
- encoder,
- cleanup: () => {
- this._streamMapping.delete(replayedStreamId);
- try {
- streamController.close();
- }
- catch {
- // Controller might already be closed
- }
- }
- });
- return new Response(readable, { headers });
- }
- catch (error) {
- this.onerror?.(error);
- return this.createJsonErrorResponse(500, -32000, 'Error replaying events');
- }
- }
- /**
- * Writes an event to an SSE stream via controller with proper formatting
- */
- writeSSEEvent(controller, encoder, message, eventId) {
- try {
- let eventData = `event: message\n`;
- // Include event ID if provided - this is important for resumability
- if (eventId) {
- eventData += `id: ${eventId}\n`;
- }
- eventData += `data: ${JSON.stringify(message)}\n\n`;
- controller.enqueue(encoder.encode(eventData));
- return true;
- }
- catch (error) {
- this.onerror?.(error);
- return false;
- }
- }
- /**
- * Handles unsupported requests (PUT, PATCH, etc.)
- */
- handleUnsupportedRequest() {
- this.onerror?.(new Error('Method not allowed.'));
- return new Response(JSON.stringify({
- jsonrpc: '2.0',
- error: {
- code: -32000,
- message: 'Method not allowed.'
- },
- id: null
- }), {
- status: 405,
- headers: {
- Allow: 'GET, POST, DELETE',
- 'Content-Type': 'application/json'
- }
- });
- }
- /**
- * Handles POST requests containing JSON-RPC messages
- */
- async handlePostRequest(req, options) {
- try {
- // Validate the Accept header
- const acceptHeader = req.headers.get('accept');
- // The client MUST include an Accept header, listing both application/json and text/event-stream as supported content types.
- if (!acceptHeader?.includes('application/json') || !acceptHeader.includes('text/event-stream')) {
- this.onerror?.(new Error('Not Acceptable: Client must accept both application/json and text/event-stream'));
- return this.createJsonErrorResponse(406, -32000, 'Not Acceptable: Client must accept both application/json and text/event-stream');
- }
- const ct = req.headers.get('content-type');
- if (!ct || !ct.includes('application/json')) {
- this.onerror?.(new Error('Unsupported Media Type: Content-Type must be application/json'));
- return this.createJsonErrorResponse(415, -32000, 'Unsupported Media Type: Content-Type must be application/json');
- }
- // Build request info from headers and URL
- const requestInfo = {
- headers: Object.fromEntries(req.headers.entries()),
- url: new URL(req.url)
- };
- let rawMessage;
- if (options?.parsedBody !== undefined) {
- rawMessage = options.parsedBody;
- }
- else {
- try {
- rawMessage = await req.json();
- }
- catch {
- this.onerror?.(new Error('Parse error: Invalid JSON'));
- return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON');
- }
- }
- let messages;
- // handle batch and single messages
- try {
- if (Array.isArray(rawMessage)) {
- messages = rawMessage.map(msg => types_js_1.JSONRPCMessageSchema.parse(msg));
- }
- else {
- messages = [types_js_1.JSONRPCMessageSchema.parse(rawMessage)];
- }
- }
- catch {
- this.onerror?.(new Error('Parse error: Invalid JSON-RPC message'));
- return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON-RPC message');
- }
- // Check if this is an initialization request
- // https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle/
- const isInitializationRequest = messages.some(types_js_1.isInitializeRequest);
- if (isInitializationRequest) {
- // If it's a server with session management and the session ID is already set we should reject the request
- // to avoid re-initialization.
- if (this._initialized && this.sessionId !== undefined) {
- this.onerror?.(new Error('Invalid Request: Server already initialized'));
- return this.createJsonErrorResponse(400, -32600, 'Invalid Request: Server already initialized');
- }
- if (messages.length > 1) {
- this.onerror?.(new Error('Invalid Request: Only one initialization request is allowed'));
- return this.createJsonErrorResponse(400, -32600, 'Invalid Request: Only one initialization request is allowed');
- }
- this.sessionId = this.sessionIdGenerator?.();
- this._initialized = true;
- // If we have a session ID and an onsessioninitialized handler, call it immediately
- // This is needed in cases where the server needs to keep track of multiple sessions
- if (this.sessionId && this._onsessioninitialized) {
- await Promise.resolve(this._onsessioninitialized(this.sessionId));
- }
- }
- if (!isInitializationRequest) {
- // If an Mcp-Session-Id is returned by the server during initialization,
- // clients using the Streamable HTTP transport MUST include it
- // in the Mcp-Session-Id header on all of their subsequent HTTP requests.
- const sessionError = this.validateSession(req);
- if (sessionError) {
- return sessionError;
- }
- // Mcp-Protocol-Version header is required for all requests after initialization.
- const protocolError = this.validateProtocolVersion(req);
- if (protocolError) {
- return protocolError;
- }
- }
- // check if it contains requests
- const hasRequests = messages.some(types_js_1.isJSONRPCRequest);
- if (!hasRequests) {
- // if it only contains notifications or responses, return 202
- for (const message of messages) {
- this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo });
- }
- return new Response(null, { status: 202 });
- }
- // The default behavior is to use SSE streaming
- // but in some cases server will return JSON responses
- const streamId = crypto.randomUUID();
- // Extract protocol version for priming event decision.
- // For initialize requests, get from request params.
- // For other requests, get from header (already validated).
- const initRequest = messages.find(m => (0, types_js_1.isInitializeRequest)(m));
- const clientProtocolVersion = initRequest
- ? initRequest.params.protocolVersion
- : (req.headers.get('mcp-protocol-version') ?? types_js_1.DEFAULT_NEGOTIATED_PROTOCOL_VERSION);
- if (this._enableJsonResponse) {
- // For JSON response mode, return a Promise that resolves when all responses are ready
- return new Promise(resolve => {
- this._streamMapping.set(streamId, {
- resolveJson: resolve,
- cleanup: () => {
- this._streamMapping.delete(streamId);
- }
- });
- for (const message of messages) {
- if ((0, types_js_1.isJSONRPCRequest)(message)) {
- this._requestToStreamMapping.set(message.id, streamId);
- }
- }
- for (const message of messages) {
- this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo });
- }
- });
- }
- // SSE streaming mode - use ReadableStream with controller for more reliable data pushing
- const encoder = new TextEncoder();
- let streamController;
- const readable = new ReadableStream({
- start: controller => {
- streamController = controller;
- },
- cancel: () => {
- // Stream was cancelled by client
- this._streamMapping.delete(streamId);
- }
- });
- const headers = {
- 'Content-Type': 'text/event-stream',
- 'Cache-Control': 'no-cache',
- Connection: 'keep-alive'
- };
- // After initialization, always include the session ID if we have one
- if (this.sessionId !== undefined) {
- headers['mcp-session-id'] = this.sessionId;
- }
- // Store the response for this request to send messages back through this connection
- // We need to track by request ID to maintain the connection
- for (const message of messages) {
- if ((0, types_js_1.isJSONRPCRequest)(message)) {
- this._streamMapping.set(streamId, {
- controller: streamController,
- encoder,
- cleanup: () => {
- this._streamMapping.delete(streamId);
- try {
- streamController.close();
- }
- catch {
- // Controller might already be closed
- }
- }
- });
- this._requestToStreamMapping.set(message.id, streamId);
- }
- }
- // Write priming event if event store is configured (after mapping is set up)
- await this.writePrimingEvent(streamController, encoder, streamId, clientProtocolVersion);
- // handle each message
- for (const message of messages) {
- // Build closeSSEStream callback for requests when eventStore is configured
- // AND client supports resumability (protocol version >= 2025-11-25).
- // Old clients can't resume if the stream is closed early because they
- // didn't receive a priming event with an event ID.
- let closeSSEStream;
- let closeStandaloneSSEStream;
- if ((0, types_js_1.isJSONRPCRequest)(message) && this._eventStore && clientProtocolVersion >= '2025-11-25') {
- closeSSEStream = () => {
- this.closeSSEStream(message.id);
- };
- closeStandaloneSSEStream = () => {
- this.closeStandaloneSSEStream();
- };
- }
- this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo, closeSSEStream, closeStandaloneSSEStream });
- }
- // The server SHOULD NOT close the SSE stream before sending all JSON-RPC responses
- // This will be handled by the send() method when responses are ready
- return new Response(readable, { status: 200, headers });
- }
- catch (error) {
- // return JSON-RPC formatted error
- this.onerror?.(error);
- return this.createJsonErrorResponse(400, -32700, 'Parse error', { data: String(error) });
- }
- }
- /**
- * Handles DELETE requests to terminate sessions
- */
- async handleDeleteRequest(req) {
- const sessionError = this.validateSession(req);
- if (sessionError) {
- return sessionError;
- }
- const protocolError = this.validateProtocolVersion(req);
- if (protocolError) {
- return protocolError;
- }
- await Promise.resolve(this._onsessionclosed?.(this.sessionId));
- await this.close();
- return new Response(null, { status: 200 });
- }
- /**
- * Validates session ID for non-initialization requests.
- * Returns Response error if invalid, undefined otherwise
- */
- validateSession(req) {
- if (this.sessionIdGenerator === undefined) {
- // If the sessionIdGenerator ID is not set, the session management is disabled
- // and we don't need to validate the session ID
- return undefined;
- }
- if (!this._initialized) {
- // If the server has not been initialized yet, reject all requests
- this.onerror?.(new Error('Bad Request: Server not initialized'));
- return this.createJsonErrorResponse(400, -32000, 'Bad Request: Server not initialized');
- }
- const sessionId = req.headers.get('mcp-session-id');
- if (!sessionId) {
- // Non-initialization requests without a session ID should return 400 Bad Request
- this.onerror?.(new Error('Bad Request: Mcp-Session-Id header is required'));
- return this.createJsonErrorResponse(400, -32000, 'Bad Request: Mcp-Session-Id header is required');
- }
- if (sessionId !== this.sessionId) {
- // Reject requests with invalid session ID with 404 Not Found
- this.onerror?.(new Error('Session not found'));
- return this.createJsonErrorResponse(404, -32001, 'Session not found');
- }
- return undefined;
- }
- /**
- * Validates the MCP-Protocol-Version header on incoming requests.
- *
- * For initialization: Version negotiation handles unknown versions gracefully
- * (server responds with its supported version).
- *
- * For subsequent requests with MCP-Protocol-Version header:
- * - Accept if in supported list
- * - 400 if unsupported
- *
- * For HTTP requests without the MCP-Protocol-Version header:
- * - Accept and default to the version negotiated at initialization
- */
- validateProtocolVersion(req) {
- const protocolVersion = req.headers.get('mcp-protocol-version');
- if (protocolVersion !== null && !types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {
- this.onerror?.(new Error(`Bad Request: Unsupported protocol version: ${protocolVersion}` +
- ` (supported versions: ${types_js_1.SUPPORTED_PROTOCOL_VERSIONS.join(', ')})`));
- return this.createJsonErrorResponse(400, -32000, `Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${types_js_1.SUPPORTED_PROTOCOL_VERSIONS.join(', ')})`);
- }
- return undefined;
- }
- async close() {
- // Close all SSE connections
- this._streamMapping.forEach(({ cleanup }) => {
- cleanup();
- });
- this._streamMapping.clear();
- // Clear any pending responses
- this._requestResponseMap.clear();
- this.onclose?.();
- }
- /**
- * Close an SSE stream for a specific request, triggering client reconnection.
- * Use this to implement polling behavior during long-running operations -
- * client will reconnect after the retry interval specified in the priming event.
- */
- closeSSEStream(requestId) {
- const streamId = this._requestToStreamMapping.get(requestId);
- if (!streamId)
- return;
- const stream = this._streamMapping.get(streamId);
- if (stream) {
- stream.cleanup();
- }
- }
- /**
- * Close the standalone GET SSE stream, triggering client reconnection.
- * Use this to implement polling behavior for server-initiated notifications.
- */
- closeStandaloneSSEStream() {
- const stream = this._streamMapping.get(this._standaloneSseStreamId);
- if (stream) {
- stream.cleanup();
- }
- }
- async send(message, options) {
- let requestId = options?.relatedRequestId;
- if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) {
- // If the message is a response, use the request ID from the message
- requestId = message.id;
- }
- // Check if this message should be sent on the standalone SSE stream (no request ID)
- // Ignore notifications from tools (which have relatedRequestId set)
- // Those will be sent via dedicated response SSE streams
- if (requestId === undefined) {
- // For standalone SSE streams, we can only send requests and notifications
- if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) {
- throw new Error('Cannot send a response on a standalone SSE stream unless resuming a previous client request');
- }
- // Generate and store event ID if event store is provided
- // Store even if stream is disconnected so events can be replayed on reconnect
- let eventId;
- if (this._eventStore) {
- // Stores the event and gets the generated event ID
- eventId = await this._eventStore.storeEvent(this._standaloneSseStreamId, message);
- }
- const standaloneSse = this._streamMapping.get(this._standaloneSseStreamId);
- if (standaloneSse === undefined) {
- // Stream is disconnected - event is stored for replay, nothing more to do
- return;
- }
- // Send the message to the standalone SSE stream
- if (standaloneSse.controller && standaloneSse.encoder) {
- this.writeSSEEvent(standaloneSse.controller, standaloneSse.encoder, message, eventId);
- }
- return;
- }
- // Get the response for this request
- const streamId = this._requestToStreamMapping.get(requestId);
- if (!streamId) {
- throw new Error(`No connection established for request ID: ${String(requestId)}`);
- }
- const stream = this._streamMapping.get(streamId);
- if (!this._enableJsonResponse && stream?.controller && stream?.encoder) {
- // For SSE responses, generate event ID if event store is provided
- let eventId;
- if (this._eventStore) {
- eventId = await this._eventStore.storeEvent(streamId, message);
- }
- // Write the event to the response stream
- this.writeSSEEvent(stream.controller, stream.encoder, message, eventId);
- }
- if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) {
- this._requestResponseMap.set(requestId, message);
- const relatedIds = Array.from(this._requestToStreamMapping.entries())
- .filter(([_, sid]) => sid === streamId)
- .map(([id]) => id);
- // Check if we have responses for all requests using this connection
- const allResponsesReady = relatedIds.every(id => this._requestResponseMap.has(id));
- if (allResponsesReady) {
- if (!stream) {
- throw new Error(`No connection established for request ID: ${String(requestId)}`);
- }
- if (this._enableJsonResponse && stream.resolveJson) {
- // All responses ready, send as JSON
- 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));
- if (responses.length === 1) {
- stream.resolveJson(new Response(JSON.stringify(responses[0]), { status: 200, headers }));
- }
- else {
- stream.resolveJson(new Response(JSON.stringify(responses), { status: 200, headers }));
- }
- }
- else {
- // End the SSE stream
- stream.cleanup();
- }
- // Clean up
- for (const id of relatedIds) {
- this._requestResponseMap.delete(id);
- this._requestToStreamMapping.delete(id);
- }
- }
- }
- }
- }
- exports.WebStandardStreamableHTTPServerTransport = WebStandardStreamableHTTPServerTransport;
- //# sourceMappingURL=webStandardStreamableHttp.js.map
|