@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.
- package/dist/audit-disk.d.ts +23 -0
- package/dist/audit-disk.d.ts.map +1 -0
- package/dist/audit-disk.js +74 -0
- package/dist/audit-disk.js.map +1 -0
- package/dist/browser-tools/analyze-site/detect-apis.d.ts +36 -0
- package/dist/browser-tools/analyze-site/detect-apis.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site/detect-apis.js +383 -0
- package/dist/browser-tools/analyze-site/detect-apis.js.map +1 -0
- package/dist/browser-tools/analyze-site/detect-auth.d.ts +72 -0
- package/dist/browser-tools/analyze-site/detect-auth.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site/detect-auth.js +384 -0
- package/dist/browser-tools/analyze-site/detect-auth.js.map +1 -0
- package/dist/browser-tools/analyze-site/detect-dom.d.ts +65 -0
- package/dist/browser-tools/analyze-site/detect-dom.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site/detect-dom.js +45 -0
- package/dist/browser-tools/analyze-site/detect-dom.js.map +1 -0
- package/dist/browser-tools/analyze-site/detect-framework.d.ts +48 -0
- package/dist/browser-tools/analyze-site/detect-framework.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site/detect-framework.js +31 -0
- package/dist/browser-tools/analyze-site/detect-framework.js.map +1 -0
- package/dist/browser-tools/analyze-site/detect-globals.d.ts +41 -0
- package/dist/browser-tools/analyze-site/detect-globals.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site/detect-globals.js +42 -0
- package/dist/browser-tools/analyze-site/detect-globals.js.map +1 -0
- package/dist/browser-tools/analyze-site/detect-storage.d.ts +39 -0
- package/dist/browser-tools/analyze-site/detect-storage.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site/detect-storage.js +34 -0
- package/dist/browser-tools/analyze-site/detect-storage.js.map +1 -0
- package/dist/browser-tools/analyze-site/index.d.ts +52 -0
- package/dist/browser-tools/analyze-site/index.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site/index.js +827 -0
- package/dist/browser-tools/analyze-site/index.js.map +1 -0
- package/dist/browser-tools/analyze-site.d.ts +17 -0
- package/dist/browser-tools/analyze-site.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site.js +41 -0
- package/dist/browser-tools/analyze-site.js.map +1 -0
- package/dist/browser-tools/clear-console-logs.d.ts +9 -0
- package/dist/browser-tools/clear-console-logs.d.ts.map +1 -0
- package/dist/browser-tools/clear-console-logs.js +16 -0
- package/dist/browser-tools/clear-console-logs.js.map +1 -0
- package/dist/browser-tools/click-element.d.ts +10 -0
- package/dist/browser-tools/click-element.d.ts.map +1 -0
- package/dist/browser-tools/click-element.js +22 -0
- package/dist/browser-tools/click-element.js.map +1 -0
- package/dist/browser-tools/close-tab.d.ts +9 -0
- package/dist/browser-tools/close-tab.d.ts.map +1 -0
- package/dist/browser-tools/close-tab.js +16 -0
- package/dist/browser-tools/close-tab.js.map +1 -0
- package/dist/browser-tools/definition.d.ts +26 -0
- package/dist/browser-tools/definition.d.ts.map +1 -0
- package/dist/browser-tools/definition.js +16 -0
- package/dist/browser-tools/definition.js.map +1 -0
- package/dist/browser-tools/delete-cookies.d.ts +10 -0
- package/dist/browser-tools/delete-cookies.d.ts.map +1 -0
- package/dist/browser-tools/delete-cookies.js +19 -0
- package/dist/browser-tools/delete-cookies.js.map +1 -0
- package/dist/browser-tools/disable-network-capture.d.ts +9 -0
- package/dist/browser-tools/disable-network-capture.d.ts.map +1 -0
- package/dist/browser-tools/disable-network-capture.js +16 -0
- package/dist/browser-tools/disable-network-capture.js.map +1 -0
- package/dist/browser-tools/enable-network-capture.d.ts +12 -0
- package/dist/browser-tools/enable-network-capture.d.ts.map +1 -0
- package/dist/browser-tools/enable-network-capture.js +42 -0
- package/dist/browser-tools/enable-network-capture.js.map +1 -0
- package/dist/browser-tools/execute-script.d.ts +19 -0
- package/dist/browser-tools/execute-script.d.ts.map +1 -0
- package/dist/browser-tools/execute-script.js +51 -0
- package/dist/browser-tools/execute-script.js.map +1 -0
- package/dist/browser-tools/extension-check-adapter.d.ts +11 -0
- package/dist/browser-tools/extension-check-adapter.d.ts.map +1 -0
- package/dist/browser-tools/extension-check-adapter.js +22 -0
- package/dist/browser-tools/extension-check-adapter.js.map +1 -0
- package/dist/browser-tools/extension-force-reconnect.d.ts +9 -0
- package/dist/browser-tools/extension-force-reconnect.d.ts.map +1 -0
- package/dist/browser-tools/extension-force-reconnect.js +19 -0
- package/dist/browser-tools/extension-force-reconnect.js.map +1 -0
- package/dist/browser-tools/extension-get-logs.d.ts +24 -0
- package/dist/browser-tools/extension-get-logs.d.ts.map +1 -0
- package/dist/browser-tools/extension-get-logs.js +34 -0
- package/dist/browser-tools/extension-get-logs.js.map +1 -0
- package/dist/browser-tools/extension-get-side-panel.d.ts +8 -0
- package/dist/browser-tools/extension-get-side-panel.d.ts.map +1 -0
- package/dist/browser-tools/extension-get-side-panel.js +17 -0
- package/dist/browser-tools/extension-get-side-panel.js.map +1 -0
- package/dist/browser-tools/extension-get-state.d.ts +9 -0
- package/dist/browser-tools/extension-get-state.d.ts.map +1 -0
- package/dist/browser-tools/extension-get-state.js +19 -0
- package/dist/browser-tools/extension-get-state.js.map +1 -0
- package/dist/browser-tools/focus-tab.d.ts +9 -0
- package/dist/browser-tools/focus-tab.d.ts.map +1 -0
- package/dist/browser-tools/focus-tab.js +17 -0
- package/dist/browser-tools/focus-tab.js.map +1 -0
- package/dist/browser-tools/get-console-logs.d.ts +18 -0
- package/dist/browser-tools/get-console-logs.d.ts.map +1 -0
- package/dist/browser-tools/get-console-logs.js +30 -0
- package/dist/browser-tools/get-console-logs.js.map +1 -0
- package/dist/browser-tools/get-cookies.d.ts +10 -0
- package/dist/browser-tools/get-cookies.d.ts.map +1 -0
- package/dist/browser-tools/get-cookies.js +23 -0
- package/dist/browser-tools/get-cookies.js.map +1 -0
- package/dist/browser-tools/get-network-requests.d.ts +10 -0
- package/dist/browser-tools/get-network-requests.d.ts.map +1 -0
- package/dist/browser-tools/get-network-requests.js +27 -0
- package/dist/browser-tools/get-network-requests.js.map +1 -0
- package/dist/browser-tools/get-page-html.d.ts +11 -0
- package/dist/browser-tools/get-page-html.d.ts.map +1 -0
- package/dist/browser-tools/get-page-html.js +32 -0
- package/dist/browser-tools/get-page-html.js.map +1 -0
- package/dist/browser-tools/get-resource-content.d.ts +11 -0
- package/dist/browser-tools/get-resource-content.d.ts.map +1 -0
- package/dist/browser-tools/get-resource-content.js +31 -0
- package/dist/browser-tools/get-resource-content.js.map +1 -0
- package/dist/browser-tools/get-storage.d.ts +14 -0
- package/dist/browser-tools/get-storage.d.ts.map +1 -0
- package/dist/browser-tools/get-storage.js +28 -0
- package/dist/browser-tools/get-storage.js.map +1 -0
- package/dist/browser-tools/get-tab-content.d.ts +11 -0
- package/dist/browser-tools/get-tab-content.d.ts.map +1 -0
- package/dist/browser-tools/get-tab-content.js +29 -0
- package/dist/browser-tools/get-tab-content.js.map +1 -0
- package/dist/browser-tools/get-tab-info.d.ts +9 -0
- package/dist/browser-tools/get-tab-info.d.ts.map +1 -0
- package/dist/browser-tools/get-tab-info.js +17 -0
- package/dist/browser-tools/get-tab-info.js.map +1 -0
- package/dist/browser-tools/handle-dialog.d.ts +14 -0
- package/dist/browser-tools/handle-dialog.d.ts.map +1 -0
- package/dist/browser-tools/handle-dialog.js +30 -0
- package/dist/browser-tools/handle-dialog.js.map +1 -0
- package/dist/browser-tools/hover-element.d.ts +11 -0
- package/dist/browser-tools/hover-element.d.ts.map +1 -0
- package/dist/browser-tools/hover-element.js +24 -0
- package/dist/browser-tools/hover-element.js.map +1 -0
- package/dist/browser-tools/index.d.ts +7 -0
- package/dist/browser-tools/index.d.ts.map +1 -0
- package/dist/browser-tools/index.js +81 -0
- package/dist/browser-tools/index.js.map +1 -0
- package/dist/browser-tools/list-resources.d.ts +10 -0
- package/dist/browser-tools/list-resources.d.ts.map +1 -0
- package/dist/browser-tools/list-resources.js +29 -0
- package/dist/browser-tools/list-resources.js.map +1 -0
- package/dist/browser-tools/list-tabs.d.ts +7 -0
- package/dist/browser-tools/list-tabs.d.ts.map +1 -0
- package/dist/browser-tools/list-tabs.js +16 -0
- package/dist/browser-tools/list-tabs.js.map +1 -0
- package/dist/browser-tools/navigate-tab.d.ts +10 -0
- package/dist/browser-tools/navigate-tab.d.ts.map +1 -0
- package/dist/browser-tools/navigate-tab.js +18 -0
- package/dist/browser-tools/navigate-tab.js.map +1 -0
- package/dist/browser-tools/open-tab.d.ts +9 -0
- package/dist/browser-tools/open-tab.d.ts.map +1 -0
- package/dist/browser-tools/open-tab.js +18 -0
- package/dist/browser-tools/open-tab.js.map +1 -0
- package/dist/browser-tools/press-key.d.ts +17 -0
- package/dist/browser-tools/press-key.d.ts.map +1 -0
- package/dist/browser-tools/press-key.js +43 -0
- package/dist/browser-tools/press-key.js.map +1 -0
- package/dist/browser-tools/query-elements.d.ts +12 -0
- package/dist/browser-tools/query-elements.d.ts.map +1 -0
- package/dist/browser-tools/query-elements.js +30 -0
- package/dist/browser-tools/query-elements.js.map +1 -0
- package/dist/browser-tools/reload-extension.d.ts +13 -0
- package/dist/browser-tools/reload-extension.d.ts.map +1 -0
- package/dist/browser-tools/reload-extension.js +35 -0
- package/dist/browser-tools/reload-extension.js.map +1 -0
- package/dist/browser-tools/screenshot-tab.d.ts +9 -0
- package/dist/browser-tools/screenshot-tab.d.ts.map +1 -0
- package/dist/browser-tools/screenshot-tab.js +22 -0
- package/dist/browser-tools/screenshot-tab.js.map +1 -0
- package/dist/browser-tools/scroll.d.ts +23 -0
- package/dist/browser-tools/scroll.d.ts.map +1 -0
- package/dist/browser-tools/scroll.js +56 -0
- package/dist/browser-tools/scroll.js.map +1 -0
- package/dist/browser-tools/select-option.d.ts +12 -0
- package/dist/browser-tools/select-option.d.ts.map +1 -0
- package/dist/browser-tools/select-option.js +25 -0
- package/dist/browser-tools/select-option.js.map +1 -0
- package/dist/browser-tools/set-cookie.d.ts +16 -0
- package/dist/browser-tools/set-cookie.d.ts.map +1 -0
- package/dist/browser-tools/set-cookie.js +42 -0
- package/dist/browser-tools/set-cookie.js.map +1 -0
- package/dist/browser-tools/type-text.d.ts +12 -0
- package/dist/browser-tools/type-text.d.ts.map +1 -0
- package/dist/browser-tools/type-text.js +25 -0
- package/dist/browser-tools/type-text.js.map +1 -0
- package/dist/browser-tools/url-validation.d.ts +13 -0
- package/dist/browser-tools/url-validation.d.ts.map +1 -0
- package/dist/browser-tools/url-validation.js +23 -0
- package/dist/browser-tools/url-validation.js.map +1 -0
- package/dist/browser-tools/wait-for-element.d.ts +12 -0
- package/dist/browser-tools/wait-for-element.d.ts.map +1 -0
- package/dist/browser-tools/wait-for-element.js +29 -0
- package/dist/browser-tools/wait-for-element.js.map +1 -0
- package/dist/config.d.ts +99 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +344 -0
- package/dist/config.js.map +1 -0
- package/dist/dev-mode.d.ts +14 -0
- package/dist/dev-mode.d.ts.map +1 -0
- package/dist/dev-mode.js +15 -0
- package/dist/dev-mode.js.map +1 -0
- package/dist/discovery-legacy.d.ts +32 -0
- package/dist/discovery-legacy.d.ts.map +1 -0
- package/dist/discovery-legacy.js +415 -0
- package/dist/discovery-legacy.js.map +1 -0
- package/dist/discovery.d.ts +28 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +97 -0
- package/dist/discovery.js.map +1 -0
- package/dist/extension-install.d.ts +27 -0
- package/dist/extension-install.d.ts.map +1 -0
- package/dist/extension-install.js +75 -0
- package/dist/extension-install.js.map +1 -0
- package/dist/extension-protocol.d.ts +130 -0
- package/dist/extension-protocol.d.ts.map +1 -0
- package/dist/extension-protocol.js +869 -0
- package/dist/extension-protocol.js.map +1 -0
- package/dist/file-watcher.d.ts +75 -0
- package/dist/file-watcher.d.ts.map +1 -0
- package/dist/file-watcher.js +616 -0
- package/dist/file-watcher.js.map +1 -0
- package/dist/http-routes.d.ts +88 -0
- package/dist/http-routes.d.ts.map +1 -0
- package/dist/http-routes.js +545 -0
- package/dist/http-routes.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +187 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +100 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +402 -0
- package/dist/loader.js.map +1 -0
- package/dist/log-buffer.d.ts +33 -0
- package/dist/log-buffer.d.ts.map +1 -0
- package/dist/log-buffer.js +64 -0
- package/dist/log-buffer.js.map +1 -0
- package/dist/logger.d.ts +34 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +81 -0
- package/dist/logger.js.map +1 -0
- package/dist/manifest-schema.d.ts +14 -0
- package/dist/manifest-schema.d.ts.map +1 -0
- package/dist/manifest-schema.js +51 -0
- package/dist/manifest-schema.js.map +1 -0
- package/dist/mcp-setup.d.ts +131 -0
- package/dist/mcp-setup.d.ts.map +1 -0
- package/dist/mcp-setup.js +673 -0
- package/dist/mcp-setup.js.map +1 -0
- package/dist/permissions.d.ts +59 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +141 -0
- package/dist/permissions.js.map +1 -0
- package/dist/registry.d.ts +78 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +187 -0
- package/dist/registry.js.map +1 -0
- package/dist/reload.d.ts +52 -0
- package/dist/reload.d.ts.map +1 -0
- package/dist/reload.js +326 -0
- package/dist/reload.js.map +1 -0
- package/dist/resolver.d.ts +53 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +272 -0
- package/dist/resolver.js.map +1 -0
- package/dist/sanitize-error.d.ts +8 -0
- package/dist/sanitize-error.d.ts.map +1 -0
- package/dist/sanitize-error.js +25 -0
- package/dist/sanitize-error.js.map +1 -0
- package/dist/sanitize-tool-output.d.ts +20 -0
- package/dist/sanitize-tool-output.d.ts.map +1 -0
- package/dist/sanitize-tool-output.js +52 -0
- package/dist/sanitize-tool-output.js.map +1 -0
- package/dist/sdk-version.d.ts +11 -0
- package/dist/sdk-version.d.ts.map +1 -0
- package/dist/sdk-version.js +23 -0
- package/dist/sdk-version.js.map +1 -0
- package/dist/shutdown.d.ts +28 -0
- package/dist/shutdown.d.ts.map +1 -0
- package/dist/shutdown.js +68 -0
- package/dist/shutdown.js.map +1 -0
- package/dist/skip-confirmation.d.ts +15 -0
- package/dist/skip-confirmation.d.ts.map +1 -0
- package/dist/skip-confirmation.js +16 -0
- package/dist/skip-confirmation.js.map +1 -0
- package/dist/skip-sanitization.d.ts +17 -0
- package/dist/skip-sanitization.d.ts.map +1 -0
- package/dist/skip-sanitization.js +18 -0
- package/dist/skip-sanitization.js.map +1 -0
- package/dist/skip-verification.d.ts +11 -0
- package/dist/skip-verification.d.ts.map +1 -0
- package/dist/skip-verification.js +12 -0
- package/dist/skip-verification.js.map +1 -0
- package/dist/state.d.ts +290 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +111 -0
- package/dist/state.js.map +1 -0
- package/dist/verify-plugin.d.ts +53 -0
- package/dist/verify-plugin.d.ts.map +1 -0
- package/dist/verify-plugin.js +123 -0
- package/dist/verify-plugin.js.map +1 -0
- package/dist/version-check.d.ts +35 -0
- package/dist/version-check.d.ts.map +1 -0
- package/dist/version-check.js +111 -0
- package/dist/version-check.js.map +1 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +22 -0
- package/dist/version.js.map +1 -0
- 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
|