@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,673 @@
1
+ /**
2
+ * MCP server factory.
3
+ * Creates a low-level Server instance and registers tools dynamically from
4
+ * discovered plugins and built-in browser tools.
5
+ *
6
+ * Uses the low-level Server API (not McpServer) because plugin manifests provide
7
+ * raw JSON Schema objects for tool input/output — McpServer.registerTool() requires
8
+ * Zod schemas and cannot accept pre-computed JSON Schema. The Server class deprecation
9
+ * message acknowledges this: "Only use Server for advanced use cases."
10
+ *
11
+ * The Server import uses dynamic import() to satisfy import-x/no-deprecated. The
12
+ * @typescript-eslint/no-deprecated rule is addressed by typing through the awaited
13
+ * module rather than referencing the deprecated class name directly.
14
+ *
15
+ * Hot reload:
16
+ * `registerMcpHandlers` is separated from `createMcpServer` so that existing
17
+ * MCP sessions can have their tools/list and tools/call handler logic refreshed
18
+ * on hot reload. After bun --hot re-evaluates this module, calling
19
+ * `registerMcpHandlers(server, state)` on each existing session replaces the
20
+ * old handler closures with new ones that reference the fresh module imports
21
+ * (dispatchToExtension, sendInvocationStart, etc.).
22
+ */
23
+ import { dispatchToExtension, isDispatchError, sendInvocationStart, sendInvocationEnd, sendConfirmationRequest, } from './extension-protocol.js';
24
+ import { log } from './logger.js';
25
+ import { evaluatePermission } from './permissions.js';
26
+ import { getResource, getPrompt, listAllResources, listAllPrompts, trustTierPrefix } from './registry.js';
27
+ import { sanitizeErrorMessage } from './sanitize-error.js';
28
+ import { prefixedToolName, isToolEnabled, isBrowserToolEnabled, appendAuditEntry, isSessionAllowed } from './state.js';
29
+ import { version } from './version.js';
30
+ import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, McpError as SdkMcpError, ErrorCode, } from '@modelcontextprotocol/sdk/types.js';
31
+ import { z } from 'zod';
32
+ /** Maximum concurrent tool dispatches per plugin to prevent tab performance degradation */
33
+ const MAX_CONCURRENT_DISPATCHES_PER_PLUGIN = 5;
34
+ /** Keys that could trigger prototype pollution in JSON deserialization */
35
+ const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
36
+ /**
37
+ * Recursively remove dangerous keys from objects to prevent prototype pollution
38
+ * in MCP clients that use naive JSON deserialization.
39
+ */
40
+ const sanitizeOutput = (obj, depth = 0) => {
41
+ if (depth > 50 || obj === null || obj === undefined || typeof obj !== 'object')
42
+ return obj;
43
+ if (Array.isArray(obj))
44
+ return obj.map(item => sanitizeOutput(item, depth + 1));
45
+ const result = {};
46
+ for (const [key, value] of Object.entries(obj)) {
47
+ if (!DANGEROUS_KEYS.has(key))
48
+ result[key] = sanitizeOutput(value, depth + 1);
49
+ }
50
+ return result;
51
+ };
52
+ /**
53
+ * Extract the target domain hostname for a browser tool call.
54
+ *
55
+ * - Tools with a `url` param (get_cookies, set_cookie, delete_cookies, open_tab):
56
+ * parse the hostname from the URL
57
+ * - Tools with a `tabId` param: dispatch browser.getTabInfo to get the tab's URL,
58
+ * then parse the hostname
59
+ * - Tools with neither: return null (observe-tier tools, extension diagnostics)
60
+ */
61
+ const resolveToolDomain = async (toolName, args, state) => {
62
+ // URL-based tools: parse domain from the url parameter
63
+ const urlArg = args.url;
64
+ if (typeof urlArg === 'string' && urlArg !== '') {
65
+ try {
66
+ return new URL(urlArg).hostname;
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ // Tab-based tools: get the tab's URL via a lightweight dispatch
73
+ const tabIdArg = args.tabId;
74
+ if (typeof tabIdArg === 'number') {
75
+ try {
76
+ const tabInfo = (await dispatchToExtension(state, 'browser.getTabInfo', { tabId: tabIdArg }));
77
+ if (typeof tabInfo.url === 'string' && tabInfo.url !== '') {
78
+ return new URL(tabInfo.url).hostname;
79
+ }
80
+ }
81
+ catch {
82
+ // Tab may be closed or unreachable — domain resolution is best-effort
83
+ }
84
+ return null;
85
+ }
86
+ return null;
87
+ };
88
+ /**
89
+ * Truncate tool parameters into a short preview for the confirmation dialog.
90
+ * Shows the first ~200 characters of the JSON-stringified args.
91
+ */
92
+ const truncateParamsPreview = (args) => {
93
+ const json = JSON.stringify(args, null, 2);
94
+ if (json.length <= 200)
95
+ return json;
96
+ return json.slice(0, 200) + '…';
97
+ };
98
+ /**
99
+ * Dynamically import the MCP SDK Server constructor.
100
+ *
101
+ * Each call performs a fresh dynamic import(). Under bun --hot, module-level
102
+ * caches reset on every re-evaluation, so caching here would be misleading —
103
+ * it would appear to persist but actually reset to null on each reload. The
104
+ * dynamic import is fast (resolved from the module cache by the runtime) and
105
+ * only runs once per server creation or reload cycle.
106
+ */
107
+ const getServerCtor = async () => {
108
+ const mod = (await import('@modelcontextprotocol/sdk/server/index.js'));
109
+ return mod.Server;
110
+ };
111
+ /**
112
+ * Format a structured error response for MCP clients.
113
+ *
114
+ * When the error data contains structured fields (category, retryable, retryAfterMs),
115
+ * produces a human-readable prefix line followed by a machine-readable JSON block.
116
+ * When only the code is present (legacy), produces [CODE] message.
117
+ */
118
+ const formatStructuredError = (code, message, data) => {
119
+ const category = typeof data?.category === 'string' ? data.category : undefined;
120
+ const retryable = typeof data?.retryable === 'boolean' ? data.retryable : undefined;
121
+ const retryAfterMs = typeof data?.retryAfterMs === 'number' ? data.retryAfterMs : undefined;
122
+ const hasStructuredFields = category !== undefined || retryable !== undefined || retryAfterMs !== undefined;
123
+ if (!hasStructuredFields) {
124
+ return `[${code}] ${message}`;
125
+ }
126
+ // Build the human-readable prefix with only present fields
127
+ const parts = [`code=${code}`];
128
+ if (category !== undefined)
129
+ parts.push(`category=${category}`);
130
+ if (retryable !== undefined)
131
+ parts.push(`retryable=${String(retryable)}`);
132
+ if (retryAfterMs !== undefined)
133
+ parts.push(`retryAfterMs=${retryAfterMs}`);
134
+ const prefix = `[ERROR ${parts.join(' ')}] ${message}`;
135
+ // Build the machine-readable JSON block with only present fields
136
+ const jsonObj = { code };
137
+ if (category !== undefined)
138
+ jsonObj.category = category;
139
+ if (retryable !== undefined)
140
+ jsonObj.retryable = retryable;
141
+ if (retryAfterMs !== undefined)
142
+ jsonObj.retryAfterMs = retryAfterMs;
143
+ return `${prefix}\n\n\`\`\`json\n${JSON.stringify(jsonObj)}\n\`\`\``;
144
+ };
145
+ /** Format a ZodError into a readable validation message listing each failing field */
146
+ const formatZodError = (err) => {
147
+ const issues = err.issues.map(issue => {
148
+ const path = issue.path.length > 0 ? issue.path.join('.') : '(root)';
149
+ return ` - ${path}: ${issue.message}`;
150
+ });
151
+ return `Invalid arguments:\n${issues.join('\n')}`;
152
+ };
153
+ /**
154
+ * Rebuild cached browser tool JSON schemas on state.
155
+ * Called after state.browserTools changes (during reload).
156
+ * Plugin tool lookups are handled by the immutable registry.
157
+ */
158
+ const rebuildCachedBrowserTools = (state) => {
159
+ state.cachedBrowserTools = state.browserTools.map((bt) => {
160
+ const schema = z.toJSONSchema(bt.input);
161
+ delete schema['$schema'];
162
+ return {
163
+ name: bt.name,
164
+ description: bt.description,
165
+ inputSchema: schema,
166
+ tool: bt,
167
+ };
168
+ });
169
+ };
170
+ /**
171
+ * Register (or re-register) tools/list and tools/call handlers on an MCP Server
172
+ * instance. Each handler creates fresh closures over the current module's imports
173
+ * (dispatchToolToExtension, sendInvocationStart, etc.), ensuring that after hot
174
+ * reload, existing sessions invoke the latest handler logic.
175
+ *
176
+ * Called by:
177
+ * 1. `createMcpServer` — for new sessions
178
+ * 2. Hot reload sequence in reload.ts — for existing sessions
179
+ */
180
+ const registerMcpHandlers = (server, state) => {
181
+ // Handler: tools/list — return enabled plugin tools + browser tools.
182
+ // Delegates to getEnabledToolsList() which filters disabled plugin tools
183
+ // and always includes browser tools.
184
+ server.setRequestHandler(ListToolsRequestSchema, () => ({
185
+ tools: getEnabledToolsList(state),
186
+ }));
187
+ // Handler: resources/list — return all resources from all plugins with prefixed URIs.
188
+ server.setRequestHandler(ListResourcesRequestSchema, () => ({
189
+ resources: listAllResources(state.registry),
190
+ }));
191
+ // Handler: resources/read — dispatch to extension to read resource in page context.
192
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
193
+ const uri = request.params.uri;
194
+ if (typeof uri !== 'string' || uri.length === 0) {
195
+ throw new SdkMcpError(ErrorCode.InvalidParams, 'Missing or invalid "uri" parameter');
196
+ }
197
+ const result = getResource(state.registry, uri);
198
+ if (!result) {
199
+ throw new SdkMcpError(ErrorCode.InvalidParams, `Resource not found: ${uri}`);
200
+ }
201
+ const { plugin, resource } = result;
202
+ log.debug('resource.read:', uri, '→', plugin.name + '/' + resource.uri);
203
+ if (!state.extensionWs) {
204
+ throw new SdkMcpError(ErrorCode.InternalError, 'Extension not connected. Please ensure the OpenTabs Chrome extension is running.');
205
+ }
206
+ try {
207
+ const dispatchResult = await dispatchToExtension(state, 'resource.read', { plugin: plugin.name, uri: resource.uri }, { label: `${plugin.name}/resource:${resource.uri}` });
208
+ // Extension wraps adapter output in { output: ... } — unwrap it
209
+ const raw = dispatchResult;
210
+ const contents = (raw.output ?? raw);
211
+ return {
212
+ contents: [
213
+ {
214
+ uri,
215
+ text: contents.text,
216
+ blob: contents.blob,
217
+ mimeType: contents.mimeType ?? resource.mimeType,
218
+ },
219
+ ],
220
+ };
221
+ }
222
+ catch (err) {
223
+ if (isDispatchError(err)) {
224
+ throw new SdkMcpError(err.code, err.message);
225
+ }
226
+ const msg = err instanceof Error ? err.message : String(err);
227
+ throw new SdkMcpError(ErrorCode.InternalError, `Resource read error: ${msg}`);
228
+ }
229
+ });
230
+ // Handler: prompts/list — return all prompts from all plugins with prefixed names.
231
+ server.setRequestHandler(ListPromptsRequestSchema, () => ({
232
+ prompts: listAllPrompts(state.registry),
233
+ }));
234
+ // Handler: prompts/get — dispatch to extension to render prompt in page context.
235
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
236
+ const promptName = request.params.name;
237
+ if (typeof promptName !== 'string' || promptName.length === 0) {
238
+ throw new SdkMcpError(ErrorCode.InvalidParams, 'Missing or invalid "name" parameter');
239
+ }
240
+ const result = getPrompt(state.registry, promptName);
241
+ if (!result) {
242
+ throw new SdkMcpError(ErrorCode.InvalidParams, `Prompt not found: ${promptName}`);
243
+ }
244
+ const { plugin, prompt } = result;
245
+ const args = request.params.arguments ?? {};
246
+ log.debug('prompt.get:', promptName, '→', plugin.name + '/' + prompt.name);
247
+ if (!state.extensionWs) {
248
+ throw new SdkMcpError(ErrorCode.InternalError, 'Extension not connected. Please ensure the OpenTabs Chrome extension is running.');
249
+ }
250
+ try {
251
+ const dispatchResult = await dispatchToExtension(state, 'prompt.get', { plugin: plugin.name, prompt: prompt.name, arguments: args }, { label: `${plugin.name}/prompt:${prompt.name}` });
252
+ // Extension wraps adapter output in { output: ... } — unwrap it
253
+ const raw = dispatchResult;
254
+ const messages = (raw.output ?? raw);
255
+ return {
256
+ messages: Array.isArray(messages) ? messages : [],
257
+ };
258
+ }
259
+ catch (err) {
260
+ if (isDispatchError(err)) {
261
+ throw new SdkMcpError(err.code, err.message);
262
+ }
263
+ const msg = err instanceof Error ? err.message : String(err);
264
+ throw new SdkMcpError(ErrorCode.InternalError, `Prompt get error: ${msg}`);
265
+ }
266
+ });
267
+ // Handler: tools/call — dispatch to extension or handle browser tool locally.
268
+ // Uses pre-built lookup maps for O(1) tool resolution.
269
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
270
+ const toolName = request.params.name;
271
+ const args = request.params.arguments ?? {};
272
+ // Check cached browser tools first (O(n) over small fixed set).
273
+ // Browser tools are few and fixed.
274
+ const cachedBt = state.cachedBrowserTools.find(c => c.name === toolName);
275
+ if (cachedBt) {
276
+ if (!isBrowserToolEnabled(state, toolName)) {
277
+ return {
278
+ content: [{ type: 'text', text: `Tool ${toolName} is disabled via configuration` }],
279
+ isError: true,
280
+ };
281
+ }
282
+ // Validate args through the tool's Zod input schema
283
+ const parseResult = cachedBt.tool.input.safeParse(args);
284
+ if (!parseResult.success) {
285
+ return {
286
+ content: [
287
+ {
288
+ type: 'text',
289
+ text: formatZodError(parseResult.error),
290
+ },
291
+ ],
292
+ isError: true,
293
+ };
294
+ }
295
+ // Permission evaluation: resolve domain, check session permissions,
296
+ // evaluate against policy, and hold for confirmation if needed.
297
+ const parsedArgs = parseResult.data;
298
+ const domain = await resolveToolDomain(toolName, parsedArgs, state);
299
+ // Check session permissions first (set by previous "Allow Always" actions)
300
+ const permission = isSessionAllowed(state.sessionPermissions, toolName, domain)
301
+ ? 'allow'
302
+ : evaluatePermission(toolName, domain, state);
303
+ if (permission === 'deny') {
304
+ return {
305
+ content: [
306
+ {
307
+ type: 'text',
308
+ text: `PERMISSION_DENIED: Tool "${toolName}" is denied${domain ? ` for domain "${domain}"` : ''} by permission policy. Ask the user to update their OpenTabs permission configuration if this tool is needed.`,
309
+ },
310
+ ],
311
+ isError: true,
312
+ };
313
+ }
314
+ if (permission === 'ask') {
315
+ // Send progress notification to MCP client (if progressToken is available)
316
+ const progressToken = extra._meta?.progressToken;
317
+ if (progressToken !== undefined) {
318
+ extra
319
+ .sendNotification({
320
+ method: 'notifications/progress',
321
+ params: {
322
+ progressToken,
323
+ progress: 0,
324
+ total: 1,
325
+ message: 'Waiting for human approval in the OpenTabs side panel...',
326
+ },
327
+ })
328
+ .catch(() => {
329
+ // Fire-and-forget
330
+ });
331
+ }
332
+ try {
333
+ const paramsPreview = truncateParamsPreview(parsedArgs);
334
+ const tabIdArg = parsedArgs.tabId;
335
+ const decision = await sendConfirmationRequest(state, toolName, domain, typeof tabIdArg === 'number' ? tabIdArg : undefined, paramsPreview);
336
+ if (decision === 'deny') {
337
+ return {
338
+ content: [
339
+ {
340
+ type: 'text',
341
+ text: `PERMISSION_DENIED: The user denied "${toolName}"${domain ? ` on "${domain}"` : ''}. Inform the user that the operation was blocked by their decision.`,
342
+ },
343
+ ],
344
+ isError: true,
345
+ };
346
+ }
347
+ // decision is 'allow_once' or 'allow_always' — proceed with dispatch
348
+ // (allow_always session rules are handled by handleConfirmationResponse in extension-protocol)
349
+ }
350
+ catch (err) {
351
+ const msg = err instanceof Error ? err.message : String(err);
352
+ if (msg === 'CONFIRMATION_TIMEOUT') {
353
+ return {
354
+ content: [
355
+ {
356
+ type: 'text',
357
+ text: `CONFIRMATION_TIMEOUT: Human approval for "${toolName}"${domain ? ` on "${domain}"` : ''} timed out after 30 seconds. The user did not respond in the OpenTabs side panel. Ask the user to try again.`,
358
+ },
359
+ ],
360
+ isError: true,
361
+ };
362
+ }
363
+ return {
364
+ content: [
365
+ {
366
+ type: 'text',
367
+ text: `Confirmation error: ${msg}`,
368
+ },
369
+ ],
370
+ isError: true,
371
+ };
372
+ }
373
+ }
374
+ const btStartTs = Date.now();
375
+ let btSuccess = true;
376
+ let btErrorInfo;
377
+ try {
378
+ const result = await cachedBt.tool.handler(parseResult.data, state);
379
+ const cleaned = sanitizeOutput(result);
380
+ return {
381
+ content: [{ type: 'text', text: JSON.stringify(cleaned, null, 2) }],
382
+ };
383
+ }
384
+ catch (err) {
385
+ btSuccess = false;
386
+ const msg = err instanceof Error ? err.message : String(err);
387
+ btErrorInfo = { code: 'UNKNOWN', message: msg };
388
+ return {
389
+ content: [
390
+ {
391
+ type: 'text',
392
+ text: `Browser tool error: ${msg}`,
393
+ },
394
+ ],
395
+ isError: true,
396
+ };
397
+ }
398
+ finally {
399
+ appendAuditEntry(state, {
400
+ timestamp: new Date(btStartTs).toISOString(),
401
+ tool: toolName,
402
+ plugin: 'browser',
403
+ success: btSuccess,
404
+ durationMs: Date.now() - btStartTs,
405
+ error: btErrorInfo,
406
+ });
407
+ }
408
+ }
409
+ // O(1) plugin tool lookup + enabled check via pre-built map
410
+ const callableCheck = checkToolCallable(state, toolName);
411
+ if (!callableCheck.ok) {
412
+ return {
413
+ content: [{ type: 'text', text: callableCheck.error }],
414
+ isError: true,
415
+ };
416
+ }
417
+ const { pluginName: foundPlugin, toolName: foundTool } = callableCheck;
418
+ log.debug('tool.call:', toolName, '→', foundPlugin + '/' + foundTool);
419
+ // Safe to assert: checkToolCallable verified the tool exists in registry.toolLookup
420
+ const lookup = state.registry.toolLookup.get(toolName);
421
+ // Validate args against the tool's JSON Schema before dispatching.
422
+ // The validator is pre-compiled at discovery time for performance.
423
+ // If schema compilation failed, reject the call entirely — unvalidated
424
+ // input must never reach plugin handlers.
425
+ if (!lookup.validate) {
426
+ return {
427
+ content: [
428
+ {
429
+ type: 'text',
430
+ text: `Tool "${toolName}" cannot be called: schema compilation failed. ${lookup.validationErrors()}`,
431
+ },
432
+ ],
433
+ isError: true,
434
+ };
435
+ }
436
+ // Wrap validation in try-catch: compiled Ajv validators can throw on
437
+ // pathological input (e.g., regex catastrophic backtracking from a
438
+ // community plugin's pattern keyword). Normal schemas complete in
439
+ // microseconds; this guard catches the unexpected edge case.
440
+ let valid;
441
+ try {
442
+ valid = lookup.validate(args);
443
+ }
444
+ catch (err) {
445
+ log.warn(`Schema validation threw for tool "${toolName}":`, err);
446
+ return {
447
+ content: [
448
+ {
449
+ type: 'text',
450
+ text: `Tool "${toolName}" validation failed unexpectedly. The tool's schema may be invalid.`,
451
+ },
452
+ ],
453
+ isError: true,
454
+ };
455
+ }
456
+ if (!valid) {
457
+ return {
458
+ content: [
459
+ {
460
+ type: 'text',
461
+ text: `Invalid arguments for tool "${toolName}":\n${lookup.validationErrors()}`,
462
+ },
463
+ ],
464
+ isError: true,
465
+ };
466
+ }
467
+ log.debug('tool.call: input validated for', toolName);
468
+ // Concurrency limit: prevent a runaway MCP client from flooding a single
469
+ // plugin's tab with simultaneous executeScript calls. Each dispatch runs
470
+ // in the page's MAIN world, so too many concurrent dispatches can degrade
471
+ // the target tab's performance.
472
+ const currentDispatches = state.activeDispatches.get(foundPlugin) ?? 0;
473
+ if (currentDispatches >= MAX_CONCURRENT_DISPATCHES_PER_PLUGIN) {
474
+ return {
475
+ content: [
476
+ {
477
+ type: 'text',
478
+ text: `Too many concurrent dispatches for plugin "${foundPlugin}" (limit: ${MAX_CONCURRENT_DISPATCHES_PER_PLUGIN}). Wait for in-flight requests to complete.`,
479
+ },
480
+ ],
481
+ isError: true,
482
+ };
483
+ }
484
+ // Send invocation start notification to extension (for side panel)
485
+ sendInvocationStart(state, foundPlugin, foundTool);
486
+ const startTs = Date.now();
487
+ let success = true;
488
+ let errorInfo;
489
+ try {
490
+ state.activeDispatches.set(foundPlugin, currentDispatches + 1);
491
+ if (!state.extensionWs) {
492
+ success = false;
493
+ return {
494
+ content: [
495
+ {
496
+ type: 'text',
497
+ text: 'Extension not connected. Please ensure the OpenTabs Chrome extension is running.',
498
+ },
499
+ ],
500
+ isError: true,
501
+ };
502
+ }
503
+ log.debug('tool.call: dispatching', foundPlugin + '/' + foundTool);
504
+ // Extract progressToken from MCP request _meta and build onProgress callback
505
+ const progressToken = extra._meta?.progressToken;
506
+ const onProgress = progressToken !== undefined
507
+ ? (progress, total, message) => {
508
+ const params = { progressToken, progress, total };
509
+ if (message !== undefined)
510
+ params.message = message;
511
+ extra.sendNotification({ method: 'notifications/progress', params }).catch(() => {
512
+ // Fire-and-forget — errors in the progress chain must not affect tool execution
513
+ });
514
+ }
515
+ : undefined;
516
+ const result = await dispatchToExtension(state, 'tool.dispatch', { plugin: foundPlugin, tool: foundTool, input: args }, { label: `${foundPlugin}/${foundTool}`, progressToken, onProgress });
517
+ const rawOutput = result.output ?? result;
518
+ const cleaned = sanitizeOutput(rawOutput);
519
+ return {
520
+ content: [{ type: 'text', text: JSON.stringify(cleaned, null, 2) }],
521
+ };
522
+ }
523
+ catch (err) {
524
+ success = false;
525
+ if (isDispatchError(err)) {
526
+ const code = err.code;
527
+ let errorMsg = err.message;
528
+ if (code === -32001) {
529
+ errorMsg = `Tab closed: ${errorMsg}`;
530
+ }
531
+ else if (code === -32002) {
532
+ errorMsg = `Tab unavailable: ${errorMsg}`;
533
+ }
534
+ const toolErrorCode = err.data?.code;
535
+ const category = typeof err.data?.category === 'string' ? err.data.category : undefined;
536
+ if (typeof toolErrorCode === 'string') {
537
+ errorMsg = formatStructuredError(toolErrorCode, errorMsg, err.data);
538
+ errorInfo = { code: toolErrorCode, message: err.message, category };
539
+ }
540
+ else {
541
+ errorInfo = { code: String(code), message: err.message };
542
+ }
543
+ return {
544
+ content: [{ type: 'text', text: errorMsg }],
545
+ isError: true,
546
+ };
547
+ }
548
+ const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
549
+ errorInfo = { code: 'UNKNOWN', message: msg };
550
+ return {
551
+ content: [
552
+ {
553
+ type: 'text',
554
+ text: `Tool dispatch error: ${msg}`,
555
+ },
556
+ ],
557
+ isError: true,
558
+ };
559
+ }
560
+ finally {
561
+ const prev = state.activeDispatches.get(foundPlugin) ?? 1;
562
+ if (prev <= 1) {
563
+ state.activeDispatches.delete(foundPlugin);
564
+ }
565
+ else {
566
+ state.activeDispatches.set(foundPlugin, prev - 1);
567
+ }
568
+ const durationMs = Date.now() - startTs;
569
+ log.debug('tool.call:', foundPlugin + '/' + foundTool, 'completed in', `${durationMs}ms`);
570
+ sendInvocationEnd(state, foundPlugin, foundTool, durationMs, success);
571
+ appendAuditEntry(state, {
572
+ timestamp: new Date(startTs).toISOString(),
573
+ tool: toolName,
574
+ plugin: foundPlugin,
575
+ success,
576
+ durationMs,
577
+ error: errorInfo,
578
+ });
579
+ }
580
+ });
581
+ };
582
+ /**
583
+ * Create a new low-level MCP Server instance with the OpenTabs server info
584
+ * and register handlers for tools/list and tools/call.
585
+ */
586
+ const createMcpServer = async (state) => {
587
+ const ServerCtor = await getServerCtor();
588
+ const server = new ServerCtor({ name: 'opentabs', version }, {
589
+ capabilities: {
590
+ tools: { listChanged: true },
591
+ resources: { listChanged: true },
592
+ prompts: { listChanged: true },
593
+ logging: {},
594
+ },
595
+ });
596
+ registerMcpHandlers(server, state);
597
+ return server;
598
+ };
599
+ /**
600
+ * Notify a connected MCP client that the tool list has changed.
601
+ * Logs a warning if the notification fails (e.g., transport in a bad state)
602
+ * to aid debugging when a client doesn't see a tool update.
603
+ */
604
+ const notifyToolListChanged = (server) => {
605
+ server.sendToolListChanged().catch((err) => {
606
+ log.warn('Failed to notify tool list change:', err);
607
+ });
608
+ };
609
+ /**
610
+ * Notify a connected MCP client that the resource list has changed.
611
+ */
612
+ const notifyResourceListChanged = (server) => {
613
+ server.sendResourceListChanged().catch((err) => {
614
+ log.warn('Failed to notify resource list change:', err);
615
+ });
616
+ };
617
+ /**
618
+ * Notify a connected MCP client that the prompt list has changed.
619
+ */
620
+ const notifyPromptListChanged = (server) => {
621
+ server.sendPromptListChanged().catch((err) => {
622
+ log.warn('Failed to notify prompt list change:', err);
623
+ });
624
+ };
625
+ /**
626
+ * Returns the list of enabled tools for MCP tools/list responses.
627
+ * Plugin tools are filtered by the toolConfig (disabled tools are excluded).
628
+ * Browser tools are filtered by the browserToolPolicy (disabled tools are excluded).
629
+ */
630
+ export const getEnabledToolsList = (state) => {
631
+ const tools = [];
632
+ for (const plugin of state.registry.plugins.values()) {
633
+ for (const toolDef of plugin.tools) {
634
+ const prefixed = prefixedToolName(plugin.name, toolDef.name);
635
+ if (!isToolEnabled(state, prefixed))
636
+ continue;
637
+ tools.push({
638
+ name: prefixed,
639
+ description: trustTierPrefix(plugin.trustTier) + toolDef.description,
640
+ inputSchema: toolDef.input_schema,
641
+ });
642
+ }
643
+ }
644
+ for (const cached of state.cachedBrowserTools) {
645
+ if (!isBrowserToolEnabled(state, cached.name))
646
+ continue;
647
+ tools.push({
648
+ name: cached.name,
649
+ description: cached.description,
650
+ inputSchema: cached.inputSchema,
651
+ });
652
+ }
653
+ return tools;
654
+ };
655
+ /**
656
+ * Check if a prefixed plugin tool name is callable: exists in the tool lookup
657
+ * and is enabled in the tool config. Browser tools are handled separately
658
+ * (before this check) in the tools/call handler.
659
+ *
660
+ * @param state - Server state containing the plugin registry and tool config
661
+ * @param prefixedToolName - Fully prefixed tool name (e.g., 'slack_send_message')
662
+ * @returns An ok result with pluginName/toolName if callable, or an error result with a message
663
+ */
664
+ export const checkToolCallable = (state, prefixedToolName) => {
665
+ const lookup = state.registry.toolLookup.get(prefixedToolName);
666
+ if (!lookup)
667
+ return { ok: false, error: `Tool ${prefixedToolName} not found` };
668
+ if (!isToolEnabled(state, prefixedToolName))
669
+ return { ok: false, error: `Tool ${prefixedToolName} is disabled` };
670
+ return { ok: true, pluginName: lookup.pluginName, toolName: lookup.toolName };
671
+ };
672
+ export { createMcpServer, registerMcpHandlers, rebuildCachedBrowserTools, notifyToolListChanged, notifyResourceListChanged, notifyPromptListChanged, sanitizeOutput, };
673
+ //# sourceMappingURL=mcp-setup.js.map