@portel/photon 1.4.1 → 1.6.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 (395) hide show
  1. package/README.md +326 -1177
  2. package/dist/auto-ui/beam.d.ts +14 -0
  3. package/dist/auto-ui/beam.d.ts.map +1 -0
  4. package/dist/auto-ui/beam.js +3057 -0
  5. package/dist/auto-ui/beam.js.map +1 -0
  6. package/dist/auto-ui/bridge/index.d.ts +37 -0
  7. package/dist/auto-ui/bridge/index.d.ts.map +1 -0
  8. package/dist/auto-ui/bridge/index.js +555 -0
  9. package/dist/auto-ui/bridge/index.js.map +1 -0
  10. package/dist/auto-ui/bridge/openai-shim.d.ts +20 -0
  11. package/dist/auto-ui/bridge/openai-shim.d.ts.map +1 -0
  12. package/dist/auto-ui/bridge/openai-shim.js +231 -0
  13. package/dist/auto-ui/bridge/openai-shim.js.map +1 -0
  14. package/dist/auto-ui/bridge/photon-app.d.ts +162 -0
  15. package/dist/auto-ui/bridge/photon-app.d.ts.map +1 -0
  16. package/dist/auto-ui/bridge/photon-app.js +460 -0
  17. package/dist/auto-ui/bridge/photon-app.js.map +1 -0
  18. package/dist/auto-ui/bridge/types.d.ts +128 -0
  19. package/dist/auto-ui/bridge/types.d.ts.map +1 -0
  20. package/dist/auto-ui/bridge/types.js +7 -0
  21. package/dist/auto-ui/bridge/types.js.map +1 -0
  22. package/dist/auto-ui/components/card.d.ts +13 -0
  23. package/dist/auto-ui/components/card.d.ts.map +1 -0
  24. package/dist/auto-ui/components/card.js +64 -0
  25. package/dist/auto-ui/components/card.js.map +1 -0
  26. package/dist/auto-ui/components/form.d.ts +15 -0
  27. package/dist/auto-ui/components/form.d.ts.map +1 -0
  28. package/dist/auto-ui/components/form.js +72 -0
  29. package/dist/auto-ui/components/form.js.map +1 -0
  30. package/dist/auto-ui/components/list.d.ts +13 -0
  31. package/dist/auto-ui/components/list.d.ts.map +1 -0
  32. package/dist/auto-ui/components/list.js +58 -0
  33. package/dist/auto-ui/components/list.js.map +1 -0
  34. package/dist/auto-ui/components/progress.d.ts +18 -0
  35. package/dist/auto-ui/components/progress.d.ts.map +1 -0
  36. package/dist/auto-ui/components/progress.js +125 -0
  37. package/dist/auto-ui/components/progress.js.map +1 -0
  38. package/dist/auto-ui/components/table.d.ts +13 -0
  39. package/dist/auto-ui/components/table.d.ts.map +1 -0
  40. package/dist/auto-ui/components/table.js +82 -0
  41. package/dist/auto-ui/components/table.js.map +1 -0
  42. package/dist/auto-ui/components/tree.d.ts +13 -0
  43. package/dist/auto-ui/components/tree.d.ts.map +1 -0
  44. package/dist/auto-ui/components/tree.js +61 -0
  45. package/dist/auto-ui/components/tree.js.map +1 -0
  46. package/dist/auto-ui/daemon-tools.d.ts +45 -0
  47. package/dist/auto-ui/daemon-tools.d.ts.map +1 -0
  48. package/dist/auto-ui/daemon-tools.js +580 -0
  49. package/dist/auto-ui/daemon-tools.js.map +1 -0
  50. package/dist/auto-ui/design-system/index.d.ts +21 -0
  51. package/dist/auto-ui/design-system/index.d.ts.map +1 -0
  52. package/dist/auto-ui/design-system/index.js +27 -0
  53. package/dist/auto-ui/design-system/index.js.map +1 -0
  54. package/dist/auto-ui/design-system/tokens.d.ts +9 -0
  55. package/dist/auto-ui/design-system/tokens.d.ts.map +1 -0
  56. package/dist/auto-ui/design-system/tokens.js +27 -0
  57. package/dist/auto-ui/design-system/tokens.js.map +1 -0
  58. package/dist/auto-ui/design-system/transaction-ui.d.ts +70 -0
  59. package/dist/auto-ui/design-system/transaction-ui.d.ts.map +1 -0
  60. package/dist/auto-ui/design-system/transaction-ui.js +982 -0
  61. package/dist/auto-ui/design-system/transaction-ui.js.map +1 -0
  62. package/dist/auto-ui/frontend/index.html +84 -0
  63. package/dist/auto-ui/index.d.ts +23 -0
  64. package/dist/auto-ui/index.d.ts.map +1 -0
  65. package/dist/auto-ui/index.js +28 -0
  66. package/dist/auto-ui/index.js.map +1 -0
  67. package/dist/auto-ui/openapi-generator.d.ts +71 -0
  68. package/dist/auto-ui/openapi-generator.d.ts.map +1 -0
  69. package/dist/auto-ui/openapi-generator.js +223 -0
  70. package/dist/auto-ui/openapi-generator.js.map +1 -0
  71. package/dist/auto-ui/photon-bridge.d.ts +159 -0
  72. package/dist/auto-ui/photon-bridge.d.ts.map +1 -0
  73. package/dist/auto-ui/photon-bridge.js +262 -0
  74. package/dist/auto-ui/photon-bridge.js.map +1 -0
  75. package/dist/auto-ui/photon-host.d.ts +113 -0
  76. package/dist/auto-ui/photon-host.d.ts.map +1 -0
  77. package/dist/auto-ui/photon-host.js +284 -0
  78. package/dist/auto-ui/photon-host.js.map +1 -0
  79. package/dist/auto-ui/platform-compat.d.ts +71 -0
  80. package/dist/auto-ui/platform-compat.d.ts.map +1 -0
  81. package/dist/auto-ui/platform-compat.js +628 -0
  82. package/dist/auto-ui/platform-compat.js.map +1 -0
  83. package/dist/auto-ui/playground-html.d.ts +15 -0
  84. package/dist/auto-ui/playground-html.d.ts.map +1 -0
  85. package/dist/auto-ui/playground-html.js +1113 -0
  86. package/dist/auto-ui/playground-html.js.map +1 -0
  87. package/dist/auto-ui/playground-server.d.ts +7 -0
  88. package/dist/auto-ui/playground-server.d.ts.map +1 -0
  89. package/dist/auto-ui/playground-server.js +840 -0
  90. package/dist/auto-ui/playground-server.js.map +1 -0
  91. package/dist/auto-ui/registry.d.ts +13 -0
  92. package/dist/auto-ui/registry.d.ts.map +1 -0
  93. package/dist/auto-ui/registry.js +62 -0
  94. package/dist/auto-ui/registry.js.map +1 -0
  95. package/dist/auto-ui/renderer.d.ts +14 -0
  96. package/dist/auto-ui/renderer.d.ts.map +1 -0
  97. package/dist/auto-ui/renderer.js +88 -0
  98. package/dist/auto-ui/renderer.js.map +1 -0
  99. package/dist/auto-ui/rendering/components.d.ts +29 -0
  100. package/dist/auto-ui/rendering/components.d.ts.map +1 -0
  101. package/dist/auto-ui/rendering/components.js +773 -0
  102. package/dist/auto-ui/rendering/components.js.map +1 -0
  103. package/dist/auto-ui/rendering/field-analyzer.d.ts +48 -0
  104. package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -0
  105. package/dist/auto-ui/rendering/field-analyzer.js +270 -0
  106. package/dist/auto-ui/rendering/field-analyzer.js.map +1 -0
  107. package/dist/auto-ui/rendering/field-renderers.d.ts +64 -0
  108. package/dist/auto-ui/rendering/field-renderers.d.ts.map +1 -0
  109. package/dist/auto-ui/rendering/field-renderers.js +317 -0
  110. package/dist/auto-ui/rendering/field-renderers.js.map +1 -0
  111. package/dist/auto-ui/rendering/index.d.ts +28 -0
  112. package/dist/auto-ui/rendering/index.d.ts.map +1 -0
  113. package/dist/auto-ui/rendering/index.js +60 -0
  114. package/dist/auto-ui/rendering/index.js.map +1 -0
  115. package/dist/auto-ui/rendering/layout-selector.d.ts +48 -0
  116. package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -0
  117. package/dist/auto-ui/rendering/layout-selector.js +352 -0
  118. package/dist/auto-ui/rendering/layout-selector.js.map +1 -0
  119. package/dist/auto-ui/rendering/template-engine.d.ts +41 -0
  120. package/dist/auto-ui/rendering/template-engine.d.ts.map +1 -0
  121. package/dist/auto-ui/rendering/template-engine.js +238 -0
  122. package/dist/auto-ui/rendering/template-engine.js.map +1 -0
  123. package/dist/auto-ui/streamable-http-transport.d.ts +103 -0
  124. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -0
  125. package/dist/auto-ui/streamable-http-transport.js +1875 -0
  126. package/dist/auto-ui/streamable-http-transport.js.map +1 -0
  127. package/dist/auto-ui/types.d.ts +384 -0
  128. package/dist/auto-ui/types.d.ts.map +1 -0
  129. package/dist/auto-ui/types.js +92 -0
  130. package/dist/auto-ui/types.js.map +1 -0
  131. package/dist/beam.bundle.js +63137 -0
  132. package/dist/beam.bundle.js.map +7 -0
  133. package/dist/claude-code-plugin.d.ts.map +1 -1
  134. package/dist/claude-code-plugin.js +30 -30
  135. package/dist/claude-code-plugin.js.map +1 -1
  136. package/dist/cli/commands/info.d.ts +11 -0
  137. package/dist/cli/commands/info.d.ts.map +1 -0
  138. package/dist/cli/commands/info.js +313 -0
  139. package/dist/cli/commands/info.js.map +1 -0
  140. package/dist/cli/commands/marketplace.d.ts +11 -0
  141. package/dist/cli/commands/marketplace.d.ts.map +1 -0
  142. package/dist/cli/commands/marketplace.js +198 -0
  143. package/dist/cli/commands/marketplace.js.map +1 -0
  144. package/dist/cli/commands/package-app.d.ts +9 -0
  145. package/dist/cli/commands/package-app.d.ts.map +1 -0
  146. package/dist/cli/commands/package-app.js +191 -0
  147. package/dist/cli/commands/package-app.js.map +1 -0
  148. package/dist/cli/commands/package.d.ts +11 -0
  149. package/dist/cli/commands/package.d.ts.map +1 -0
  150. package/dist/cli/commands/package.js +573 -0
  151. package/dist/cli/commands/package.js.map +1 -0
  152. package/dist/cli-alias.d.ts.map +1 -1
  153. package/dist/cli-alias.js +30 -28
  154. package/dist/cli-alias.js.map +1 -1
  155. package/dist/cli-formatter.d.ts +8 -24
  156. package/dist/cli-formatter.d.ts.map +1 -1
  157. package/dist/cli-formatter.js +8 -325
  158. package/dist/cli-formatter.js.map +1 -1
  159. package/dist/cli.d.ts +15 -1
  160. package/dist/cli.d.ts.map +1 -1
  161. package/dist/cli.js +1166 -1131
  162. package/dist/cli.js.map +1 -1
  163. package/dist/daemon/client.d.ts +84 -3
  164. package/dist/daemon/client.d.ts.map +1 -1
  165. package/dist/daemon/client.js +561 -11
  166. package/dist/daemon/client.js.map +1 -1
  167. package/dist/daemon/manager.d.ts +51 -12
  168. package/dist/daemon/manager.d.ts.map +1 -1
  169. package/dist/daemon/manager.js +122 -61
  170. package/dist/daemon/manager.js.map +1 -1
  171. package/dist/daemon/protocol.d.ts +62 -6
  172. package/dist/daemon/protocol.d.ts.map +1 -1
  173. package/dist/daemon/protocol.js +76 -1
  174. package/dist/daemon/protocol.js.map +1 -1
  175. package/dist/daemon/server.d.ts +6 -6
  176. package/dist/daemon/server.js +743 -133
  177. package/dist/daemon/server.js.map +1 -1
  178. package/dist/daemon/session-manager.d.ts +8 -1
  179. package/dist/daemon/session-manager.d.ts.map +1 -1
  180. package/dist/daemon/session-manager.js +32 -9
  181. package/dist/daemon/session-manager.js.map +1 -1
  182. package/dist/deploy/cloudflare.d.ts +12 -0
  183. package/dist/deploy/cloudflare.d.ts.map +1 -0
  184. package/dist/deploy/cloudflare.js +216 -0
  185. package/dist/deploy/cloudflare.js.map +1 -0
  186. package/dist/index.d.ts +1 -0
  187. package/dist/index.d.ts.map +1 -1
  188. package/dist/index.js +3 -0
  189. package/dist/index.js.map +1 -1
  190. package/dist/loader.d.ts +191 -21
  191. package/dist/loader.d.ts.map +1 -1
  192. package/dist/loader.js +1186 -319
  193. package/dist/loader.js.map +1 -1
  194. package/dist/markdown-utils.d.ts +8 -0
  195. package/dist/markdown-utils.d.ts.map +1 -0
  196. package/dist/markdown-utils.js +63 -0
  197. package/dist/markdown-utils.js.map +1 -0
  198. package/dist/marketplace-manager.d.ts +10 -0
  199. package/dist/marketplace-manager.d.ts.map +1 -1
  200. package/dist/marketplace-manager.js +112 -28
  201. package/dist/marketplace-manager.js.map +1 -1
  202. package/dist/mcp-client.d.ts +9 -0
  203. package/dist/mcp-client.d.ts.map +1 -0
  204. package/dist/mcp-client.js +11 -0
  205. package/dist/mcp-client.js.map +1 -0
  206. package/dist/mcp-elicitation.d.ts +32 -0
  207. package/dist/mcp-elicitation.d.ts.map +1 -0
  208. package/dist/mcp-elicitation.js +26 -0
  209. package/dist/mcp-elicitation.js.map +1 -0
  210. package/dist/path-resolver.d.ts +9 -12
  211. package/dist/path-resolver.d.ts.map +1 -1
  212. package/dist/path-resolver.js +13 -43
  213. package/dist/path-resolver.js.map +1 -1
  214. package/dist/photon-cli-runner.d.ts.map +1 -1
  215. package/dist/photon-cli-runner.js +204 -77
  216. package/dist/photon-cli-runner.js.map +1 -1
  217. package/dist/photon-doc-extractor.d.ts +89 -0
  218. package/dist/photon-doc-extractor.d.ts.map +1 -1
  219. package/dist/photon-doc-extractor.js +560 -32
  220. package/dist/photon-doc-extractor.js.map +1 -1
  221. package/dist/photons/maker.photon.d.ts +182 -0
  222. package/dist/photons/maker.photon.d.ts.map +1 -0
  223. package/dist/photons/maker.photon.js +504 -0
  224. package/dist/photons/maker.photon.js.map +1 -0
  225. package/dist/photons/maker.photon.ts +626 -0
  226. package/dist/photons/marketplace.photon.d.ts +110 -0
  227. package/dist/photons/marketplace.photon.d.ts.map +1 -0
  228. package/dist/photons/marketplace.photon.js +260 -0
  229. package/dist/photons/marketplace.photon.js.map +1 -0
  230. package/dist/photons/marketplace.photon.ts +378 -0
  231. package/dist/photons/tunnel.photon.d.ts +80 -0
  232. package/dist/photons/tunnel.photon.d.ts.map +1 -0
  233. package/dist/photons/tunnel.photon.js +269 -0
  234. package/dist/photons/tunnel.photon.js.map +1 -0
  235. package/dist/photons/tunnel.photon.ts +345 -0
  236. package/dist/security-scanner.d.ts.map +1 -1
  237. package/dist/security-scanner.js +18 -15
  238. package/dist/security-scanner.js.map +1 -1
  239. package/dist/serv/auth/jwt.d.ts +89 -0
  240. package/dist/serv/auth/jwt.d.ts.map +1 -0
  241. package/dist/serv/auth/jwt.js +239 -0
  242. package/dist/serv/auth/jwt.js.map +1 -0
  243. package/dist/serv/auth/oauth.d.ts +117 -0
  244. package/dist/serv/auth/oauth.d.ts.map +1 -0
  245. package/dist/serv/auth/oauth.js +395 -0
  246. package/dist/serv/auth/oauth.js.map +1 -0
  247. package/dist/serv/auth/well-known.d.ts +60 -0
  248. package/dist/serv/auth/well-known.d.ts.map +1 -0
  249. package/dist/serv/auth/well-known.js +154 -0
  250. package/dist/serv/auth/well-known.js.map +1 -0
  251. package/dist/serv/db/d1-client.d.ts +65 -0
  252. package/dist/serv/db/d1-client.d.ts.map +1 -0
  253. package/dist/serv/db/d1-client.js +137 -0
  254. package/dist/serv/db/d1-client.js.map +1 -0
  255. package/dist/serv/db/d1-stores.d.ts +62 -0
  256. package/dist/serv/db/d1-stores.d.ts.map +1 -0
  257. package/dist/serv/db/d1-stores.js +307 -0
  258. package/dist/serv/db/d1-stores.js.map +1 -0
  259. package/dist/serv/index.d.ts +114 -0
  260. package/dist/serv/index.d.ts.map +1 -0
  261. package/dist/serv/index.js +172 -0
  262. package/dist/serv/index.js.map +1 -0
  263. package/dist/serv/local.d.ts +118 -0
  264. package/dist/serv/local.d.ts.map +1 -0
  265. package/dist/serv/local.js +392 -0
  266. package/dist/serv/local.js.map +1 -0
  267. package/dist/serv/middleware/auth.d.ts +66 -0
  268. package/dist/serv/middleware/auth.d.ts.map +1 -0
  269. package/dist/serv/middleware/auth.js +178 -0
  270. package/dist/serv/middleware/auth.js.map +1 -0
  271. package/dist/serv/middleware/tenant.d.ts +94 -0
  272. package/dist/serv/middleware/tenant.d.ts.map +1 -0
  273. package/dist/serv/middleware/tenant.js +152 -0
  274. package/dist/serv/middleware/tenant.js.map +1 -0
  275. package/dist/serv/runtime/executor.d.ts +76 -0
  276. package/dist/serv/runtime/executor.d.ts.map +1 -0
  277. package/dist/serv/runtime/executor.js +105 -0
  278. package/dist/serv/runtime/executor.js.map +1 -0
  279. package/dist/serv/runtime/index.d.ts +8 -0
  280. package/dist/serv/runtime/index.d.ts.map +1 -0
  281. package/dist/serv/runtime/index.js +10 -0
  282. package/dist/serv/runtime/index.js.map +1 -0
  283. package/dist/serv/runtime/oauth-context.d.ts +121 -0
  284. package/dist/serv/runtime/oauth-context.d.ts.map +1 -0
  285. package/dist/serv/runtime/oauth-context.js +153 -0
  286. package/dist/serv/runtime/oauth-context.js.map +1 -0
  287. package/dist/serv/session/kv-store.d.ts +54 -0
  288. package/dist/serv/session/kv-store.d.ts.map +1 -0
  289. package/dist/serv/session/kv-store.js +149 -0
  290. package/dist/serv/session/kv-store.js.map +1 -0
  291. package/dist/serv/session/store.d.ts +113 -0
  292. package/dist/serv/session/store.d.ts.map +1 -0
  293. package/dist/serv/session/store.js +284 -0
  294. package/dist/serv/session/store.js.map +1 -0
  295. package/dist/serv/types/index.d.ts +147 -0
  296. package/dist/serv/types/index.d.ts.map +1 -0
  297. package/dist/serv/types/index.js +8 -0
  298. package/dist/serv/types/index.js.map +1 -0
  299. package/dist/serv/vault/token-vault.d.ts +102 -0
  300. package/dist/serv/vault/token-vault.d.ts.map +1 -0
  301. package/dist/serv/vault/token-vault.js +177 -0
  302. package/dist/serv/vault/token-vault.js.map +1 -0
  303. package/dist/server.d.ts +184 -0
  304. package/dist/server.d.ts.map +1 -1
  305. package/dist/server.js +1995 -86
  306. package/dist/server.js.map +1 -1
  307. package/dist/shared/cli-sections.d.ts +6 -0
  308. package/dist/shared/cli-sections.d.ts.map +1 -0
  309. package/dist/shared/cli-sections.js +16 -0
  310. package/dist/shared/cli-sections.js.map +1 -0
  311. package/dist/shared/cli-utils.d.ts +81 -0
  312. package/dist/shared/cli-utils.d.ts.map +1 -0
  313. package/dist/shared/cli-utils.js +174 -0
  314. package/dist/shared/cli-utils.js.map +1 -0
  315. package/dist/shared/config-docs.d.ts +6 -0
  316. package/dist/shared/config-docs.d.ts.map +1 -0
  317. package/dist/shared/config-docs.js +6 -0
  318. package/dist/shared/config-docs.js.map +1 -0
  319. package/dist/shared/error-handler.d.ts +128 -0
  320. package/dist/shared/error-handler.d.ts.map +1 -0
  321. package/dist/shared/error-handler.js +342 -0
  322. package/dist/shared/error-handler.js.map +1 -0
  323. package/dist/shared/logger.d.ts +42 -0
  324. package/dist/shared/logger.d.ts.map +1 -0
  325. package/dist/shared/logger.js +123 -0
  326. package/dist/shared/logger.js.map +1 -0
  327. package/dist/shared/performance.d.ts +65 -0
  328. package/dist/shared/performance.d.ts.map +1 -0
  329. package/dist/shared/performance.js +136 -0
  330. package/dist/shared/performance.js.map +1 -0
  331. package/dist/shared/task-runner.d.ts +2 -0
  332. package/dist/shared/task-runner.d.ts.map +1 -0
  333. package/dist/shared/task-runner.js +16 -0
  334. package/dist/shared/task-runner.js.map +1 -0
  335. package/dist/shared/validation.d.ts +6 -0
  336. package/dist/shared/validation.d.ts.map +1 -0
  337. package/dist/shared/validation.js +6 -0
  338. package/dist/shared/validation.js.map +1 -0
  339. package/dist/shared-utils.d.ts +63 -0
  340. package/dist/shared-utils.d.ts.map +1 -0
  341. package/dist/shared-utils.js +123 -0
  342. package/dist/shared-utils.js.map +1 -0
  343. package/dist/template-manager.d.ts +23 -2
  344. package/dist/template-manager.d.ts.map +1 -1
  345. package/dist/template-manager.js +176 -87
  346. package/dist/template-manager.js.map +1 -1
  347. package/dist/test-client.d.ts.map +1 -1
  348. package/dist/test-client.js +10 -8
  349. package/dist/test-client.js.map +1 -1
  350. package/dist/test-runner.d.ts +52 -0
  351. package/dist/test-runner.d.ts.map +1 -0
  352. package/dist/test-runner.js +785 -0
  353. package/dist/test-runner.js.map +1 -0
  354. package/dist/testing.d.ts +103 -0
  355. package/dist/testing.d.ts.map +1 -0
  356. package/dist/testing.js +163 -0
  357. package/dist/testing.js.map +1 -0
  358. package/dist/version-checker.d.ts.map +1 -1
  359. package/dist/version-checker.js +2 -2
  360. package/dist/version-checker.js.map +1 -1
  361. package/dist/version.d.ts +10 -0
  362. package/dist/version.d.ts.map +1 -0
  363. package/dist/version.js +21 -0
  364. package/dist/version.js.map +1 -0
  365. package/dist/watcher.d.ts +6 -3
  366. package/dist/watcher.d.ts.map +1 -1
  367. package/dist/watcher.js +49 -10
  368. package/dist/watcher.js.map +1 -1
  369. package/package.json +57 -7
  370. package/templates/cloudflare/worker.ts.template +381 -0
  371. package/templates/cloudflare/wrangler.toml.template +9 -0
  372. package/dist/base.d.ts +0 -58
  373. package/dist/base.d.ts.map +0 -1
  374. package/dist/base.js +0 -92
  375. package/dist/base.js.map +0 -1
  376. package/dist/dependency-manager.d.ts +0 -49
  377. package/dist/dependency-manager.d.ts.map +0 -1
  378. package/dist/dependency-manager.js +0 -165
  379. package/dist/dependency-manager.js.map +0 -1
  380. package/dist/registry-manager.d.ts +0 -76
  381. package/dist/registry-manager.d.ts.map +0 -1
  382. package/dist/registry-manager.js +0 -220
  383. package/dist/registry-manager.js.map +0 -1
  384. package/dist/schema-extractor.d.ts +0 -110
  385. package/dist/schema-extractor.d.ts.map +0 -1
  386. package/dist/schema-extractor.js +0 -727
  387. package/dist/schema-extractor.js.map +0 -1
  388. package/dist/test-marketplace-sources.d.ts +0 -5
  389. package/dist/test-marketplace-sources.d.ts.map +0 -1
  390. package/dist/test-marketplace-sources.js +0 -53
  391. package/dist/test-marketplace-sources.js.map +0 -1
  392. package/dist/types.d.ts +0 -109
  393. package/dist/types.d.ts.map +0 -1
  394. package/dist/types.js +0 -12
  395. package/dist/types.js.map +0 -1
@@ -0,0 +1,1875 @@
1
+ /**
2
+ * Streamable HTTP Transport for MCP
3
+ *
4
+ * Implements the MCP Streamable HTTP transport specification (2025-03-26).
5
+ * This allows standard MCP clients (like Claude Desktop) to connect to Beam.
6
+ *
7
+ * Endpoint: /mcp
8
+ * - POST: Client sends JSON-RPC requests, server responds with JSON or SSE
9
+ * - GET: Opens SSE stream for server-initiated messages
10
+ *
11
+ * Configuration Schema (SEP-1596 inspired):
12
+ * - Returns configurationSchema in initialize response
13
+ * - Uses JSON Schema for rich UI generation (dropdowns, file pickers, etc.)
14
+ * - beam/configure tool for submitting configuration
15
+ * - beam/browse tool for server filesystem browsing
16
+ *
17
+ * @see https://modelcontextprotocol.io/specification/2025-03-26/basic/transports
18
+ */
19
+ import { randomUUID } from 'crypto';
20
+ import { readdir, stat, readFile, writeFile } from 'fs/promises';
21
+ import { join, dirname } from 'path';
22
+ import { homedir } from 'os';
23
+ import { PHOTON_VERSION } from '../version.js';
24
+ import { buildToolMetadataExtensions } from './types.js';
25
+ // ════════════════════════════════════════════════════════════════════════════════
26
+ // SESSION MANAGEMENT
27
+ // ════════════════════════════════════════════════════════════════════════════════
28
+ const sessions = new Map();
29
+ const pendingElicitations = new Map();
30
+ // Clean up old sessions periodically (30 min timeout)
31
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
32
+ setInterval(() => {
33
+ const now = Date.now();
34
+ for (const [id, session] of sessions) {
35
+ if (now - session.lastActivity.getTime() > SESSION_TIMEOUT_MS) {
36
+ sessions.delete(id);
37
+ }
38
+ }
39
+ }, 60 * 1000);
40
+ function getOrCreateSession(sessionId) {
41
+ if (sessionId && sessions.has(sessionId)) {
42
+ const session = sessions.get(sessionId);
43
+ session.lastActivity = new Date();
44
+ return session;
45
+ }
46
+ const newSession = {
47
+ id: randomUUID(),
48
+ initialized: false,
49
+ createdAt: new Date(),
50
+ lastActivity: new Date(),
51
+ };
52
+ sessions.set(newSession.id, newSession);
53
+ return newSession;
54
+ }
55
+ // ════════════════════════════════════════════════════════════════════════════════
56
+ // CONFIGURATION SCHEMA GENERATION
57
+ // ════════════════════════════════════════════════════════════════════════════════
58
+ /**
59
+ * Convert ConfigParam to JSON Schema property
60
+ */
61
+ function configParamToJsonSchema(param) {
62
+ const schema = {
63
+ description: `Environment variable: ${param.envVar}`,
64
+ 'x-env-var': param.envVar,
65
+ };
66
+ // Map TypeScript types to JSON Schema types
67
+ switch (param.type.toLowerCase()) {
68
+ case 'number':
69
+ schema.type = 'number';
70
+ break;
71
+ case 'boolean':
72
+ schema.type = 'boolean';
73
+ break;
74
+ case 'string':
75
+ default:
76
+ schema.type = 'string';
77
+ // Check for common sensitive parameter names - use OpenAPI standard
78
+ if (/password|secret|token|key|credential/i.test(param.name)) {
79
+ schema.format = 'password';
80
+ schema.writeOnly = true;
81
+ }
82
+ // Check for path-like parameter names
83
+ else if (/path|file|dir|directory|folder/i.test(param.name)) {
84
+ schema.format = 'path';
85
+ }
86
+ break;
87
+ }
88
+ // Add default value if present
89
+ if (param.hasDefault && param.defaultValue !== undefined) {
90
+ schema.default = param.defaultValue;
91
+ }
92
+ return schema;
93
+ }
94
+ /**
95
+ * Generate configurationSchema for all photons with constructor params
96
+ * Uses JSON Schema format for rich UI generation
97
+ * Includes both unconfigured and configured photons (for reconfiguration)
98
+ */
99
+ function generateConfigurationSchema(photons) {
100
+ const schema = {};
101
+ for (const photon of photons) {
102
+ const params = photon.requiredParams;
103
+ if (!params || params.length === 0)
104
+ continue;
105
+ const properties = {};
106
+ const required = [];
107
+ for (const param of params) {
108
+ properties[param.name] = configParamToJsonSchema(param);
109
+ // Mark as required if not optional and no default
110
+ if (!param.isOptional && !param.hasDefault) {
111
+ required.push(param.name);
112
+ }
113
+ }
114
+ schema[photon.name] = {
115
+ type: 'object',
116
+ properties,
117
+ required: required.length > 0 ? required : undefined,
118
+ 'x-error-message': !photon.configured
119
+ ? photon.errorMessage
120
+ : undefined,
121
+ 'x-internal': photon.internal,
122
+ 'x-configured': photon.configured || undefined,
123
+ };
124
+ }
125
+ return schema;
126
+ }
127
+ const handlers = {
128
+ // ─────────────────────────────────────────────────────────────────────────────
129
+ // Lifecycle
130
+ // ─────────────────────────────────────────────────────────────────────────────
131
+ initialize: async (req, session, ctx) => {
132
+ session.initialized = true;
133
+ // Capture client info and detect Beam clients
134
+ const clientInfo = req.params?.clientInfo;
135
+ if (clientInfo) {
136
+ session.clientInfo = clientInfo;
137
+ session.isBeam = clientInfo.name === 'beam';
138
+ }
139
+ // Generate configuration schema for unconfigured photons
140
+ const configurationSchema = generateConfigurationSchema(ctx.photons);
141
+ return {
142
+ jsonrpc: '2.0',
143
+ id: req.id,
144
+ result: {
145
+ protocolVersion: '2025-03-26',
146
+ serverInfo: {
147
+ name: 'beam-mcp',
148
+ version: PHOTON_VERSION,
149
+ },
150
+ capabilities: {
151
+ tools: { listChanged: true },
152
+ resources: { listChanged: true },
153
+ },
154
+ // SEP-1596 inspired: configuration schema for unconfigured photons
155
+ // Uses JSON Schema for rich UI generation
156
+ configurationSchema: Object.keys(configurationSchema).length > 0 ? configurationSchema : undefined,
157
+ },
158
+ };
159
+ },
160
+ 'notifications/initialized': async (req, session) => {
161
+ // Notification - no response needed
162
+ return { jsonrpc: '2.0' };
163
+ },
164
+ // Handle elicitation response from frontend
165
+ 'beam/elicitation-response': async (req, session) => {
166
+ const params = req.params;
167
+ const elicitationId = params?.elicitationId;
168
+ if (!elicitationId) {
169
+ return {
170
+ jsonrpc: '2.0',
171
+ id: req.id,
172
+ error: { code: -32602, message: 'Missing elicitationId' },
173
+ };
174
+ }
175
+ const pending = pendingElicitations.get(elicitationId);
176
+ if (!pending) {
177
+ return {
178
+ jsonrpc: '2.0',
179
+ id: req.id,
180
+ error: { code: -32602, message: 'Unknown elicitationId' },
181
+ };
182
+ }
183
+ pendingElicitations.delete(elicitationId);
184
+ if (params?.cancelled) {
185
+ pending.reject(new Error('Elicitation cancelled by user'));
186
+ }
187
+ else {
188
+ pending.resolve(params?.value);
189
+ }
190
+ return { jsonrpc: '2.0', id: req.id, result: { success: true } };
191
+ },
192
+ // Client notifies what resource they're viewing (for on-demand subscriptions)
193
+ // photonId: hash of photon path (unique across servers)
194
+ // itemId: whatever the photon uses to identify the item (e.g., board name)
195
+ // lastEventId: optional - for replay of missed events on reconnect
196
+ 'beam/viewing': async (req, session, ctx) => {
197
+ const params = req.params;
198
+ const photonId = params?.photonId;
199
+ const itemId = params?.itemId;
200
+ const lastEventId = params?.lastEventId;
201
+ if (photonId && itemId && ctx.subscriptionManager) {
202
+ ctx.subscriptionManager.onClientViewingBoard(session.id, photonId, itemId, lastEventId);
203
+ }
204
+ // Notification - no response needed
205
+ return { jsonrpc: '2.0' };
206
+ },
207
+ ping: async (req) => {
208
+ return { jsonrpc: '2.0', id: req.id, result: {} };
209
+ },
210
+ // ─────────────────────────────────────────────────────────────────────────────
211
+ // Tools
212
+ // ─────────────────────────────────────────────────────────────────────────────
213
+ 'tools/list': async (req, session, ctx) => {
214
+ const tools = [];
215
+ // Add configured photon methods as tools
216
+ for (const photon of ctx.photons) {
217
+ if (!photon.configured || !photon.methods)
218
+ continue;
219
+ for (const method of photon.methods) {
220
+ tools.push({
221
+ name: `${photon.name}/${method.name}`,
222
+ description: method.description || `Execute ${method.name}`,
223
+ inputSchema: method.params || { type: 'object', properties: {} },
224
+ 'x-photon-id': photon.id, // Unique ID (hash of path) for subscriptions
225
+ 'x-photon-path': photon.path, // File path for View Source
226
+ 'x-photon-description': photon.description,
227
+ 'x-photon-icon': photon.icon,
228
+ 'x-photon-internal': photon.internal,
229
+ 'x-photon-prompt-count': photon.promptCount ?? 0,
230
+ 'x-photon-resource-count': photon.resourceCount ?? 0,
231
+ ...buildToolMetadataExtensions(method),
232
+ // MCP Apps standard: _meta.ui for linked UI resources and visibility
233
+ ...(method.linkedUi || method.visibility
234
+ ? {
235
+ _meta: {
236
+ ui: {
237
+ ...(method.linkedUi
238
+ ? { resourceUri: `ui://${photon.name}/${method.linkedUi}` }
239
+ : {}),
240
+ ...(method.visibility ? { visibility: method.visibility } : {}),
241
+ },
242
+ },
243
+ }
244
+ : {}),
245
+ });
246
+ }
247
+ }
248
+ // Add external MCP tools (from mcpServers in config.json)
249
+ if (ctx.externalMCPs) {
250
+ for (const mcp of ctx.externalMCPs) {
251
+ if (!mcp.connected || !mcp.methods)
252
+ continue;
253
+ for (const method of mcp.methods) {
254
+ tools.push({
255
+ name: `${mcp.name}/${method.name}`,
256
+ description: method.description || `Execute ${method.name}`,
257
+ inputSchema: method.params || { type: 'object', properties: {} },
258
+ 'x-external-mcp': true, // Marker for frontend to identify external MCPs
259
+ 'x-external-mcp-id': mcp.id,
260
+ 'x-photon-icon': mcp.icon || '🔌',
261
+ 'x-photon-description': mcp.description,
262
+ 'x-photon-prompt-count': mcp.promptCount ?? 0,
263
+ 'x-photon-resource-count': mcp.resourceCount ?? 0,
264
+ 'x-has-mcp-app': mcp.hasApp ?? false, // MCP Apps Extension detected
265
+ 'x-mcp-app-uri': mcp.appResourceUri, // MCP App resource URI (default/first)
266
+ 'x-mcp-app-uris': mcp.appResourceUris || [], // All MCP App resource URIs
267
+ ...buildToolMetadataExtensions(method),
268
+ // MCP Apps standard: _meta.ui for linked UI resources and visibility
269
+ ...(method.linkedUi || method.visibility
270
+ ? {
271
+ _meta: {
272
+ ui: {
273
+ ...(method.linkedUi ? { resourceUri: method.linkedUi } : {}),
274
+ ...(method.visibility ? { visibility: method.visibility } : {}),
275
+ },
276
+ },
277
+ }
278
+ : {}),
279
+ });
280
+ }
281
+ }
282
+ }
283
+ // Add beam system tools (internal — hidden from sidebar)
284
+ tools.push({
285
+ name: 'beam/configure',
286
+ 'x-photon-internal': true,
287
+ description: 'Configure a photon with required parameters. Use initialize response configurationSchema to get required fields.',
288
+ inputSchema: {
289
+ type: 'object',
290
+ properties: {
291
+ photon: {
292
+ type: 'string',
293
+ description: 'Name of the photon to configure',
294
+ },
295
+ config: {
296
+ type: 'object',
297
+ description: 'Configuration values (key-value pairs matching the configurationSchema)',
298
+ additionalProperties: true,
299
+ },
300
+ },
301
+ required: ['photon', 'config'],
302
+ },
303
+ });
304
+ tools.push({
305
+ name: 'beam/browse',
306
+ description: 'Browse server filesystem for file/directory selection',
307
+ inputSchema: {
308
+ type: 'object',
309
+ properties: {
310
+ path: {
311
+ type: 'string',
312
+ description: 'Directory path to list (defaults to home directory)',
313
+ },
314
+ filter: {
315
+ type: 'string',
316
+ description: 'File extension filter (e.g., ".pem,.crt" or "*.photon.ts")',
317
+ },
318
+ },
319
+ },
320
+ });
321
+ tools.push({
322
+ name: 'beam/reload',
323
+ description: 'Reload a photon to pick up file changes',
324
+ inputSchema: {
325
+ type: 'object',
326
+ properties: {
327
+ photon: {
328
+ type: 'string',
329
+ description: 'Name of the photon to reload',
330
+ },
331
+ },
332
+ required: ['photon'],
333
+ },
334
+ });
335
+ tools.push({
336
+ name: 'beam/remove',
337
+ description: 'Remove a photon from the workspace',
338
+ inputSchema: {
339
+ type: 'object',
340
+ properties: {
341
+ photon: {
342
+ type: 'string',
343
+ description: 'Name of the photon to remove',
344
+ },
345
+ },
346
+ required: ['photon'],
347
+ },
348
+ });
349
+ tools.push({
350
+ name: 'beam/photon-help',
351
+ description: 'Get rich documentation for a photon',
352
+ inputSchema: {
353
+ type: 'object',
354
+ properties: {
355
+ photon: {
356
+ type: 'string',
357
+ description: 'Name of the photon to get help for',
358
+ },
359
+ },
360
+ required: ['photon'],
361
+ },
362
+ });
363
+ tools.push({
364
+ name: 'beam/update-metadata',
365
+ description: 'Update photon or method metadata (icon, description)',
366
+ inputSchema: {
367
+ type: 'object',
368
+ properties: {
369
+ photon: {
370
+ type: 'string',
371
+ description: 'Name of the photon',
372
+ },
373
+ method: {
374
+ type: 'string',
375
+ description: 'Name of the method (optional, for method metadata)',
376
+ },
377
+ metadata: {
378
+ type: 'object',
379
+ description: 'Metadata to update (icon, description)',
380
+ properties: {
381
+ icon: { type: 'string' },
382
+ description: { type: 'string' },
383
+ },
384
+ },
385
+ },
386
+ required: ['photon', 'metadata'],
387
+ },
388
+ });
389
+ tools.push({
390
+ name: 'beam/reconnect-mcp',
391
+ 'x-photon-internal': true,
392
+ description: 'Reconnect a disconnected external MCP server',
393
+ inputSchema: {
394
+ type: 'object',
395
+ properties: {
396
+ name: {
397
+ type: 'string',
398
+ description: 'Name of the external MCP to reconnect',
399
+ },
400
+ },
401
+ required: ['name'],
402
+ },
403
+ });
404
+ tools.push({
405
+ name: 'beam/studio-read',
406
+ 'x-photon-internal': true,
407
+ description: 'Read a photon source file for editing in Studio',
408
+ inputSchema: {
409
+ type: 'object',
410
+ properties: {
411
+ name: {
412
+ type: 'string',
413
+ description: 'Name of the photon to read',
414
+ },
415
+ },
416
+ required: ['name'],
417
+ },
418
+ });
419
+ tools.push({
420
+ name: 'beam/studio-write',
421
+ 'x-photon-internal': true,
422
+ description: 'Write photon source and trigger hot-reload',
423
+ inputSchema: {
424
+ type: 'object',
425
+ properties: {
426
+ name: {
427
+ type: 'string',
428
+ description: 'Name of the photon to write',
429
+ },
430
+ source: {
431
+ type: 'string',
432
+ description: 'The new source code',
433
+ },
434
+ },
435
+ required: ['name', 'source'],
436
+ },
437
+ });
438
+ tools.push({
439
+ name: 'beam/studio-parse',
440
+ 'x-photon-internal': true,
441
+ description: 'Parse photon source and return extracted schema',
442
+ inputSchema: {
443
+ type: 'object',
444
+ properties: {
445
+ source: {
446
+ type: 'string',
447
+ description: 'Source code to parse',
448
+ },
449
+ },
450
+ required: ['source'],
451
+ },
452
+ });
453
+ // Filter out app-only tools for external (non-Beam) MCP clients
454
+ const visibleTools = session.isBeam
455
+ ? tools
456
+ : tools.filter((t) => {
457
+ const vis = t._meta?.ui?.visibility;
458
+ if (vis && Array.isArray(vis) && vis.includes('app') && !vis.includes('model')) {
459
+ return false;
460
+ }
461
+ return true;
462
+ });
463
+ return { jsonrpc: '2.0', id: req.id, result: { tools: visibleTools } };
464
+ },
465
+ 'tools/call': async (req, session, ctx) => {
466
+ const { name, arguments: args } = req.params;
467
+ // Handle beam system tools
468
+ if (name === 'beam/configure') {
469
+ return handleBeamConfigure(req, ctx, args || {});
470
+ }
471
+ if (name === 'beam/browse') {
472
+ return handleBeamBrowse(req, args || {});
473
+ }
474
+ if (name === 'beam/reload') {
475
+ return handleBeamReload(req, ctx, args || {});
476
+ }
477
+ if (name === 'beam/remove') {
478
+ return handleBeamRemove(req, ctx, args || {});
479
+ }
480
+ if (name === 'beam/update-metadata') {
481
+ return handleBeamUpdateMetadata(req, ctx, args || {});
482
+ }
483
+ if (name === 'beam/reconnect-mcp') {
484
+ return handleBeamReconnectMCP(req, ctx, args || {});
485
+ }
486
+ if (name === 'beam/photon-help') {
487
+ return handleBeamPhotonHelp(req, ctx, args || {});
488
+ }
489
+ if (name === 'beam/studio-read') {
490
+ return handleBeamStudioRead(req, ctx, args || {});
491
+ }
492
+ if (name === 'beam/studio-write') {
493
+ return handleBeamStudioWrite(req, ctx, args || {});
494
+ }
495
+ if (name === 'beam/studio-parse') {
496
+ return handleBeamStudioParse(req, args || {});
497
+ }
498
+ // Parse tool name: server-name/method-name
499
+ const slashIndex = name.indexOf('/');
500
+ if (slashIndex === -1) {
501
+ return {
502
+ jsonrpc: '2.0',
503
+ id: req.id,
504
+ result: {
505
+ content: [{ type: 'text', text: `Invalid tool name: ${name}` }],
506
+ isError: true,
507
+ },
508
+ };
509
+ }
510
+ const serverName = name.slice(0, slashIndex);
511
+ const methodName = name.slice(slashIndex + 1);
512
+ // Check if this is an external MCP tool call
513
+ // Prefer SDK client for full CallToolResult support (structuredContent)
514
+ if (ctx.externalMCPSDKClients?.has(serverName)) {
515
+ const sdkClient = ctx.externalMCPSDKClients.get(serverName);
516
+ try {
517
+ // SDK client.callTool returns full CallToolResult with structuredContent
518
+ const result = await sdkClient.callTool({ name: methodName, arguments: args || {} });
519
+ return {
520
+ jsonrpc: '2.0',
521
+ id: req.id,
522
+ result: {
523
+ content: result.content,
524
+ structuredContent: result.structuredContent,
525
+ isError: result.isError ?? false,
526
+ },
527
+ };
528
+ }
529
+ catch (error) {
530
+ const message = error instanceof Error ? error.message : String(error);
531
+ return {
532
+ jsonrpc: '2.0',
533
+ id: req.id,
534
+ result: {
535
+ content: [{ type: 'text', text: `Error: ${message}` }],
536
+ isError: true,
537
+ },
538
+ };
539
+ }
540
+ }
541
+ // Fallback to wrapper client (no structuredContent support)
542
+ if (ctx.externalMCPClients?.has(serverName)) {
543
+ const client = ctx.externalMCPClients.get(serverName);
544
+ try {
545
+ const result = await client.call(methodName, args || {});
546
+ return {
547
+ jsonrpc: '2.0',
548
+ id: req.id,
549
+ result: {
550
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
551
+ isError: false,
552
+ },
553
+ };
554
+ }
555
+ catch (error) {
556
+ const message = error instanceof Error ? error.message : String(error);
557
+ return {
558
+ jsonrpc: '2.0',
559
+ id: req.id,
560
+ result: {
561
+ content: [{ type: 'text', text: `Error: ${message}` }],
562
+ isError: true,
563
+ },
564
+ };
565
+ }
566
+ }
567
+ // Handle as photon tool call
568
+ const photonName = serverName;
569
+ // Find photon info for UI metadata
570
+ const photonInfo = ctx.photons.find((p) => p.name === photonName);
571
+ const methodInfo = photonInfo?.configured
572
+ ? photonInfo.methods?.find((m) => m.name === methodName)
573
+ : undefined;
574
+ // Build UI metadata
575
+ const uiMetadata = {};
576
+ if (methodInfo?.outputFormat) {
577
+ uiMetadata['x-output-format'] = methodInfo.outputFormat;
578
+ }
579
+ const mcp = ctx.photonMCPs.get(photonName);
580
+ if (!mcp?.instance) {
581
+ // Check if it's a disconnected external MCP
582
+ const externalMCP = ctx.externalMCPs?.find((m) => m.name === photonName);
583
+ if (externalMCP) {
584
+ return {
585
+ jsonrpc: '2.0',
586
+ id: req.id,
587
+ result: {
588
+ content: [
589
+ {
590
+ type: 'text',
591
+ text: `External MCP "${photonName}" is not connected${externalMCP.errorMessage ? `: ${externalMCP.errorMessage}` : ''}`,
592
+ },
593
+ ],
594
+ isError: true,
595
+ },
596
+ };
597
+ }
598
+ return {
599
+ jsonrpc: '2.0',
600
+ id: req.id,
601
+ result: {
602
+ content: [{ type: 'text', text: `Photon not found: ${photonName}` }],
603
+ isError: true,
604
+ },
605
+ };
606
+ }
607
+ // Check instance first, then prototype, then static methods on class
608
+ let method = mcp.instance[methodName];
609
+ let isStatic = false;
610
+ if (typeof method !== 'function') {
611
+ method = Object.getPrototypeOf(mcp.instance)?.[methodName];
612
+ }
613
+ // Check for static method on class constructor
614
+ if (typeof method !== 'function' && mcp.classConstructor) {
615
+ method = mcp.classConstructor[methodName];
616
+ isStatic = true;
617
+ }
618
+ if (typeof method !== 'function') {
619
+ return {
620
+ jsonrpc: '2.0',
621
+ id: req.id,
622
+ result: {
623
+ content: [{ type: 'text', text: `Method not found: ${methodName}` }],
624
+ isError: true,
625
+ },
626
+ };
627
+ }
628
+ try {
629
+ // Create outputHandler to capture emits for real-time UI updates
630
+ const outputHandler = (yieldValue) => {
631
+ if (!ctx.broadcast)
632
+ return;
633
+ // Forward progress events as MCP notifications
634
+ if (yieldValue?.emit === 'progress') {
635
+ const rawValue = typeof yieldValue.value === 'number' ? yieldValue.value : 0;
636
+ const progress = rawValue <= 1 ? rawValue * 100 : rawValue;
637
+ ctx.broadcast({
638
+ jsonrpc: '2.0',
639
+ method: 'notifications/progress',
640
+ params: {
641
+ progressToken: `progress_${photonName}_${methodName}`,
642
+ progress,
643
+ total: 100,
644
+ message: yieldValue.message || null,
645
+ },
646
+ });
647
+ return;
648
+ }
649
+ // Forward status events as MCP notifications
650
+ if (yieldValue?.emit === 'status') {
651
+ ctx.broadcast({
652
+ jsonrpc: '2.0',
653
+ method: 'notifications/progress',
654
+ params: {
655
+ progressToken: `progress_${photonName}_${methodName}`,
656
+ progress: 0,
657
+ total: 100,
658
+ message: yieldValue.message || '',
659
+ },
660
+ });
661
+ return;
662
+ }
663
+ // Forward toast events as beam notifications
664
+ if (yieldValue?.emit === 'toast') {
665
+ ctx.broadcast({
666
+ jsonrpc: '2.0',
667
+ method: 'beam/toast',
668
+ params: {
669
+ message: yieldValue.message || '',
670
+ type: yieldValue.type || 'info',
671
+ duration: yieldValue.duration,
672
+ },
673
+ });
674
+ return;
675
+ }
676
+ // Forward thinking events as beam notifications
677
+ if (yieldValue?.emit === 'thinking') {
678
+ ctx.broadcast({
679
+ jsonrpc: '2.0',
680
+ method: 'beam/thinking',
681
+ params: {
682
+ active: yieldValue.active ?? true,
683
+ },
684
+ });
685
+ return;
686
+ }
687
+ // Forward log events as beam notifications
688
+ if (yieldValue?.emit === 'log') {
689
+ ctx.broadcast({
690
+ jsonrpc: '2.0',
691
+ method: 'beam/log',
692
+ params: {
693
+ message: yieldValue.message || '',
694
+ level: yieldValue.level || 'info',
695
+ data: yieldValue.data,
696
+ },
697
+ });
698
+ return;
699
+ }
700
+ // Forward channel events (task-moved, task-updated, etc.) with full delta
701
+ // These contain specific event type + data for efficient UI updates
702
+ if (yieldValue?.channel && yieldValue?.event) {
703
+ ctx.broadcast({
704
+ type: 'channel-event',
705
+ photon: photonName,
706
+ channel: yieldValue.channel,
707
+ event: yieldValue.event,
708
+ data: yieldValue.data,
709
+ });
710
+ }
711
+ // Note: board-update emits are intentionally not forwarded here
712
+ // Channel events provide more specific info for real-time updates
713
+ };
714
+ // Create inputProvider to handle ask yields (elicitation)
715
+ const inputProvider = async (ask) => {
716
+ if (!ctx.broadcast) {
717
+ throw new Error('No broadcast connection for elicitation');
718
+ }
719
+ // Generate unique elicitation ID
720
+ const elicitationId = randomUUID();
721
+ return new Promise((resolve, reject) => {
722
+ // Store pending elicitation
723
+ pendingElicitations.set(elicitationId, {
724
+ resolve,
725
+ reject,
726
+ sessionId: session?.id || '',
727
+ });
728
+ // Broadcast elicitation request to frontend
729
+ ctx.broadcast({
730
+ jsonrpc: '2.0',
731
+ method: 'beam/elicitation',
732
+ params: {
733
+ elicitationId,
734
+ ...ask,
735
+ },
736
+ });
737
+ // Timeout after 5 minutes
738
+ setTimeout(() => {
739
+ if (pendingElicitations.has(elicitationId)) {
740
+ pendingElicitations.delete(elicitationId);
741
+ reject(new Error('Elicitation timeout - no response received'));
742
+ }
743
+ }, 300000);
744
+ });
745
+ };
746
+ // Use loader.executeTool if available (sets up execution context for this.emit())
747
+ // Fall back to direct method call for backward compatibility
748
+ let result;
749
+ if (ctx.loader) {
750
+ result = await ctx.loader.executeTool(mcp, methodName, args || {}, {
751
+ outputHandler,
752
+ inputProvider,
753
+ });
754
+ }
755
+ else {
756
+ // For static methods, don't bind to instance
757
+ result = isStatic ? await method(args || {}) : await method.call(mcp.instance, args || {});
758
+ }
759
+ // Handle async generators (when not using loader)
760
+ if (result && typeof result[Symbol.asyncIterator] === 'function') {
761
+ const chunks = [];
762
+ let returnValue = undefined;
763
+ // Manually iterate to capture both yielded values AND the return value
764
+ // Note: for-await-of doesn't capture return values, only yielded values
765
+ const iterator = result[Symbol.asyncIterator]();
766
+ while (true) {
767
+ const { value, done } = await iterator.next();
768
+ if (done) {
769
+ // Generator returned - capture the return value
770
+ returnValue = value;
771
+ break;
772
+ }
773
+ // Process yielded values
774
+ if (value?.emit === 'result') {
775
+ chunks.push(value.data);
776
+ }
777
+ else if (value?.emit === 'board-update' && ctx.broadcast) {
778
+ // Forward board-update from generator
779
+ ctx.broadcast({
780
+ type: 'board-update',
781
+ photon: photonName,
782
+ board: value.board,
783
+ });
784
+ }
785
+ else if (value?.emit !== 'progress') {
786
+ chunks.push(value);
787
+ }
788
+ }
789
+ // Use return value if no chunks were yielded, otherwise use chunks
790
+ const finalResult = chunks.length > 0
791
+ ? (chunks.length === 1 ? chunks[0] : chunks)
792
+ : returnValue;
793
+ const genResponse = {
794
+ jsonrpc: '2.0',
795
+ id: req.id,
796
+ result: {
797
+ content: [{ type: 'text', text: JSON.stringify(finalResult, null, 2) }],
798
+ isError: false,
799
+ ...uiMetadata,
800
+ },
801
+ };
802
+ // Broadcast tool result as MCP Apps notification for linked-UI methods
803
+ if (ctx.broadcast && methodInfo?.linkedUi) {
804
+ ctx.broadcast({
805
+ jsonrpc: '2.0',
806
+ method: 'ui/notifications/tool-result',
807
+ params: {
808
+ toolName: `${photonName}/${methodName}`,
809
+ result: genResponse.result,
810
+ isError: false,
811
+ },
812
+ });
813
+ }
814
+ return genResponse;
815
+ }
816
+ const toolResponse = {
817
+ jsonrpc: '2.0',
818
+ id: req.id,
819
+ result: {
820
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
821
+ isError: false,
822
+ ...uiMetadata,
823
+ },
824
+ };
825
+ // Broadcast tool result as MCP Apps notification for linked-UI methods
826
+ if (ctx.broadcast && methodInfo?.linkedUi) {
827
+ ctx.broadcast({
828
+ jsonrpc: '2.0',
829
+ method: 'ui/notifications/tool-result',
830
+ params: {
831
+ toolName: `${photonName}/${methodName}`,
832
+ result: toolResponse.result,
833
+ isError: false,
834
+ },
835
+ });
836
+ }
837
+ return toolResponse;
838
+ }
839
+ catch (error) {
840
+ const message = error instanceof Error ? error.message : String(error);
841
+ return {
842
+ jsonrpc: '2.0',
843
+ id: req.id,
844
+ result: {
845
+ content: [{ type: 'text', text: `Error: ${message}` }],
846
+ isError: true,
847
+ },
848
+ };
849
+ }
850
+ },
851
+ // ─────────────────────────────────────────────────────────────────────────────
852
+ // Resources (MCP Apps ui:// scheme)
853
+ // ─────────────────────────────────────────────────────────────────────────────
854
+ 'resources/list': async (req, session, ctx) => {
855
+ const resources = [];
856
+ for (const photon of ctx.photons) {
857
+ if (!photon.configured || !photon.assets?.ui)
858
+ continue;
859
+ for (const uiAsset of photon.assets.ui) {
860
+ const uri = uiAsset.uri || `ui://${photon.name}/${uiAsset.id}`;
861
+ resources.push({
862
+ uri,
863
+ name: uiAsset.id,
864
+ mimeType: uiAsset.mimeType || 'text/html;profile=mcp-app',
865
+ description: uiAsset.linkedTool
866
+ ? `UI template for ${photon.name}/${uiAsset.linkedTool}`
867
+ : `UI template: ${uiAsset.id}`,
868
+ });
869
+ }
870
+ }
871
+ return { jsonrpc: '2.0', id: req.id, result: { resources } };
872
+ },
873
+ 'resources/read': async (req, session, ctx) => {
874
+ const { uri } = req.params;
875
+ // Parse ui:// URI
876
+ const match = uri.match(/^ui:\/\/([^/]+)\/(.+)$/);
877
+ if (!match) {
878
+ return {
879
+ jsonrpc: '2.0',
880
+ id: req.id,
881
+ error: { code: -32602, message: `Invalid URI: ${uri}` },
882
+ };
883
+ }
884
+ const [, photonName, uiId] = match;
885
+ const content = await ctx.loadUIAsset(photonName, uiId);
886
+ if (!content) {
887
+ return {
888
+ jsonrpc: '2.0',
889
+ id: req.id,
890
+ error: { code: -32602, message: `Resource not found: ${uri}` },
891
+ };
892
+ }
893
+ return {
894
+ jsonrpc: '2.0',
895
+ id: req.id,
896
+ result: {
897
+ contents: [{ uri, mimeType: 'text/html;profile=mcp-app', text: content }],
898
+ },
899
+ };
900
+ },
901
+ };
902
+ // ════════════════════════════════════════════════════════════════════════════════
903
+ // BEAM SYSTEM TOOLS
904
+ // ════════════════════════════════════════════════════════════════════════════════
905
+ /**
906
+ * Handle beam/configure tool - configure a photon with provided values
907
+ */
908
+ async function handleBeamConfigure(req, ctx, args) {
909
+ const { photon: photonName, config } = args;
910
+ if (!photonName) {
911
+ return {
912
+ jsonrpc: '2.0',
913
+ id: req.id,
914
+ result: {
915
+ content: [{ type: 'text', text: 'Error: photon name is required' }],
916
+ isError: true,
917
+ },
918
+ };
919
+ }
920
+ if (!config || typeof config !== 'object') {
921
+ return {
922
+ jsonrpc: '2.0',
923
+ id: req.id,
924
+ result: {
925
+ content: [{ type: 'text', text: 'Error: config object is required' }],
926
+ isError: true,
927
+ },
928
+ };
929
+ }
930
+ // Check if configurePhoton callback is available
931
+ if (!ctx.configurePhoton) {
932
+ return {
933
+ jsonrpc: '2.0',
934
+ id: req.id,
935
+ result: {
936
+ content: [{ type: 'text', text: 'Error: Configuration not supported in this context' }],
937
+ isError: true,
938
+ },
939
+ };
940
+ }
941
+ try {
942
+ const result = await ctx.configurePhoton(photonName, config);
943
+ if (result.success) {
944
+ return {
945
+ jsonrpc: '2.0',
946
+ id: req.id,
947
+ result: {
948
+ content: [
949
+ {
950
+ type: 'text',
951
+ text: `Successfully configured ${photonName}. Tools list will be updated.`,
952
+ },
953
+ ],
954
+ isError: false,
955
+ },
956
+ };
957
+ }
958
+ else {
959
+ return {
960
+ jsonrpc: '2.0',
961
+ id: req.id,
962
+ result: {
963
+ content: [{ type: 'text', text: `Failed to configure ${photonName}: ${result.error}` }],
964
+ isError: true,
965
+ },
966
+ };
967
+ }
968
+ }
969
+ catch (error) {
970
+ const message = error instanceof Error ? error.message : String(error);
971
+ return {
972
+ jsonrpc: '2.0',
973
+ id: req.id,
974
+ result: {
975
+ content: [{ type: 'text', text: `Error configuring ${photonName}: ${message}` }],
976
+ isError: true,
977
+ },
978
+ };
979
+ }
980
+ }
981
+ /**
982
+ * Handle beam/browse tool - browse server filesystem
983
+ */
984
+ async function handleBeamBrowse(req, args) {
985
+ const { path: requestedPath, filter } = args;
986
+ // Default to home directory
987
+ let targetPath = requestedPath || homedir();
988
+ // Handle relative navigation (.. for parent)
989
+ if (targetPath.endsWith('/..') || targetPath === '..') {
990
+ targetPath = dirname(targetPath.replace(/\/?\.\.$/, ''));
991
+ }
992
+ try {
993
+ const stats = await stat(targetPath);
994
+ if (!stats.isDirectory()) {
995
+ targetPath = dirname(targetPath);
996
+ }
997
+ const entries = await readdir(targetPath, { withFileTypes: true });
998
+ // Parse filter
999
+ const filters = filter ? filter.split(',').map((f) => f.trim().toLowerCase()) : [];
1000
+ const items = entries
1001
+ .filter((entry) => {
1002
+ // Always show directories
1003
+ if (entry.isDirectory())
1004
+ return true;
1005
+ // No filter = show all
1006
+ if (filters.length === 0)
1007
+ return true;
1008
+ const fileName = entry.name.toLowerCase();
1009
+ return filters.some((f) => {
1010
+ // Handle glob patterns like "*.photon.ts"
1011
+ if (f.startsWith('*.')) {
1012
+ const suffix = f.slice(1);
1013
+ return fileName.endsWith(suffix);
1014
+ }
1015
+ // Handle extension patterns like ".ts" or "ts"
1016
+ const ext = f.startsWith('.') ? f : `.${f}`;
1017
+ return fileName.endsWith(ext);
1018
+ });
1019
+ })
1020
+ .map((entry) => ({
1021
+ name: entry.name,
1022
+ path: join(targetPath, entry.name),
1023
+ isDirectory: entry.isDirectory(),
1024
+ }))
1025
+ .sort((a, b) => {
1026
+ // Directories first, then alphabetical
1027
+ if (a.isDirectory !== b.isDirectory) {
1028
+ return a.isDirectory ? -1 : 1;
1029
+ }
1030
+ return a.name.localeCompare(b.name);
1031
+ });
1032
+ // Calculate parent path
1033
+ const parent = dirname(targetPath);
1034
+ return {
1035
+ jsonrpc: '2.0',
1036
+ id: req.id,
1037
+ result: {
1038
+ content: [
1039
+ {
1040
+ type: 'text',
1041
+ text: JSON.stringify({
1042
+ path: targetPath,
1043
+ parent: parent !== targetPath ? parent : null,
1044
+ items,
1045
+ }, null, 2),
1046
+ },
1047
+ ],
1048
+ isError: false,
1049
+ },
1050
+ };
1051
+ }
1052
+ catch (error) {
1053
+ const message = error instanceof Error ? error.message : String(error);
1054
+ return {
1055
+ jsonrpc: '2.0',
1056
+ id: req.id,
1057
+ result: {
1058
+ content: [{ type: 'text', text: `Error browsing ${targetPath}: ${message}` }],
1059
+ isError: true,
1060
+ },
1061
+ };
1062
+ }
1063
+ }
1064
+ /**
1065
+ * Handle beam/reload tool - reload a photon
1066
+ */
1067
+ async function handleBeamReload(req, ctx, args) {
1068
+ const { photon: photonName } = args;
1069
+ if (!photonName) {
1070
+ return {
1071
+ jsonrpc: '2.0',
1072
+ id: req.id,
1073
+ result: {
1074
+ content: [{ type: 'text', text: 'Error: photon name is required' }],
1075
+ isError: true,
1076
+ },
1077
+ };
1078
+ }
1079
+ if (!ctx.reloadPhoton) {
1080
+ return {
1081
+ jsonrpc: '2.0',
1082
+ id: req.id,
1083
+ result: {
1084
+ content: [{ type: 'text', text: 'Error: Reload not supported in this context' }],
1085
+ isError: true,
1086
+ },
1087
+ };
1088
+ }
1089
+ try {
1090
+ const result = await ctx.reloadPhoton(photonName);
1091
+ if (result.success) {
1092
+ // Notify Beam clients about the reload
1093
+ broadcastToBeam('beam/hot-reload', { photon: result.photon });
1094
+ return {
1095
+ jsonrpc: '2.0',
1096
+ id: req.id,
1097
+ result: {
1098
+ content: [{ type: 'text', text: `Successfully reloaded ${photonName}` }],
1099
+ isError: false,
1100
+ },
1101
+ };
1102
+ }
1103
+ else {
1104
+ return {
1105
+ jsonrpc: '2.0',
1106
+ id: req.id,
1107
+ result: {
1108
+ content: [{ type: 'text', text: `Failed to reload ${photonName}: ${result.error}` }],
1109
+ isError: true,
1110
+ },
1111
+ };
1112
+ }
1113
+ }
1114
+ catch (error) {
1115
+ const message = error instanceof Error ? error.message : String(error);
1116
+ return {
1117
+ jsonrpc: '2.0',
1118
+ id: req.id,
1119
+ result: {
1120
+ content: [{ type: 'text', text: `Error reloading ${photonName}: ${message}` }],
1121
+ isError: true,
1122
+ },
1123
+ };
1124
+ }
1125
+ }
1126
+ /**
1127
+ * Handle beam/remove tool - remove a photon from the workspace
1128
+ */
1129
+ async function handleBeamRemove(req, ctx, args) {
1130
+ const { photon: photonName } = args;
1131
+ if (!photonName) {
1132
+ return {
1133
+ jsonrpc: '2.0',
1134
+ id: req.id,
1135
+ result: {
1136
+ content: [{ type: 'text', text: 'Error: photon name is required' }],
1137
+ isError: true,
1138
+ },
1139
+ };
1140
+ }
1141
+ if (!ctx.removePhoton) {
1142
+ return {
1143
+ jsonrpc: '2.0',
1144
+ id: req.id,
1145
+ result: {
1146
+ content: [{ type: 'text', text: 'Error: Remove not supported in this context' }],
1147
+ isError: true,
1148
+ },
1149
+ };
1150
+ }
1151
+ try {
1152
+ const result = await ctx.removePhoton(photonName);
1153
+ if (result.success) {
1154
+ return {
1155
+ jsonrpc: '2.0',
1156
+ id: req.id,
1157
+ result: {
1158
+ content: [{ type: 'text', text: `Successfully removed ${photonName}` }],
1159
+ isError: false,
1160
+ },
1161
+ };
1162
+ }
1163
+ else {
1164
+ return {
1165
+ jsonrpc: '2.0',
1166
+ id: req.id,
1167
+ result: {
1168
+ content: [{ type: 'text', text: `Failed to remove ${photonName}: ${result.error}` }],
1169
+ isError: true,
1170
+ },
1171
+ };
1172
+ }
1173
+ }
1174
+ catch (error) {
1175
+ const message = error instanceof Error ? error.message : String(error);
1176
+ return {
1177
+ jsonrpc: '2.0',
1178
+ id: req.id,
1179
+ result: {
1180
+ content: [{ type: 'text', text: `Error removing ${photonName}: ${message}` }],
1181
+ isError: true,
1182
+ },
1183
+ };
1184
+ }
1185
+ }
1186
+ /**
1187
+ * Handle beam/update-metadata tool - update photon or method metadata
1188
+ */
1189
+ async function handleBeamUpdateMetadata(req, ctx, args) {
1190
+ const { photon: photonName, method: methodName, metadata, } = args;
1191
+ if (!photonName) {
1192
+ return {
1193
+ jsonrpc: '2.0',
1194
+ id: req.id,
1195
+ result: {
1196
+ content: [{ type: 'text', text: 'Error: photon name is required' }],
1197
+ isError: true,
1198
+ },
1199
+ };
1200
+ }
1201
+ if (!metadata || typeof metadata !== 'object') {
1202
+ return {
1203
+ jsonrpc: '2.0',
1204
+ id: req.id,
1205
+ result: {
1206
+ content: [{ type: 'text', text: 'Error: metadata object is required' }],
1207
+ isError: true,
1208
+ },
1209
+ };
1210
+ }
1211
+ if (!ctx.updateMetadata) {
1212
+ return {
1213
+ jsonrpc: '2.0',
1214
+ id: req.id,
1215
+ result: {
1216
+ content: [{ type: 'text', text: 'Error: Update metadata not supported in this context' }],
1217
+ isError: true,
1218
+ },
1219
+ };
1220
+ }
1221
+ try {
1222
+ const result = await ctx.updateMetadata(photonName, methodName || null, metadata);
1223
+ if (result.success) {
1224
+ return {
1225
+ jsonrpc: '2.0',
1226
+ id: req.id,
1227
+ result: {
1228
+ content: [
1229
+ {
1230
+ type: 'text',
1231
+ text: `Successfully updated metadata for ${methodName ? `${photonName}/${methodName}` : photonName}`,
1232
+ },
1233
+ ],
1234
+ isError: false,
1235
+ },
1236
+ };
1237
+ }
1238
+ else {
1239
+ return {
1240
+ jsonrpc: '2.0',
1241
+ id: req.id,
1242
+ result: {
1243
+ content: [{ type: 'text', text: `Failed to update metadata: ${result.error}` }],
1244
+ isError: true,
1245
+ },
1246
+ };
1247
+ }
1248
+ }
1249
+ catch (error) {
1250
+ const message = error instanceof Error ? error.message : String(error);
1251
+ return {
1252
+ jsonrpc: '2.0',
1253
+ id: req.id,
1254
+ result: {
1255
+ content: [{ type: 'text', text: `Error updating metadata: ${message}` }],
1256
+ isError: true,
1257
+ },
1258
+ };
1259
+ }
1260
+ }
1261
+ /**
1262
+ * Handle beam/photon-help tool - get rich documentation for a photon
1263
+ */
1264
+ async function handleBeamPhotonHelp(req, ctx, args) {
1265
+ const { photon: photonName } = args;
1266
+ if (!photonName) {
1267
+ return {
1268
+ jsonrpc: '2.0',
1269
+ id: req.id,
1270
+ result: {
1271
+ content: [{ type: 'text', text: 'Error: photon name is required' }],
1272
+ isError: true,
1273
+ },
1274
+ };
1275
+ }
1276
+ if (!ctx.generatePhotonHelp) {
1277
+ return {
1278
+ jsonrpc: '2.0',
1279
+ id: req.id,
1280
+ result: {
1281
+ content: [{ type: 'text', text: 'Error: Help generation not supported in this context' }],
1282
+ isError: true,
1283
+ },
1284
+ };
1285
+ }
1286
+ try {
1287
+ const markdown = await ctx.generatePhotonHelp(photonName);
1288
+ return {
1289
+ jsonrpc: '2.0',
1290
+ id: req.id,
1291
+ result: {
1292
+ content: [{ type: 'text', text: markdown }],
1293
+ isError: false,
1294
+ },
1295
+ };
1296
+ }
1297
+ catch (error) {
1298
+ const message = error instanceof Error ? error.message : String(error);
1299
+ return {
1300
+ jsonrpc: '2.0',
1301
+ id: req.id,
1302
+ result: {
1303
+ content: [{ type: 'text', text: `Error generating help: ${message}` }],
1304
+ isError: true,
1305
+ },
1306
+ };
1307
+ }
1308
+ }
1309
+ /**
1310
+ * Handle beam/reconnect-mcp tool - reconnect a disconnected external MCP
1311
+ */
1312
+ async function handleBeamReconnectMCP(req, ctx, args) {
1313
+ const { name: mcpName } = args;
1314
+ if (!mcpName) {
1315
+ return {
1316
+ jsonrpc: '2.0',
1317
+ id: req.id,
1318
+ result: {
1319
+ content: [{ type: 'text', text: 'Error: MCP name is required' }],
1320
+ isError: true,
1321
+ },
1322
+ };
1323
+ }
1324
+ if (!ctx.reconnectExternalMCP) {
1325
+ return {
1326
+ jsonrpc: '2.0',
1327
+ id: req.id,
1328
+ result: {
1329
+ content: [{ type: 'text', text: 'Error: Reconnection not supported in this context' }],
1330
+ isError: true,
1331
+ },
1332
+ };
1333
+ }
1334
+ try {
1335
+ const result = await ctx.reconnectExternalMCP(mcpName);
1336
+ if (result.success) {
1337
+ return {
1338
+ jsonrpc: '2.0',
1339
+ id: req.id,
1340
+ result: {
1341
+ content: [
1342
+ {
1343
+ type: 'text',
1344
+ text: `Successfully reconnected to external MCP "${mcpName}". Tools list will be updated.`,
1345
+ },
1346
+ ],
1347
+ isError: false,
1348
+ },
1349
+ };
1350
+ }
1351
+ else {
1352
+ return {
1353
+ jsonrpc: '2.0',
1354
+ id: req.id,
1355
+ result: {
1356
+ content: [{ type: 'text', text: `Failed to reconnect to "${mcpName}": ${result.error}` }],
1357
+ isError: true,
1358
+ },
1359
+ };
1360
+ }
1361
+ }
1362
+ catch (error) {
1363
+ const message = error instanceof Error ? error.message : String(error);
1364
+ return {
1365
+ jsonrpc: '2.0',
1366
+ id: req.id,
1367
+ result: {
1368
+ content: [{ type: 'text', text: `Error reconnecting to "${mcpName}": ${message}` }],
1369
+ isError: true,
1370
+ },
1371
+ };
1372
+ }
1373
+ }
1374
+ /**
1375
+ * Handle beam/studio-read — read a photon source file for editing
1376
+ */
1377
+ async function handleBeamStudioRead(req, ctx, args) {
1378
+ const { name: photonName } = args;
1379
+ if (!photonName) {
1380
+ return {
1381
+ jsonrpc: '2.0',
1382
+ id: req.id,
1383
+ result: {
1384
+ content: [{ type: 'text', text: 'Error: photon name is required' }],
1385
+ isError: true,
1386
+ },
1387
+ };
1388
+ }
1389
+ // Find the photon by name to get its file path
1390
+ const photon = ctx.photons.find((p) => p.name === photonName);
1391
+ if (!photon || !photon.path) {
1392
+ return {
1393
+ jsonrpc: '2.0',
1394
+ id: req.id,
1395
+ result: {
1396
+ content: [{ type: 'text', text: `Error: photon "${photonName}" not found or has no path` }],
1397
+ isError: true,
1398
+ },
1399
+ };
1400
+ }
1401
+ try {
1402
+ const source = await readFile(photon.path, 'utf-8');
1403
+ return {
1404
+ jsonrpc: '2.0',
1405
+ id: req.id,
1406
+ result: {
1407
+ content: [{ type: 'text', text: JSON.stringify({ source, path: photon.path }) }],
1408
+ isError: false,
1409
+ },
1410
+ };
1411
+ }
1412
+ catch (error) {
1413
+ const message = error instanceof Error ? error.message : String(error);
1414
+ return {
1415
+ jsonrpc: '2.0',
1416
+ id: req.id,
1417
+ result: {
1418
+ content: [{ type: 'text', text: `Error reading source: ${message}` }],
1419
+ isError: true,
1420
+ },
1421
+ };
1422
+ }
1423
+ }
1424
+ /**
1425
+ * Handle beam/studio-write — write photon source and trigger hot-reload
1426
+ */
1427
+ async function handleBeamStudioWrite(req, ctx, args) {
1428
+ const { name: photonName, source } = args;
1429
+ if (!photonName || typeof source !== 'string') {
1430
+ return {
1431
+ jsonrpc: '2.0',
1432
+ id: req.id,
1433
+ result: {
1434
+ content: [{ type: 'text', text: 'Error: photon name and source are required' }],
1435
+ isError: true,
1436
+ },
1437
+ };
1438
+ }
1439
+ const photon = ctx.photons.find((p) => p.name === photonName);
1440
+ if (!photon || !photon.path) {
1441
+ return {
1442
+ jsonrpc: '2.0',
1443
+ id: req.id,
1444
+ result: {
1445
+ content: [{ type: 'text', text: `Error: photon "${photonName}" not found or has no path` }],
1446
+ isError: true,
1447
+ },
1448
+ };
1449
+ }
1450
+ try {
1451
+ // Write source to disk
1452
+ await writeFile(photon.path, source, 'utf-8');
1453
+ // Parse the new source for preview
1454
+ let parseResult = null;
1455
+ try {
1456
+ const { SchemaExtractor } = await import('@portel/photon-core');
1457
+ const extractor = new SchemaExtractor();
1458
+ const { tools: schemas } = extractor.extractAllFromSource(source);
1459
+ const classMatch = source.match(/export\s+default\s+class\s+(\w+)/);
1460
+ const descMatch = source.match(/\/\*\*\s*\n\s*\*\s*(.+)/);
1461
+ const versionMatch = source.match(/@version\s+(\S+)/);
1462
+ const runtimeMatch = source.match(/@runtime\s+(\S+)/);
1463
+ const iconMatch = source.match(/@icon\s+(\S+)/);
1464
+ const statefulMatch = source.match(/@stateful\s+true/);
1465
+ const depsMatch = source.match(/@dependencies\s+(.+)/);
1466
+ const tagsMatch = source.match(/@tags\s+(.+)/);
1467
+ parseResult = {
1468
+ className: classMatch?.[1] || 'Unknown',
1469
+ description: descMatch?.[1]?.replace(/\s*\*\/$/, '').trim(),
1470
+ icon: iconMatch?.[1],
1471
+ version: versionMatch?.[1],
1472
+ runtime: runtimeMatch?.[1],
1473
+ stateful: !!statefulMatch,
1474
+ dependencies: depsMatch?.[1]?.split(',').map((d) => d.trim()).filter(Boolean),
1475
+ tags: tagsMatch?.[1]?.split(',').map((t) => t.trim()).filter(Boolean),
1476
+ methods: schemas
1477
+ .filter((s) => !['onInitialize', 'onShutdown', 'constructor'].includes(s.name))
1478
+ .map((s) => ({
1479
+ name: s.name,
1480
+ description: s.description,
1481
+ icon: s.icon,
1482
+ params: s.inputSchema,
1483
+ autorun: s.autorun,
1484
+ outputFormat: s.outputFormat,
1485
+ buttonLabel: s.buttonLabel,
1486
+ webhook: s.webhook,
1487
+ scheduled: s.scheduled || s.cron,
1488
+ locked: s.locked,
1489
+ })),
1490
+ };
1491
+ }
1492
+ catch {
1493
+ // Parse is best-effort — don't fail the write
1494
+ }
1495
+ // Trigger hot-reload if available
1496
+ if (ctx.reloadPhoton) {
1497
+ try {
1498
+ const reloadResult = await ctx.reloadPhoton(photonName);
1499
+ if (reloadResult.success) {
1500
+ broadcastToBeam('beam/hot-reload', { photon: reloadResult.photon });
1501
+ }
1502
+ }
1503
+ catch {
1504
+ // Reload failure doesn't fail the write
1505
+ }
1506
+ }
1507
+ return {
1508
+ jsonrpc: '2.0',
1509
+ id: req.id,
1510
+ result: {
1511
+ content: [
1512
+ {
1513
+ type: 'text',
1514
+ text: JSON.stringify({ success: true, parseResult }),
1515
+ },
1516
+ ],
1517
+ isError: false,
1518
+ },
1519
+ };
1520
+ }
1521
+ catch (error) {
1522
+ const message = error instanceof Error ? error.message : String(error);
1523
+ return {
1524
+ jsonrpc: '2.0',
1525
+ id: req.id,
1526
+ result: {
1527
+ content: [{ type: 'text', text: JSON.stringify({ success: false, error: message }) }],
1528
+ isError: true,
1529
+ },
1530
+ };
1531
+ }
1532
+ }
1533
+ /**
1534
+ * Handle beam/studio-parse — parse photon source and return schema
1535
+ */
1536
+ async function handleBeamStudioParse(req, args) {
1537
+ const { source } = args;
1538
+ if (typeof source !== 'string') {
1539
+ return {
1540
+ jsonrpc: '2.0',
1541
+ id: req.id,
1542
+ result: {
1543
+ content: [{ type: 'text', text: 'Error: source is required' }],
1544
+ isError: true,
1545
+ },
1546
+ };
1547
+ }
1548
+ try {
1549
+ const { SchemaExtractor } = await import('@portel/photon-core');
1550
+ const extractor = new SchemaExtractor();
1551
+ const { tools: schemas } = extractor.extractAllFromSource(source);
1552
+ const classMatch = source.match(/export\s+default\s+class\s+(\w+)/);
1553
+ const descMatch = source.match(/\/\*\*\s*\n\s*\*\s*(.+)/);
1554
+ const versionMatch = source.match(/@version\s+(\S+)/);
1555
+ const runtimeMatch = source.match(/@runtime\s+(\S+)/);
1556
+ const iconMatch = source.match(/@icon\s+(\S+)/);
1557
+ const statefulMatch = source.match(/@stateful\s+true/);
1558
+ const depsMatch = source.match(/@dependencies\s+(.+)/);
1559
+ const tagsMatch = source.match(/@tags\s+(.+)/);
1560
+ const errors = [];
1561
+ const warnings = [];
1562
+ if (!classMatch)
1563
+ errors.push('No default export class found');
1564
+ if (!descMatch)
1565
+ warnings.push('Missing class description (first line in JSDoc)');
1566
+ const result = {
1567
+ className: classMatch?.[1] || 'Unknown',
1568
+ description: descMatch?.[1]?.replace(/\s*\*\/$/, '').trim(),
1569
+ icon: iconMatch?.[1],
1570
+ version: versionMatch?.[1],
1571
+ runtime: runtimeMatch?.[1],
1572
+ stateful: !!statefulMatch,
1573
+ dependencies: depsMatch?.[1]?.split(',').map((d) => d.trim()).filter(Boolean),
1574
+ tags: tagsMatch?.[1]?.split(',').map((t) => t.trim()).filter(Boolean),
1575
+ methods: schemas
1576
+ .filter((s) => !['onInitialize', 'onShutdown', 'constructor'].includes(s.name))
1577
+ .map((s) => ({
1578
+ name: s.name,
1579
+ description: s.description,
1580
+ icon: s.icon,
1581
+ params: s.inputSchema,
1582
+ autorun: s.autorun,
1583
+ outputFormat: s.outputFormat,
1584
+ buttonLabel: s.buttonLabel,
1585
+ webhook: s.webhook,
1586
+ scheduled: s.scheduled || s.cron,
1587
+ locked: s.locked,
1588
+ })),
1589
+ errors: errors.length > 0 ? errors : undefined,
1590
+ warnings: warnings.length > 0 ? warnings : undefined,
1591
+ };
1592
+ return {
1593
+ jsonrpc: '2.0',
1594
+ id: req.id,
1595
+ result: {
1596
+ content: [{ type: 'text', text: JSON.stringify(result) }],
1597
+ isError: false,
1598
+ },
1599
+ };
1600
+ }
1601
+ catch (error) {
1602
+ const message = error instanceof Error ? error.message : String(error);
1603
+ return {
1604
+ jsonrpc: '2.0',
1605
+ id: req.id,
1606
+ result: {
1607
+ content: [
1608
+ {
1609
+ type: 'text',
1610
+ text: JSON.stringify({
1611
+ className: 'Unknown',
1612
+ methods: [],
1613
+ errors: [`Parse error: ${message}`],
1614
+ }),
1615
+ },
1616
+ ],
1617
+ isError: false,
1618
+ },
1619
+ };
1620
+ }
1621
+ }
1622
+ /**
1623
+ * Handle MCP Streamable HTTP requests
1624
+ */
1625
+ export async function handleStreamableHTTP(req, res, options) {
1626
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
1627
+ // Only handle /mcp endpoint
1628
+ if (url.pathname !== '/mcp') {
1629
+ return false;
1630
+ }
1631
+ // CORS headers
1632
+ res.setHeader('Access-Control-Allow-Origin', '*');
1633
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
1634
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id');
1635
+ res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
1636
+ // Handle preflight
1637
+ if (req.method === 'OPTIONS') {
1638
+ res.writeHead(204);
1639
+ res.end();
1640
+ return true;
1641
+ }
1642
+ // Get or create session
1643
+ // Check header first, then query parameter (for SSE which can't set headers)
1644
+ let sessionId = req.headers['mcp-session-id'];
1645
+ if (!sessionId) {
1646
+ sessionId = url.searchParams.get('sessionId') || undefined;
1647
+ }
1648
+ const session = getOrCreateSession(sessionId);
1649
+ // GET - Open SSE stream for server notifications
1650
+ if (req.method === 'GET') {
1651
+ const accept = req.headers.accept || '';
1652
+ if (!accept.includes('text/event-stream')) {
1653
+ res.writeHead(406);
1654
+ res.end('Accept header must include text/event-stream');
1655
+ return true;
1656
+ }
1657
+ res.writeHead(200, {
1658
+ 'Content-Type': 'text/event-stream',
1659
+ 'Cache-Control': 'no-cache',
1660
+ Connection: 'keep-alive',
1661
+ 'X-Accel-Buffering': 'no', // Disable nginx buffering
1662
+ 'Mcp-Session-Id': session.id,
1663
+ });
1664
+ // Disable Nagle's algorithm for immediate writes
1665
+ res.socket?.setNoDelay(true);
1666
+ // Store SSE response for server-initiated messages
1667
+ session.sseResponse = res;
1668
+ // Keep connection alive
1669
+ const keepAlive = setInterval(() => {
1670
+ res.write(': keepalive\n\n');
1671
+ }, 30000);
1672
+ req.on('close', () => {
1673
+ clearInterval(keepAlive);
1674
+ session.sseResponse = undefined;
1675
+ // Clean up subscriptions when client disconnects
1676
+ if (options.subscriptionManager) {
1677
+ options.subscriptionManager.onClientDisconnect(session.id);
1678
+ }
1679
+ });
1680
+ return true;
1681
+ }
1682
+ // POST - Handle JSON-RPC requests
1683
+ if (req.method === 'POST') {
1684
+ const accept = req.headers.accept || '';
1685
+ const wantsSSE = accept.includes('text/event-stream');
1686
+ // Read body
1687
+ let body = '';
1688
+ for await (const chunk of req) {
1689
+ body += chunk;
1690
+ }
1691
+ let requests;
1692
+ try {
1693
+ const parsed = JSON.parse(body);
1694
+ requests = Array.isArray(parsed) ? parsed : [parsed];
1695
+ }
1696
+ catch {
1697
+ res.writeHead(400);
1698
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
1699
+ return true;
1700
+ }
1701
+ const context = {
1702
+ photons: options.photons,
1703
+ photonMCPs: options.photonMCPs,
1704
+ externalMCPs: options.externalMCPs,
1705
+ externalMCPClients: options.externalMCPClients,
1706
+ externalMCPSDKClients: options.externalMCPSDKClients,
1707
+ reconnectExternalMCP: options.reconnectExternalMCP,
1708
+ loadUIAsset: options.loadUIAsset,
1709
+ configurePhoton: options.configurePhoton,
1710
+ reloadPhoton: options.reloadPhoton,
1711
+ removePhoton: options.removePhoton,
1712
+ updateMetadata: options.updateMetadata,
1713
+ generatePhotonHelp: options.generatePhotonHelp,
1714
+ loader: options.loader,
1715
+ broadcast: options.broadcast,
1716
+ subscriptionManager: options.subscriptionManager,
1717
+ };
1718
+ // Process requests
1719
+ const responses = [];
1720
+ for (const request of requests) {
1721
+ const handler = handlers[request.method];
1722
+ if (!handler) {
1723
+ if (request.id !== undefined) {
1724
+ responses.push({
1725
+ jsonrpc: '2.0',
1726
+ id: request.id,
1727
+ error: { code: -32601, message: `Method not found: ${request.method}` },
1728
+ });
1729
+ }
1730
+ continue;
1731
+ }
1732
+ const response = await handler(request, session, context);
1733
+ // Only include responses for requests (not notifications)
1734
+ if (request.id !== undefined && response.id !== undefined) {
1735
+ responses.push(response);
1736
+ }
1737
+ }
1738
+ // Send response
1739
+ if (responses.length === 0) {
1740
+ // All were notifications
1741
+ res.writeHead(202);
1742
+ res.end();
1743
+ }
1744
+ else if (wantsSSE) {
1745
+ // SSE response
1746
+ res.writeHead(200, {
1747
+ 'Content-Type': 'text/event-stream',
1748
+ 'Cache-Control': 'no-cache',
1749
+ 'Mcp-Session-Id': session.id,
1750
+ });
1751
+ for (const response of responses) {
1752
+ res.write(`data: ${JSON.stringify(response)}\n\n`);
1753
+ }
1754
+ res.end();
1755
+ }
1756
+ else {
1757
+ // JSON response
1758
+ res.writeHead(200, {
1759
+ 'Content-Type': 'application/json',
1760
+ 'Mcp-Session-Id': session.id,
1761
+ });
1762
+ const result = responses.length === 1 ? responses[0] : responses;
1763
+ res.end(JSON.stringify(result));
1764
+ }
1765
+ return true;
1766
+ }
1767
+ // Method not allowed
1768
+ res.writeHead(405);
1769
+ res.end('Method not allowed');
1770
+ return true;
1771
+ }
1772
+ /**
1773
+ * Send a notification to all connected SSE clients
1774
+ * @param method - The notification method name
1775
+ * @param params - Optional parameters for the notification
1776
+ * @param beamOnly - If true, only send to Beam clients (clientInfo.name === "beam")
1777
+ */
1778
+ export function broadcastNotification(method, params, beamOnly = false) {
1779
+ const notification = {
1780
+ jsonrpc: '2.0',
1781
+ method,
1782
+ params,
1783
+ };
1784
+ for (const session of sessions.values()) {
1785
+ if (session.sseResponse && !session.sseResponse.writableEnded) {
1786
+ // Skip non-Beam clients if beamOnly is true
1787
+ if (beamOnly && !session.isBeam)
1788
+ continue;
1789
+ session.sseResponse.write(`data: ${JSON.stringify(notification)}\n\n`);
1790
+ }
1791
+ }
1792
+ }
1793
+ /**
1794
+ * Send a notification to Beam clients only
1795
+ */
1796
+ export function broadcastToBeam(method, params) {
1797
+ broadcastNotification(method, params, true);
1798
+ }
1799
+ /**
1800
+ * Get count of active sessions (for debugging)
1801
+ */
1802
+ export function getActiveSessionCount() {
1803
+ let total = 0;
1804
+ let beam = 0;
1805
+ for (const session of sessions.values()) {
1806
+ if (session.sseResponse && !session.sseResponse.writableEnded) {
1807
+ total++;
1808
+ if (session.isBeam)
1809
+ beam++;
1810
+ }
1811
+ }
1812
+ return { total, beam };
1813
+ }
1814
+ /**
1815
+ * Send a notification to a specific session by ID
1816
+ * Used for replaying missed events on reconnect
1817
+ */
1818
+ export function sendToSession(sessionId, method, params) {
1819
+ const session = sessions.get(sessionId);
1820
+ if (!session?.sseResponse || session.sseResponse.writableEnded) {
1821
+ return false;
1822
+ }
1823
+ const notification = {
1824
+ jsonrpc: '2.0',
1825
+ method,
1826
+ params,
1827
+ };
1828
+ session.sseResponse.write(`data: ${JSON.stringify(notification)}\n\n`);
1829
+ return true;
1830
+ }
1831
+ /**
1832
+ * Request elicitation from the frontend for an external MCP.
1833
+ * This is used when external MCP servers send elicitation/create requests.
1834
+ *
1835
+ * @param mcpName - Name of the external MCP requesting elicitation
1836
+ * @param request - The elicitation request params from the MCP server
1837
+ * @returns Promise resolving to the user's response
1838
+ */
1839
+ export function requestExternalElicitation(mcpName, request) {
1840
+ const elicitationId = randomUUID();
1841
+ return new Promise((resolve, reject) => {
1842
+ // Store pending elicitation
1843
+ pendingElicitations.set(elicitationId, {
1844
+ resolve: (value) => {
1845
+ resolve({ action: 'accept', content: value });
1846
+ },
1847
+ reject: (error) => {
1848
+ if (error.message.includes('cancelled')) {
1849
+ resolve({ action: 'cancel' });
1850
+ }
1851
+ else {
1852
+ resolve({ action: 'decline' });
1853
+ }
1854
+ },
1855
+ sessionId: '', // External MCP elicitations aren't tied to a specific session
1856
+ });
1857
+ // Broadcast elicitation request to all Beam clients
1858
+ broadcastToBeam('beam/elicitation', {
1859
+ elicitationId,
1860
+ mcpName,
1861
+ message: request.message,
1862
+ mode: request.mode,
1863
+ schema: request.requestedSchema,
1864
+ url: request.url,
1865
+ });
1866
+ // Timeout after 5 minutes
1867
+ setTimeout(() => {
1868
+ if (pendingElicitations.has(elicitationId)) {
1869
+ pendingElicitations.delete(elicitationId);
1870
+ resolve({ action: 'cancel' });
1871
+ }
1872
+ }, 300000);
1873
+ });
1874
+ }
1875
+ //# sourceMappingURL=streamable-http-transport.js.map