@opentabs-dev/mcp-server 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/dist/audit-disk.d.ts +23 -0
  2. package/dist/audit-disk.d.ts.map +1 -0
  3. package/dist/audit-disk.js +74 -0
  4. package/dist/audit-disk.js.map +1 -0
  5. package/dist/browser-tools/analyze-site/detect-apis.d.ts +36 -0
  6. package/dist/browser-tools/analyze-site/detect-apis.d.ts.map +1 -0
  7. package/dist/browser-tools/analyze-site/detect-apis.js +383 -0
  8. package/dist/browser-tools/analyze-site/detect-apis.js.map +1 -0
  9. package/dist/browser-tools/analyze-site/detect-auth.d.ts +72 -0
  10. package/dist/browser-tools/analyze-site/detect-auth.d.ts.map +1 -0
  11. package/dist/browser-tools/analyze-site/detect-auth.js +384 -0
  12. package/dist/browser-tools/analyze-site/detect-auth.js.map +1 -0
  13. package/dist/browser-tools/analyze-site/detect-dom.d.ts +65 -0
  14. package/dist/browser-tools/analyze-site/detect-dom.d.ts.map +1 -0
  15. package/dist/browser-tools/analyze-site/detect-dom.js +45 -0
  16. package/dist/browser-tools/analyze-site/detect-dom.js.map +1 -0
  17. package/dist/browser-tools/analyze-site/detect-framework.d.ts +48 -0
  18. package/dist/browser-tools/analyze-site/detect-framework.d.ts.map +1 -0
  19. package/dist/browser-tools/analyze-site/detect-framework.js +31 -0
  20. package/dist/browser-tools/analyze-site/detect-framework.js.map +1 -0
  21. package/dist/browser-tools/analyze-site/detect-globals.d.ts +41 -0
  22. package/dist/browser-tools/analyze-site/detect-globals.d.ts.map +1 -0
  23. package/dist/browser-tools/analyze-site/detect-globals.js +42 -0
  24. package/dist/browser-tools/analyze-site/detect-globals.js.map +1 -0
  25. package/dist/browser-tools/analyze-site/detect-storage.d.ts +39 -0
  26. package/dist/browser-tools/analyze-site/detect-storage.d.ts.map +1 -0
  27. package/dist/browser-tools/analyze-site/detect-storage.js +34 -0
  28. package/dist/browser-tools/analyze-site/detect-storage.js.map +1 -0
  29. package/dist/browser-tools/analyze-site/index.d.ts +52 -0
  30. package/dist/browser-tools/analyze-site/index.d.ts.map +1 -0
  31. package/dist/browser-tools/analyze-site/index.js +827 -0
  32. package/dist/browser-tools/analyze-site/index.js.map +1 -0
  33. package/dist/browser-tools/analyze-site.d.ts +17 -0
  34. package/dist/browser-tools/analyze-site.d.ts.map +1 -0
  35. package/dist/browser-tools/analyze-site.js +41 -0
  36. package/dist/browser-tools/analyze-site.js.map +1 -0
  37. package/dist/browser-tools/clear-console-logs.d.ts +9 -0
  38. package/dist/browser-tools/clear-console-logs.d.ts.map +1 -0
  39. package/dist/browser-tools/clear-console-logs.js +16 -0
  40. package/dist/browser-tools/clear-console-logs.js.map +1 -0
  41. package/dist/browser-tools/click-element.d.ts +10 -0
  42. package/dist/browser-tools/click-element.d.ts.map +1 -0
  43. package/dist/browser-tools/click-element.js +22 -0
  44. package/dist/browser-tools/click-element.js.map +1 -0
  45. package/dist/browser-tools/close-tab.d.ts +9 -0
  46. package/dist/browser-tools/close-tab.d.ts.map +1 -0
  47. package/dist/browser-tools/close-tab.js +16 -0
  48. package/dist/browser-tools/close-tab.js.map +1 -0
  49. package/dist/browser-tools/definition.d.ts +26 -0
  50. package/dist/browser-tools/definition.d.ts.map +1 -0
  51. package/dist/browser-tools/definition.js +16 -0
  52. package/dist/browser-tools/definition.js.map +1 -0
  53. package/dist/browser-tools/delete-cookies.d.ts +10 -0
  54. package/dist/browser-tools/delete-cookies.d.ts.map +1 -0
  55. package/dist/browser-tools/delete-cookies.js +19 -0
  56. package/dist/browser-tools/delete-cookies.js.map +1 -0
  57. package/dist/browser-tools/disable-network-capture.d.ts +9 -0
  58. package/dist/browser-tools/disable-network-capture.d.ts.map +1 -0
  59. package/dist/browser-tools/disable-network-capture.js +16 -0
  60. package/dist/browser-tools/disable-network-capture.js.map +1 -0
  61. package/dist/browser-tools/enable-network-capture.d.ts +12 -0
  62. package/dist/browser-tools/enable-network-capture.d.ts.map +1 -0
  63. package/dist/browser-tools/enable-network-capture.js +42 -0
  64. package/dist/browser-tools/enable-network-capture.js.map +1 -0
  65. package/dist/browser-tools/execute-script.d.ts +19 -0
  66. package/dist/browser-tools/execute-script.d.ts.map +1 -0
  67. package/dist/browser-tools/execute-script.js +51 -0
  68. package/dist/browser-tools/execute-script.js.map +1 -0
  69. package/dist/browser-tools/extension-check-adapter.d.ts +11 -0
  70. package/dist/browser-tools/extension-check-adapter.d.ts.map +1 -0
  71. package/dist/browser-tools/extension-check-adapter.js +22 -0
  72. package/dist/browser-tools/extension-check-adapter.js.map +1 -0
  73. package/dist/browser-tools/extension-force-reconnect.d.ts +9 -0
  74. package/dist/browser-tools/extension-force-reconnect.d.ts.map +1 -0
  75. package/dist/browser-tools/extension-force-reconnect.js +19 -0
  76. package/dist/browser-tools/extension-force-reconnect.js.map +1 -0
  77. package/dist/browser-tools/extension-get-logs.d.ts +24 -0
  78. package/dist/browser-tools/extension-get-logs.d.ts.map +1 -0
  79. package/dist/browser-tools/extension-get-logs.js +34 -0
  80. package/dist/browser-tools/extension-get-logs.js.map +1 -0
  81. package/dist/browser-tools/extension-get-side-panel.d.ts +8 -0
  82. package/dist/browser-tools/extension-get-side-panel.d.ts.map +1 -0
  83. package/dist/browser-tools/extension-get-side-panel.js +17 -0
  84. package/dist/browser-tools/extension-get-side-panel.js.map +1 -0
  85. package/dist/browser-tools/extension-get-state.d.ts +9 -0
  86. package/dist/browser-tools/extension-get-state.d.ts.map +1 -0
  87. package/dist/browser-tools/extension-get-state.js +19 -0
  88. package/dist/browser-tools/extension-get-state.js.map +1 -0
  89. package/dist/browser-tools/focus-tab.d.ts +9 -0
  90. package/dist/browser-tools/focus-tab.d.ts.map +1 -0
  91. package/dist/browser-tools/focus-tab.js +17 -0
  92. package/dist/browser-tools/focus-tab.js.map +1 -0
  93. package/dist/browser-tools/get-console-logs.d.ts +18 -0
  94. package/dist/browser-tools/get-console-logs.d.ts.map +1 -0
  95. package/dist/browser-tools/get-console-logs.js +30 -0
  96. package/dist/browser-tools/get-console-logs.js.map +1 -0
  97. package/dist/browser-tools/get-cookies.d.ts +10 -0
  98. package/dist/browser-tools/get-cookies.d.ts.map +1 -0
  99. package/dist/browser-tools/get-cookies.js +23 -0
  100. package/dist/browser-tools/get-cookies.js.map +1 -0
  101. package/dist/browser-tools/get-network-requests.d.ts +10 -0
  102. package/dist/browser-tools/get-network-requests.d.ts.map +1 -0
  103. package/dist/browser-tools/get-network-requests.js +27 -0
  104. package/dist/browser-tools/get-network-requests.js.map +1 -0
  105. package/dist/browser-tools/get-page-html.d.ts +11 -0
  106. package/dist/browser-tools/get-page-html.d.ts.map +1 -0
  107. package/dist/browser-tools/get-page-html.js +32 -0
  108. package/dist/browser-tools/get-page-html.js.map +1 -0
  109. package/dist/browser-tools/get-resource-content.d.ts +11 -0
  110. package/dist/browser-tools/get-resource-content.d.ts.map +1 -0
  111. package/dist/browser-tools/get-resource-content.js +31 -0
  112. package/dist/browser-tools/get-resource-content.js.map +1 -0
  113. package/dist/browser-tools/get-storage.d.ts +14 -0
  114. package/dist/browser-tools/get-storage.d.ts.map +1 -0
  115. package/dist/browser-tools/get-storage.js +28 -0
  116. package/dist/browser-tools/get-storage.js.map +1 -0
  117. package/dist/browser-tools/get-tab-content.d.ts +11 -0
  118. package/dist/browser-tools/get-tab-content.d.ts.map +1 -0
  119. package/dist/browser-tools/get-tab-content.js +29 -0
  120. package/dist/browser-tools/get-tab-content.js.map +1 -0
  121. package/dist/browser-tools/get-tab-info.d.ts +9 -0
  122. package/dist/browser-tools/get-tab-info.d.ts.map +1 -0
  123. package/dist/browser-tools/get-tab-info.js +17 -0
  124. package/dist/browser-tools/get-tab-info.js.map +1 -0
  125. package/dist/browser-tools/handle-dialog.d.ts +14 -0
  126. package/dist/browser-tools/handle-dialog.d.ts.map +1 -0
  127. package/dist/browser-tools/handle-dialog.js +30 -0
  128. package/dist/browser-tools/handle-dialog.js.map +1 -0
  129. package/dist/browser-tools/hover-element.d.ts +11 -0
  130. package/dist/browser-tools/hover-element.d.ts.map +1 -0
  131. package/dist/browser-tools/hover-element.js +24 -0
  132. package/dist/browser-tools/hover-element.js.map +1 -0
  133. package/dist/browser-tools/index.d.ts +7 -0
  134. package/dist/browser-tools/index.d.ts.map +1 -0
  135. package/dist/browser-tools/index.js +81 -0
  136. package/dist/browser-tools/index.js.map +1 -0
  137. package/dist/browser-tools/list-resources.d.ts +10 -0
  138. package/dist/browser-tools/list-resources.d.ts.map +1 -0
  139. package/dist/browser-tools/list-resources.js +29 -0
  140. package/dist/browser-tools/list-resources.js.map +1 -0
  141. package/dist/browser-tools/list-tabs.d.ts +7 -0
  142. package/dist/browser-tools/list-tabs.d.ts.map +1 -0
  143. package/dist/browser-tools/list-tabs.js +16 -0
  144. package/dist/browser-tools/list-tabs.js.map +1 -0
  145. package/dist/browser-tools/navigate-tab.d.ts +10 -0
  146. package/dist/browser-tools/navigate-tab.d.ts.map +1 -0
  147. package/dist/browser-tools/navigate-tab.js +18 -0
  148. package/dist/browser-tools/navigate-tab.js.map +1 -0
  149. package/dist/browser-tools/open-tab.d.ts +9 -0
  150. package/dist/browser-tools/open-tab.d.ts.map +1 -0
  151. package/dist/browser-tools/open-tab.js +18 -0
  152. package/dist/browser-tools/open-tab.js.map +1 -0
  153. package/dist/browser-tools/press-key.d.ts +17 -0
  154. package/dist/browser-tools/press-key.d.ts.map +1 -0
  155. package/dist/browser-tools/press-key.js +43 -0
  156. package/dist/browser-tools/press-key.js.map +1 -0
  157. package/dist/browser-tools/query-elements.d.ts +12 -0
  158. package/dist/browser-tools/query-elements.d.ts.map +1 -0
  159. package/dist/browser-tools/query-elements.js +30 -0
  160. package/dist/browser-tools/query-elements.js.map +1 -0
  161. package/dist/browser-tools/reload-extension.d.ts +13 -0
  162. package/dist/browser-tools/reload-extension.d.ts.map +1 -0
  163. package/dist/browser-tools/reload-extension.js +35 -0
  164. package/dist/browser-tools/reload-extension.js.map +1 -0
  165. package/dist/browser-tools/screenshot-tab.d.ts +9 -0
  166. package/dist/browser-tools/screenshot-tab.d.ts.map +1 -0
  167. package/dist/browser-tools/screenshot-tab.js +22 -0
  168. package/dist/browser-tools/screenshot-tab.js.map +1 -0
  169. package/dist/browser-tools/scroll.d.ts +23 -0
  170. package/dist/browser-tools/scroll.d.ts.map +1 -0
  171. package/dist/browser-tools/scroll.js +56 -0
  172. package/dist/browser-tools/scroll.js.map +1 -0
  173. package/dist/browser-tools/select-option.d.ts +12 -0
  174. package/dist/browser-tools/select-option.d.ts.map +1 -0
  175. package/dist/browser-tools/select-option.js +25 -0
  176. package/dist/browser-tools/select-option.js.map +1 -0
  177. package/dist/browser-tools/set-cookie.d.ts +16 -0
  178. package/dist/browser-tools/set-cookie.d.ts.map +1 -0
  179. package/dist/browser-tools/set-cookie.js +42 -0
  180. package/dist/browser-tools/set-cookie.js.map +1 -0
  181. package/dist/browser-tools/type-text.d.ts +12 -0
  182. package/dist/browser-tools/type-text.d.ts.map +1 -0
  183. package/dist/browser-tools/type-text.js +25 -0
  184. package/dist/browser-tools/type-text.js.map +1 -0
  185. package/dist/browser-tools/url-validation.d.ts +13 -0
  186. package/dist/browser-tools/url-validation.d.ts.map +1 -0
  187. package/dist/browser-tools/url-validation.js +23 -0
  188. package/dist/browser-tools/url-validation.js.map +1 -0
  189. package/dist/browser-tools/wait-for-element.d.ts +12 -0
  190. package/dist/browser-tools/wait-for-element.d.ts.map +1 -0
  191. package/dist/browser-tools/wait-for-element.js +29 -0
  192. package/dist/browser-tools/wait-for-element.js.map +1 -0
  193. package/dist/config.d.ts +99 -0
  194. package/dist/config.d.ts.map +1 -0
  195. package/dist/config.js +344 -0
  196. package/dist/config.js.map +1 -0
  197. package/dist/dev-mode.d.ts +14 -0
  198. package/dist/dev-mode.d.ts.map +1 -0
  199. package/dist/dev-mode.js +15 -0
  200. package/dist/dev-mode.js.map +1 -0
  201. package/dist/discovery-legacy.d.ts +32 -0
  202. package/dist/discovery-legacy.d.ts.map +1 -0
  203. package/dist/discovery-legacy.js +415 -0
  204. package/dist/discovery-legacy.js.map +1 -0
  205. package/dist/discovery.d.ts +28 -0
  206. package/dist/discovery.d.ts.map +1 -0
  207. package/dist/discovery.js +97 -0
  208. package/dist/discovery.js.map +1 -0
  209. package/dist/extension-install.d.ts +27 -0
  210. package/dist/extension-install.d.ts.map +1 -0
  211. package/dist/extension-install.js +75 -0
  212. package/dist/extension-install.js.map +1 -0
  213. package/dist/extension-protocol.d.ts +130 -0
  214. package/dist/extension-protocol.d.ts.map +1 -0
  215. package/dist/extension-protocol.js +869 -0
  216. package/dist/extension-protocol.js.map +1 -0
  217. package/dist/file-watcher.d.ts +75 -0
  218. package/dist/file-watcher.d.ts.map +1 -0
  219. package/dist/file-watcher.js +616 -0
  220. package/dist/file-watcher.js.map +1 -0
  221. package/dist/http-routes.d.ts +88 -0
  222. package/dist/http-routes.d.ts.map +1 -0
  223. package/dist/http-routes.js +545 -0
  224. package/dist/http-routes.js.map +1 -0
  225. package/dist/index.d.ts +45 -0
  226. package/dist/index.d.ts.map +1 -0
  227. package/dist/index.js +187 -0
  228. package/dist/index.js.map +1 -0
  229. package/dist/loader.d.ts +100 -0
  230. package/dist/loader.d.ts.map +1 -0
  231. package/dist/loader.js +402 -0
  232. package/dist/loader.js.map +1 -0
  233. package/dist/log-buffer.d.ts +33 -0
  234. package/dist/log-buffer.d.ts.map +1 -0
  235. package/dist/log-buffer.js +64 -0
  236. package/dist/log-buffer.js.map +1 -0
  237. package/dist/logger.d.ts +34 -0
  238. package/dist/logger.d.ts.map +1 -0
  239. package/dist/logger.js +81 -0
  240. package/dist/logger.js.map +1 -0
  241. package/dist/manifest-schema.d.ts +14 -0
  242. package/dist/manifest-schema.d.ts.map +1 -0
  243. package/dist/manifest-schema.js +51 -0
  244. package/dist/manifest-schema.js.map +1 -0
  245. package/dist/mcp-setup.d.ts +131 -0
  246. package/dist/mcp-setup.d.ts.map +1 -0
  247. package/dist/mcp-setup.js +673 -0
  248. package/dist/mcp-setup.js.map +1 -0
  249. package/dist/permissions.d.ts +59 -0
  250. package/dist/permissions.d.ts.map +1 -0
  251. package/dist/permissions.js +141 -0
  252. package/dist/permissions.js.map +1 -0
  253. package/dist/registry.d.ts +78 -0
  254. package/dist/registry.d.ts.map +1 -0
  255. package/dist/registry.js +187 -0
  256. package/dist/registry.js.map +1 -0
  257. package/dist/reload.d.ts +52 -0
  258. package/dist/reload.d.ts.map +1 -0
  259. package/dist/reload.js +326 -0
  260. package/dist/reload.js.map +1 -0
  261. package/dist/resolver.d.ts +53 -0
  262. package/dist/resolver.d.ts.map +1 -0
  263. package/dist/resolver.js +272 -0
  264. package/dist/resolver.js.map +1 -0
  265. package/dist/sanitize-error.d.ts +8 -0
  266. package/dist/sanitize-error.d.ts.map +1 -0
  267. package/dist/sanitize-error.js +25 -0
  268. package/dist/sanitize-error.js.map +1 -0
  269. package/dist/sanitize-tool-output.d.ts +20 -0
  270. package/dist/sanitize-tool-output.d.ts.map +1 -0
  271. package/dist/sanitize-tool-output.js +52 -0
  272. package/dist/sanitize-tool-output.js.map +1 -0
  273. package/dist/sdk-version.d.ts +11 -0
  274. package/dist/sdk-version.d.ts.map +1 -0
  275. package/dist/sdk-version.js +23 -0
  276. package/dist/sdk-version.js.map +1 -0
  277. package/dist/shutdown.d.ts +28 -0
  278. package/dist/shutdown.d.ts.map +1 -0
  279. package/dist/shutdown.js +68 -0
  280. package/dist/shutdown.js.map +1 -0
  281. package/dist/skip-confirmation.d.ts +15 -0
  282. package/dist/skip-confirmation.d.ts.map +1 -0
  283. package/dist/skip-confirmation.js +16 -0
  284. package/dist/skip-confirmation.js.map +1 -0
  285. package/dist/skip-sanitization.d.ts +17 -0
  286. package/dist/skip-sanitization.d.ts.map +1 -0
  287. package/dist/skip-sanitization.js +18 -0
  288. package/dist/skip-sanitization.js.map +1 -0
  289. package/dist/skip-verification.d.ts +11 -0
  290. package/dist/skip-verification.d.ts.map +1 -0
  291. package/dist/skip-verification.js +12 -0
  292. package/dist/skip-verification.js.map +1 -0
  293. package/dist/state.d.ts +290 -0
  294. package/dist/state.d.ts.map +1 -0
  295. package/dist/state.js +111 -0
  296. package/dist/state.js.map +1 -0
  297. package/dist/verify-plugin.d.ts +53 -0
  298. package/dist/verify-plugin.d.ts.map +1 -0
  299. package/dist/verify-plugin.js +123 -0
  300. package/dist/verify-plugin.js.map +1 -0
  301. package/dist/version-check.d.ts +35 -0
  302. package/dist/version-check.d.ts.map +1 -0
  303. package/dist/version-check.js +111 -0
  304. package/dist/version-check.js.map +1 -0
  305. package/dist/version.d.ts +10 -0
  306. package/dist/version.d.ts.map +1 -0
  307. package/dist/version.js +22 -0
  308. package/dist/version.js.map +1 -0
  309. package/package.json +28 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * HTTP and WebSocket route handlers.
3
+ *
4
+ * Extracted from index.ts so that the entry point is a thin frozen shell
5
+ * (Bun.serve() delegate, HotState management, reload orchestration) while
6
+ * all routing logic lives here and hot-reloads freely.
7
+ *
8
+ * Includes sweepStaleSessions() to prevent memory leaks in the sessionServers
9
+ * array. If an MCP client drops the TCP connection without a proper close
10
+ * (network partition, OOM kill), the onsessionclosed / transport.onclose
11
+ * callbacks may never fire, leaving ghost entries. The sweep runs on each
12
+ * hot reload and removes entries whose transport is no longer in the map.
13
+ */
14
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
15
+ import type { McpServerInstance } from './mcp-setup.js';
16
+ import type { ServerState } from './state.js';
17
+ import type { WsHandle } from '@opentabs-dev/shared';
18
+ /** Opaque HotState accessor — index.ts injects the getter */
19
+ type GetHotState = () => {
20
+ reloadCount: number;
21
+ lastReloadTimestamp: number;
22
+ lastReloadDurationMs: number;
23
+ } | undefined;
24
+ /** Dependencies injected by index.ts to avoid circular imports */
25
+ interface RouteDeps {
26
+ state: ServerState;
27
+ transports: Map<string, WebStandardStreamableHTTPServerTransport>;
28
+ sessionServers: McpServerInstance[];
29
+ getHotState: GetHotState;
30
+ }
31
+ /**
32
+ * Constant-time string comparison using crypto.timingSafeEqual.
33
+ * Rejects early on length mismatch (length is not secret — only content is).
34
+ */
35
+ declare const constantTimeEqual: (a: string, b: string) => boolean;
36
+ /**
37
+ * Check Bearer token in the Authorization header against the server's shared secret.
38
+ * Returns a 401 Response if authentication fails, or null if authentication succeeds.
39
+ * When no secret is configured (wsSecret is null), all requests are allowed through.
40
+ * Uses constant-time comparison to prevent timing side-channel attacks.
41
+ */
42
+ declare const checkBearerAuth: (req: Request, wsSecret: string | null) => Response | null;
43
+ /**
44
+ * Check whether a Host header value refers to a localhost address.
45
+ * Strips the optional port suffix before comparing against the allowed set.
46
+ * Handles IPv6 bracket notation (e.g., `[::1]:9515`).
47
+ */
48
+ declare const isLocalhostHost: (hostHeader: string) => boolean;
49
+ /** Hot-reloadable handler functions for the Bun.serve() delegate shell */
50
+ interface HotHandlers {
51
+ /** HTTP request handler — all routing logic */
52
+ fetch: (req: Request, bunServer: {
53
+ upgrade: (req: Request, opts: {
54
+ data: unknown;
55
+ headers?: HeadersInit;
56
+ }) => boolean;
57
+ timeout: (req: Request, seconds: number) => void;
58
+ }) => Promise<Response | undefined>;
59
+ /** Extension WebSocket opened */
60
+ wsOpen: (ws: WsHandle) => void;
61
+ /** Extension WebSocket message received */
62
+ wsMessage: (ws: WsHandle, message: string | ArrayBuffer | Uint8Array) => void;
63
+ /** Extension WebSocket closed */
64
+ wsClose: (ws: WsHandle) => void;
65
+ }
66
+ /**
67
+ * Create all hot-reloadable handler functions.
68
+ * Called on every module evaluation (first load + hot reloads) to produce
69
+ * fresh closures over the latest module imports.
70
+ */
71
+ declare const createHandlers: (deps: RouteDeps) => HotHandlers;
72
+ /**
73
+ * Remove sessionServers entries whose transport is no longer in the transports
74
+ * map. This prevents unbounded growth when MCP clients disconnect ungracefully
75
+ * (network partition, OOM kill) and the onsessionclosed / transport.onclose
76
+ * callbacks never fire.
77
+ *
78
+ * Each session server is tracked in state.sessionTransportIds (a WeakMap keyed
79
+ * by the McpServerInstance) with the transport session ID it was connected to.
80
+ * A session is stale if its transport ID is absent from the active transports
81
+ * map — meaning the transport was cleaned up but the session server was not.
82
+ *
83
+ * Called on each hot reload from reload.ts.
84
+ */
85
+ declare const sweepStaleSessions: (state: ServerState, transports: Map<string, WebStandardStreamableHTTPServerTransport>, sessionServers: McpServerInstance[]) => number;
86
+ export type { HotHandlers };
87
+ export { checkBearerAuth, constantTimeEqual, createHandlers, isLocalhostHost, sweepStaleSessions };
88
+ //# sourceMappingURL=http-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-routes.d.ts","sourceRoot":"","sources":["../src/http-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAkBH,OAAO,EAAE,wCAAwC,EAAE,MAAM,+DAA+D,CAAC;AAIzH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,6DAA6D;AAC7D,KAAK,WAAW,GAAG,MAAM;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,mBAAmB,EAAE,MAAM,CAAC;IAAC,oBAAoB,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC;AAExH,kEAAkE;AAClE,UAAU,SAAS;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,wCAAwC,CAAC,CAAC;IAClE,cAAc,EAAE,iBAAiB,EAAE,CAAC;IACpC,WAAW,EAAE,WAAW,CAAC;CAC1B;AAgCD;;;GAGG;AACH,QAAA,MAAM,iBAAiB,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,OAGjD,CAAC;AAEF;;;;;GAKG;AACH,QAAA,MAAM,eAAe,GAAI,KAAK,OAAO,EAAE,UAAU,MAAM,GAAG,IAAI,KAAG,QAAQ,GAAG,IAQ3E,CAAC;AAKF;;;;GAIG;AACH,QAAA,MAAM,eAAe,GAAI,YAAY,MAAM,KAAG,OAa7C,CAAC;AAicF,0EAA0E;AAC1E,UAAU,WAAW;IACnB,+CAA+C;IAC/C,KAAK,EAAE,CACL,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE;QACT,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE;YAAE,IAAI,EAAE,OAAO,CAAC;YAAC,OAAO,CAAC,EAAE,WAAW,CAAA;SAAE,KAAK,OAAO,CAAC;QACnF,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;KAClD,KACE,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;IACnC,iCAAiC;IACjC,MAAM,EAAE,CAAC,EAAE,EAAE,QAAQ,KAAK,IAAI,CAAC;IAC/B,2CAA2C;IAC3C,SAAS,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,KAAK,IAAI,CAAC;IAC9E,iCAAiC;IACjC,OAAO,EAAE,CAAC,EAAE,EAAE,QAAQ,KAAK,IAAI,CAAC;CACjC;AAED;;;;GAIG;AACH,QAAA,MAAM,cAAc,GAAI,MAAM,SAAS,KAAG,WAQzC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,QAAA,MAAM,kBAAkB,GACtB,OAAO,WAAW,EAClB,YAAY,GAAG,CAAC,MAAM,EAAE,wCAAwC,CAAC,EACjE,gBAAgB,iBAAiB,EAAE,KAClC,MAqBF,CAAC;AAEF,YAAY,EAAE,WAAW,EAAE,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,cAAc,EAAE,eAAe,EAAE,kBAAkB,EAAE,CAAC"}
@@ -0,0 +1,545 @@
1
+ /**
2
+ * HTTP and WebSocket route handlers.
3
+ *
4
+ * Extracted from index.ts so that the entry point is a thin frozen shell
5
+ * (Bun.serve() delegate, HotState management, reload orchestration) while
6
+ * all routing logic lives here and hot-reloads freely.
7
+ *
8
+ * Includes sweepStaleSessions() to prevent memory leaks in the sessionServers
9
+ * array. If an MCP client drops the TCP connection without a proper close
10
+ * (network partition, OOM kill), the onsessionclosed / transport.onclose
11
+ * callbacks may never fire, leaving ghost entries. The sweep runs on each
12
+ * hot reload and removes entries whose transport is no longer in the map.
13
+ */
14
+ import { saveToolConfig } from './config.js';
15
+ import { isDev } from './dev-mode.js';
16
+ import { handleExtensionMessage, sendSyncFull, sendExtensionReload, rejectAllPendingConfirmations, } from './extension-protocol.js';
17
+ import { getLogCount } from './log-buffer.js';
18
+ import { log } from './logger.js';
19
+ import { createMcpServer, notifyToolListChanged } from './mcp-setup.js';
20
+ import { performConfigReload } from './reload.js';
21
+ import { sanitizeErrorMessage } from './sanitize-error.js';
22
+ import { sdkVersion } from './sdk-version.js';
23
+ import { getNextRequestId, prefixedToolName, STATE_SCHEMA_VERSION } from './state.js';
24
+ import { version } from './version.js';
25
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
26
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
27
+ import { timingSafeEqual } from 'node:crypto';
28
+ /** Callbacks for extension protocol → MCP server integration */
29
+ const createMcpCallbacks = (state, sessionServers) => ({
30
+ onToolConfigChanged: () => {
31
+ for (const srv of sessionServers) {
32
+ notifyToolListChanged(srv);
33
+ }
34
+ },
35
+ onToolConfigPersist: () => {
36
+ saveToolConfig(state, { ...state.toolConfig }).catch(() => {
37
+ // Error already logged by saveToolConfig
38
+ });
39
+ },
40
+ onPluginLog: entry => {
41
+ const mcpLevel = entry.level;
42
+ const logger = `plugin:${entry.plugin}`;
43
+ const data = entry.data !== undefined ? `${entry.message} ${JSON.stringify(entry.data)}` : entry.message;
44
+ // Write to console (flows to server.log via start.ts tee pipeline)
45
+ const levelTag = entry.level.toUpperCase();
46
+ const dataStr = entry.data !== undefined ? ` ${JSON.stringify(entry.data)}` : '';
47
+ console.log(`[plugin:${entry.plugin}] ${entry.ts} ${levelTag} ${entry.message}${dataStr}`);
48
+ for (const srv of sessionServers) {
49
+ srv.sendLoggingMessage({ level: mcpLevel, logger, data }).catch(() => {
50
+ // Best-effort — client may have disconnected
51
+ });
52
+ }
53
+ },
54
+ });
55
+ /**
56
+ * Constant-time string comparison using crypto.timingSafeEqual.
57
+ * Rejects early on length mismatch (length is not secret — only content is).
58
+ */
59
+ const constantTimeEqual = (a, b) => {
60
+ if (a.length !== b.length)
61
+ return false;
62
+ return timingSafeEqual(Buffer.from(a), Buffer.from(b));
63
+ };
64
+ /**
65
+ * Check Bearer token in the Authorization header against the server's shared secret.
66
+ * Returns a 401 Response if authentication fails, or null if authentication succeeds.
67
+ * When no secret is configured (wsSecret is null), all requests are allowed through.
68
+ * Uses constant-time comparison to prevent timing side-channel attacks.
69
+ */
70
+ const checkBearerAuth = (req, wsSecret) => {
71
+ if (!wsSecret)
72
+ return null;
73
+ const authHeader = req.headers.get('Authorization');
74
+ const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
75
+ if (!token || !constantTimeEqual(token, wsSecret)) {
76
+ return new Response('Unauthorized', { status: 401 });
77
+ }
78
+ return null;
79
+ };
80
+ /** Allowed hostnames for the Host header (DNS rebinding protection) */
81
+ const ALLOWED_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
82
+ /**
83
+ * Check whether a Host header value refers to a localhost address.
84
+ * Strips the optional port suffix before comparing against the allowed set.
85
+ * Handles IPv6 bracket notation (e.g., `[::1]:9515`).
86
+ */
87
+ const isLocalhostHost = (hostHeader) => {
88
+ let hostname;
89
+ if (hostHeader.startsWith('[')) {
90
+ // IPv6 bracket notation: [::1] or [::1]:9515
91
+ const closeBracket = hostHeader.indexOf(']');
92
+ if (closeBracket === -1)
93
+ return false;
94
+ hostname = hostHeader.slice(1, closeBracket);
95
+ }
96
+ else {
97
+ // IPv4 or hostname: localhost, localhost:9515, 127.0.0.1:9515
98
+ const colonIdx = hostHeader.lastIndexOf(':');
99
+ hostname = colonIdx === -1 ? hostHeader : hostHeader.slice(0, colonIdx);
100
+ }
101
+ return ALLOWED_HOSTS.has(hostname);
102
+ };
103
+ // --- Rate limiting for administrative endpoints ---
104
+ const endpointCallTimestamps = new Map();
105
+ const checkEndpointRateLimit = (endpoint, maxPerMinute) => {
106
+ const now = Date.now();
107
+ const timestamps = (endpointCallTimestamps.get(endpoint) ?? []).filter(t => now - t < 60_000);
108
+ if (timestamps.length >= maxPerMinute) {
109
+ endpointCallTimestamps.set(endpoint, timestamps);
110
+ return false;
111
+ }
112
+ timestamps.push(now);
113
+ endpointCallTimestamps.set(endpoint, timestamps);
114
+ return true;
115
+ };
116
+ /** Compute aggregate audit statistics from the audit log buffer */
117
+ const computeAuditSummary = (auditLog) => {
118
+ const totalInvocations = auditLog.length;
119
+ let successCount = 0;
120
+ let failureCount = 0;
121
+ let totalDurationMs = 0;
122
+ let last24hTotal = 0;
123
+ let last24hSuccess = 0;
124
+ let last24hFailure = 0;
125
+ const cutoff = Date.now() - 86_400_000;
126
+ for (const entry of auditLog) {
127
+ if (entry.success) {
128
+ successCount++;
129
+ }
130
+ else {
131
+ failureCount++;
132
+ }
133
+ totalDurationMs += entry.durationMs;
134
+ if (new Date(entry.timestamp).getTime() >= cutoff) {
135
+ last24hTotal++;
136
+ if (entry.success) {
137
+ last24hSuccess++;
138
+ }
139
+ else {
140
+ last24hFailure++;
141
+ }
142
+ }
143
+ }
144
+ const avgDurationMs = totalInvocations > 0 ? Math.round((totalDurationMs / totalInvocations) * 10) / 10 : 0;
145
+ return {
146
+ totalInvocations,
147
+ successCount,
148
+ failureCount,
149
+ last24h: {
150
+ total: last24hTotal,
151
+ success: last24hSuccess,
152
+ failure: last24hFailure,
153
+ },
154
+ avgDurationMs,
155
+ };
156
+ };
157
+ const createHandleFetch = ({ state, transports, sessionServers, getHotState }) => async (req, bunServer) => {
158
+ const url = new URL(req.url);
159
+ // --- Host header validation (DNS rebinding protection) ---
160
+ // Reject requests with a non-localhost Host header. A DNS rebinding
161
+ // attack re-maps a malicious domain to 127.0.0.1, so the browser sends
162
+ // requests to our loopback server with Host: evil.com. Checking the Host
163
+ // header is the standard mitigation (CVE-2025-66414 class).
164
+ const hostHeader = req.headers.get('Host');
165
+ if (!hostHeader || !isLocalhostHost(hostHeader)) {
166
+ return new Response('Forbidden: invalid Host header', { status: 403 });
167
+ }
168
+ // --- CORS protection ---
169
+ // MCP clients (Claude Code, etc.) don't run in browsers, so legitimate
170
+ // requests never carry an Origin header. Reject requests with an Origin
171
+ // header to prevent DNS rebinding attacks from malicious web pages.
172
+ //
173
+ // Chrome extension requests carry an Origin of `chrome-extension://...`
174
+ // and must be allowed through — the extension's background script
175
+ // fetches /ws-info to obtain the authenticated WebSocket URL.
176
+ const origin = req.headers.get('Origin');
177
+ if (origin && !origin.startsWith('chrome-extension://')) {
178
+ return new Response('Forbidden: browser requests are not allowed', { status: 403 });
179
+ }
180
+ // --- WebSocket upgrade for extension ---
181
+ if (url.pathname === '/ws') {
182
+ // Authenticate WebSocket connections using a shared secret sent via
183
+ // the Sec-WebSocket-Protocol header (not URL query params, which leak
184
+ // into server logs, browser history, and proxy logs).
185
+ // The client sends protocols: ['opentabs', '<secret>'] and the server
186
+ // echoes 'opentabs' as the accepted subprotocol.
187
+ // Uses constant-time comparison to prevent timing side-channel attacks.
188
+ if (state.wsSecret) {
189
+ const protocols = req.headers.get('sec-websocket-protocol');
190
+ const parts = protocols?.split(',').map(p => p.trim()) ?? [];
191
+ let secretMatched = false;
192
+ for (const part of parts) {
193
+ if (constantTimeEqual(part, state.wsSecret)) {
194
+ secretMatched = true;
195
+ }
196
+ }
197
+ if (!secretMatched) {
198
+ return new Response('Unauthorized', { status: 401 });
199
+ }
200
+ const upgraded = bunServer.upgrade(req, {
201
+ data: undefined,
202
+ headers: { 'sec-websocket-protocol': 'opentabs' },
203
+ });
204
+ if (!upgraded) {
205
+ return new Response('WebSocket upgrade failed', { status: 400 });
206
+ }
207
+ return undefined;
208
+ }
209
+ const upgraded = bunServer.upgrade(req, { data: undefined });
210
+ if (!upgraded) {
211
+ return new Response('WebSocket upgrade failed', { status: 400 });
212
+ }
213
+ return undefined;
214
+ }
215
+ // --- WebSocket info endpoint (for extension authentication) ---
216
+ // Returns the WebSocket URL and secret as separate fields. The secret
217
+ // is sent via the Sec-WebSocket-Protocol header during the upgrade,
218
+ // keeping it out of URLs, logs, and browser history.
219
+ //
220
+ // Requires Bearer auth. The extension bootstraps the secret from
221
+ // auth.json (bundled in the extension directory at install time)
222
+ // and uses it for all /ws-info requests.
223
+ if (url.pathname === '/ws-info' && req.method === 'GET') {
224
+ const authError = checkBearerAuth(req, state.wsSecret);
225
+ if (authError)
226
+ return authError;
227
+ const wsUrl = `ws://${url.host}/ws`;
228
+ return Response.json({ wsUrl, wsSecret: state.wsSecret });
229
+ }
230
+ // --- Health endpoint ---
231
+ // Unauthenticated requests get a minimal status response (alive check only).
232
+ // Authenticated requests get the full response with plugin details, paths, etc.
233
+ if (url.pathname === '/health' && req.method === 'GET') {
234
+ const authenticated = checkBearerAuth(req, state.wsSecret) === null;
235
+ if (!authenticated) {
236
+ return Response.json({
237
+ status: 'ok',
238
+ version,
239
+ extensionConnected: state.extensionWs !== null,
240
+ });
241
+ }
242
+ const hs = getHotState();
243
+ const pluginDetails = [...state.registry.plugins.values()].map(p => ({
244
+ name: p.name,
245
+ displayName: p.displayName,
246
+ toolCount: p.tools.length,
247
+ tools: p.tools.map(t => prefixedToolName(p.name, t.name)),
248
+ tabState: state.tabMapping.get(p.name)?.state ?? 'closed',
249
+ source: p.source,
250
+ sdkVersion: p.sdkVersion ?? null,
251
+ logBufferSize: getLogCount(p.name),
252
+ ...(p.iconSvg ? { iconSvg: p.iconSvg } : {}),
253
+ }));
254
+ const toolCount = state.registry.toolLookup.size + state.cachedBrowserTools.length;
255
+ const uptimeSeconds = Math.floor((Date.now() - state.startedAt) / 1000);
256
+ const pendingPlugins = state.fileWatcherEntries.filter(e => e.pluginName.startsWith('(pending:')).length;
257
+ const watchedPlugins = state.fileWatcherEntries.length - pendingPlugins;
258
+ const auditSummary = computeAuditSummary(state.auditLog);
259
+ const disabledBrowserTools = state.cachedBrowserTools
260
+ .filter(c => state.browserToolPolicy[c.name] === false)
261
+ .map(c => c.name);
262
+ return Response.json({
263
+ status: 'ok',
264
+ version,
265
+ sdkVersion,
266
+ mode: isDev() ? 'dev' : 'production',
267
+ extensionConnected: state.extensionWs !== null,
268
+ mcpClients: transports.size,
269
+ plugins: state.registry.plugins.size,
270
+ pluginDetails,
271
+ failedPlugins: [...state.registry.failures],
272
+ discoveryErrors: [...state.discoveryErrors],
273
+ toolCount,
274
+ disabledBrowserTools,
275
+ confirmationBypassed: state.skipConfirmation,
276
+ uptime: uptimeSeconds,
277
+ reloadCount: hs?.reloadCount ?? 0,
278
+ lastReloadTimestamp: hs?.lastReloadTimestamp ?? 0,
279
+ lastReloadDurationMs: hs?.lastReloadDurationMs ?? 0,
280
+ stateSchemaVersion: STATE_SCHEMA_VERSION,
281
+ fileWatcher: {
282
+ watchedPlugins,
283
+ pendingPlugins,
284
+ lastPollAt: state.mtimeLastPollAt,
285
+ pollDetections: state.mtimePollDetections,
286
+ },
287
+ auditSummary,
288
+ });
289
+ }
290
+ // --- Audit log endpoint ---
291
+ if (url.pathname === '/audit' && req.method === 'GET') {
292
+ const authError = checkBearerAuth(req, state.wsSecret);
293
+ if (authError)
294
+ return authError;
295
+ const limitParam = parseInt(url.searchParams.get('limit') ?? '50', 10);
296
+ const limit = Math.max(1, Math.min(500, Number.isNaN(limitParam) ? 50 : limitParam));
297
+ const pluginFilter = url.searchParams.get('plugin');
298
+ const toolFilter = url.searchParams.get('tool');
299
+ const successParam = url.searchParams.get('success');
300
+ const successFilter = successParam === 'true' ? true : successParam === 'false' ? false : undefined;
301
+ let entries = [...state.auditLog].reverse();
302
+ if (pluginFilter)
303
+ entries = entries.filter(e => e.plugin === pluginFilter);
304
+ if (toolFilter)
305
+ entries = entries.filter(e => e.tool === toolFilter);
306
+ if (successFilter !== undefined)
307
+ entries = entries.filter(e => e.success === successFilter);
308
+ entries = entries.slice(0, limit);
309
+ return Response.json(entries);
310
+ }
311
+ // --- Config/plugin rediscovery endpoint ---
312
+ if (url.pathname === '/reload' && req.method === 'POST') {
313
+ const authError = checkBearerAuth(req, state.wsSecret);
314
+ if (authError)
315
+ return authError;
316
+ if (!checkEndpointRateLimit('/reload', 10)) {
317
+ return new Response('Too Many Requests', { status: 429, headers: { 'Retry-After': '60' } });
318
+ }
319
+ try {
320
+ const result = await performConfigReload(state, sessionServers, transports);
321
+ return Response.json({
322
+ ok: true,
323
+ plugins: result.plugins,
324
+ durationMs: result.durationMs,
325
+ });
326
+ }
327
+ catch (err) {
328
+ log.error('Config reload failed:', err);
329
+ const rawMsg = err instanceof Error ? err.message : String(err);
330
+ return Response.json({ ok: false, error: sanitizeErrorMessage(rawMsg) }, { status: 500 });
331
+ }
332
+ }
333
+ // --- Extension reload endpoint ---
334
+ if (url.pathname === '/extension/reload' && req.method === 'POST') {
335
+ const authError = checkBearerAuth(req, state.wsSecret);
336
+ if (authError)
337
+ return authError;
338
+ if (!checkEndpointRateLimit('/extension/reload', 10)) {
339
+ return new Response('Too Many Requests', { status: 429, headers: { 'Retry-After': '60' } });
340
+ }
341
+ if (!state.extensionWs) {
342
+ return Response.json({ ok: false, error: 'Extension not connected' }, { status: 503 });
343
+ }
344
+ const id = getNextRequestId();
345
+ state.extensionWs.send(JSON.stringify({ jsonrpc: '2.0', method: 'extension.reload', id }));
346
+ return Response.json({ ok: true, message: 'Reload signal sent to extension' });
347
+ }
348
+ // --- MCP Streamable HTTP transport ---
349
+ if (url.pathname === '/mcp') {
350
+ const authError = checkBearerAuth(req, state.wsSecret);
351
+ if (authError)
352
+ return authError;
353
+ // Disable Bun's per-connection idle timeout for MCP requests.
354
+ // Tool dispatches can take up to DISPATCH_TIMEOUT_MS (30s) and the
355
+ // Streamable HTTP transport holds the response open until the tool
356
+ // result arrives. The default idle timeout (10s) would close the
357
+ // connection before long-running dispatches complete.
358
+ bunServer.timeout(req, 0);
359
+ const sessionId = req.headers.get('mcp-session-id');
360
+ if (req.method === 'POST') {
361
+ // Existing session
362
+ if (sessionId) {
363
+ const existingTransport = transports.get(sessionId);
364
+ if (existingTransport) {
365
+ return existingTransport.handleRequest(req);
366
+ }
367
+ }
368
+ // New session — rate-limit session creation to prevent resource exhaustion
369
+ if (!checkEndpointRateLimit('/mcp-session-create', 5)) {
370
+ return new Response('Too Many Requests', { status: 429, headers: { 'Retry-After': '60' } });
371
+ }
372
+ // Check if it's an initialize request
373
+ const body = await req.json().catch(() => null);
374
+ if (body && isInitializeRequest(body)) {
375
+ let sessionServer = null;
376
+ const removeSession = () => {
377
+ if (sessionServer) {
378
+ const idx = sessionServers.indexOf(sessionServer);
379
+ if (idx !== -1)
380
+ sessionServers.splice(idx, 1);
381
+ sessionServer = null;
382
+ }
383
+ };
384
+ const transport = new WebStandardStreamableHTTPServerTransport({
385
+ sessionIdGenerator: () => crypto.randomUUID(),
386
+ onsessioninitialized: (sid) => {
387
+ transports.set(sid, transport);
388
+ // Track which transport this session is connected to for stale sweep
389
+ if (sessionServer) {
390
+ state.sessionTransportIds.set(sessionServer, sid);
391
+ }
392
+ log.info(`MCP client connected (session: ${sid})`);
393
+ },
394
+ onsessionclosed: (sid) => {
395
+ transports.delete(sid);
396
+ removeSession();
397
+ log.info(`MCP client disconnected (session: ${sid})`);
398
+ },
399
+ });
400
+ transport.onclose = () => {
401
+ if (transport.sessionId) {
402
+ transports.delete(transport.sessionId);
403
+ }
404
+ removeSession();
405
+ };
406
+ try {
407
+ sessionServer = await createMcpServer(state);
408
+ sessionServers.push(sessionServer);
409
+ await sessionServer.connect(transport);
410
+ return await transport.handleRequest(req, { parsedBody: body });
411
+ }
412
+ catch (err) {
413
+ removeSession();
414
+ throw err;
415
+ }
416
+ }
417
+ return Response.json({
418
+ jsonrpc: '2.0',
419
+ error: {
420
+ code: -32600,
421
+ message: 'Bad Request: missing session or not an initialize request',
422
+ },
423
+ id: null,
424
+ }, { status: 400 });
425
+ }
426
+ if (req.method === 'GET') {
427
+ const getTransport = sessionId ? transports.get(sessionId) : undefined;
428
+ if (getTransport) {
429
+ return getTransport.handleRequest(req);
430
+ }
431
+ return new Response('Missing or invalid session', { status: 400 });
432
+ }
433
+ if (req.method === 'DELETE') {
434
+ const delTransport = sessionId ? transports.get(sessionId) : undefined;
435
+ if (delTransport) {
436
+ return delTransport.handleRequest(req);
437
+ }
438
+ return new Response('Missing or invalid session', { status: 400 });
439
+ }
440
+ return new Response('Method not allowed', { status: 405 });
441
+ }
442
+ return new Response('Not Found', { status: 404 });
443
+ };
444
+ const createHandleWsOpen = (state) => (ws) => {
445
+ const previousWs = state.extensionWs;
446
+ // Assign the new WS BEFORE closing the previous one. Bun fires the
447
+ // close handler synchronously during ws.close(), so if extensionWs
448
+ // still pointed at the old WS the close handler would see
449
+ // `state.extensionWs === ws` (true) and reject all pending dispatches
450
+ // with "Extension disconnected" — even though a new connection is
451
+ // already taking over.
452
+ log.info('Extension WebSocket connected');
453
+ state.extensionWs = ws;
454
+ if (previousWs && previousWs !== ws) {
455
+ log.info('Closing previous extension WebSocket (replaced by new connection)');
456
+ try {
457
+ previousWs.close(1000, 'Replaced by new connection');
458
+ }
459
+ catch {
460
+ // Already closed
461
+ }
462
+ }
463
+ void sendSyncFull(state).then(() => {
464
+ // After sync.full completes, check if there's a pending extension reload
465
+ // (extension files were updated while extension was disconnected).
466
+ // The delay ensures sync.full is processed before the extension reloads.
467
+ if (state.pendingExtensionReload) {
468
+ state.pendingExtensionReload = false;
469
+ log.info('Sending deferred extension reload (version was updated while extension was disconnected)');
470
+ setTimeout(() => sendExtensionReload(state), 500);
471
+ }
472
+ });
473
+ };
474
+ const createHandleWsMessage = (state, mcpCallbacks) => (ws, message) => {
475
+ const text = typeof message === 'string' ? message : new TextDecoder().decode(message);
476
+ handleExtensionMessage(state, text, mcpCallbacks, ws);
477
+ };
478
+ const createHandleWsClose = (state) => (ws) => {
479
+ log.info('Extension WebSocket disconnected');
480
+ if (state.extensionWs === ws) {
481
+ state.extensionWs = null;
482
+ // Reject all pending confirmations immediately so tool dispatch promises
483
+ // resolve with an error instead of hanging until confirmation timeout.
484
+ rejectAllPendingConfirmations(state);
485
+ // Reject all pending dispatches immediately so MCP clients get a fast
486
+ // error instead of hanging until the 30-second dispatch timeout fires.
487
+ if (state.pendingDispatches.size > 0) {
488
+ log.info(`Rejecting ${state.pendingDispatches.size} pending dispatch(es) due to extension disconnect`);
489
+ for (const [id, pending] of state.pendingDispatches) {
490
+ state.pendingDispatches.delete(id);
491
+ clearTimeout(pending.timerId);
492
+ pending.reject(new Error('Extension disconnected'));
493
+ }
494
+ }
495
+ }
496
+ };
497
+ /**
498
+ * Create all hot-reloadable handler functions.
499
+ * Called on every module evaluation (first load + hot reloads) to produce
500
+ * fresh closures over the latest module imports.
501
+ */
502
+ const createHandlers = (deps) => {
503
+ const mcpCallbacks = createMcpCallbacks(deps.state, deps.sessionServers);
504
+ return {
505
+ fetch: createHandleFetch(deps),
506
+ wsOpen: createHandleWsOpen(deps.state),
507
+ wsMessage: createHandleWsMessage(deps.state, mcpCallbacks),
508
+ wsClose: createHandleWsClose(deps.state),
509
+ };
510
+ };
511
+ /**
512
+ * Remove sessionServers entries whose transport is no longer in the transports
513
+ * map. This prevents unbounded growth when MCP clients disconnect ungracefully
514
+ * (network partition, OOM kill) and the onsessionclosed / transport.onclose
515
+ * callbacks never fire.
516
+ *
517
+ * Each session server is tracked in state.sessionTransportIds (a WeakMap keyed
518
+ * by the McpServerInstance) with the transport session ID it was connected to.
519
+ * A session is stale if its transport ID is absent from the active transports
520
+ * map — meaning the transport was cleaned up but the session server was not.
521
+ *
522
+ * Called on each hot reload from reload.ts.
523
+ */
524
+ const sweepStaleSessions = (state, transports, sessionServers) => {
525
+ let swept = 0;
526
+ for (let i = sessionServers.length - 1; i >= 0; i--) {
527
+ const srv = sessionServers[i];
528
+ if (!srv)
529
+ continue;
530
+ const transportId = state.sessionTransportIds.get(srv);
531
+ // If we have a recorded transport ID and that transport is gone, the session is stale.
532
+ // If there is no recorded transport ID, the session may be in-flight (created but
533
+ // onsessioninitialized hasn't fired yet) — keep it to avoid trimming valid sessions.
534
+ if (transportId !== undefined && !transports.has(transportId)) {
535
+ sessionServers.splice(i, 1);
536
+ swept++;
537
+ }
538
+ }
539
+ if (swept > 0) {
540
+ log.info(`Swept ${swept} stale MCP session(s) (${transports.size} active transport(s) remain)`);
541
+ }
542
+ return swept;
543
+ };
544
+ export { checkBearerAuth, constantTimeEqual, createHandlers, isLocalhostHost, sweepStaleSessions };
545
+ //# sourceMappingURL=http-routes.js.map