webStandardStreamableHttp.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  1. "use strict";
  2. /**
  3. * Web Standards Streamable HTTP Server Transport
  4. *
  5. * This is the core transport implementation using Web Standard APIs (Request, Response, ReadableStream).
  6. * It can run on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc.
  7. *
  8. * For Node.js Express/HTTP compatibility, use `StreamableHTTPServerTransport` which wraps this transport.
  9. */
  10. Object.defineProperty(exports, "__esModule", { value: true });
  11. exports.WebStandardStreamableHTTPServerTransport = void 0;
  12. const types_js_1 = require("../types.js");
  13. /**
  14. * Server transport for Web Standards Streamable HTTP: this implements the MCP Streamable HTTP transport specification
  15. * using Web Standard APIs (Request, Response, ReadableStream).
  16. *
  17. * This transport works on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc.
  18. *
  19. * Usage example:
  20. *
  21. * ```typescript
  22. * // Stateful mode - server sets the session ID
  23. * const statefulTransport = new WebStandardStreamableHTTPServerTransport({
  24. * sessionIdGenerator: () => crypto.randomUUID(),
  25. * });
  26. *
  27. * // Stateless mode - explicitly set session ID to undefined
  28. * const statelessTransport = new WebStandardStreamableHTTPServerTransport({
  29. * sessionIdGenerator: undefined,
  30. * });
  31. *
  32. * // Hono.js usage
  33. * app.all('/mcp', async (c) => {
  34. * return transport.handleRequest(c.req.raw);
  35. * });
  36. *
  37. * // Cloudflare Workers usage
  38. * export default {
  39. * async fetch(request: Request): Promise<Response> {
  40. * return transport.handleRequest(request);
  41. * }
  42. * };
  43. * ```
  44. *
  45. * In stateful mode:
  46. * - Session ID is generated and included in response headers
  47. * - Session ID is always included in initialization responses
  48. * - Requests with invalid session IDs are rejected with 404 Not Found
  49. * - Non-initialization requests without a session ID are rejected with 400 Bad Request
  50. * - State is maintained in-memory (connections, message history)
  51. *
  52. * In stateless mode:
  53. * - No Session ID is included in any responses
  54. * - No session validation is performed
  55. */
  56. class WebStandardStreamableHTTPServerTransport {
  57. constructor(options = {}) {
  58. this._started = false;
  59. this._hasHandledRequest = false;
  60. this._streamMapping = new Map();
  61. this._requestToStreamMapping = new Map();
  62. this._requestResponseMap = new Map();
  63. this._initialized = false;
  64. this._enableJsonResponse = false;
  65. this._standaloneSseStreamId = '_GET_stream';
  66. this.sessionIdGenerator = options.sessionIdGenerator;
  67. this._enableJsonResponse = options.enableJsonResponse ?? false;
  68. this._eventStore = options.eventStore;
  69. this._onsessioninitialized = options.onsessioninitialized;
  70. this._onsessionclosed = options.onsessionclosed;
  71. this._allowedHosts = options.allowedHosts;
  72. this._allowedOrigins = options.allowedOrigins;
  73. this._enableDnsRebindingProtection = options.enableDnsRebindingProtection ?? false;
  74. this._retryInterval = options.retryInterval;
  75. }
  76. /**
  77. * Starts the transport. This is required by the Transport interface but is a no-op
  78. * for the Streamable HTTP transport as connections are managed per-request.
  79. */
  80. async start() {
  81. if (this._started) {
  82. throw new Error('Transport already started');
  83. }
  84. this._started = true;
  85. }
  86. /**
  87. * Helper to create a JSON error response
  88. */
  89. createJsonErrorResponse(status, code, message, options) {
  90. const error = { code, message };
  91. if (options?.data !== undefined) {
  92. error.data = options.data;
  93. }
  94. return new Response(JSON.stringify({
  95. jsonrpc: '2.0',
  96. error,
  97. id: null
  98. }), {
  99. status,
  100. headers: {
  101. 'Content-Type': 'application/json',
  102. ...options?.headers
  103. }
  104. });
  105. }
  106. /**
  107. * Validates request headers for DNS rebinding protection.
  108. * @returns Error response if validation fails, undefined if validation passes.
  109. */
  110. validateRequestHeaders(req) {
  111. // Skip validation if protection is not enabled
  112. if (!this._enableDnsRebindingProtection) {
  113. return undefined;
  114. }
  115. // Validate Host header if allowedHosts is configured
  116. if (this._allowedHosts && this._allowedHosts.length > 0) {
  117. const hostHeader = req.headers.get('host');
  118. if (!hostHeader || !this._allowedHosts.includes(hostHeader)) {
  119. const error = `Invalid Host header: ${hostHeader}`;
  120. this.onerror?.(new Error(error));
  121. return this.createJsonErrorResponse(403, -32000, error);
  122. }
  123. }
  124. // Validate Origin header if allowedOrigins is configured
  125. if (this._allowedOrigins && this._allowedOrigins.length > 0) {
  126. const originHeader = req.headers.get('origin');
  127. if (originHeader && !this._allowedOrigins.includes(originHeader)) {
  128. const error = `Invalid Origin header: ${originHeader}`;
  129. this.onerror?.(new Error(error));
  130. return this.createJsonErrorResponse(403, -32000, error);
  131. }
  132. }
  133. return undefined;
  134. }
  135. /**
  136. * Handles an incoming HTTP request, whether GET, POST, or DELETE
  137. * Returns a Response object (Web Standard)
  138. */
  139. async handleRequest(req, options) {
  140. // In stateless mode (no sessionIdGenerator), each request must use a fresh transport.
  141. // Reusing a stateless transport causes message ID collisions between clients.
  142. if (!this.sessionIdGenerator && this._hasHandledRequest) {
  143. throw new Error('Stateless transport cannot be reused across requests. Create a new transport per request.');
  144. }
  145. this._hasHandledRequest = true;
  146. // Validate request headers for DNS rebinding protection
  147. const validationError = this.validateRequestHeaders(req);
  148. if (validationError) {
  149. return validationError;
  150. }
  151. switch (req.method) {
  152. case 'POST':
  153. return this.handlePostRequest(req, options);
  154. case 'GET':
  155. return this.handleGetRequest(req);
  156. case 'DELETE':
  157. return this.handleDeleteRequest(req);
  158. default:
  159. return this.handleUnsupportedRequest();
  160. }
  161. }
  162. /**
  163. * Writes a priming event to establish resumption capability.
  164. * Only sends if eventStore is configured (opt-in for resumability) and
  165. * the client's protocol version supports empty SSE data (>= 2025-11-25).
  166. */
  167. async writePrimingEvent(controller, encoder, streamId, protocolVersion) {
  168. if (!this._eventStore) {
  169. return;
  170. }
  171. // Priming events have empty data which older clients cannot handle.
  172. // Only send priming events to clients with protocol version >= 2025-11-25
  173. // which includes the fix for handling empty SSE data.
  174. if (protocolVersion < '2025-11-25') {
  175. return;
  176. }
  177. const primingEventId = await this._eventStore.storeEvent(streamId, {});
  178. let primingEvent = `id: ${primingEventId}\ndata: \n\n`;
  179. if (this._retryInterval !== undefined) {
  180. primingEvent = `id: ${primingEventId}\nretry: ${this._retryInterval}\ndata: \n\n`;
  181. }
  182. controller.enqueue(encoder.encode(primingEvent));
  183. }
  184. /**
  185. * Handles GET requests for SSE stream
  186. */
  187. async handleGetRequest(req) {
  188. // The client MUST include an Accept header, listing text/event-stream as a supported content type.
  189. const acceptHeader = req.headers.get('accept');
  190. if (!acceptHeader?.includes('text/event-stream')) {
  191. this.onerror?.(new Error('Not Acceptable: Client must accept text/event-stream'));
  192. return this.createJsonErrorResponse(406, -32000, 'Not Acceptable: Client must accept text/event-stream');
  193. }
  194. // If an Mcp-Session-Id is returned by the server during initialization,
  195. // clients using the Streamable HTTP transport MUST include it
  196. // in the Mcp-Session-Id header on all of their subsequent HTTP requests.
  197. const sessionError = this.validateSession(req);
  198. if (sessionError) {
  199. return sessionError;
  200. }
  201. const protocolError = this.validateProtocolVersion(req);
  202. if (protocolError) {
  203. return protocolError;
  204. }
  205. // Handle resumability: check for Last-Event-ID header
  206. if (this._eventStore) {
  207. const lastEventId = req.headers.get('last-event-id');
  208. if (lastEventId) {
  209. return this.replayEvents(lastEventId);
  210. }
  211. }
  212. // Check if there's already an active standalone SSE stream for this session
  213. if (this._streamMapping.get(this._standaloneSseStreamId) !== undefined) {
  214. // Only one GET SSE stream is allowed per session
  215. this.onerror?.(new Error('Conflict: Only one SSE stream is allowed per session'));
  216. return this.createJsonErrorResponse(409, -32000, 'Conflict: Only one SSE stream is allowed per session');
  217. }
  218. const encoder = new TextEncoder();
  219. let streamController;
  220. // Create a ReadableStream with a controller we can use to push SSE events
  221. const readable = new ReadableStream({
  222. start: controller => {
  223. streamController = controller;
  224. },
  225. cancel: () => {
  226. // Stream was cancelled by client
  227. this._streamMapping.delete(this._standaloneSseStreamId);
  228. }
  229. });
  230. const headers = {
  231. 'Content-Type': 'text/event-stream',
  232. 'Cache-Control': 'no-cache, no-transform',
  233. Connection: 'keep-alive'
  234. };
  235. // After initialization, always include the session ID if we have one
  236. if (this.sessionId !== undefined) {
  237. headers['mcp-session-id'] = this.sessionId;
  238. }
  239. // Store the stream mapping with the controller for pushing data
  240. this._streamMapping.set(this._standaloneSseStreamId, {
  241. controller: streamController,
  242. encoder,
  243. cleanup: () => {
  244. this._streamMapping.delete(this._standaloneSseStreamId);
  245. try {
  246. streamController.close();
  247. }
  248. catch {
  249. // Controller might already be closed
  250. }
  251. }
  252. });
  253. return new Response(readable, { headers });
  254. }
  255. /**
  256. * Replays events that would have been sent after the specified event ID
  257. * Only used when resumability is enabled
  258. */
  259. async replayEvents(lastEventId) {
  260. if (!this._eventStore) {
  261. this.onerror?.(new Error('Event store not configured'));
  262. return this.createJsonErrorResponse(400, -32000, 'Event store not configured');
  263. }
  264. try {
  265. // If getStreamIdForEventId is available, use it for conflict checking
  266. let streamId;
  267. if (this._eventStore.getStreamIdForEventId) {
  268. streamId = await this._eventStore.getStreamIdForEventId(lastEventId);
  269. if (!streamId) {
  270. this.onerror?.(new Error('Invalid event ID format'));
  271. return this.createJsonErrorResponse(400, -32000, 'Invalid event ID format');
  272. }
  273. // Check conflict with the SAME streamId we'll use for mapping
  274. if (this._streamMapping.get(streamId) !== undefined) {
  275. this.onerror?.(new Error('Conflict: Stream already has an active connection'));
  276. return this.createJsonErrorResponse(409, -32000, 'Conflict: Stream already has an active connection');
  277. }
  278. }
  279. const headers = {
  280. 'Content-Type': 'text/event-stream',
  281. 'Cache-Control': 'no-cache, no-transform',
  282. Connection: 'keep-alive'
  283. };
  284. if (this.sessionId !== undefined) {
  285. headers['mcp-session-id'] = this.sessionId;
  286. }
  287. // Create a ReadableStream with controller for SSE
  288. const encoder = new TextEncoder();
  289. let streamController;
  290. const readable = new ReadableStream({
  291. start: controller => {
  292. streamController = controller;
  293. },
  294. cancel: () => {
  295. // Stream was cancelled by client
  296. // Cleanup will be handled by the mapping
  297. }
  298. });
  299. // Replay events - returns the streamId for backwards compatibility
  300. const replayedStreamId = await this._eventStore.replayEventsAfter(lastEventId, {
  301. send: async (eventId, message) => {
  302. const success = this.writeSSEEvent(streamController, encoder, message, eventId);
  303. if (!success) {
  304. this.onerror?.(new Error('Failed replay events'));
  305. try {
  306. streamController.close();
  307. }
  308. catch {
  309. // Controller might already be closed
  310. }
  311. }
  312. }
  313. });
  314. this._streamMapping.set(replayedStreamId, {
  315. controller: streamController,
  316. encoder,
  317. cleanup: () => {
  318. this._streamMapping.delete(replayedStreamId);
  319. try {
  320. streamController.close();
  321. }
  322. catch {
  323. // Controller might already be closed
  324. }
  325. }
  326. });
  327. return new Response(readable, { headers });
  328. }
  329. catch (error) {
  330. this.onerror?.(error);
  331. return this.createJsonErrorResponse(500, -32000, 'Error replaying events');
  332. }
  333. }
  334. /**
  335. * Writes an event to an SSE stream via controller with proper formatting
  336. */
  337. writeSSEEvent(controller, encoder, message, eventId) {
  338. try {
  339. let eventData = `event: message\n`;
  340. // Include event ID if provided - this is important for resumability
  341. if (eventId) {
  342. eventData += `id: ${eventId}\n`;
  343. }
  344. eventData += `data: ${JSON.stringify(message)}\n\n`;
  345. controller.enqueue(encoder.encode(eventData));
  346. return true;
  347. }
  348. catch (error) {
  349. this.onerror?.(error);
  350. return false;
  351. }
  352. }
  353. /**
  354. * Handles unsupported requests (PUT, PATCH, etc.)
  355. */
  356. handleUnsupportedRequest() {
  357. this.onerror?.(new Error('Method not allowed.'));
  358. return new Response(JSON.stringify({
  359. jsonrpc: '2.0',
  360. error: {
  361. code: -32000,
  362. message: 'Method not allowed.'
  363. },
  364. id: null
  365. }), {
  366. status: 405,
  367. headers: {
  368. Allow: 'GET, POST, DELETE',
  369. 'Content-Type': 'application/json'
  370. }
  371. });
  372. }
  373. /**
  374. * Handles POST requests containing JSON-RPC messages
  375. */
  376. async handlePostRequest(req, options) {
  377. try {
  378. // Validate the Accept header
  379. const acceptHeader = req.headers.get('accept');
  380. // The client MUST include an Accept header, listing both application/json and text/event-stream as supported content types.
  381. if (!acceptHeader?.includes('application/json') || !acceptHeader.includes('text/event-stream')) {
  382. this.onerror?.(new Error('Not Acceptable: Client must accept both application/json and text/event-stream'));
  383. return this.createJsonErrorResponse(406, -32000, 'Not Acceptable: Client must accept both application/json and text/event-stream');
  384. }
  385. const ct = req.headers.get('content-type');
  386. if (!ct || !ct.includes('application/json')) {
  387. this.onerror?.(new Error('Unsupported Media Type: Content-Type must be application/json'));
  388. return this.createJsonErrorResponse(415, -32000, 'Unsupported Media Type: Content-Type must be application/json');
  389. }
  390. // Build request info from headers and URL
  391. const requestInfo = {
  392. headers: Object.fromEntries(req.headers.entries()),
  393. url: new URL(req.url)
  394. };
  395. let rawMessage;
  396. if (options?.parsedBody !== undefined) {
  397. rawMessage = options.parsedBody;
  398. }
  399. else {
  400. try {
  401. rawMessage = await req.json();
  402. }
  403. catch {
  404. this.onerror?.(new Error('Parse error: Invalid JSON'));
  405. return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON');
  406. }
  407. }
  408. let messages;
  409. // handle batch and single messages
  410. try {
  411. if (Array.isArray(rawMessage)) {
  412. messages = rawMessage.map(msg => types_js_1.JSONRPCMessageSchema.parse(msg));
  413. }
  414. else {
  415. messages = [types_js_1.JSONRPCMessageSchema.parse(rawMessage)];
  416. }
  417. }
  418. catch {
  419. this.onerror?.(new Error('Parse error: Invalid JSON-RPC message'));
  420. return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON-RPC message');
  421. }
  422. // Check if this is an initialization request
  423. // https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle/
  424. const isInitializationRequest = messages.some(types_js_1.isInitializeRequest);
  425. if (isInitializationRequest) {
  426. // If it's a server with session management and the session ID is already set we should reject the request
  427. // to avoid re-initialization.
  428. if (this._initialized && this.sessionId !== undefined) {
  429. this.onerror?.(new Error('Invalid Request: Server already initialized'));
  430. return this.createJsonErrorResponse(400, -32600, 'Invalid Request: Server already initialized');
  431. }
  432. if (messages.length > 1) {
  433. this.onerror?.(new Error('Invalid Request: Only one initialization request is allowed'));
  434. return this.createJsonErrorResponse(400, -32600, 'Invalid Request: Only one initialization request is allowed');
  435. }
  436. this.sessionId = this.sessionIdGenerator?.();
  437. this._initialized = true;
  438. // If we have a session ID and an onsessioninitialized handler, call it immediately
  439. // This is needed in cases where the server needs to keep track of multiple sessions
  440. if (this.sessionId && this._onsessioninitialized) {
  441. await Promise.resolve(this._onsessioninitialized(this.sessionId));
  442. }
  443. }
  444. if (!isInitializationRequest) {
  445. // If an Mcp-Session-Id is returned by the server during initialization,
  446. // clients using the Streamable HTTP transport MUST include it
  447. // in the Mcp-Session-Id header on all of their subsequent HTTP requests.
  448. const sessionError = this.validateSession(req);
  449. if (sessionError) {
  450. return sessionError;
  451. }
  452. // Mcp-Protocol-Version header is required for all requests after initialization.
  453. const protocolError = this.validateProtocolVersion(req);
  454. if (protocolError) {
  455. return protocolError;
  456. }
  457. }
  458. // check if it contains requests
  459. const hasRequests = messages.some(types_js_1.isJSONRPCRequest);
  460. if (!hasRequests) {
  461. // if it only contains notifications or responses, return 202
  462. for (const message of messages) {
  463. this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo });
  464. }
  465. return new Response(null, { status: 202 });
  466. }
  467. // The default behavior is to use SSE streaming
  468. // but in some cases server will return JSON responses
  469. const streamId = crypto.randomUUID();
  470. // Extract protocol version for priming event decision.
  471. // For initialize requests, get from request params.
  472. // For other requests, get from header (already validated).
  473. const initRequest = messages.find(m => (0, types_js_1.isInitializeRequest)(m));
  474. const clientProtocolVersion = initRequest
  475. ? initRequest.params.protocolVersion
  476. : (req.headers.get('mcp-protocol-version') ?? types_js_1.DEFAULT_NEGOTIATED_PROTOCOL_VERSION);
  477. if (this._enableJsonResponse) {
  478. // For JSON response mode, return a Promise that resolves when all responses are ready
  479. return new Promise(resolve => {
  480. this._streamMapping.set(streamId, {
  481. resolveJson: resolve,
  482. cleanup: () => {
  483. this._streamMapping.delete(streamId);
  484. }
  485. });
  486. for (const message of messages) {
  487. if ((0, types_js_1.isJSONRPCRequest)(message)) {
  488. this._requestToStreamMapping.set(message.id, streamId);
  489. }
  490. }
  491. for (const message of messages) {
  492. this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo });
  493. }
  494. });
  495. }
  496. // SSE streaming mode - use ReadableStream with controller for more reliable data pushing
  497. const encoder = new TextEncoder();
  498. let streamController;
  499. const readable = new ReadableStream({
  500. start: controller => {
  501. streamController = controller;
  502. },
  503. cancel: () => {
  504. // Stream was cancelled by client
  505. this._streamMapping.delete(streamId);
  506. }
  507. });
  508. const headers = {
  509. 'Content-Type': 'text/event-stream',
  510. 'Cache-Control': 'no-cache',
  511. Connection: 'keep-alive'
  512. };
  513. // After initialization, always include the session ID if we have one
  514. if (this.sessionId !== undefined) {
  515. headers['mcp-session-id'] = this.sessionId;
  516. }
  517. // Store the response for this request to send messages back through this connection
  518. // We need to track by request ID to maintain the connection
  519. for (const message of messages) {
  520. if ((0, types_js_1.isJSONRPCRequest)(message)) {
  521. this._streamMapping.set(streamId, {
  522. controller: streamController,
  523. encoder,
  524. cleanup: () => {
  525. this._streamMapping.delete(streamId);
  526. try {
  527. streamController.close();
  528. }
  529. catch {
  530. // Controller might already be closed
  531. }
  532. }
  533. });
  534. this._requestToStreamMapping.set(message.id, streamId);
  535. }
  536. }
  537. // Write priming event if event store is configured (after mapping is set up)
  538. await this.writePrimingEvent(streamController, encoder, streamId, clientProtocolVersion);
  539. // handle each message
  540. for (const message of messages) {
  541. // Build closeSSEStream callback for requests when eventStore is configured
  542. // AND client supports resumability (protocol version >= 2025-11-25).
  543. // Old clients can't resume if the stream is closed early because they
  544. // didn't receive a priming event with an event ID.
  545. let closeSSEStream;
  546. let closeStandaloneSSEStream;
  547. if ((0, types_js_1.isJSONRPCRequest)(message) && this._eventStore && clientProtocolVersion >= '2025-11-25') {
  548. closeSSEStream = () => {
  549. this.closeSSEStream(message.id);
  550. };
  551. closeStandaloneSSEStream = () => {
  552. this.closeStandaloneSSEStream();
  553. };
  554. }
  555. this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo, closeSSEStream, closeStandaloneSSEStream });
  556. }
  557. // The server SHOULD NOT close the SSE stream before sending all JSON-RPC responses
  558. // This will be handled by the send() method when responses are ready
  559. return new Response(readable, { status: 200, headers });
  560. }
  561. catch (error) {
  562. // return JSON-RPC formatted error
  563. this.onerror?.(error);
  564. return this.createJsonErrorResponse(400, -32700, 'Parse error', { data: String(error) });
  565. }
  566. }
  567. /**
  568. * Handles DELETE requests to terminate sessions
  569. */
  570. async handleDeleteRequest(req) {
  571. const sessionError = this.validateSession(req);
  572. if (sessionError) {
  573. return sessionError;
  574. }
  575. const protocolError = this.validateProtocolVersion(req);
  576. if (protocolError) {
  577. return protocolError;
  578. }
  579. await Promise.resolve(this._onsessionclosed?.(this.sessionId));
  580. await this.close();
  581. return new Response(null, { status: 200 });
  582. }
  583. /**
  584. * Validates session ID for non-initialization requests.
  585. * Returns Response error if invalid, undefined otherwise
  586. */
  587. validateSession(req) {
  588. if (this.sessionIdGenerator === undefined) {
  589. // If the sessionIdGenerator ID is not set, the session management is disabled
  590. // and we don't need to validate the session ID
  591. return undefined;
  592. }
  593. if (!this._initialized) {
  594. // If the server has not been initialized yet, reject all requests
  595. this.onerror?.(new Error('Bad Request: Server not initialized'));
  596. return this.createJsonErrorResponse(400, -32000, 'Bad Request: Server not initialized');
  597. }
  598. const sessionId = req.headers.get('mcp-session-id');
  599. if (!sessionId) {
  600. // Non-initialization requests without a session ID should return 400 Bad Request
  601. this.onerror?.(new Error('Bad Request: Mcp-Session-Id header is required'));
  602. return this.createJsonErrorResponse(400, -32000, 'Bad Request: Mcp-Session-Id header is required');
  603. }
  604. if (sessionId !== this.sessionId) {
  605. // Reject requests with invalid session ID with 404 Not Found
  606. this.onerror?.(new Error('Session not found'));
  607. return this.createJsonErrorResponse(404, -32001, 'Session not found');
  608. }
  609. return undefined;
  610. }
  611. /**
  612. * Validates the MCP-Protocol-Version header on incoming requests.
  613. *
  614. * For initialization: Version negotiation handles unknown versions gracefully
  615. * (server responds with its supported version).
  616. *
  617. * For subsequent requests with MCP-Protocol-Version header:
  618. * - Accept if in supported list
  619. * - 400 if unsupported
  620. *
  621. * For HTTP requests without the MCP-Protocol-Version header:
  622. * - Accept and default to the version negotiated at initialization
  623. */
  624. validateProtocolVersion(req) {
  625. const protocolVersion = req.headers.get('mcp-protocol-version');
  626. if (protocolVersion !== null && !types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {
  627. this.onerror?.(new Error(`Bad Request: Unsupported protocol version: ${protocolVersion}` +
  628. ` (supported versions: ${types_js_1.SUPPORTED_PROTOCOL_VERSIONS.join(', ')})`));
  629. return this.createJsonErrorResponse(400, -32000, `Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${types_js_1.SUPPORTED_PROTOCOL_VERSIONS.join(', ')})`);
  630. }
  631. return undefined;
  632. }
  633. async close() {
  634. // Close all SSE connections
  635. this._streamMapping.forEach(({ cleanup }) => {
  636. cleanup();
  637. });
  638. this._streamMapping.clear();
  639. // Clear any pending responses
  640. this._requestResponseMap.clear();
  641. this.onclose?.();
  642. }
  643. /**
  644. * Close an SSE stream for a specific request, triggering client reconnection.
  645. * Use this to implement polling behavior during long-running operations -
  646. * client will reconnect after the retry interval specified in the priming event.
  647. */
  648. closeSSEStream(requestId) {
  649. const streamId = this._requestToStreamMapping.get(requestId);
  650. if (!streamId)
  651. return;
  652. const stream = this._streamMapping.get(streamId);
  653. if (stream) {
  654. stream.cleanup();
  655. }
  656. }
  657. /**
  658. * Close the standalone GET SSE stream, triggering client reconnection.
  659. * Use this to implement polling behavior for server-initiated notifications.
  660. */
  661. closeStandaloneSSEStream() {
  662. const stream = this._streamMapping.get(this._standaloneSseStreamId);
  663. if (stream) {
  664. stream.cleanup();
  665. }
  666. }
  667. async send(message, options) {
  668. let requestId = options?.relatedRequestId;
  669. if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) {
  670. // If the message is a response, use the request ID from the message
  671. requestId = message.id;
  672. }
  673. // Check if this message should be sent on the standalone SSE stream (no request ID)
  674. // Ignore notifications from tools (which have relatedRequestId set)
  675. // Those will be sent via dedicated response SSE streams
  676. if (requestId === undefined) {
  677. // For standalone SSE streams, we can only send requests and notifications
  678. if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) {
  679. throw new Error('Cannot send a response on a standalone SSE stream unless resuming a previous client request');
  680. }
  681. // Generate and store event ID if event store is provided
  682. // Store even if stream is disconnected so events can be replayed on reconnect
  683. let eventId;
  684. if (this._eventStore) {
  685. // Stores the event and gets the generated event ID
  686. eventId = await this._eventStore.storeEvent(this._standaloneSseStreamId, message);
  687. }
  688. const standaloneSse = this._streamMapping.get(this._standaloneSseStreamId);
  689. if (standaloneSse === undefined) {
  690. // Stream is disconnected - event is stored for replay, nothing more to do
  691. return;
  692. }
  693. // Send the message to the standalone SSE stream
  694. if (standaloneSse.controller && standaloneSse.encoder) {
  695. this.writeSSEEvent(standaloneSse.controller, standaloneSse.encoder, message, eventId);
  696. }
  697. return;
  698. }
  699. // Get the response for this request
  700. const streamId = this._requestToStreamMapping.get(requestId);
  701. if (!streamId) {
  702. throw new Error(`No connection established for request ID: ${String(requestId)}`);
  703. }
  704. const stream = this._streamMapping.get(streamId);
  705. if (!this._enableJsonResponse && stream?.controller && stream?.encoder) {
  706. // For SSE responses, generate event ID if event store is provided
  707. let eventId;
  708. if (this._eventStore) {
  709. eventId = await this._eventStore.storeEvent(streamId, message);
  710. }
  711. // Write the event to the response stream
  712. this.writeSSEEvent(stream.controller, stream.encoder, message, eventId);
  713. }
  714. if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) {
  715. this._requestResponseMap.set(requestId, message);
  716. const relatedIds = Array.from(this._requestToStreamMapping.entries())
  717. .filter(([_, sid]) => sid === streamId)
  718. .map(([id]) => id);
  719. // Check if we have responses for all requests using this connection
  720. const allResponsesReady = relatedIds.every(id => this._requestResponseMap.has(id));
  721. if (allResponsesReady) {
  722. if (!stream) {
  723. throw new Error(`No connection established for request ID: ${String(requestId)}`);
  724. }
  725. if (this._enableJsonResponse && stream.resolveJson) {
  726. // All responses ready, send as JSON
  727. const headers = {
  728. 'Content-Type': 'application/json'
  729. };
  730. if (this.sessionId !== undefined) {
  731. headers['mcp-session-id'] = this.sessionId;
  732. }
  733. const responses = relatedIds.map(id => this._requestResponseMap.get(id));
  734. if (responses.length === 1) {
  735. stream.resolveJson(new Response(JSON.stringify(responses[0]), { status: 200, headers }));
  736. }
  737. else {
  738. stream.resolveJson(new Response(JSON.stringify(responses), { status: 200, headers }));
  739. }
  740. }
  741. else {
  742. // End the SSE stream
  743. stream.cleanup();
  744. }
  745. // Clean up
  746. for (const id of relatedIds) {
  747. this._requestResponseMap.delete(id);
  748. this._requestToStreamMapping.delete(id);
  749. }
  750. }
  751. }
  752. }
  753. }
  754. exports.WebStandardStreamableHTTPServerTransport = WebStandardStreamableHTTPServerTransport;
  755. //# sourceMappingURL=webStandardStreamableHttp.js.map