webStandardStreamableHttp.js 34 KB

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