@anchrd/intel 0.59.0

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 (364) hide show
  1. package/README.md +496 -0
  2. package/bin/intel.mjs +134 -0
  3. package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +42 -0
  4. package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +89 -0
  5. package/dist/adapters/cloudflare/cloudflare.d.ts +29 -0
  6. package/dist/adapters/cloudflare/cloudflare.js +416 -0
  7. package/dist/adapters/cloudflare/cloudflare.types.d.ts +31 -0
  8. package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
  9. package/dist/adapters/content/content.d.ts +3 -0
  10. package/dist/adapters/content/content.js +128 -0
  11. package/dist/adapters/content/content.types.d.ts +38 -0
  12. package/dist/adapters/content/content.types.js +1 -0
  13. package/dist/adapters/db/db-audit.d.ts +62 -0
  14. package/dist/adapters/db/db-audit.js +150 -0
  15. package/dist/adapters/db/db-boards.d.ts +6 -0
  16. package/dist/adapters/db/db-boards.js +324 -0
  17. package/dist/adapters/db/db-feed.d.ts +30 -0
  18. package/dist/adapters/db/db-feed.js +276 -0
  19. package/dist/adapters/db/db-flows.d.ts +42 -0
  20. package/dist/adapters/db/db-flows.js +1311 -0
  21. package/dist/adapters/db/db-grants.d.ts +133 -0
  22. package/dist/adapters/db/db-grants.js +269 -0
  23. package/dist/adapters/db/db-indexing.d.ts +3 -0
  24. package/dist/adapters/db/db-indexing.js +200 -0
  25. package/dist/adapters/db/db-oauth.d.ts +3 -0
  26. package/dist/adapters/db/db-oauth.js +26 -0
  27. package/dist/adapters/db/db-prompts.d.ts +46 -0
  28. package/dist/adapters/db/db-prompts.js +144 -0
  29. package/dist/adapters/db/db.d.ts +6 -0
  30. package/dist/adapters/db/db.js +1687 -0
  31. package/dist/adapters/db/db.types.d.ts +16 -0
  32. package/dist/adapters/db/db.types.js +1 -0
  33. package/dist/adapters/document-converter/document-converter.d.ts +2 -0
  34. package/dist/adapters/document-converter/document-converter.js +26 -0
  35. package/dist/adapters/document-converter/document-converter.types.d.ts +17 -0
  36. package/dist/adapters/document-converter/document-converter.types.js +1 -0
  37. package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
  38. package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
  39. package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
  40. package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
  41. package/dist/adapters/index-queue/index-queue.d.ts +10 -0
  42. package/dist/adapters/index-queue/index-queue.js +12 -0
  43. package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
  44. package/dist/adapters/index-queue/index-queue.types.js +1 -0
  45. package/dist/adapters/json-schema/json-schema.d.ts +2 -0
  46. package/dist/adapters/json-schema/json-schema.js +77 -0
  47. package/dist/adapters/openid/openid.d.ts +5 -0
  48. package/dist/adapters/openid/openid.js +301 -0
  49. package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
  50. package/dist/adapters/portal-tokens/portal-tokens.js +79 -0
  51. package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
  52. package/dist/adapters/remote-tools/remote-tools.js +129 -0
  53. package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
  54. package/dist/adapters/semantic-index/semantic-index.js +139 -0
  55. package/dist/adapters/semantic-index/semantic-index.types.d.ts +56 -0
  56. package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
  57. package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
  58. package/dist/adapters/session-cookie/session-cookie.js +87 -0
  59. package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
  60. package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
  61. package/dist/audit/audit.d.ts +2 -0
  62. package/dist/audit/audit.js +87 -0
  63. package/dist/audit/audit.types.d.ts +23 -0
  64. package/dist/audit/audit.types.js +1 -0
  65. package/dist/auth/auth.d.ts +2 -0
  66. package/dist/auth/auth.js +284 -0
  67. package/dist/auth/auth.types.d.ts +108 -0
  68. package/dist/auth/auth.types.js +1 -0
  69. package/dist/boards/boards.d.ts +44 -0
  70. package/dist/boards/boards.js +447 -0
  71. package/dist/boards/boards.types.d.ts +117 -0
  72. package/dist/boards/boards.types.js +1 -0
  73. package/dist/build/build.d.ts +2 -0
  74. package/dist/build/build.js +192 -0
  75. package/dist/build/build.types.d.ts +17 -0
  76. package/dist/build/build.types.js +1 -0
  77. package/dist/bundle/bundle.d.ts +4 -0
  78. package/dist/bundle/bundle.js +1194 -0
  79. package/dist/bundle/bundle.types.d.ts +33 -0
  80. package/dist/bundle/bundle.types.js +1 -0
  81. package/dist/cli/cli.d.ts +4 -0
  82. package/dist/cli/cli.js +262 -0
  83. package/dist/cli/cli.types.d.ts +14 -0
  84. package/dist/cli/cli.types.js +1 -0
  85. package/dist/contract/audit.d.ts +50 -0
  86. package/dist/contract/audit.js +72 -0
  87. package/dist/contract/board.d.ts +150 -0
  88. package/dist/contract/board.js +262 -0
  89. package/dist/contract/bundle.d.ts +147 -0
  90. package/dist/contract/bundle.js +139 -0
  91. package/dist/contract/contract.d.ts +112 -0
  92. package/dist/contract/contract.js +143 -0
  93. package/dist/contract/feed.d.ts +126 -0
  94. package/dist/contract/feed.js +149 -0
  95. package/dist/contract/flow-run.d.ts +346 -0
  96. package/dist/contract/flow-run.js +194 -0
  97. package/dist/contract/flow.d.ts +1057 -0
  98. package/dist/contract/flow.js +552 -0
  99. package/dist/contract/node.d.ts +551 -0
  100. package/dist/contract/node.js +456 -0
  101. package/dist/contract/share.d.ts +216 -0
  102. package/dist/contract/share.js +134 -0
  103. package/dist/contract/table.d.ts +171 -0
  104. package/dist/contract/table.js +117 -0
  105. package/dist/contract/tool.d.ts +122 -0
  106. package/dist/contract/tool.js +172 -0
  107. package/dist/feed/feed.d.ts +2 -0
  108. package/dist/feed/feed.js +77 -0
  109. package/dist/feed/feed.types.d.ts +23 -0
  110. package/dist/feed/feed.types.js +1 -0
  111. package/dist/flows/flows.d.ts +41 -0
  112. package/dist/flows/flows.js +2216 -0
  113. package/dist/flows/flows.types.d.ts +339 -0
  114. package/dist/flows/flows.types.js +1 -0
  115. package/dist/http/http.d.ts +9 -0
  116. package/dist/http/http.js +874 -0
  117. package/dist/http/http.types.d.ts +22 -0
  118. package/dist/http/http.types.js +1 -0
  119. package/dist/indexing/indexing.d.ts +6 -0
  120. package/dist/indexing/indexing.js +239 -0
  121. package/dist/indexing/indexing.types.d.ts +11 -0
  122. package/dist/indexing/indexing.types.js +1 -0
  123. package/dist/intel/intel.d.ts +3 -0
  124. package/dist/intel/intel.js +194 -0
  125. package/dist/intel/intel.types.d.ts +24 -0
  126. package/dist/intel/intel.types.js +1 -0
  127. package/dist/mcp/mcp.d.ts +2 -0
  128. package/dist/mcp/mcp.js +1292 -0
  129. package/dist/mcp/mcp.types.d.ts +25 -0
  130. package/dist/mcp/mcp.types.js +1 -0
  131. package/dist/nodes/document-links/document-links.d.ts +20 -0
  132. package/dist/nodes/document-links/document-links.js +57 -0
  133. package/dist/nodes/nodes.d.ts +2 -0
  134. package/dist/nodes/nodes.js +1667 -0
  135. package/dist/nodes/nodes.types.d.ts +511 -0
  136. package/dist/nodes/nodes.types.js +1 -0
  137. package/dist/prepare/prepare.d.ts +4 -0
  138. package/dist/prepare/prepare.js +16 -0
  139. package/dist/prepare/prepare.types.d.ts +9 -0
  140. package/dist/prepare/prepare.types.js +1 -0
  141. package/dist/prompts/prompts.d.ts +2 -0
  142. package/dist/prompts/prompts.js +65 -0
  143. package/dist/prompts/prompts.types.d.ts +71 -0
  144. package/dist/prompts/prompts.types.js +1 -0
  145. package/dist/shared/callers/callers.d.ts +28 -0
  146. package/dist/shared/callers/callers.js +30 -0
  147. package/dist/shared/csv/csv.d.ts +13 -0
  148. package/dist/shared/csv/csv.js +85 -0
  149. package/dist/shared/document-text/document-text.d.ts +21 -0
  150. package/dist/shared/document-text/document-text.js +31 -0
  151. package/dist/shared/gate-authorization/gate-authorization.d.ts +5 -0
  152. package/dist/shared/gate-authorization/gate-authorization.js +26 -0
  153. package/dist/shared/grant-expiry/grant-expiry.d.ts +19 -0
  154. package/dist/shared/grant-expiry/grant-expiry.js +26 -0
  155. package/dist/shared/intel-error/intel-error.d.ts +22 -0
  156. package/dist/shared/intel-error/intel-error.js +27 -0
  157. package/dist/shared/plain-title/plain-title.d.ts +16 -0
  158. package/dist/shared/plain-title/plain-title.js +45 -0
  159. package/dist/shared/problem-details/problem-details.d.ts +30 -0
  160. package/dist/shared/problem-details/problem-details.js +18 -0
  161. package/dist/shared/report-unexpected-error/report-unexpected-error.d.ts +1 -0
  162. package/dist/shared/report-unexpected-error/report-unexpected-error.js +37 -0
  163. package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
  164. package/dist/shared/safe-return-path/safe-return-path.js +11 -0
  165. package/dist/shared/sha256/sha256.d.ts +2 -0
  166. package/dist/shared/sha256/sha256.js +14 -0
  167. package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
  168. package/dist/tools/tool-servers/tool-servers.js +177 -0
  169. package/dist/tools/tools.d.ts +2 -0
  170. package/dist/tools/tools.js +207 -0
  171. package/dist/tools/tools.types.d.ts +56 -0
  172. package/dist/tools/tools.types.js +1 -0
  173. package/examples/branding/favicon.svg +5 -0
  174. package/examples/branding/logo.svg +5 -0
  175. package/examples/branding/theme.css +16 -0
  176. package/examples/dev.vars.example +7 -0
  177. package/examples/intel.json +9 -0
  178. package/migrations/0000_intel_foundation.sql +203 -0
  179. package/migrations/0001_portal_native_tools.sql +21 -0
  180. package/migrations/0002_flows_in_the_knowledge_tree.sql +34 -0
  181. package/migrations/0003_folder_permissions.sql +211 -0
  182. package/migrations/0004_subflow_runs.sql +14 -0
  183. package/migrations/0005_flow_node_cleanup.sql +28 -0
  184. package/migrations/0005_tables_in_the_knowledge_tree.sql +68 -0
  185. package/migrations/0006_links_are_written_in_the_text.sql +20 -0
  186. package/migrations/0007_no_node_waits.sql +69 -0
  187. package/migrations/0008_three_layers.sql +130 -0
  188. package/migrations/0009_no_context_policy.sql +48 -0
  189. package/migrations/0010_no_run_waits_a_year.sql +27 -0
  190. package/migrations/0011_one_name_for_the_tree.sql +53 -0
  191. package/migrations/0012_table_snapshots.sql +29 -0
  192. package/migrations/0017_a_vector_per_card.sql +38 -0
  193. package/migrations/0018_no_context_policy_at_last.sql +97 -0
  194. package/migrations/0019_one_name_for_the_grants.sql +52 -0
  195. package/migrations/0020_cascade_purge_replay.sql +11 -0
  196. package/migrations/0021_a_flow_carries_its_own_grant.sql +44 -0
  197. package/migrations/0022_a_cursor_over_the_journal.sql +14 -0
  198. package/migrations/0023_a_board_and_its_tasks.sql +116 -0
  199. package/migrations/0024_the_archive_is_a_column.sql +33 -0
  200. package/migrations/0025_archiving_takes_the_subtree.sql +29 -0
  201. package/migrations/0026_one_feed_over_two_kinds.sql +23 -0
  202. package/migrations/0027_the_runs_of_every_flow.sql +27 -0
  203. package/migrations/0028_a_prompt_name_over_two_kinds.sql +70 -0
  204. package/package.json +118 -0
  205. package/src/contract/CLAUDE.md +152 -0
  206. package/src/contract/README.md +102 -0
  207. package/src/contract/audit.ts +85 -0
  208. package/src/contract/board.ts +306 -0
  209. package/src/contract/bundle.ts +157 -0
  210. package/src/contract/contract.ts +161 -0
  211. package/src/contract/feed.ts +165 -0
  212. package/src/contract/flow-run.ts +241 -0
  213. package/src/contract/flow.ts +675 -0
  214. package/src/contract/node.ts +583 -0
  215. package/src/contract/share.ts +196 -0
  216. package/src/contract/table.ts +178 -0
  217. package/src/contract/tool.ts +202 -0
  218. package/ui/README.md +110 -0
  219. package/ui/components.json +21 -0
  220. package/ui/index.html +13 -0
  221. package/ui/package.json +11 -0
  222. package/ui/scripts/lint-tokens.mjs +187 -0
  223. package/ui/src/access-summary/access-summary.tsx +385 -0
  224. package/ui/src/app/action-slot/action-slot.tsx +27 -0
  225. package/ui/src/app/app-sidebar/app-sidebar.tsx +71 -0
  226. package/ui/src/app/app-tree/app-tree.tsx +1175 -0
  227. package/ui/src/app/app.tsx +87 -0
  228. package/ui/src/app/header-search/header-search.tsx +294 -0
  229. package/ui/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
  230. package/ui/src/app/settings-dialog/settings-dialog.tsx +135 -0
  231. package/ui/src/app/sidebar-preferences/sidebar-preferences.ts +68 -0
  232. package/ui/src/app/sidebar-preferences/sidebar-preferences.types.ts +15 -0
  233. package/ui/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +86 -0
  234. package/ui/src/app/tree-expansion/tree-expansion.ts +107 -0
  235. package/ui/src/app/tree-expansion/tree-expansion.types.ts +31 -0
  236. package/ui/src/app/tree-move/tree-move.tsx +427 -0
  237. package/ui/src/app/tree-reveal/tree-reveal.ts +64 -0
  238. package/ui/src/app/tree-reveal/tree-reveal.types.ts +16 -0
  239. package/ui/src/app/user-footer/user-footer.tsx +188 -0
  240. package/ui/src/app/view-menu/view-menu.tsx +115 -0
  241. package/ui/src/app-root/app-root.tsx +109 -0
  242. package/ui/src/app-root/app-root.types.ts +27 -0
  243. package/ui/src/archive/archive.tsx +417 -0
  244. package/ui/src/attachment-detail/attachment-detail.ts +52 -0
  245. package/ui/src/attachment-viewer/attachment-viewer.tsx +166 -0
  246. package/ui/src/blocknote-view/blocknote-view.tsx +30 -0
  247. package/ui/src/board/board-assignee/board-assignee-picker.tsx +132 -0
  248. package/ui/src/board/board-assignee/board-assignee.ts +48 -0
  249. package/ui/src/board/board-assignee/board-assignee.tsx +75 -0
  250. package/ui/src/board/board-chip/board-chip.tsx +46 -0
  251. package/ui/src/board/board-crumbs/board-crumbs.ts +56 -0
  252. package/ui/src/board/board-crumbs/board-crumbs.tsx +111 -0
  253. package/ui/src/board/board-data/board-data.ts +195 -0
  254. package/ui/src/board/board-data/board-data.types.ts +38 -0
  255. package/ui/src/board/board-dates/board-dates.ts +40 -0
  256. package/ui/src/board/board-dates/board-dates.tsx +105 -0
  257. package/ui/src/board/board-kanban/board-kanban.ts +162 -0
  258. package/ui/src/board/board-kanban/board-kanban.tsx +680 -0
  259. package/ui/src/board/board-open-task/board-open-task.ts +32 -0
  260. package/ui/src/board/board-panel/board-panel.tsx +259 -0
  261. package/ui/src/board/board-settings/board-settings.tsx +226 -0
  262. package/ui/src/board/board-status/board-status.tsx +60 -0
  263. package/ui/src/board/board-stripes/board-stripes.ts +128 -0
  264. package/ui/src/board/board-table/board-table.ts +53 -0
  265. package/ui/src/board/board-table/board-table.tsx +610 -0
  266. package/ui/src/board/board-task/board-task.ts +108 -0
  267. package/ui/src/board/board-task/board-task.tsx +335 -0
  268. package/ui/src/board/board-title-row/board-title-row.tsx +66 -0
  269. package/ui/src/branding/branding.tsx +17 -0
  270. package/ui/src/branding/custom-logo.ts +1 -0
  271. package/ui/src/branding/favicon.default.svg +5 -0
  272. package/ui/src/branding/favicon.svg +5 -0
  273. package/ui/src/components/ui/avatar.tsx +39 -0
  274. package/ui/src/components/ui/button.tsx +64 -0
  275. package/ui/src/components/ui/collapsible.tsx +20 -0
  276. package/ui/src/components/ui/command.tsx +160 -0
  277. package/ui/src/components/ui/dialog.tsx +143 -0
  278. package/ui/src/components/ui/dropdown-menu.tsx +187 -0
  279. package/ui/src/components/ui/input.tsx +21 -0
  280. package/ui/src/components/ui/popover.tsx +41 -0
  281. package/ui/src/components/ui/select.tsx +163 -0
  282. package/ui/src/components/ui/separator.tsx +26 -0
  283. package/ui/src/components/ui/sheet.tsx +136 -0
  284. package/ui/src/components/ui/sidebar.tsx +693 -0
  285. package/ui/src/components/ui/skeleton.tsx +13 -0
  286. package/ui/src/components/ui/switch.tsx +25 -0
  287. package/ui/src/components/ui/table.tsx +82 -0
  288. package/ui/src/components/ui/tabs.tsx +52 -0
  289. package/ui/src/components/ui/tooltip.tsx +51 -0
  290. package/ui/src/data/intel-data-provider/intel-data-provider.ts +685 -0
  291. package/ui/src/data/intel-data-provider/intel-data-provider.types.ts +266 -0
  292. package/ui/src/data/query-client.ts +18 -0
  293. package/ui/src/data/request-refusal/refusal-notice.tsx +36 -0
  294. package/ui/src/data/request-refusal/request-refusal.ts +51 -0
  295. package/ui/src/data/sign-in/sign-in.ts +97 -0
  296. package/ui/src/data/sign-in/sign-in.types.ts +15 -0
  297. package/ui/src/document-link/document-link.tsx +114 -0
  298. package/ui/src/document-markdown/document-markdown.ts +195 -0
  299. package/ui/src/editor-schema/editor-schema.ts +23 -0
  300. package/ui/src/entry-picker/entry-picker.tsx +238 -0
  301. package/ui/src/feed/feed.tsx +344 -0
  302. package/ui/src/file-preview/file-preview-view.tsx +496 -0
  303. package/ui/src/file-preview/file-preview.tsx +28 -0
  304. package/ui/src/file-preview/pdf-file-preview.tsx +5 -0
  305. package/ui/src/file-preview/presentation-file-preview.tsx +21 -0
  306. package/ui/src/file-preview/spreadsheet-file-preview.tsx +5 -0
  307. package/ui/src/file-preview/word-file-preview.tsx +5 -0
  308. package/ui/src/flow-runs/flow-runs.tsx +238 -0
  309. package/ui/src/flows/flows.tsx +1646 -0
  310. package/ui/src/flows/node-icon/node-icon.ts +34 -0
  311. package/ui/src/flows/node-palette/node-palette.tsx +214 -0
  312. package/ui/src/flows/node-palette/node-palette.types.ts +15 -0
  313. package/ui/src/folder-contents/folder-contents.tsx +321 -0
  314. package/ui/src/folder-index/folder-index.ts +55 -0
  315. package/ui/src/frontmatter/frontmatter.tsx +193 -0
  316. package/ui/src/graph-pane/graph-pane.tsx +53 -0
  317. package/ui/src/hooks/use-mobile.ts +19 -0
  318. package/ui/src/i18n/custom.json +4 -0
  319. package/ui/src/i18n/de.json +609 -0
  320. package/ui/src/i18n/en.json +609 -0
  321. package/ui/src/i18n/es.json +609 -0
  322. package/ui/src/i18n/i18n-context.tsx +65 -0
  323. package/ui/src/i18n/i18n-languages/i18n-languages.ts +83 -0
  324. package/ui/src/i18n/i18n.ts +32 -0
  325. package/ui/src/i18n/i18n.types.ts +49 -0
  326. package/ui/src/kind-icon.ts +97 -0
  327. package/ui/src/lib/utils.ts +6 -0
  328. package/ui/src/main.tsx +68 -0
  329. package/ui/src/modal/modal.tsx +53 -0
  330. package/ui/src/node-details/node-details.tsx +545 -0
  331. package/ui/src/node-editor/node-editor.tsx +304 -0
  332. package/ui/src/node-graph/graph-notice.tsx +21 -0
  333. package/ui/src/node-graph/node-graph.ts +214 -0
  334. package/ui/src/node-graph/node-graph.tsx +295 -0
  335. package/ui/src/node-import/node-import.tsx +418 -0
  336. package/ui/src/node-save-error.ts +46 -0
  337. package/ui/src/node-source/node-source.tsx +128 -0
  338. package/ui/src/node-table/node-table.tsx +107 -0
  339. package/ui/src/nodes/nodes.tsx +240 -0
  340. package/ui/src/rename-resource/rename-resource.ts +80 -0
  341. package/ui/src/resolved-names/resolved-names.tsx +67 -0
  342. package/ui/src/resource-error.ts +33 -0
  343. package/ui/src/resource-menu/resource-menu.tsx +1156 -0
  344. package/ui/src/router/router-context.ts +6 -0
  345. package/ui/src/router/router.tsx +67 -0
  346. package/ui/src/router/router.types.ts +8 -0
  347. package/ui/src/router/selection-search.ts +88 -0
  348. package/ui/src/save-button/save-button.tsx +94 -0
  349. package/ui/src/section-hint/section-hint.tsx +40 -0
  350. package/ui/src/sign-in-refused/sign-in-refused.tsx +35 -0
  351. package/ui/src/styles.css +189 -0
  352. package/ui/src/table-columns/table-columns.tsx +379 -0
  353. package/ui/src/theme/custom.css +1 -0
  354. package/ui/src/theme/theme-context.tsx +89 -0
  355. package/ui/src/theme/theme.ts +70 -0
  356. package/ui/src/time/relative-time.tsx +41 -0
  357. package/ui/src/time/time-context.tsx +78 -0
  358. package/ui/src/time/time.ts +165 -0
  359. package/ui/src/title-row/title-row.tsx +185 -0
  360. package/ui/src/tools/tools.tsx +546 -0
  361. package/ui/src/user-name/user-name.ts +61 -0
  362. package/ui/src/verbatim/verbatim.tsx +68 -0
  363. package/ui/tsconfig.json +17 -0
  364. package/ui/vite.config.ts +129 -0
@@ -0,0 +1,1292 @@
1
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3
+ import { GetPromptRequestSchema, ListPromptsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
+ import { z } from "zod";
5
+ import { AuditListRequest } from "../contract/audit.js";
6
+ import { BoardAssigneeSearchInput, BoardGetInput, BoardTaskCreateInput, BoardTaskUpdateInput, BoardUpdateInput, DirectoryResolveInput, } from "../contract/board.js";
7
+ import { IdempotencyKey, IntelId } from "../contract/contract.js";
8
+ import { FeedListRequest } from "../contract/feed.js";
9
+ import { ArchiveFlowInput, CreateFlowInput, GetFlowInput, GetFlowVersionInput, ListFlowsInput, PreviewFlowPublishInput, PublishFlowInput, PurgeFlowInput, RelationGraphInput, SaveFlowVersionInput, UnpublishFlowInput, UpdateFlowInput, } from "../contract/flow.js";
10
+ import { CancelFlowRunInput, CompleteFlowRunStepInput, GetFlowRunInput, ListFlowRunsInput, StartFlowRunInput, } from "../contract/flow-run.js";
11
+ import { ArchiveNodeInput, CreateNodeInput, GetAttachmentInput, GetNodeInput, GetNodeVersionInput, ListNodesInput, NodeGraphInput, PurgeNodeInput, PurgeNodePreviewInput, ResolveNodeLinksInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, UpdateNodeInput, } from "../contract/node.js";
12
+ import { ListEffectiveAccessInput, ListFlowEffectiveAccessInput, ListFlowGrantsInput, ListGrantsInput, RevokeFlowGrantInput, RevokeGrantInput, ShareFlowInput, ShareInput, } from "../contract/share.js";
13
+ import { AppendTableRowsInput, DefineTableInput, DeleteTableRowsInput, GetTableInput, RedefineTableInput, UpdateTableRowsInput, } from "../contract/table.js";
14
+ import { ExecuteToolInput, TestToolInput } from "../contract/tool.js";
15
+ import { permits } from "../shared/gate-authorization/gate-authorization.js";
16
+ import { IntelError } from "../shared/intel-error/intel-error.js";
17
+ function text(value) {
18
+ return { content: [{ type: "text", text: JSON.stringify(value) }], isError: false };
19
+ }
20
+ /**
21
+ * ⚠️ The one thing a tool error loses on its way out, and the whole reason this `catch` exists
22
+ * (#597).
23
+ *
24
+ * A thrown `IntelError` is turned into a tool error by the SDK's `createToolError`, which keeps
25
+ * `error instanceof Error ? error.message : String(error)` and nothing else — so `callers`, the
26
+ * structured half of a refusal that NAMES flows (#448, #593), never reaches a model over MCP.
27
+ *
28
+ * It arrives here as a SECOND content block. The first one is the sentence, byte for byte what the
29
+ * SDK would have sent, so a reader that only takes text loses nothing: the machine-readable half
30
+ * stands beside the prose rather than instead of it.
31
+ *
32
+ * ⚠️ It wraps the handlers whose service can refuse WITH data — `node_grant_revoke` and
33
+ * `flow_purge` today — and deliberately NOT all sixty. For every other refusal there is nothing to
34
+ * carry, and turning each of their errors into JSON would change every tool for the sake of two.
35
+ * What keeps that from becoming a place to forget is where the rule is written down: at the field
36
+ * itself (`IntelError.callers`), in `packages/intel/CLAUDE.md`, and as two tests in `mcp.e2e.ts` —
37
+ * whoever attaches `callers` to a new refusal wraps its handler here.
38
+ */
39
+ async function refusing(run) {
40
+ try {
41
+ return text(await run());
42
+ }
43
+ catch (error) {
44
+ if (!(error instanceof IntelError) || !error.callers)
45
+ throw error;
46
+ return {
47
+ content: [
48
+ { type: "text", text: error.message },
49
+ {
50
+ type: "text",
51
+ text: JSON.stringify({ code: error.code, callers: error.callers }),
52
+ },
53
+ ],
54
+ isError: true,
55
+ };
56
+ }
57
+ }
58
+ /**
59
+ * ⚠️ One name per tool, and the second one is gone for good (#149).
60
+ *
61
+ * Every tool here carried a `knowledge_*` alias beside its name from #125 until today. The alias
62
+ * was never politeness: a tool name lives in somebody else's configuration — a portal endpoint, an
63
+ * agent definition, a saved prompt — so renaming it here alone breaks them at the moment we deploy
64
+ * rather than at a moment they chose. ADR-0005 §8 therefore kept both, **for a transition**.
65
+ *
66
+ * A transition without an end date is not a transition, it is a second permanent name — and two
67
+ * names for one tool is exactly the friction #125 removed. It ends here. What follows for anybody
68
+ * writing a tool: there is one name, and a rename is a breaking change that needs its own ticket
69
+ * and its own notice, not a quiet second entry.
70
+ */
71
+ // MCP returns resource payloads inline as base64, so an attachment cannot be streamed to the client
72
+ // and must fit in Worker memory twice over. Refuse oversized attachments instead of losing the
73
+ // isolate; HTTP still serves them as a stream.
74
+ const AttachmentInlineLimit = 10 * 1024 * 1024;
75
+ async function base64(stream) {
76
+ const bytes = new Uint8Array(await new Response(stream).arrayBuffer());
77
+ const chunkSize = 24_576;
78
+ let encoded = "";
79
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
80
+ let binary = "";
81
+ for (const byte of bytes.subarray(offset, offset + chunkSize)) {
82
+ binary += String.fromCharCode(byte);
83
+ }
84
+ encoded += btoa(binary);
85
+ }
86
+ return encoded;
87
+ }
88
+ export async function handleMcp(request, deps) {
89
+ const actor = {
90
+ id: deps.authorization.identity.id,
91
+ email: deps.authorization.identity.email,
92
+ isAdmin: deps.authorization.can("intel", "admin"),
93
+ };
94
+ const toolActor = {
95
+ id: deps.authorization.identity.id,
96
+ email: deps.authorization.identity.email,
97
+ canExecute: permits(deps.authorization, "tools", "execute"),
98
+ };
99
+ const flowActor = {
100
+ id: deps.authorization.identity.id,
101
+ email: deps.authorization.identity.email,
102
+ canRun: permits(deps.authorization, "flows", "run"),
103
+ isAdmin: deps.authorization.can("intel", "admin"),
104
+ };
105
+ const bundleActor = {
106
+ id: deps.authorization.identity.id,
107
+ email: deps.authorization.identity.email,
108
+ isAdmin: deps.authorization.can("intel", "admin"),
109
+ canReadFlows: permits(deps.authorization, "flows", "read"),
110
+ canCreateFlows: permits(deps.authorization, "flows", "create"),
111
+ };
112
+ const server = new McpServer({ name: "intel", version: "0.1.0" });
113
+ const EmptyInput = z.strictObject({});
114
+ // `null` is the root: the whole tree as this caller may read it. The field is required rather
115
+ // than defaulted so "everything" is always said, never fallen into.
116
+ const ExportManifestInput = z.strictObject({
117
+ nodeId: IntelId.nullable().describe('Folder or node to plan the export of, or `null` for the whole tree as this caller may read it. Required rather than defaulted, so "everything" is always said and never fallen into.'),
118
+ });
119
+ // The zip travels inline as base64, which is MCP's one way of carrying bytes. ~14M characters is
120
+ // ~10 MB of zip — the same order as the attachment inline limit, and for the same isolate-memory
121
+ // reason. Bigger bundles take the HTTP door, which streams.
122
+ const ImportBundleInput = z.strictObject({
123
+ nodeId: IntelId.nullable().describe("Folder the bundle lands under, or `null` for the top level. The bundle's own structure is kept beneath it."),
124
+ zipBase64: z
125
+ .string()
126
+ .min(1)
127
+ .max(14_000_000)
128
+ .describe("The zip bundle, base64-encoded. Around 10 MB of zip; a bigger bundle takes the HTTP door, which streams instead of holding it in memory."),
129
+ idempotencyKey: IdempotencyKey,
130
+ });
131
+ function decodeZipBase64(value) {
132
+ if (value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
133
+ throw new IntelError(400, "import_invalid_bundle", "zipBase64 is not valid base64");
134
+ }
135
+ try {
136
+ return Uint8Array.from(atob(value), (character) => character.charCodeAt(0));
137
+ }
138
+ catch {
139
+ throw new IntelError(400, "import_invalid_bundle", "zipBase64 is not valid base64");
140
+ }
141
+ }
142
+ // The caller's own identity (#148): the MCP twin of `GET /session`, and like it behind no
143
+ // capability at all — the answer is the token's own subject, and an agent that may connect may
144
+ // ask who it is connected as. Deliberately id, email, and name and nothing else: no
145
+ // capabilities, no token, nothing a caller could mistake for a permission.
146
+ server.registerTool("session_get", {
147
+ title: "Get session identity",
148
+ description: "Get the identity this connection acts as: user id, email, and display name. The MCP counterpart of GET /session; it answers from the token's own subject and never lists permissions.",
149
+ inputSchema: EmptyInput,
150
+ annotations: {
151
+ title: "Get session identity",
152
+ readOnlyHint: true,
153
+ destructiveHint: false,
154
+ idempotentHint: true,
155
+ openWorldHint: false,
156
+ },
157
+ }, async () => text({
158
+ id: deps.authorization.identity.id,
159
+ email: deps.authorization.identity.email,
160
+ name: deps.authorization.identity.name ?? null,
161
+ }));
162
+ // The change journal, read forward from a stable position (#620). It is what makes Intel the
163
+ // simplest of all signal sources: a consumer keeps its own cursor and asks what happened since,
164
+ // and Intel never learns that the consumer exists. That is the D24 test passed rather than
165
+ // argued — remove every reader and nothing piles up here.
166
+ //
167
+ // ⚠️ **The capability follows the KIND asked for** (#774), which is why this tool stands outside
168
+ // both capability blocks rather than inside one. An audit row says the same thing its resource
169
+ // says, so the journal asks what reading that resource asks — `nodes/read` for `node`,
170
+ // `flows/read` for `flow` and `flow-run`. A capability of its own would be a second answer to one
171
+ // question and the two would drift; `nodes/read` for all three would drop the first of the three
172
+ // checks for exactly the two kinds #774 opened.
173
+ //
174
+ // ⚠️ **Offered on EITHER capability, decided per call.** Registration is per connection and the
175
+ // kind arrives per call, so the two cannot be the same gate. Holding one of the two is what makes
176
+ // the tool worth listing at all; which of the three journals it then answers is the handler's
177
+ // question. Same shape, same order, same refusal as `GET /audit` — one business layer, three thin
178
+ // surfaces.
179
+ if (permits(deps.authorization, "nodes", "read") ||
180
+ permits(deps.authorization, "flows", "read")) {
181
+ server.registerTool("audit_list", {
182
+ title: "List change events",
183
+ description: "Read the change journal forward from a stable position: what happened to the resources you may read, oldest first. `resourceType` picks which of the three journals to read — `node`, `flow` or `flow-run` — and each has its own cursor; do not carry one over to another. Pass `nextCursor` back as `after` to continue where you stopped — it says WHERE YOU GOT TO and comes back on the last page as well. Whether another page may follow is `hasMore`; a null `nextCursor` means nothing was returned, not that you have caught up. Events about permanently deleted nodes and flows are never returned, to anybody.",
184
+ inputSchema: AuditListRequest,
185
+ annotations: {
186
+ title: "List change events",
187
+ readOnlyHint: true,
188
+ destructiveHint: false,
189
+ idempotentHint: true,
190
+ openWorldHint: false,
191
+ },
192
+ }, async (input) => {
193
+ // ⚠️ Before the service, so a refusal costs no journal read. A 403 that arrived after the
194
+ // rows were read is a 403 that already read them.
195
+ if (!permits(deps.authorization, input.resourceType === "node" ? "nodes" : "flows", "read")) {
196
+ throw new IntelError(403, "permission_required", "Permission required");
197
+ }
198
+ return text(await deps.audit.list(actor, input));
199
+ });
200
+ }
201
+ if (permits(deps.authorization, "nodes", "read")) {
202
+ // The board as one answer (#648). Four tools, not five: a move is an update of `status` and
203
+ // `position`, and deleting a task is `node_archive` — nothing here removes a `nodes` row.
204
+ server.registerTool("board_get", {
205
+ title: "Read a board",
206
+ description: "Read one board in a single answer: its columns and every card you may see, without the card bodies. Filter by column, assignee, due date or what a card waits for — each filter narrows the query itself. Open a card's text with node_version_get.",
207
+ inputSchema: BoardGetInput,
208
+ annotations: {
209
+ title: "Read a board",
210
+ readOnlyHint: true,
211
+ destructiveHint: false,
212
+ idempotentHint: true,
213
+ openWorldHint: false,
214
+ },
215
+ }, async (input) => text(await deps.boards.get(actor, input)));
216
+ // ⚠️ Two tools rather than one, because they answer two questions that differ in exactly one
217
+ // way: `board_assignee_list` offers whom a card MAY be given to and is narrowed to this board
218
+ // (D70); `directory_resolve` names people who are ALREADY on cards and is not narrowed,
219
+ // because somebody whose access was withdrawn is still who the card belongs to.
220
+ //
221
+ // ⚠️ `list`, NOT `search`, although a query narrows it. `search` is the ONE bare verb at the
222
+ // root of this surface (Jack's decision 2026-08-09, `packages/intel/CLAUDE.md`), and a second one
223
+ // behind a domain would make that exception look like a pattern. The verb for "reads many" is
224
+ // `list`, and a filter does not change which verb a call is.
225
+ server.registerTool("board_assignee_list", {
226
+ title: "Who a card on this board can be given to",
227
+ description: "Find the people you may set as assignee on a card of this board. Only people who can actually open THIS board are offered, so the same query against two boards can give two different answers. Leave the query EMPTY to get up to three of them without searching, plus a count of how many more there are. One character answers empty rather than everybody; from two on it filters. Use directory_resolve to name somebody already on a card.",
228
+ inputSchema: BoardAssigneeSearchInput,
229
+ annotations: {
230
+ title: "Who a card on this board can be given to",
231
+ readOnlyHint: true,
232
+ destructiveHint: false,
233
+ idempotentHint: true,
234
+ // ⚠️ The answer comes from gate, not from this database, and it changes when an account
235
+ // or a grant does. `false` would promise a stability nothing here controls.
236
+ openWorldHint: true,
237
+ },
238
+ }, async (input) => text(await deps.boards.searchAssignees(actor, input)));
239
+ server.registerTool("directory_resolve", {
240
+ title: "What the people on these cards are called",
241
+ description: "Turn assignee ids into names, so a card can say who it belongs to instead of showing an identifier. Ids that cannot be named are absent from the answer rather than reported as missing. Not narrowed to a board: somebody whose access was withdrawn is still who the card belongs to.",
242
+ inputSchema: DirectoryResolveInput,
243
+ annotations: {
244
+ title: "What the people on these cards are called",
245
+ readOnlyHint: true,
246
+ destructiveHint: false,
247
+ idempotentHint: true,
248
+ openWorldHint: true,
249
+ },
250
+ }, async (input) => text(await deps.boards.resolveNames(actor, input)));
251
+ server.registerTool("board_task_create", {
252
+ title: "Add a card to a board",
253
+ description: "File a new card on a board. Omit the status to put it in the first column. Name a parent task to make it a subtask — it lands on the same board, and moving an existing card under another one is `node_update` with a new parent. The card is a node like any other: its own address, its own permissions, its own history — and its own body. This call answers with the BOARD rather than the new card, so to write that body, make the card with `node_create` under the board instead: it answers with the node itself, files it on the board just the same, and hands you the id `node_version_create` asks for.",
254
+ inputSchema: BoardTaskCreateInput,
255
+ annotations: {
256
+ title: "Add a card to a board",
257
+ readOnlyHint: false,
258
+ destructiveHint: false,
259
+ idempotentHint: false,
260
+ openWorldHint: false,
261
+ },
262
+ }, async (input) => text(await deps.boards.createTask(actor, input)));
263
+ server.registerTool("board_task_update", {
264
+ title: "Change a card",
265
+ description: "Change a card's column, order, assignee, labels or dates. Moving a card is this call with a new status — send a position to place it, or leave it out to put it at the end of the column. An absent field is left alone; null clears one. Rename a card with node_update. To move a card UNDER another card, use `node_update` with the other card as its parent — this tool changes what a card is, not where it hangs.",
266
+ inputSchema: BoardTaskUpdateInput,
267
+ annotations: {
268
+ title: "Change a card",
269
+ readOnlyHint: false,
270
+ destructiveHint: false,
271
+ idempotentHint: true,
272
+ openWorldHint: false,
273
+ },
274
+ }, async (input) => text(await deps.boards.updateTask(actor, input)));
275
+ server.registerTool("board_update", {
276
+ title: "Set a board's columns",
277
+ description: "Replace a board's column list, in order. Columns are replaced rather than merged, so send the complete list. A column marked terminal is one where a card counts as finished.",
278
+ inputSchema: BoardUpdateInput,
279
+ annotations: {
280
+ title: "Set a board's columns",
281
+ readOnlyHint: false,
282
+ destructiveHint: false,
283
+ idempotentHint: true,
284
+ openWorldHint: false,
285
+ },
286
+ }, async (input) => text(await deps.boards.update(actor, input)));
287
+ server.registerTool("feed_list", {
288
+ title: "List recent activity",
289
+ description: "Read the change journal backwards: what most recently happened to the nodes you may read, newest first, one entry per event. Each entry carries the node's current title and the folders above it, and only the folders you may open. Pass `nextCursor` back as `before` to keep going further into the past; a null `nextCursor` means you have reached the beginning. Pass `actor` to see one person's or one agent's work alone. This is the human-facing view — for catching up on everything in order, use audit_list, and do not mix the two cursors.",
290
+ inputSchema: FeedListRequest,
291
+ annotations: {
292
+ title: "List recent activity",
293
+ readOnlyHint: true,
294
+ destructiveHint: false,
295
+ idempotentHint: true,
296
+ openWorldHint: false,
297
+ },
298
+ }, async (input) => text(await deps.feed.list(actor, input)));
299
+ server.registerTool("node_list", {
300
+ title: "List nodes",
301
+ description: "List authorized folders, documents, attachments, and tables under one parent.",
302
+ inputSchema: ListNodesInput,
303
+ annotations: {
304
+ title: "List nodes",
305
+ readOnlyHint: true,
306
+ destructiveHint: false,
307
+ idempotentHint: true,
308
+ openWorldHint: false,
309
+ },
310
+ }, async (input) => text(await deps.nodes.list(actor, input)));
311
+ server.registerTool("node_get", {
312
+ title: "Get node",
313
+ description: "Get one authorized node and its current immutable content version.",
314
+ inputSchema: GetNodeInput,
315
+ annotations: {
316
+ title: "Get node",
317
+ readOnlyHint: true,
318
+ destructiveHint: false,
319
+ idempotentHint: true,
320
+ openWorldHint: false,
321
+ },
322
+ }, async (input) => text(await deps.nodes.get(actor, input.nodeId)));
323
+ server.registerTool("node_version_list", {
324
+ title: "List node versions",
325
+ description: "List immutable versions of one authorized node.",
326
+ inputSchema: GetNodeInput,
327
+ annotations: {
328
+ title: "List node versions",
329
+ readOnlyHint: true,
330
+ destructiveHint: false,
331
+ idempotentHint: true,
332
+ openWorldHint: false,
333
+ },
334
+ }, async (input) => text(await deps.nodes.listVersions(actor, input.nodeId)));
335
+ // The content behind one row of the version list (#147): what a version-pinned search citation
336
+ // points at. No `knowledge_*` alias — the tool is new under the post-#125 naming and never
337
+ // carried the old prefix.
338
+ server.registerTool("node_version_get", {
339
+ title: "Get node version content",
340
+ description: "Read one pinned immutable version of an authorized node, content included — the version a search citation names, whether or not it is current. For an attachment the answer is its metadata without the bytes. A version that does not belong to the node is not found.",
341
+ inputSchema: GetNodeVersionInput,
342
+ annotations: {
343
+ title: "Get node version content",
344
+ readOnlyHint: true,
345
+ destructiveHint: false,
346
+ idempotentHint: true,
347
+ openWorldHint: false,
348
+ },
349
+ }, async (input) => text(await deps.nodes.getVersion(actor, input.nodeId, input.versionId)));
350
+ server.registerTool("node_attachment_get", {
351
+ title: "Get node attachment",
352
+ description: "Get authorized immutable attachment metadata, and with includeContent the file itself. " +
353
+ "The resourceUri in the answer is this server's own address; a portal that aggregates " +
354
+ "several servers prefixes it, so read the file through includeContent unless the URI " +
355
+ "came from resources/list.",
356
+ inputSchema: GetAttachmentInput,
357
+ annotations: {
358
+ title: "Get node attachment",
359
+ readOnlyHint: true,
360
+ destructiveHint: false,
361
+ idempotentHint: true,
362
+ openWorldHint: false,
363
+ },
364
+ },
365
+ /**
366
+ * ⚠️ The size is checked BEFORE the stream is opened, which the resource handler below cannot
367
+ * do — it is handed the body by the SDK and has to cancel it. Here the metadata read is its
368
+ * own call, so an oversized attachment costs no R2 read at all.
369
+ *
370
+ * ⚠️ The bytes go out as an embedded RESOURCE block, never as text. A base64 string in a text
371
+ * block is something a model will try to read; a resource block is a file to a client that
372
+ * understands one, and the metadata stays in the first block either way (#773).
373
+ */
374
+ async (input) => {
375
+ const metadata = await deps.nodes.getAttachment(actor, input.nodeId);
376
+ if (!input.includeContent)
377
+ return text(metadata);
378
+ if (metadata.version.size > AttachmentInlineLimit) {
379
+ throw new IntelError(413, "attachment_too_large", "Attachment is too large to inline; read it over HTTP instead");
380
+ }
381
+ const { attachment, body } = await deps.nodes.readAttachment(actor, input.nodeId);
382
+ return {
383
+ content: [
384
+ { type: "text", text: JSON.stringify(attachment) },
385
+ {
386
+ type: "resource",
387
+ resource: {
388
+ uri: `intel://nodes/${attachment.node.id}/attachment`,
389
+ mimeType: attachment.version.mediaType,
390
+ blob: await base64(body),
391
+ },
392
+ },
393
+ ],
394
+ isError: false,
395
+ };
396
+ });
397
+ server.registerResource("node-attachment",
398
+ /**
399
+ * ⚠️ `list` is what makes the attachments findable at all, and it was `undefined` until #773.
400
+ * A client that cannot enumerate them has to GUESS the URI — and the one place it could copy
401
+ * one from, `node_attachment_get`, answers this server's own address, which a portal that
402
+ * aggregates several servers prefixes. Measured on 2026-08-24: the URI out of that tool was
403
+ * refused as `Resource not found` while the prefixed spelling delivered the file.
404
+ *
405
+ * ⚠️ The listing is COMPLETE, and the loop below is why it has to be. `ListResourcesCallback`
406
+ * takes `RequestHandlerExtra` and nothing else — the SDK hands a template's list callback no
407
+ * cursor (checked against the 1.30.0 type: `(extra) => ListResourcesResult`), so a
408
+ * `nextCursor` in the answer is one nothing would ever send back. That leaves two honest
409
+ * options and one dishonest one: answer everything, refuse, or cut the list at some limit and
410
+ * let it read as complete. The cut is the one that is out — a reader would never learn which
411
+ * of their files are missing.
412
+ *
413
+ * The paging is therefore INTERNAL: D1 is asked in pages so one statement never has to carry
414
+ * the whole set, and the loop ends when the service stops handing back a cursor.
415
+ */
416
+ new ResourceTemplate("intel://nodes/{nodeId}/attachment", {
417
+ list: async () => {
418
+ const resources = [];
419
+ let after = null;
420
+ do {
421
+ const page = await deps.nodes.listAttachments(actor, { limit: 100, after });
422
+ resources.push(...page.items.map((node) => ({
423
+ uri: `intel://nodes/${node.id}/attachment`,
424
+ name: node.title,
425
+ // ⚠️ No `mimeType`: it lives on the VERSION, so answering one would cost a version
426
+ // read per row. The read itself carries the real one.
427
+ description: node.description ?? undefined,
428
+ })));
429
+ after = page.nextCursor;
430
+ } while (after !== null);
431
+ return { resources };
432
+ },
433
+ }), { title: "Intel node attachment" }, async (uri, variables) => {
434
+ const { attachment, body } = await deps.nodes.readAttachment(actor, String(variables.nodeId));
435
+ if (attachment.version.size > AttachmentInlineLimit) {
436
+ await body.cancel();
437
+ throw new IntelError(413, "attachment_too_large", "Attachment is too large to inline; read it over HTTP instead");
438
+ }
439
+ return {
440
+ contents: [
441
+ {
442
+ uri: uri.href,
443
+ mimeType: attachment.version.mediaType,
444
+ blob: await base64(body),
445
+ },
446
+ ],
447
+ };
448
+ });
449
+ server.registerResource("node-document", new ResourceTemplate("intel://nodes/{nodeId}", { list: undefined }), { title: "Intel node document", mimeType: "application/json" }, async (uri, variables) => {
450
+ const document = await deps.nodes.get(actor, String(variables.nodeId));
451
+ return {
452
+ contents: [
453
+ { uri: uri.href, mimeType: "application/json", text: JSON.stringify(document) },
454
+ ],
455
+ };
456
+ });
457
+ server.registerTool("search", {
458
+ title: "Search nodes",
459
+ description: "Search only authorized indexed nodes and return version-pinned citations. Pass scopeId " +
460
+ "with a folder node id to search that folder and everything beneath it instead of " +
461
+ "everything readable.",
462
+ inputSchema: SearchInput,
463
+ annotations: {
464
+ title: "Search nodes",
465
+ readOnlyHint: true,
466
+ destructiveHint: false,
467
+ idempotentHint: true,
468
+ openWorldHint: false,
469
+ },
470
+ }, async (input) => text(await deps.nodes.search(actor, input)));
471
+ server.registerTool("node_table_get", {
472
+ title: "Get node table",
473
+ description: "Read one authorized table as column names and rows.",
474
+ inputSchema: GetTableInput,
475
+ annotations: {
476
+ title: "Get node table",
477
+ readOnlyHint: true,
478
+ destructiveHint: false,
479
+ idempotentHint: true,
480
+ openWorldHint: false,
481
+ },
482
+ }, async (input) => text(await deps.nodes.getTable(actor, input.nodeId)));
483
+ server.registerTool("node_link_list", {
484
+ title: "List node links",
485
+ description: "List authorized outgoing links and backlinks for one node.",
486
+ inputSchema: GetNodeInput,
487
+ annotations: {
488
+ title: "List node links",
489
+ readOnlyHint: true,
490
+ destructiveHint: false,
491
+ idempotentHint: true,
492
+ openWorldHint: false,
493
+ },
494
+ }, async (input) => text(await deps.nodes.listLinks(actor, input.nodeId)));
495
+ // The reader's half of a document link (#41): the titles of the linked documents this caller
496
+ // may see. There is no tool to create or delete a link — a relationship is written in the text
497
+ // and `node_version_create` is what records it, so there is only one way to make one.
498
+ //
499
+ // ⚠️ A target this caller may not reach, or one that is gone, is simply absent from the answer.
500
+ // The two are indistinguishable on purpose: telling them apart would confirm that a document
501
+ // exists somewhere they cannot look.
502
+ server.registerTool("node_link_resolve", {
503
+ title: "Resolve node links",
504
+ description: "Resolve document link targets to the titles this caller is authorized to see. Targets that are unreachable or deleted are absent from the result.",
505
+ inputSchema: ResolveNodeLinksInput,
506
+ annotations: {
507
+ title: "Resolve node links",
508
+ readOnlyHint: true,
509
+ destructiveHint: false,
510
+ idempotentHint: true,
511
+ openWorldHint: false,
512
+ },
513
+ }, async (input) => text(await deps.nodes.resolveLinks(actor, input)));
514
+ server.registerTool("node_graph", {
515
+ title: "Get node graph",
516
+ description: "Get the authorized nodes and explicit relationships for discovery.",
517
+ inputSchema: NodeGraphInput,
518
+ annotations: {
519
+ title: "Get node graph",
520
+ readOnlyHint: true,
521
+ destructiveHint: false,
522
+ idempotentHint: true,
523
+ openWorldHint: false,
524
+ },
525
+ }, async (input) => text(await deps.nodes.graph(actor, input)));
526
+ // The export as MCP offers it (#136): the manifest — every readable entry with its bundle path
527
+ // and media type. The contents behind the paths are read one by one through `node_get`,
528
+ // `node_table_get`, and `node_attachment_get`. ⚠️ Deliberately no binary zip over MCP: a
529
+ // resource payload travels inline as base64 and would have to fit in Worker memory twice, the
530
+ // same limit that caps `node_attachment_get` at 10 MB — a whole subtree is the case that limit
531
+ // exists for. HTTP `GET /nodes/:nodeId/export` streams the real archive.
532
+ server.registerTool("node_export", {
533
+ title: "Export nodes",
534
+ description: "Plan a bundle export: the manifest of one authorized subtree (or the whole tree when nodeId is null) with each entry's bundle path, kind, and media type. Content bytes are fetched per node; the binary zip is served over HTTP.",
535
+ inputSchema: ExportManifestInput,
536
+ annotations: {
537
+ title: "Export nodes",
538
+ readOnlyHint: true,
539
+ destructiveHint: false,
540
+ idempotentHint: true,
541
+ openWorldHint: false,
542
+ },
543
+ }, async (input) => text(await deps.bundle.manifest(bundleActor, input.nodeId)));
544
+ }
545
+ if (deps.authorization.can("intel", "admin")) {
546
+ server.registerTool("node_reindex", {
547
+ title: "Reindex nodes",
548
+ description: "Queue every current canonical node version for derived index rebuild.",
549
+ inputSchema: EmptyInput,
550
+ annotations: {
551
+ title: "Reindex nodes",
552
+ readOnlyHint: false,
553
+ destructiveHint: false,
554
+ idempotentHint: true,
555
+ openWorldHint: false,
556
+ },
557
+ }, async () => text(await deps.nodes.reindex(actor)));
558
+ }
559
+ if (permits(deps.authorization, "nodes", "create")) {
560
+ server.registerTool("node_create", {
561
+ title: "Create node",
562
+ description: "Create a governed node: a folder, a board, a document, a card, an attachment or a table. A card made under a board lands on that board, and the answer carries the new node's id — which is what `node_version_create` needs to give it a body. ⚠️ `index` is a reserved name inside a folder (`index`, `Index` and `index.md` alike): a document called that IS the text of the folder it sits in. The browser shows it in the folder's details instead of as a row beneath it, so a folder gets described by writing one. Here nothing is hidden — it is listed and read like any other document.",
563
+ inputSchema: CreateNodeInput,
564
+ annotations: {
565
+ title: "Create node",
566
+ readOnlyHint: false,
567
+ destructiveHint: false,
568
+ idempotentHint: true,
569
+ openWorldHint: false,
570
+ },
571
+ }, async (input) => text(await deps.nodes.create(actor, input)));
572
+ }
573
+ if (permits(deps.authorization, "nodes", "write")) {
574
+ server.registerTool("node_version_create", {
575
+ title: "Create node version",
576
+ description: "Append an immutable content version using an optimistic base version. Documents and board cards both take one — writing a card here is how a ticket gets a body instead of only a title.",
577
+ inputSchema: SaveNodeVersionInput,
578
+ annotations: {
579
+ title: "Create node version",
580
+ readOnlyHint: false,
581
+ destructiveHint: false,
582
+ idempotentHint: true,
583
+ openWorldHint: false,
584
+ },
585
+ }, async (input) => text(await deps.nodes.save(actor, input)));
586
+ server.registerTool("node_attachment_create", {
587
+ title: "Create node attachment version",
588
+ description: "Append immutable base64 file bytes to an attachment node.",
589
+ inputSchema: SaveAttachmentInput,
590
+ annotations: {
591
+ title: "Create node attachment version",
592
+ readOnlyHint: false,
593
+ destructiveHint: false,
594
+ idempotentHint: true,
595
+ openWorldHint: false,
596
+ },
597
+ }, async (input) => text(await deps.nodes.saveAttachment(actor, input)));
598
+ // The import half of the bundle round trip (#137). At minimum the manifest way: a zip made by
599
+ // the export — or a naked zipped folder — lands as a new subtree under the target. Same
600
+ // service as HTTP, so authorization, limits, remapping, and the all-or-nothing write cannot
601
+ // differ by surface; only the transport differs (inline base64 here, a streamed body there).
602
+ server.registerTool("node_import", {
603
+ title: "Import nodes",
604
+ description: "Import a zip bundle (base64) as a new subtree under an authorized target folder — null files at the root. Kinds come from manifest.json when present, from file extensions otherwise. Always creates new nodes; links and flow references between bundled entries are rewritten to the new IDs. Bundles beyond ~10 MB take the streaming HTTP route POST /nodes/:nodeId/import.",
605
+ inputSchema: ImportBundleInput,
606
+ annotations: {
607
+ title: "Import nodes",
608
+ readOnlyHint: false,
609
+ destructiveHint: false,
610
+ idempotentHint: true,
611
+ openWorldHint: false,
612
+ },
613
+ }, async (input) => text(await deps.bundle.importBundle(bundleActor, {
614
+ targetNodeId: input.nodeId,
615
+ zip: decodeZipBase64(input.zipBase64),
616
+ idempotencyKey: input.idempotencyKey,
617
+ })));
618
+ server.registerTool("node_table_create", {
619
+ title: "Create node table columns",
620
+ description: "Write the column names of an empty table. The header is the contract every append is checked against and cannot be rewritten.",
621
+ inputSchema: DefineTableInput,
622
+ annotations: {
623
+ title: "Create node table columns",
624
+ readOnlyHint: false,
625
+ destructiveHint: false,
626
+ idempotentHint: true,
627
+ openWorldHint: false,
628
+ },
629
+ }, async (input) => text(await deps.nodes.defineTable(actor, input)));
630
+ // The tool #40 exists for: an agent collecting findings on a schedule appends them without
631
+ // reading or resending what is already there, and two agents appending at once lose nothing.
632
+ server.registerTool("node_table_row_create", {
633
+ title: "Create node table rows",
634
+ description: "Append rows to a table without reading or resending its existing content. Each row must have exactly as many cells as the table has columns; a row that does not is rejected and nothing is written.",
635
+ inputSchema: AppendTableRowsInput,
636
+ annotations: {
637
+ title: "Create node table rows",
638
+ readOnlyHint: false,
639
+ destructiveHint: false,
640
+ // The idempotency key makes a repeat of the same call a no-op; without one, appending
641
+ // twice appends twice, which is what appending means.
642
+ idempotentHint: true,
643
+ openWorldHint: false,
644
+ },
645
+ }, async (input) => text(await deps.nodes.appendTableRows(actor, input)));
646
+ // The other half of #135: an agent that wrote a wrong finding can correct it. Positions address
647
+ // rows — they carry no IDs on purpose — so every mutation names the version the positions were
648
+ // read from and is refused with `version_conflict` when the table moved on. No `knowledge_*`
649
+ // alias: these tools are new under the post-#125 naming and never carried the old prefix.
650
+ server.registerTool("node_table_row_update", {
651
+ title: "Update node table rows",
652
+ description: "Replace rows at the given zero-based positions. Requires the baseVersionId the positions were read from (see node_table_get); answers version_conflict and writes nothing when the table has changed since. Each replacement row must match the column count.",
653
+ inputSchema: UpdateTableRowsInput,
654
+ annotations: {
655
+ title: "Update node table rows",
656
+ readOnlyHint: false,
657
+ // Replacing a cell discards what stood there; the history keeps it, the table does not.
658
+ destructiveHint: true,
659
+ idempotentHint: true,
660
+ openWorldHint: false,
661
+ },
662
+ }, async (input) => text(await deps.nodes.updateTableRows(actor, input)));
663
+ server.registerTool("node_table_row_delete", {
664
+ title: "Delete node table rows",
665
+ description: "Remove rows at the given zero-based positions. Requires the baseVersionId the positions were read from; answers version_conflict and writes nothing when the table has changed since.",
666
+ inputSchema: DeleteTableRowsInput,
667
+ annotations: {
668
+ title: "Delete node table rows",
669
+ readOnlyHint: false,
670
+ destructiveHint: true,
671
+ idempotentHint: true,
672
+ openWorldHint: false,
673
+ },
674
+ }, async (input) => text(await deps.nodes.deleteTableRows(actor, input)));
675
+ server.registerTool("node_table_update", {
676
+ title: "Update node table columns",
677
+ description: "Change the header of a defined table through an explicit column mapping: each new column names the current column that fills it (rename or keep), names none to start empty (add), and a current column no entry names is removed with its cells. Requires the baseVersionId the mapping was read from. Defining over an existing header without a mapping stays refused.",
678
+ inputSchema: RedefineTableInput,
679
+ annotations: {
680
+ title: "Update node table columns",
681
+ readOnlyHint: false,
682
+ destructiveHint: true,
683
+ idempotentHint: true,
684
+ openWorldHint: false,
685
+ },
686
+ }, async (input) => text(await deps.nodes.redefineTable(actor, input)));
687
+ server.registerTool("node_update", {
688
+ title: "Update node",
689
+ description: "Rename, move, or describe a node.",
690
+ inputSchema: UpdateNodeInput,
691
+ annotations: {
692
+ title: "Update node",
693
+ readOnlyHint: false,
694
+ destructiveHint: false,
695
+ idempotentHint: true,
696
+ openWorldHint: false,
697
+ },
698
+ }, async (input) => text(await deps.nodes.update(actor, input)));
699
+ server.registerTool("node_archive", {
700
+ title: "Archive node",
701
+ description: "Archive or restore one node. Archiving hides it from listings and search and takes its vectors out of the index; nothing is deleted and every version stays readable, so restoring is the same call with `archived: false`.",
702
+ inputSchema: ArchiveNodeInput,
703
+ annotations: {
704
+ title: "Archive node",
705
+ readOnlyHint: false,
706
+ destructiveHint: true,
707
+ idempotentHint: true,
708
+ // ⚠️ `true` until #418, and the reason is worth keeping: archiving an agent used to
709
+ // switch its Gate Application off (#182), which was the one node mutation that reached
710
+ // outside. With the agents parked nothing here leaves the installation — and an
711
+ // openWorldHint that is wrong in the cautious direction is still wrong, because it is
712
+ // the field a client decides on.
713
+ openWorldHint: false,
714
+ },
715
+ }, async (input) => text(await deps.nodes.archive(actor, input)));
716
+ /**
717
+ * ⚠️ The look before the irreversible step, and it is on this surface because the step itself
718
+ * is (#586). `node_purge` has stood here since #457 for the reason #436 states — a way only the
719
+ * interface has is a way an agent goes around — but until now the COUNT did not, so an agent
720
+ * could delete for good without being able to see first what went with it. A person in the
721
+ * interface is shown both numbers and then confirms.
722
+ *
723
+ * That is exactly the state `.claude/rules/destructive.md` refuses in its own words: a cascade
724
+ * turns one confirmed click into an unknown number of deletions, and "the question can no
725
+ * longer name what disappears". Here the question could not be ASKED, which is the same gap one
726
+ * step earlier — and the half that was missing was the safe half.
727
+ *
728
+ * ⚠️ It answers about a tree, not about one row: `totalItems` counts the nodes AND the flows
729
+ * beneath it, the same two tables the purge itself walks. `inboundLinks` is the other kind of
730
+ * consequence — mentions elsewhere that will break and are deliberately not refused.
731
+ */
732
+ server.registerTool("node_purge_preview", {
733
+ title: "Preview a permanent deletion",
734
+ description: "Ask before node_purge, afterwards there is no row left to count. Names and counts what deleting one ARCHIVED node for good would take with it: the nodes and flows in its subtree with their titles, capped at fifty with the total beside it, plus how many documents link to it from outside. Changes nothing.",
735
+ inputSchema: PurgeNodePreviewInput,
736
+ annotations: {
737
+ title: "Preview a permanent deletion",
738
+ readOnlyHint: true,
739
+ destructiveHint: false,
740
+ idempotentHint: true,
741
+ openWorldHint: false,
742
+ },
743
+ }, async (input) => text(await deps.nodes.purgePreview(actor, input)));
744
+ // ⚠️ It exists on this surface for the reason #436 states: a way only the interface has is a way
745
+ // an agent goes around, by looking for the row in the database. What it may do is bounded by the
746
+ // same rules the interface obeys — only an archived node, only an empty folder, never one a flow
747
+ // still uses.
748
+ server.registerTool("node_purge", {
749
+ title: "Delete node for good",
750
+ description: "Delete one ARCHIVED node and everything belonging to it — every version, its content, its grants and its entries in the search index. This cannot be undone. A living node is refused: archive it first. A folder is refused while it still holds anything, and so is a node a published flow uses as a step; documents that merely LINK to it are not, and their links break.",
751
+ inputSchema: PurgeNodeInput,
752
+ annotations: {
753
+ title: "Delete node for good",
754
+ readOnlyHint: false,
755
+ destructiveHint: true,
756
+ // ⚠️ NOT idempotent, and that is the honest answer rather than a missing flag: calling it
757
+ // twice deletes once and then answers 404. A client that repeated it on a timeout would
758
+ // otherwise read the second answer as "already done" when it may mean "somebody else's
759
+ // node with that id is gone".
760
+ idempotentHint: false,
761
+ openWorldHint: false,
762
+ },
763
+ }, async (input) => text(await deps.nodes.purge(actor, input)));
764
+ }
765
+ if (permits(deps.authorization, "nodes", "share")) {
766
+ server.registerTool("node_grant_list", {
767
+ title: "List node grants",
768
+ description: "List direct grants for nodes the caller is allowed to manage.",
769
+ inputSchema: ListGrantsInput,
770
+ annotations: {
771
+ title: "List node grants",
772
+ readOnlyHint: true,
773
+ destructiveHint: false,
774
+ idempotentHint: true,
775
+ openWorldHint: false,
776
+ },
777
+ }, async (input) => text(await deps.nodes.listGrants(actor, input.resourceId)));
778
+ /**
779
+ * ⚠️ The neighbour above answers a NARROWER question, and that is why this one has to be here
780
+ * (#586). `node_grant_list` reads the rows on this node; this resolves the inheritance — every
781
+ * grant along the folders above plus the owners, who reach it without a grant row existing at
782
+ * all. On the everyday node, whose access comes from the folder it sits in, the first answer is
783
+ * empty and this one is not.
784
+ *
785
+ * Two queries with similar names are two different questions, and `.claude/rules/destructive.md`
786
+ * records what it costs to confuse them: in #457 a refusal asked the one that sounded right and
787
+ * never fired. Here the cost is the same shape and lands on the sharing path — an agent asked
788
+ * "who can see this" reaches for the only name it has, is answered `[]`, and reports that
789
+ * nobody can, about a document the whole organization opens. Silence would not be a missing
790
+ * answer here, it would be a confident wrong one.
791
+ *
792
+ * ⚠️ The list is for reading, never for revoking: only the entries whose `resourceId` is this
793
+ * node can be revoked here, the rest belong to the folder that granted them.
794
+ */
795
+ server.registerTool("node_effective_access_list", {
796
+ title: "List effective node access",
797
+ description: "List everybody who reaches one node once inheritance is resolved: the grants on it AND on every folder above it, plus the owners, who need no grant. node_grant_list answers the narrower question — the rows sitting on this node alone — and on a node that inherits its access that answer is empty while this one is not. An entry whose resourceId is another node was granted there and can only be revoked there.",
798
+ inputSchema: ListEffectiveAccessInput,
799
+ annotations: {
800
+ title: "List effective node access",
801
+ readOnlyHint: true,
802
+ destructiveHint: false,
803
+ idempotentHint: true,
804
+ openWorldHint: false,
805
+ },
806
+ }, async (input) => text(await deps.nodes.listEffectiveAccess(actor, input.resourceId)));
807
+ server.registerTool("node_grant_create", {
808
+ title: "Grant node access",
809
+ description: "Grant inherited node access to a Gate user, verified email, or the organization.",
810
+ inputSchema: ShareInput,
811
+ annotations: {
812
+ title: "Grant node access",
813
+ readOnlyHint: false,
814
+ destructiveHint: false,
815
+ idempotentHint: true,
816
+ openWorldHint: false,
817
+ },
818
+ }, async (input) => text(await deps.nodes.share(actor, input)));
819
+ server.registerTool("node_grant_revoke", {
820
+ title: "Revoke node grant",
821
+ description: "Revoke one direct node grant by ID.",
822
+ inputSchema: RevokeGrantInput,
823
+ annotations: {
824
+ title: "Revoke node grant",
825
+ readOnlyHint: false,
826
+ destructiveHint: true,
827
+ idempotentHint: true,
828
+ openWorldHint: false,
829
+ },
830
+ }, async (input) => await refusing(() => deps.nodes.revokeGrant(actor, input)));
831
+ }
832
+ if (permits(deps.authorization, "flows", "read")) {
833
+ server.registerTool("flow_list", {
834
+ title: "List flows",
835
+ description: "List authorized flows, either all of them or the ones filed in one folder of the shared tree.",
836
+ inputSchema: ListFlowsInput,
837
+ annotations: {
838
+ title: "List flows",
839
+ readOnlyHint: true,
840
+ destructiveHint: false,
841
+ idempotentHint: true,
842
+ openWorldHint: false,
843
+ },
844
+ }, async (input) => text(await deps.flows.list(flowActor, input)));
845
+ server.registerTool("flow_get", {
846
+ title: "Get flow",
847
+ description: "Load one authorized flow and its current immutable graph on demand.",
848
+ inputSchema: GetFlowInput,
849
+ annotations: {
850
+ title: "Get flow",
851
+ readOnlyHint: true,
852
+ destructiveHint: false,
853
+ idempotentHint: true,
854
+ openWorldHint: false,
855
+ },
856
+ }, async (input) => text(await deps.flows.get(flowActor, input.flowId)));
857
+ // Named like `knowledge_versions_list`, because it is the same question about the other kind of
858
+ // thing in the tree. This pair is what turns the `versionId` that `flow_publish` and
859
+ // `flow_publish_preview` demand into something an MCP client can actually obtain (#144).
860
+ server.registerTool("flow_version_list", {
861
+ title: "List flow versions",
862
+ description: "List the immutable versions of one authorized flow, oldest first, marking which of them is published. Metadata only; flow_version_get loads a version's graph.",
863
+ inputSchema: GetFlowInput,
864
+ annotations: {
865
+ title: "List flow versions",
866
+ readOnlyHint: true,
867
+ destructiveHint: false,
868
+ idempotentHint: true,
869
+ openWorldHint: false,
870
+ },
871
+ }, async (input) => text(await deps.flows.listVersions(flowActor, input.flowId)));
872
+ server.registerTool("flow_version_get", {
873
+ title: "Get flow version",
874
+ description: "Get one immutable version of an authorized flow, including its graph.",
875
+ inputSchema: GetFlowVersionInput,
876
+ annotations: {
877
+ title: "Get flow version",
878
+ readOnlyHint: true,
879
+ destructiveHint: false,
880
+ idempotentHint: true,
881
+ openWorldHint: false,
882
+ },
883
+ }, async (input) => text(await deps.flows.getVersion(flowActor, input)));
884
+ server.registerTool("flow_call_list", {
885
+ title: "List called flows",
886
+ description: "List the flows one flow calls, read out of its graph rather than out of where it is filed.",
887
+ inputSchema: GetFlowInput,
888
+ annotations: {
889
+ title: "List called flows",
890
+ readOnlyHint: true,
891
+ destructiveHint: false,
892
+ idempotentHint: true,
893
+ openWorldHint: false,
894
+ },
895
+ }, async (input) => text(await deps.flows.listCalls(flowActor, input.flowId)));
896
+ // ⚠️ Only nodes the requesting user may see are in the answer, placeholders included: an edge to
897
+ // a grey box would already say that something is there. The same rule the screen follows,
898
+ // because it is the same service (#19).
899
+ server.registerTool("flow_graph", {
900
+ title: "Read the relation graph",
901
+ description: "Read what accesses what for one folder or one flow: which flow reads which document and which flow calls which flow.",
902
+ inputSchema: RelationGraphInput,
903
+ annotations: {
904
+ title: "Read the relation graph",
905
+ readOnlyHint: true,
906
+ destructiveHint: false,
907
+ idempotentHint: true,
908
+ openWorldHint: false,
909
+ },
910
+ }, async (input) => text(await deps.flows.relationGraph(flowActor, input)));
911
+ server.registerTool("flow_validate", {
912
+ title: "Check whether a flow would start",
913
+ description: "Check whether this flow would start for the calling user right now, and name every reason it would not. Answers rather than acts: no run is created. The answer is a snapshot for this user at this moment, because tool reachability is a live query with their own portal token.",
914
+ inputSchema: GetFlowInput,
915
+ annotations: {
916
+ title: "Check whether a flow would start",
917
+ readOnlyHint: true,
918
+ destructiveHint: false,
919
+ idempotentHint: true,
920
+ openWorldHint: false,
921
+ },
922
+ }, async (input) => text(await deps.flows.validate(flowActor, input.flowId)));
923
+ server.registerTool("flow_requirement_list", {
924
+ title: "List what a flow needs",
925
+ description: "List the documents and MCP tools one flow's graph names. Documents the calling user cannot see are counted rather than named, and no claim is made about whether anyone may reach them: for tools that cannot be known in advance, because the catalog is a live query with each user's own portal token.",
926
+ inputSchema: GetFlowInput,
927
+ annotations: {
928
+ title: "List what a flow needs",
929
+ readOnlyHint: true,
930
+ destructiveHint: false,
931
+ idempotentHint: true,
932
+ openWorldHint: false,
933
+ },
934
+ }, async (input) => text(await deps.flows.listRequirements(flowActor, input.flowId)));
935
+ server.registerTool("flow_run_get", {
936
+ title: "Get flow run",
937
+ description: "Get durable run state and the next node an AI or person must handle.",
938
+ inputSchema: GetFlowRunInput,
939
+ annotations: {
940
+ title: "Get flow run",
941
+ readOnlyHint: true,
942
+ destructiveHint: false,
943
+ idempotentHint: true,
944
+ openWorldHint: false,
945
+ },
946
+ }, async (input) => text(await deps.flows.getRun(flowActor, input.runId)));
947
+ // ⚠️ Model context like every other tool result: what a run did, never what it produced. The run
948
+ // input and output are absent because a run reaches nodes and tools with the rights of
949
+ // whoever started it, and only their own runs are the calling user's to read out that way.
950
+ server.registerTool("flow_run_list", {
951
+ title: "List flow runs",
952
+ description: 'List flow runs, newest first, with status, start, duration, what triggered them, and for a failed run the step that ended it. Name a `flowId` for one flow, or leave it out for the runs of every flow you may read — which is the way to answer "did anything run at all" without knowing a flow first. Optionally only the failed ones. Runs the calling user may not see are absent, and a failure inside a called flow they may not see is named by the calling step alone.',
953
+ inputSchema: ListFlowRunsInput,
954
+ annotations: {
955
+ title: "List flow runs",
956
+ readOnlyHint: true,
957
+ destructiveHint: false,
958
+ idempotentHint: true,
959
+ openWorldHint: false,
960
+ },
961
+ }, async (input) => text(await deps.flows.listRuns(flowActor, input)));
962
+ server.registerTool("flow_run_step_list", {
963
+ title: "List flow run steps",
964
+ description: "List every completed step of one run, oldest first, with its outcome, branch, and the reason a failed step gives, plus the call chain the run belongs to.",
965
+ inputSchema: GetFlowRunInput,
966
+ annotations: {
967
+ title: "List flow run steps",
968
+ readOnlyHint: true,
969
+ destructiveHint: false,
970
+ idempotentHint: true,
971
+ openWorldHint: false,
972
+ },
973
+ }, async (input) => text(await deps.flows.listRunSteps(flowActor, input.runId)));
974
+ }
975
+ // The flow's own sharing (#530), named by the same grammar as the node tools: domain singular,
976
+ // the way narrowing left to right, the verb last.
977
+ if (permits(deps.authorization, "flows", "share")) {
978
+ server.registerTool("flow_grant_list", {
979
+ title: "List flow grants",
980
+ description: "List the grants sitting on one flow. What the folder it is filed in passes down is not listed here.",
981
+ inputSchema: ListFlowGrantsInput,
982
+ annotations: {
983
+ title: "List flow grants",
984
+ readOnlyHint: true,
985
+ destructiveHint: false,
986
+ idempotentHint: true,
987
+ openWorldHint: false,
988
+ },
989
+ }, async (input) => text(await deps.flows.listGrants(flowActor, input.flowId)));
990
+ // ⚠️ The flow half of the node tool three groups up, and it is a SECOND tool rather than a
991
+ // widened one because it is a second question (#586): the answer is assembled from `flow_grants`
992
+ // and from the node grants of the folders above, and the owners come from the flow itself as
993
+ // well as from those folders. One tool over a `resourceId` that means either would have to guess
994
+ // which table to ask, and a caller passing the wrong kind would be answered "nobody" instead of
995
+ // being told.
996
+ server.registerTool("flow_effective_access_list", {
997
+ title: "List effective flow access",
998
+ description: "List everybody who reaches one flow once inheritance is resolved: the grants on the flow AND on the folders it is filed under, plus the owners, who need no grant. flow_grant_list answers the narrower question — the rows sitting on the flow itself — and for a flow whose access comes from its folder that answer is empty while this one is not. An entry whose resourceId is a folder was granted there and can only be revoked there.",
999
+ inputSchema: ListFlowEffectiveAccessInput,
1000
+ annotations: {
1001
+ title: "List effective flow access",
1002
+ readOnlyHint: true,
1003
+ destructiveHint: false,
1004
+ idempotentHint: true,
1005
+ openWorldHint: false,
1006
+ },
1007
+ }, async (input) => text(await deps.flows.listEffectiveAccess(flowActor, input.flowId)));
1008
+ server.registerTool("flow_grant_create", {
1009
+ title: "Grant flow access",
1010
+ description: "Grant access to one flow for a Gate user, verified email, or the organization. The grant reaches this flow alone — not the folder it is filed in, and not the flows it calls; the answer names what it does not cover.",
1011
+ inputSchema: ShareFlowInput,
1012
+ annotations: {
1013
+ title: "Grant flow access",
1014
+ readOnlyHint: false,
1015
+ destructiveHint: false,
1016
+ idempotentHint: true,
1017
+ openWorldHint: false,
1018
+ },
1019
+ }, async (input) => text(await deps.flows.share(flowActor, input)));
1020
+ server.registerTool("flow_grant_revoke", {
1021
+ title: "Revoke flow grant",
1022
+ description: "Revoke one grant sitting on a flow by ID.",
1023
+ inputSchema: RevokeFlowGrantInput,
1024
+ annotations: {
1025
+ title: "Revoke flow grant",
1026
+ readOnlyHint: false,
1027
+ destructiveHint: true,
1028
+ idempotentHint: true,
1029
+ openWorldHint: false,
1030
+ },
1031
+ }, async (input) => text(await deps.flows.revokeGrant(flowActor, input)));
1032
+ }
1033
+ if (permits(deps.authorization, "flows", "create")) {
1034
+ server.registerTool("flow_create", {
1035
+ title: "Create flow",
1036
+ description: "Create governed flow metadata before adding an immutable graph version.",
1037
+ inputSchema: CreateFlowInput,
1038
+ annotations: {
1039
+ title: "Create flow",
1040
+ readOnlyHint: false,
1041
+ destructiveHint: false,
1042
+ idempotentHint: true,
1043
+ openWorldHint: false,
1044
+ },
1045
+ }, async (input) => text(await deps.flows.create(flowActor, input)));
1046
+ }
1047
+ if (permits(deps.authorization, "flows", "write")) {
1048
+ server.registerTool("flow_update", {
1049
+ title: "Rename or move flow",
1050
+ description: "Rename a flow, change its description, or move it to another folder of the shared tree. Never changes what the flow does.",
1051
+ inputSchema: UpdateFlowInput,
1052
+ annotations: {
1053
+ title: "Rename or move flow",
1054
+ readOnlyHint: false,
1055
+ destructiveHint: false,
1056
+ idempotentHint: true,
1057
+ openWorldHint: false,
1058
+ },
1059
+ }, async (input) => text(await deps.flows.update(flowActor, input)));
1060
+ server.registerTool("flow_archive", {
1061
+ title: "Archive flow",
1062
+ description: "Archive or restore one flow using optimistic concurrency. An archived flow keeps its versions but can no longer be opened, started, or called by another flow.",
1063
+ inputSchema: ArchiveFlowInput,
1064
+ annotations: {
1065
+ title: "Archive flow",
1066
+ readOnlyHint: false,
1067
+ // What it takes away is reach, not content: nothing is deleted and `archived: false` puts
1068
+ // it back. Destructive all the same, because a flow another flow calls stops resolving.
1069
+ destructiveHint: true,
1070
+ idempotentHint: true,
1071
+ openWorldHint: false,
1072
+ },
1073
+ }, async (input) => text(await deps.flows.archive(flowActor, input)));
1074
+ // ⚠️ On this surface because a way only the UI has is a way an agent works around (#436). Its
1075
+ // limits are the same as the UI's.
1076
+ server.registerTool("flow_purge", {
1077
+ title: "Delete flow for good",
1078
+ description: "Delete one ARCHIVED flow together with every version and the record of every run it had. This cannot be undone. A living flow is refused: archive it first. So is a flow another published flow still calls, and one whose runs started runs of other flows.",
1079
+ inputSchema: PurgeFlowInput,
1080
+ annotations: {
1081
+ title: "Delete flow for good",
1082
+ readOnlyHint: false,
1083
+ destructiveHint: true,
1084
+ // Not idempotent, and that is the honest answer: twice deletes once and then answers 404.
1085
+ idempotentHint: false,
1086
+ openWorldHint: false,
1087
+ },
1088
+ }, async (input) => await refusing(() => deps.flows.purge(flowActor, input)));
1089
+ server.registerTool("flow_version_create", {
1090
+ title: "Create flow version",
1091
+ description: "Append a validated immutable flow graph version with optimistic concurrency.",
1092
+ inputSchema: SaveFlowVersionInput,
1093
+ annotations: {
1094
+ title: "Create flow version",
1095
+ readOnlyHint: false,
1096
+ destructiveHint: false,
1097
+ idempotentHint: true,
1098
+ openWorldHint: false,
1099
+ },
1100
+ }, async (input) => text(await deps.flows.save(flowActor, input)));
1101
+ }
1102
+ if (permits(deps.authorization, "flows", "publish")) {
1103
+ // The same answer the screen shows the author before publishing. Every user-visible capability
1104
+ // has its MCP equivalent, and a freeze nobody could inspect from here would be one behavior
1105
+ // through two doors.
1106
+ server.registerTool("flow_publish_preview", {
1107
+ title: "Preview flow publication",
1108
+ description: "Show which version each sub-flow call will take and which of them publishing freezes.",
1109
+ inputSchema: PreviewFlowPublishInput,
1110
+ annotations: {
1111
+ title: "Preview flow publication",
1112
+ readOnlyHint: true,
1113
+ destructiveHint: false,
1114
+ idempotentHint: true,
1115
+ openWorldHint: false,
1116
+ },
1117
+ }, async (input) => text(await deps.flows.previewPublish(flowActor, input)));
1118
+ server.registerTool("flow_publish", {
1119
+ title: "Publish flow",
1120
+ description: "Publish a version after resolving its authorized node and tool references.",
1121
+ inputSchema: PublishFlowInput,
1122
+ annotations: {
1123
+ title: "Publish flow",
1124
+ readOnlyHint: false,
1125
+ destructiveHint: false,
1126
+ idempotentHint: true,
1127
+ openWorldHint: false,
1128
+ },
1129
+ }, async (input) => text(await deps.flows.publish(flowActor, input)));
1130
+ server.registerTool("flow_unpublish", {
1131
+ title: "Unpublish flow",
1132
+ description: "Withdraw a flow's published version. Versions are kept and can be republished; new runs and calls from other flows stop resolving, runs in flight finish on the version they took.",
1133
+ inputSchema: UnpublishFlowInput,
1134
+ annotations: {
1135
+ title: "Unpublish flow",
1136
+ readOnlyHint: false,
1137
+ // Nothing is deleted and republishing is one publish away — destructive all the same,
1138
+ // because a flow another flow calls stops resolving, like flow_archive.
1139
+ destructiveHint: true,
1140
+ idempotentHint: true,
1141
+ openWorldHint: false,
1142
+ },
1143
+ }, async (input) => text(await deps.flows.unpublish(flowActor, input)));
1144
+ }
1145
+ if (permits(deps.authorization, "flows", "run")) {
1146
+ server.registerTool("flow_run_start", {
1147
+ title: "Start flow run",
1148
+ description: "Start a durable run of the published graph and return its first actionable node.",
1149
+ inputSchema: StartFlowRunInput,
1150
+ annotations: {
1151
+ title: "Start flow run",
1152
+ readOnlyHint: false,
1153
+ destructiveHint: false,
1154
+ idempotentHint: true,
1155
+ openWorldHint: false,
1156
+ },
1157
+ }, async (input) => text(await deps.flows.start(flowActor, input)));
1158
+ server.registerTool("flow_run_step_complete", {
1159
+ title: "Complete flow step",
1160
+ description: "Submit a node result or semantic branch to advance a durable run without persisting credentials.",
1161
+ inputSchema: CompleteFlowRunStepInput,
1162
+ annotations: {
1163
+ title: "Complete flow step",
1164
+ readOnlyHint: false,
1165
+ destructiveHint: true,
1166
+ idempotentHint: true,
1167
+ openWorldHint: true,
1168
+ },
1169
+ }, async (input) => text(await deps.flows.completeStep(flowActor, input)));
1170
+ server.registerTool("flow_run_cancel", {
1171
+ title: "Cancel flow run",
1172
+ description: "Cancel a running flow run without recording a step failure. Running child runs it called are cancelled with it; a run that is already terminal is refused unchanged.",
1173
+ inputSchema: CancelFlowRunInput,
1174
+ annotations: {
1175
+ title: "Cancel flow run",
1176
+ readOnlyHint: false,
1177
+ // Nothing is deleted, but work in flight is ended for the run and its children — a
1178
+ // client deciding whether to ask first has to be told that.
1179
+ destructiveHint: true,
1180
+ idempotentHint: true,
1181
+ openWorldHint: false,
1182
+ },
1183
+ }, async (input) => text(await deps.flows.cancelRun(flowActor, input)));
1184
+ }
1185
+ if (permits(deps.authorization, "tools", "read")) {
1186
+ server.registerTool("tool_list", {
1187
+ title: "List tools",
1188
+ description: "List the MCP tools the calling user can reach through the portal, live rather than cached.",
1189
+ inputSchema: EmptyInput,
1190
+ annotations: {
1191
+ title: "List tools",
1192
+ readOnlyHint: true,
1193
+ destructiveHint: false,
1194
+ idempotentHint: true,
1195
+ openWorldHint: false,
1196
+ },
1197
+ }, async () => text(await deps.tools.catalog(toolActor)));
1198
+ // The servers behind the same live list (D30), as the portal itself names them.
1199
+ server.registerTool("tool_server_list", {
1200
+ title: "List tool servers",
1201
+ description: "List the MCP servers the calling user reaches through the portal, as the portal itself names them. The handle is the prefix on every tool name from that server, which is how tool_execute is routed.",
1202
+ inputSchema: EmptyInput,
1203
+ annotations: {
1204
+ title: "List tool servers",
1205
+ readOnlyHint: true,
1206
+ destructiveHint: false,
1207
+ idempotentHint: true,
1208
+ openWorldHint: false,
1209
+ },
1210
+ }, async () => text(await deps.tools.servers(toolActor)));
1211
+ }
1212
+ if (permits(deps.authorization, "tools", "test")) {
1213
+ server.registerTool("tool_test", {
1214
+ title: "Test tool",
1215
+ description: "Validate input and execute one explicitly read-only cached MCP tool with the caller's Gate connection.",
1216
+ inputSchema: TestToolInput,
1217
+ annotations: {
1218
+ title: "Test tool",
1219
+ readOnlyHint: true,
1220
+ destructiveHint: false,
1221
+ idempotentHint: false,
1222
+ openWorldHint: true,
1223
+ },
1224
+ }, async (input) => text(await deps.tools.test(toolActor, input)));
1225
+ }
1226
+ if (permits(deps.authorization, "tools", "execute")) {
1227
+ server.registerTool("tool_execute", {
1228
+ title: "Execute tool",
1229
+ description: "Validate and execute a discovered MCP tool with the caller's just-in-time Gate connection.",
1230
+ inputSchema: ExecuteToolInput,
1231
+ annotations: {
1232
+ title: "Execute tool",
1233
+ readOnlyHint: false,
1234
+ destructiveHint: true,
1235
+ idempotentHint: false,
1236
+ openWorldHint: true,
1237
+ },
1238
+ }, async (input) => text(await deps.tools.execute(toolActor, input)));
1239
+ }
1240
+ /**
1241
+ * The slash-command surface (#775). A prompt is the one thing on this server the HUMAN chooses:
1242
+ * a tool is called when the model thinks of it, a prompt when somebody types `/name`.
1243
+ *
1244
+ * ⚠️ **Registered through the low-level handlers rather than `registerPrompt`, and the cost is
1245
+ * the reason.** `registerPrompt` takes one name at a time, so a catalogue that lives in D1 would
1246
+ * have to be read and registered while the server is being built — on EVERY request, including
1247
+ * every `tools/call` that will never look at a prompt. These two handlers run the queries only
1248
+ * when a client actually asks for prompts.
1249
+ *
1250
+ * ⚠️ **The capability is declared here because nothing else declares it.** Without
1251
+ * `registerCapabilities` the SDK would answer `prompts/list` while telling clients during
1252
+ * initialization that this server has no prompts, and a client that believes the handshake never
1253
+ * asks. `listChanged: false` is the truthful value: the catalogue changes when somebody edits a
1254
+ * document, and this server sends no notification about it.
1255
+ *
1256
+ * ⚠️ **Offered on EITHER capability, decided per call** — the shape `audit_list` uses one screen
1257
+ * up, for the same reason. Holding one of the two is what makes the surface worth offering at
1258
+ * all; which half of the catalogue it then answers is `listOffered`'s question, and it asks it
1259
+ * per row against the resource ACL underneath.
1260
+ */
1261
+ if (permits(deps.authorization, "nodes", "read") ||
1262
+ permits(deps.authorization, "flows", "read")) {
1263
+ const promptActor = {
1264
+ ...actor,
1265
+ canReadNodes: permits(deps.authorization, "nodes", "read"),
1266
+ canReadFlows: permits(deps.authorization, "flows", "read"),
1267
+ };
1268
+ server.server.registerCapabilities({ prompts: { listChanged: false } });
1269
+ server.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
1270
+ prompts: (await deps.prompts.list(promptActor)).map((entry) => ({
1271
+ name: entry.name,
1272
+ title: entry.title,
1273
+ // ⚠️ No `arguments`, and that is a decision rather than a gap (#775). A prompt without
1274
+ // arguments is completely usable — it is an instruction somebody wrote down — and the
1275
+ // obvious source of an argument schema, a flow's requirements, is a round of its own.
1276
+ // Declaring an empty `arguments` array would tell a client the question was asked and
1277
+ // answered "none", which is a different claim.
1278
+ ...(entry.description === null ? {} : { description: entry.description }),
1279
+ })),
1280
+ }));
1281
+ server.server.setRequestHandler(GetPromptRequestSchema, async (promptRequest) => {
1282
+ const { entry, text } = await deps.prompts.get(promptActor, promptRequest.params.name);
1283
+ return {
1284
+ ...(entry.description === null ? {} : { description: entry.description }),
1285
+ messages: [{ role: "user", content: { type: "text", text } }],
1286
+ };
1287
+ });
1288
+ }
1289
+ const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
1290
+ await server.connect(transport);
1291
+ return await transport.handleRequest(request);
1292
+ }