streamableHttp.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. /**
  2. * Node.js HTTP Streamable HTTP Server Transport
  3. *
  4. * This is a thin wrapper around `WebStandardStreamableHTTPServerTransport` that provides
  5. * compatibility with Node.js HTTP server (IncomingMessage/ServerResponse).
  6. *
  7. * For web-standard environments (Cloudflare Workers, Deno, Bun), use `WebStandardStreamableHTTPServerTransport` directly.
  8. */
  9. import { getRequestListener } from '@hono/node-server';
  10. import { WebStandardStreamableHTTPServerTransport } from './webStandardStreamableHttp.js';
  11. /**
  12. * Server transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification.
  13. * It supports both SSE streaming and direct HTTP responses.
  14. *
  15. * This is a wrapper around `WebStandardStreamableHTTPServerTransport` that provides Node.js HTTP compatibility.
  16. * It uses the `@hono/node-server` library to convert between Node.js HTTP and Web Standard APIs.
  17. *
  18. * Usage example:
  19. *
  20. * ```typescript
  21. * // Stateful mode - server sets the session ID
  22. * const statefulTransport = new StreamableHTTPServerTransport({
  23. * sessionIdGenerator: () => randomUUID(),
  24. * });
  25. *
  26. * // Stateless mode - explicitly set session ID to undefined
  27. * const statelessTransport = new StreamableHTTPServerTransport({
  28. * sessionIdGenerator: undefined,
  29. * });
  30. *
  31. * // Using with pre-parsed request body
  32. * app.post('/mcp', (req, res) => {
  33. * transport.handleRequest(req, res, req.body);
  34. * });
  35. * ```
  36. *
  37. * In stateful mode:
  38. * - Session ID is generated and included in response headers
  39. * - Session ID is always included in initialization responses
  40. * - Requests with invalid session IDs are rejected with 404 Not Found
  41. * - Non-initialization requests without a session ID are rejected with 400 Bad Request
  42. * - State is maintained in-memory (connections, message history)
  43. *
  44. * In stateless mode:
  45. * - No Session ID is included in any responses
  46. * - No session validation is performed
  47. */
  48. export class StreamableHTTPServerTransport {
  49. constructor(options = {}) {
  50. // Store auth and parsedBody per request for passing through to handleRequest
  51. this._requestContext = new WeakMap();
  52. this._webStandardTransport = new WebStandardStreamableHTTPServerTransport(options);
  53. // Create a request listener that wraps the web standard transport
  54. // getRequestListener converts Node.js HTTP to Web Standard and properly handles SSE streaming
  55. // overrideGlobalObjects: false prevents Hono from overwriting global Response, which would
  56. // break frameworks like Next.js whose response classes extend the native Response
  57. this._requestListener = getRequestListener(async (webRequest) => {
  58. // Get context if available (set during handleRequest)
  59. const context = this._requestContext.get(webRequest);
  60. return this._webStandardTransport.handleRequest(webRequest, {
  61. authInfo: context?.authInfo,
  62. parsedBody: context?.parsedBody
  63. });
  64. }, { overrideGlobalObjects: false });
  65. }
  66. /**
  67. * Gets the session ID for this transport instance.
  68. */
  69. get sessionId() {
  70. return this._webStandardTransport.sessionId;
  71. }
  72. /**
  73. * Sets callback for when the transport is closed.
  74. */
  75. set onclose(handler) {
  76. this._webStandardTransport.onclose = handler;
  77. }
  78. get onclose() {
  79. return this._webStandardTransport.onclose;
  80. }
  81. /**
  82. * Sets callback for transport errors.
  83. */
  84. set onerror(handler) {
  85. this._webStandardTransport.onerror = handler;
  86. }
  87. get onerror() {
  88. return this._webStandardTransport.onerror;
  89. }
  90. /**
  91. * Sets callback for incoming messages.
  92. */
  93. set onmessage(handler) {
  94. this._webStandardTransport.onmessage = handler;
  95. }
  96. get onmessage() {
  97. return this._webStandardTransport.onmessage;
  98. }
  99. /**
  100. * Starts the transport. This is required by the Transport interface but is a no-op
  101. * for the Streamable HTTP transport as connections are managed per-request.
  102. */
  103. async start() {
  104. return this._webStandardTransport.start();
  105. }
  106. /**
  107. * Closes the transport and all active connections.
  108. */
  109. async close() {
  110. return this._webStandardTransport.close();
  111. }
  112. /**
  113. * Sends a JSON-RPC message through the transport.
  114. */
  115. async send(message, options) {
  116. return this._webStandardTransport.send(message, options);
  117. }
  118. /**
  119. * Handles an incoming HTTP request, whether GET or POST.
  120. *
  121. * This method converts Node.js HTTP objects to Web Standard Request/Response
  122. * and delegates to the underlying WebStandardStreamableHTTPServerTransport.
  123. *
  124. * @param req - Node.js IncomingMessage, optionally with auth property from middleware
  125. * @param res - Node.js ServerResponse
  126. * @param parsedBody - Optional pre-parsed body from body-parser middleware
  127. */
  128. async handleRequest(req, res, parsedBody) {
  129. // Store context for this request to pass through auth and parsedBody
  130. // We need to intercept the request creation to attach this context
  131. const authInfo = req.auth;
  132. // Create a custom handler that includes our context
  133. // overrideGlobalObjects: false prevents Hono from overwriting global Response, which would
  134. // break frameworks like Next.js whose response classes extend the native Response
  135. const handler = getRequestListener(async (webRequest) => {
  136. return this._webStandardTransport.handleRequest(webRequest, {
  137. authInfo,
  138. parsedBody
  139. });
  140. }, { overrideGlobalObjects: false });
  141. // Delegate to the request listener which handles all the Node.js <-> Web Standard conversion
  142. // including proper SSE streaming support
  143. await handler(req, res);
  144. }
  145. /**
  146. * Close an SSE stream for a specific request, triggering client reconnection.
  147. * Use this to implement polling behavior during long-running operations -
  148. * client will reconnect after the retry interval specified in the priming event.
  149. */
  150. closeSSEStream(requestId) {
  151. this._webStandardTransport.closeSSEStream(requestId);
  152. }
  153. /**
  154. * Close the standalone GET SSE stream, triggering client reconnection.
  155. * Use this to implement polling behavior for server-initiated notifications.
  156. */
  157. closeStandaloneSSEStream() {
  158. this._webStandardTransport.closeStandaloneSSEStream();
  159. }
  160. }
  161. //# sourceMappingURL=streamableHttp.js.map