@portel/photon 1.4.0 → 1.5.1

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 (379) hide show
  1. package/README.md +287 -1160
  2. package/dist/auto-ui/beam.d.ts +9 -0
  3. package/dist/auto-ui/beam.d.ts.map +1 -0
  4. package/dist/auto-ui/beam.js +2381 -0
  5. package/dist/auto-ui/beam.js.map +1 -0
  6. package/dist/auto-ui/components/card.d.ts +13 -0
  7. package/dist/auto-ui/components/card.d.ts.map +1 -0
  8. package/dist/auto-ui/components/card.js +64 -0
  9. package/dist/auto-ui/components/card.js.map +1 -0
  10. package/dist/auto-ui/components/form.d.ts +15 -0
  11. package/dist/auto-ui/components/form.d.ts.map +1 -0
  12. package/dist/auto-ui/components/form.js +72 -0
  13. package/dist/auto-ui/components/form.js.map +1 -0
  14. package/dist/auto-ui/components/list.d.ts +13 -0
  15. package/dist/auto-ui/components/list.d.ts.map +1 -0
  16. package/dist/auto-ui/components/list.js +58 -0
  17. package/dist/auto-ui/components/list.js.map +1 -0
  18. package/dist/auto-ui/components/progress.d.ts +18 -0
  19. package/dist/auto-ui/components/progress.d.ts.map +1 -0
  20. package/dist/auto-ui/components/progress.js +125 -0
  21. package/dist/auto-ui/components/progress.js.map +1 -0
  22. package/dist/auto-ui/components/table.d.ts +13 -0
  23. package/dist/auto-ui/components/table.d.ts.map +1 -0
  24. package/dist/auto-ui/components/table.js +82 -0
  25. package/dist/auto-ui/components/table.js.map +1 -0
  26. package/dist/auto-ui/components/tree.d.ts +13 -0
  27. package/dist/auto-ui/components/tree.d.ts.map +1 -0
  28. package/dist/auto-ui/components/tree.js +61 -0
  29. package/dist/auto-ui/components/tree.js.map +1 -0
  30. package/dist/auto-ui/daemon-tools.d.ts +45 -0
  31. package/dist/auto-ui/daemon-tools.d.ts.map +1 -0
  32. package/dist/auto-ui/daemon-tools.js +580 -0
  33. package/dist/auto-ui/daemon-tools.js.map +1 -0
  34. package/dist/auto-ui/design-system/index.d.ts +21 -0
  35. package/dist/auto-ui/design-system/index.d.ts.map +1 -0
  36. package/dist/auto-ui/design-system/index.js +27 -0
  37. package/dist/auto-ui/design-system/index.js.map +1 -0
  38. package/dist/auto-ui/design-system/tokens.d.ts +9 -0
  39. package/dist/auto-ui/design-system/tokens.d.ts.map +1 -0
  40. package/dist/auto-ui/design-system/tokens.js +27 -0
  41. package/dist/auto-ui/design-system/tokens.js.map +1 -0
  42. package/dist/auto-ui/design-system/transaction-ui.d.ts +70 -0
  43. package/dist/auto-ui/design-system/transaction-ui.d.ts.map +1 -0
  44. package/dist/auto-ui/design-system/transaction-ui.js +982 -0
  45. package/dist/auto-ui/design-system/transaction-ui.js.map +1 -0
  46. package/dist/auto-ui/frontend/index.html +84 -0
  47. package/dist/auto-ui/index.d.ts +21 -0
  48. package/dist/auto-ui/index.d.ts.map +1 -0
  49. package/dist/auto-ui/index.js +25 -0
  50. package/dist/auto-ui/index.js.map +1 -0
  51. package/dist/auto-ui/openapi-generator.d.ts +71 -0
  52. package/dist/auto-ui/openapi-generator.d.ts.map +1 -0
  53. package/dist/auto-ui/openapi-generator.js +223 -0
  54. package/dist/auto-ui/openapi-generator.js.map +1 -0
  55. package/dist/auto-ui/photon-bridge.d.ts +159 -0
  56. package/dist/auto-ui/photon-bridge.d.ts.map +1 -0
  57. package/dist/auto-ui/photon-bridge.js +262 -0
  58. package/dist/auto-ui/photon-bridge.js.map +1 -0
  59. package/dist/auto-ui/photon-host.d.ts +113 -0
  60. package/dist/auto-ui/photon-host.d.ts.map +1 -0
  61. package/dist/auto-ui/photon-host.js +284 -0
  62. package/dist/auto-ui/photon-host.js.map +1 -0
  63. package/dist/auto-ui/platform-compat.d.ts +71 -0
  64. package/dist/auto-ui/platform-compat.d.ts.map +1 -0
  65. package/dist/auto-ui/platform-compat.js +574 -0
  66. package/dist/auto-ui/platform-compat.js.map +1 -0
  67. package/dist/auto-ui/playground-html.d.ts +15 -0
  68. package/dist/auto-ui/playground-html.d.ts.map +1 -0
  69. package/dist/auto-ui/playground-html.js +1113 -0
  70. package/dist/auto-ui/playground-html.js.map +1 -0
  71. package/dist/auto-ui/playground-server.d.ts +7 -0
  72. package/dist/auto-ui/playground-server.d.ts.map +1 -0
  73. package/dist/auto-ui/playground-server.js +840 -0
  74. package/dist/auto-ui/playground-server.js.map +1 -0
  75. package/dist/auto-ui/registry.d.ts +13 -0
  76. package/dist/auto-ui/registry.d.ts.map +1 -0
  77. package/dist/auto-ui/registry.js +62 -0
  78. package/dist/auto-ui/registry.js.map +1 -0
  79. package/dist/auto-ui/renderer.d.ts +14 -0
  80. package/dist/auto-ui/renderer.d.ts.map +1 -0
  81. package/dist/auto-ui/renderer.js +88 -0
  82. package/dist/auto-ui/renderer.js.map +1 -0
  83. package/dist/auto-ui/rendering/components.d.ts +29 -0
  84. package/dist/auto-ui/rendering/components.d.ts.map +1 -0
  85. package/dist/auto-ui/rendering/components.js +773 -0
  86. package/dist/auto-ui/rendering/components.js.map +1 -0
  87. package/dist/auto-ui/rendering/field-analyzer.d.ts +48 -0
  88. package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -0
  89. package/dist/auto-ui/rendering/field-analyzer.js +270 -0
  90. package/dist/auto-ui/rendering/field-analyzer.js.map +1 -0
  91. package/dist/auto-ui/rendering/field-renderers.d.ts +64 -0
  92. package/dist/auto-ui/rendering/field-renderers.d.ts.map +1 -0
  93. package/dist/auto-ui/rendering/field-renderers.js +317 -0
  94. package/dist/auto-ui/rendering/field-renderers.js.map +1 -0
  95. package/dist/auto-ui/rendering/index.d.ts +28 -0
  96. package/dist/auto-ui/rendering/index.d.ts.map +1 -0
  97. package/dist/auto-ui/rendering/index.js +60 -0
  98. package/dist/auto-ui/rendering/index.js.map +1 -0
  99. package/dist/auto-ui/rendering/layout-selector.d.ts +48 -0
  100. package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -0
  101. package/dist/auto-ui/rendering/layout-selector.js +352 -0
  102. package/dist/auto-ui/rendering/layout-selector.js.map +1 -0
  103. package/dist/auto-ui/rendering/template-engine.d.ts +41 -0
  104. package/dist/auto-ui/rendering/template-engine.d.ts.map +1 -0
  105. package/dist/auto-ui/rendering/template-engine.js +238 -0
  106. package/dist/auto-ui/rendering/template-engine.js.map +1 -0
  107. package/dist/auto-ui/streamable-http-transport.d.ts +79 -0
  108. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -0
  109. package/dist/auto-ui/streamable-http-transport.js +1314 -0
  110. package/dist/auto-ui/streamable-http-transport.js.map +1 -0
  111. package/dist/auto-ui/types.d.ts +310 -0
  112. package/dist/auto-ui/types.d.ts.map +1 -0
  113. package/dist/auto-ui/types.js +71 -0
  114. package/dist/auto-ui/types.js.map +1 -0
  115. package/dist/beam.bundle.js +13506 -0
  116. package/dist/beam.bundle.js.map +7 -0
  117. package/dist/claude-code-plugin.d.ts.map +1 -1
  118. package/dist/claude-code-plugin.js +30 -30
  119. package/dist/claude-code-plugin.js.map +1 -1
  120. package/dist/cli/commands/info.d.ts +11 -0
  121. package/dist/cli/commands/info.d.ts.map +1 -0
  122. package/dist/cli/commands/info.js +313 -0
  123. package/dist/cli/commands/info.js.map +1 -0
  124. package/dist/cli/commands/marketplace.d.ts +11 -0
  125. package/dist/cli/commands/marketplace.d.ts.map +1 -0
  126. package/dist/cli/commands/marketplace.js +198 -0
  127. package/dist/cli/commands/marketplace.js.map +1 -0
  128. package/dist/cli/commands/package-app.d.ts +9 -0
  129. package/dist/cli/commands/package-app.d.ts.map +1 -0
  130. package/dist/cli/commands/package-app.js +191 -0
  131. package/dist/cli/commands/package-app.js.map +1 -0
  132. package/dist/cli/commands/package.d.ts +11 -0
  133. package/dist/cli/commands/package.d.ts.map +1 -0
  134. package/dist/cli/commands/package.js +573 -0
  135. package/dist/cli/commands/package.js.map +1 -0
  136. package/dist/cli-alias.d.ts.map +1 -1
  137. package/dist/cli-alias.js +30 -28
  138. package/dist/cli-alias.js.map +1 -1
  139. package/dist/cli-formatter.d.ts +8 -24
  140. package/dist/cli-formatter.d.ts.map +1 -1
  141. package/dist/cli-formatter.js +8 -325
  142. package/dist/cli-formatter.js.map +1 -1
  143. package/dist/cli.d.ts +15 -1
  144. package/dist/cli.d.ts.map +1 -1
  145. package/dist/cli.js +1157 -1132
  146. package/dist/cli.js.map +1 -1
  147. package/dist/daemon/client.d.ts +81 -0
  148. package/dist/daemon/client.d.ts.map +1 -1
  149. package/dist/daemon/client.js +583 -13
  150. package/dist/daemon/client.js.map +1 -1
  151. package/dist/daemon/manager.d.ts +46 -12
  152. package/dist/daemon/manager.d.ts.map +1 -1
  153. package/dist/daemon/manager.js +102 -61
  154. package/dist/daemon/manager.js.map +1 -1
  155. package/dist/daemon/protocol.d.ts +74 -6
  156. package/dist/daemon/protocol.d.ts.map +1 -1
  157. package/dist/daemon/protocol.js +76 -1
  158. package/dist/daemon/protocol.js.map +1 -1
  159. package/dist/daemon/server.d.ts +6 -6
  160. package/dist/daemon/server.js +778 -117
  161. package/dist/daemon/server.js.map +1 -1
  162. package/dist/daemon/session-manager.d.ts +8 -1
  163. package/dist/daemon/session-manager.d.ts.map +1 -1
  164. package/dist/daemon/session-manager.js +32 -9
  165. package/dist/daemon/session-manager.js.map +1 -1
  166. package/dist/deploy/cloudflare.d.ts +12 -0
  167. package/dist/deploy/cloudflare.d.ts.map +1 -0
  168. package/dist/deploy/cloudflare.js +216 -0
  169. package/dist/deploy/cloudflare.js.map +1 -0
  170. package/dist/index.d.ts +1 -0
  171. package/dist/index.d.ts.map +1 -1
  172. package/dist/index.js +3 -0
  173. package/dist/index.js.map +1 -1
  174. package/dist/loader.d.ts +172 -15
  175. package/dist/loader.d.ts.map +1 -1
  176. package/dist/loader.js +1132 -267
  177. package/dist/loader.js.map +1 -1
  178. package/dist/markdown-utils.d.ts +8 -0
  179. package/dist/markdown-utils.d.ts.map +1 -0
  180. package/dist/markdown-utils.js +63 -0
  181. package/dist/markdown-utils.js.map +1 -0
  182. package/dist/marketplace-manager.d.ts +10 -0
  183. package/dist/marketplace-manager.d.ts.map +1 -1
  184. package/dist/marketplace-manager.js +112 -28
  185. package/dist/marketplace-manager.js.map +1 -1
  186. package/dist/mcp-client.d.ts +9 -0
  187. package/dist/mcp-client.d.ts.map +1 -0
  188. package/dist/mcp-client.js +11 -0
  189. package/dist/mcp-client.js.map +1 -0
  190. package/dist/mcp-elicitation.d.ts +32 -0
  191. package/dist/mcp-elicitation.d.ts.map +1 -0
  192. package/dist/mcp-elicitation.js +26 -0
  193. package/dist/mcp-elicitation.js.map +1 -0
  194. package/dist/path-resolver.d.ts +9 -12
  195. package/dist/path-resolver.d.ts.map +1 -1
  196. package/dist/path-resolver.js +13 -43
  197. package/dist/path-resolver.js.map +1 -1
  198. package/dist/photon-cli-runner.d.ts.map +1 -1
  199. package/dist/photon-cli-runner.js +216 -73
  200. package/dist/photon-cli-runner.js.map +1 -1
  201. package/dist/photon-doc-extractor.d.ts +88 -0
  202. package/dist/photon-doc-extractor.d.ts.map +1 -1
  203. package/dist/photon-doc-extractor.js +536 -27
  204. package/dist/photon-doc-extractor.js.map +1 -1
  205. package/dist/photons/maker.photon.d.ts +182 -0
  206. package/dist/photons/maker.photon.d.ts.map +1 -0
  207. package/dist/photons/maker.photon.js +504 -0
  208. package/dist/photons/maker.photon.js.map +1 -0
  209. package/dist/photons/maker.photon.ts +626 -0
  210. package/dist/photons/marketplace.photon.d.ts +110 -0
  211. package/dist/photons/marketplace.photon.d.ts.map +1 -0
  212. package/dist/photons/marketplace.photon.js +260 -0
  213. package/dist/photons/marketplace.photon.js.map +1 -0
  214. package/dist/photons/marketplace.photon.ts +378 -0
  215. package/dist/photons/tunnel.photon.d.ts +80 -0
  216. package/dist/photons/tunnel.photon.d.ts.map +1 -0
  217. package/dist/photons/tunnel.photon.js +269 -0
  218. package/dist/photons/tunnel.photon.js.map +1 -0
  219. package/dist/photons/tunnel.photon.ts +345 -0
  220. package/dist/security-scanner.d.ts.map +1 -1
  221. package/dist/security-scanner.js +18 -15
  222. package/dist/security-scanner.js.map +1 -1
  223. package/dist/serv/auth/jwt.d.ts +89 -0
  224. package/dist/serv/auth/jwt.d.ts.map +1 -0
  225. package/dist/serv/auth/jwt.js +239 -0
  226. package/dist/serv/auth/jwt.js.map +1 -0
  227. package/dist/serv/auth/oauth.d.ts +117 -0
  228. package/dist/serv/auth/oauth.d.ts.map +1 -0
  229. package/dist/serv/auth/oauth.js +395 -0
  230. package/dist/serv/auth/oauth.js.map +1 -0
  231. package/dist/serv/auth/well-known.d.ts +60 -0
  232. package/dist/serv/auth/well-known.d.ts.map +1 -0
  233. package/dist/serv/auth/well-known.js +154 -0
  234. package/dist/serv/auth/well-known.js.map +1 -0
  235. package/dist/serv/db/d1-client.d.ts +65 -0
  236. package/dist/serv/db/d1-client.d.ts.map +1 -0
  237. package/dist/serv/db/d1-client.js +137 -0
  238. package/dist/serv/db/d1-client.js.map +1 -0
  239. package/dist/serv/db/d1-stores.d.ts +62 -0
  240. package/dist/serv/db/d1-stores.d.ts.map +1 -0
  241. package/dist/serv/db/d1-stores.js +307 -0
  242. package/dist/serv/db/d1-stores.js.map +1 -0
  243. package/dist/serv/index.d.ts +114 -0
  244. package/dist/serv/index.d.ts.map +1 -0
  245. package/dist/serv/index.js +172 -0
  246. package/dist/serv/index.js.map +1 -0
  247. package/dist/serv/local.d.ts +118 -0
  248. package/dist/serv/local.d.ts.map +1 -0
  249. package/dist/serv/local.js +392 -0
  250. package/dist/serv/local.js.map +1 -0
  251. package/dist/serv/middleware/auth.d.ts +66 -0
  252. package/dist/serv/middleware/auth.d.ts.map +1 -0
  253. package/dist/serv/middleware/auth.js +178 -0
  254. package/dist/serv/middleware/auth.js.map +1 -0
  255. package/dist/serv/middleware/tenant.d.ts +94 -0
  256. package/dist/serv/middleware/tenant.d.ts.map +1 -0
  257. package/dist/serv/middleware/tenant.js +152 -0
  258. package/dist/serv/middleware/tenant.js.map +1 -0
  259. package/dist/serv/runtime/executor.d.ts +76 -0
  260. package/dist/serv/runtime/executor.d.ts.map +1 -0
  261. package/dist/serv/runtime/executor.js +105 -0
  262. package/dist/serv/runtime/executor.js.map +1 -0
  263. package/dist/serv/runtime/index.d.ts +8 -0
  264. package/dist/serv/runtime/index.d.ts.map +1 -0
  265. package/dist/serv/runtime/index.js +10 -0
  266. package/dist/serv/runtime/index.js.map +1 -0
  267. package/dist/serv/runtime/oauth-context.d.ts +121 -0
  268. package/dist/serv/runtime/oauth-context.d.ts.map +1 -0
  269. package/dist/serv/runtime/oauth-context.js +153 -0
  270. package/dist/serv/runtime/oauth-context.js.map +1 -0
  271. package/dist/serv/session/kv-store.d.ts +54 -0
  272. package/dist/serv/session/kv-store.d.ts.map +1 -0
  273. package/dist/serv/session/kv-store.js +149 -0
  274. package/dist/serv/session/kv-store.js.map +1 -0
  275. package/dist/serv/session/store.d.ts +113 -0
  276. package/dist/serv/session/store.d.ts.map +1 -0
  277. package/dist/serv/session/store.js +284 -0
  278. package/dist/serv/session/store.js.map +1 -0
  279. package/dist/serv/types/index.d.ts +147 -0
  280. package/dist/serv/types/index.d.ts.map +1 -0
  281. package/dist/serv/types/index.js +8 -0
  282. package/dist/serv/types/index.js.map +1 -0
  283. package/dist/serv/vault/token-vault.d.ts +102 -0
  284. package/dist/serv/vault/token-vault.d.ts.map +1 -0
  285. package/dist/serv/vault/token-vault.js +177 -0
  286. package/dist/serv/vault/token-vault.js.map +1 -0
  287. package/dist/server.d.ts +173 -0
  288. package/dist/server.d.ts.map +1 -1
  289. package/dist/server.js +1622 -86
  290. package/dist/server.js.map +1 -1
  291. package/dist/shared/cli-sections.d.ts +6 -0
  292. package/dist/shared/cli-sections.d.ts.map +1 -0
  293. package/dist/shared/cli-sections.js +16 -0
  294. package/dist/shared/cli-sections.js.map +1 -0
  295. package/dist/shared/cli-utils.d.ts +81 -0
  296. package/dist/shared/cli-utils.d.ts.map +1 -0
  297. package/dist/shared/cli-utils.js +174 -0
  298. package/dist/shared/cli-utils.js.map +1 -0
  299. package/dist/shared/config-docs.d.ts +6 -0
  300. package/dist/shared/config-docs.d.ts.map +1 -0
  301. package/dist/shared/config-docs.js +6 -0
  302. package/dist/shared/config-docs.js.map +1 -0
  303. package/dist/shared/error-handler.d.ts +128 -0
  304. package/dist/shared/error-handler.d.ts.map +1 -0
  305. package/dist/shared/error-handler.js +342 -0
  306. package/dist/shared/error-handler.js.map +1 -0
  307. package/dist/shared/logger.d.ts +42 -0
  308. package/dist/shared/logger.d.ts.map +1 -0
  309. package/dist/shared/logger.js +123 -0
  310. package/dist/shared/logger.js.map +1 -0
  311. package/dist/shared/performance.d.ts +65 -0
  312. package/dist/shared/performance.d.ts.map +1 -0
  313. package/dist/shared/performance.js +136 -0
  314. package/dist/shared/performance.js.map +1 -0
  315. package/dist/shared/task-runner.d.ts +2 -0
  316. package/dist/shared/task-runner.d.ts.map +1 -0
  317. package/dist/shared/task-runner.js +16 -0
  318. package/dist/shared/task-runner.js.map +1 -0
  319. package/dist/shared/validation.d.ts +6 -0
  320. package/dist/shared/validation.d.ts.map +1 -0
  321. package/dist/shared/validation.js +6 -0
  322. package/dist/shared/validation.js.map +1 -0
  323. package/dist/shared-utils.d.ts +63 -0
  324. package/dist/shared-utils.d.ts.map +1 -0
  325. package/dist/shared-utils.js +123 -0
  326. package/dist/shared-utils.js.map +1 -0
  327. package/dist/template-manager.d.ts +23 -2
  328. package/dist/template-manager.d.ts.map +1 -1
  329. package/dist/template-manager.js +177 -88
  330. package/dist/template-manager.js.map +1 -1
  331. package/dist/test-client.d.ts.map +1 -1
  332. package/dist/test-client.js +10 -8
  333. package/dist/test-client.js.map +1 -1
  334. package/dist/test-runner.d.ts +52 -0
  335. package/dist/test-runner.d.ts.map +1 -0
  336. package/dist/test-runner.js +785 -0
  337. package/dist/test-runner.js.map +1 -0
  338. package/dist/testing.d.ts +103 -0
  339. package/dist/testing.d.ts.map +1 -0
  340. package/dist/testing.js +163 -0
  341. package/dist/testing.js.map +1 -0
  342. package/dist/version-checker.d.ts.map +1 -1
  343. package/dist/version-checker.js +2 -2
  344. package/dist/version-checker.js.map +1 -1
  345. package/dist/version.d.ts +2 -0
  346. package/dist/version.d.ts.map +1 -0
  347. package/dist/version.js +5 -0
  348. package/dist/version.js.map +1 -0
  349. package/dist/watcher.d.ts +6 -3
  350. package/dist/watcher.d.ts.map +1 -1
  351. package/dist/watcher.js +49 -10
  352. package/dist/watcher.js.map +1 -1
  353. package/package.json +47 -7
  354. package/templates/cloudflare/worker.ts.template +381 -0
  355. package/templates/cloudflare/wrangler.toml.template +9 -0
  356. package/dist/base.d.ts +0 -58
  357. package/dist/base.d.ts.map +0 -1
  358. package/dist/base.js +0 -92
  359. package/dist/base.js.map +0 -1
  360. package/dist/dependency-manager.d.ts +0 -49
  361. package/dist/dependency-manager.d.ts.map +0 -1
  362. package/dist/dependency-manager.js +0 -165
  363. package/dist/dependency-manager.js.map +0 -1
  364. package/dist/registry-manager.d.ts +0 -76
  365. package/dist/registry-manager.d.ts.map +0 -1
  366. package/dist/registry-manager.js +0 -220
  367. package/dist/registry-manager.js.map +0 -1
  368. package/dist/schema-extractor.d.ts +0 -110
  369. package/dist/schema-extractor.d.ts.map +0 -1
  370. package/dist/schema-extractor.js +0 -727
  371. package/dist/schema-extractor.js.map +0 -1
  372. package/dist/test-marketplace-sources.d.ts +0 -5
  373. package/dist/test-marketplace-sources.d.ts.map +0 -1
  374. package/dist/test-marketplace-sources.js +0 -53
  375. package/dist/test-marketplace-sources.js.map +0 -1
  376. package/dist/types.d.ts +0 -109
  377. package/dist/types.d.ts.map +0 -1
  378. package/dist/types.js +0 -12
  379. package/dist/types.js.map +0 -1
package/dist/server.js CHANGED
@@ -2,23 +2,86 @@
2
2
  * Photon MCP Server
3
3
  *
4
4
  * Wraps a .photon.ts file as an MCP server using @modelcontextprotocol/sdk
5
+ * Supports both stdio and SSE transports
5
6
  */
6
7
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
8
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
8
10
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
11
+ import * as fs from 'fs/promises';
12
+ import { createServer } from 'node:http';
13
+ import { URL } from 'node:url';
9
14
  import { PhotonLoader } from './loader.js';
15
+ import { createStandaloneMCPClientFactory } from './mcp-client.js';
16
+ import { PHOTON_VERSION } from './version.js';
17
+ import { createLogger } from './shared/logger.js';
18
+ import { getErrorMessage } from './shared/error-handler.js';
19
+ import { validateOrThrow, assertString, notEmpty, inRange, oneOf, hasExtension, } from './shared/validation.js';
20
+ import { generatePlaygroundHTML } from './auto-ui/playground-html.js';
21
+ import { subscribeChannel, pingDaemon, reloadDaemon, publishToChannel } from './daemon/client.js';
22
+ import { isDaemonRunning, startDaemon } from './daemon/manager.js';
23
+ import { PhotonDocExtractor } from './photon-doc-extractor.js';
24
+ export class HotReloadDisabledError extends Error {
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = 'HotReloadDisabledError';
28
+ }
29
+ }
10
30
  export class PhotonServer {
11
31
  loader;
12
32
  mcp = null;
13
33
  server;
14
34
  options;
35
+ mcpClientFactory = null;
36
+ httpServer = null;
37
+ sseSessions = new Map();
38
+ devMode;
39
+ hotReloadDisabled = false;
40
+ lastReloadError;
41
+ statusClients = new Set();
42
+ channelUnsubscribers = [];
43
+ daemonName = null;
44
+ currentStatus = {
45
+ type: 'info',
46
+ message: 'Ready',
47
+ timestamp: Date.now(),
48
+ };
49
+ logger;
15
50
  constructor(options) {
51
+ // Validate options (filePath validation skipped for unresolved photons)
52
+ if (!options.unresolvedPhoton) {
53
+ assertString(options.filePath, 'filePath');
54
+ validateOrThrow(options.filePath, [
55
+ notEmpty('filePath'),
56
+ hasExtension('filePath', ['ts', 'js']),
57
+ ]);
58
+ }
59
+ if (options.transport) {
60
+ validateOrThrow(options.transport, [oneOf('transport', ['stdio', 'sse'])]);
61
+ }
62
+ if (options.port !== undefined) {
63
+ validateOrThrow(options.port, [inRange('port', 1, 65535)]);
64
+ }
16
65
  this.options = options;
17
- this.loader = new PhotonLoader(true); // verbose=true for server mode
66
+ this.devMode = options.devMode || false;
67
+ const baseLoggerOptions = {
68
+ component: 'photon-server',
69
+ scope: options.transport ?? 'stdio',
70
+ minimal: true,
71
+ ...options.logOptions,
72
+ };
73
+ if (!baseLoggerOptions.component) {
74
+ baseLoggerOptions.component = 'photon-server';
75
+ }
76
+ if (!baseLoggerOptions.scope) {
77
+ baseLoggerOptions.scope = this.devMode ? 'dev' : 'runtime';
78
+ }
79
+ this.logger = createLogger(baseLoggerOptions);
80
+ this.loader = new PhotonLoader(true, this.logger.child({ component: 'photon-loader', scope: 'loader' }));
18
81
  // Create MCP server instance
19
82
  this.server = new Server({
20
83
  name: 'photon-mcp',
21
- version: '1.0.0',
84
+ version: PHOTON_VERSION,
22
85
  }, {
23
86
  capabilities: {
24
87
  tools: {
@@ -30,43 +93,375 @@ export class PhotonServer {
30
93
  resources: {
31
94
  listChanged: true, // We support hot reload notifications
32
95
  },
96
+ // Note: Server doesn't declare elicitation capability - that's a client capability
97
+ // The server uses elicitInput() when the client has elicitation support
33
98
  },
34
99
  });
35
100
  // Set up protocol handlers
36
101
  this.setupHandlers();
37
102
  }
103
+ createScopedLogger(scope) {
104
+ return this.logger.child({ scope });
105
+ }
106
+ getLogger() {
107
+ return this.logger;
108
+ }
109
+ log(level, message, meta) {
110
+ this.logger.log(level, message, meta);
111
+ }
112
+ /**
113
+ * Detect UI format based on client capabilities
114
+ *
115
+ * SEP-1865 clients advertise ui capability in experimental or root capabilities.
116
+ * Legacy Photon clients may not have explicit UI capability but support photon:// URIs.
117
+ * Text-only clients have no UI support.
118
+ *
119
+ * @param server - Optional server instance (for SSE sessions), defaults to main server
120
+ */
121
+ getUIFormat(server) {
122
+ const targetServer = server || this.server;
123
+ const capabilities = targetServer.getClientCapabilities();
124
+ if (!capabilities) {
125
+ // Before initialization or no capabilities - assume legacy Photon
126
+ return 'photon';
127
+ }
128
+ // Check for SEP-1865 UI capability
129
+ // SEP-1865 clients advertise: { experimental: { ui: {} } } or { ui: {} }
130
+ const experimental = capabilities.experimental;
131
+ if (experimental?.ui || capabilities.ui) {
132
+ return 'sep-1865';
133
+ }
134
+ // Check client info for known SEP-1865 compatible clients
135
+ const clientInfo = targetServer._clientVersion;
136
+ if (clientInfo?.name) {
137
+ const name = clientInfo.name.toLowerCase();
138
+ // Known SEP-1865 compatible clients
139
+ if (name.includes('claude') || name.includes('chatgpt') || name.includes('openai')) {
140
+ return 'sep-1865';
141
+ }
142
+ }
143
+ // Default to Photon format for backward compatibility
144
+ return 'photon';
145
+ }
146
+ /**
147
+ * Build UI resource URI based on detected format
148
+ *
149
+ * @param uiId - UI template identifier
150
+ * @param server - Optional server instance (for SSE sessions)
151
+ */
152
+ buildUIResourceUri(uiId, server) {
153
+ const format = this.getUIFormat(server);
154
+ const photonName = this.mcp?.name || 'unknown';
155
+ switch (format) {
156
+ case 'sep-1865':
157
+ return `ui://${photonName}/${uiId}`;
158
+ case 'photon':
159
+ default:
160
+ return `photon://${photonName}/ui/${uiId}`;
161
+ }
162
+ }
163
+ /**
164
+ * Build tool metadata for UI based on detected format
165
+ *
166
+ * @param uiId - UI template identifier
167
+ * @param server - Optional server instance (for SSE sessions)
168
+ */
169
+ buildUIToolMeta(uiId, server) {
170
+ const format = this.getUIFormat(server);
171
+ const uri = this.buildUIResourceUri(uiId, server);
172
+ switch (format) {
173
+ case 'sep-1865':
174
+ // Official MCP Apps spec: _meta.ui.resourceUri
175
+ return { ui: { resourceUri: uri } };
176
+ case 'photon':
177
+ default:
178
+ return { outputTemplate: uri };
179
+ }
180
+ }
181
+ /**
182
+ * Get UI mimeType based on detected format
183
+ *
184
+ * @param server - Optional server instance (for SSE sessions)
185
+ */
186
+ getUIMimeType(server) {
187
+ const format = this.getUIFormat(server);
188
+ return format === 'sep-1865' ? 'text/html+mcp' : 'text/html';
189
+ }
190
+ /**
191
+ * Check if client supports elicitation
192
+ *
193
+ * Elicitation is a client capability declared during initialization.
194
+ * The server can use elicitInput() when the client supports it.
195
+ */
196
+ clientSupportsElicitation(server) {
197
+ const targetServer = server || this.server;
198
+ const capabilities = targetServer.getClientCapabilities();
199
+ if (!capabilities) {
200
+ return false;
201
+ }
202
+ // Check for elicitation capability (MCP 2025-06 spec)
203
+ return !!capabilities.elicitation;
204
+ }
205
+ /**
206
+ * Create an MCP-aware input provider for generator ask yields
207
+ *
208
+ * Uses MCP elicitInput() when client supports elicitation,
209
+ * otherwise falls back to readline prompts.
210
+ */
211
+ createMCPInputProvider(server) {
212
+ const targetServer = server || this.server;
213
+ const supportsElicitation = this.clientSupportsElicitation(server);
214
+ return async (ask) => {
215
+ // If client doesn't support elicitation, fall back to logging the ask
216
+ // (MCP servers can't use readline - they communicate via protocol)
217
+ if (!supportsElicitation) {
218
+ this.log('warn', `Client doesn't support elicitation, ask will be skipped`, {
219
+ ask: ask.ask,
220
+ message: ask.message,
221
+ });
222
+ // Return default values for non-elicitation clients
223
+ return this.getDefaultForAsk(ask);
224
+ }
225
+ try {
226
+ // Build elicitation request based on ask type
227
+ const elicitParams = this.buildElicitParams(ask);
228
+ // Call server.elicitInput() to request user input from the client
229
+ const result = await targetServer.elicitInput(elicitParams);
230
+ if (result.action === 'accept' && result.content) {
231
+ // Extract the value from the response content
232
+ return this.extractElicitValue(ask, result.content);
233
+ }
234
+ else if (result.action === 'decline' || result.action === 'cancel') {
235
+ this.log('info', `User ${result.action}ed elicitation`, { ask: ask.ask });
236
+ return this.getDefaultForAsk(ask);
237
+ }
238
+ return this.getDefaultForAsk(ask);
239
+ }
240
+ catch (error) {
241
+ this.log('error', `Elicitation failed`, { ask: ask.ask, error: getErrorMessage(error) });
242
+ return this.getDefaultForAsk(ask);
243
+ }
244
+ };
245
+ }
246
+ /**
247
+ * Build MCP elicit request params from a Photon ask yield
248
+ */
249
+ buildElicitParams(ask) {
250
+ const baseMessage = ask.message || 'Please provide input';
251
+ switch (ask.ask) {
252
+ case 'text':
253
+ case 'password':
254
+ return {
255
+ mode: 'form',
256
+ message: baseMessage,
257
+ requestedSchema: {
258
+ type: 'object',
259
+ properties: {
260
+ value: {
261
+ type: 'string',
262
+ title: ask.label || 'Input',
263
+ description: ask.hint || ask.message,
264
+ default: ask.default,
265
+ },
266
+ },
267
+ required: ask.required !== false ? ['value'] : [],
268
+ },
269
+ };
270
+ case 'confirm':
271
+ return {
272
+ mode: 'form',
273
+ message: baseMessage,
274
+ requestedSchema: {
275
+ type: 'object',
276
+ properties: {
277
+ confirmed: {
278
+ type: 'boolean',
279
+ title: 'Confirm',
280
+ description: ask.message,
281
+ default: ask.default ?? false,
282
+ },
283
+ },
284
+ required: ['confirmed'],
285
+ },
286
+ };
287
+ case 'number':
288
+ return {
289
+ mode: 'form',
290
+ message: baseMessage,
291
+ requestedSchema: {
292
+ type: 'object',
293
+ properties: {
294
+ value: {
295
+ type: 'number',
296
+ title: ask.label || 'Number',
297
+ description: ask.hint || ask.message,
298
+ default: ask.default,
299
+ minimum: ask.min,
300
+ maximum: ask.max,
301
+ },
302
+ },
303
+ required: ask.required !== false ? ['value'] : [],
304
+ },
305
+ };
306
+ case 'select':
307
+ // For select, we use enum in the schema
308
+ const options = (ask.options || []).map((o) => (typeof o === 'string' ? o : o.value));
309
+ const labels = (ask.options || []).map((o) => (typeof o === 'string' ? o : o.label));
310
+ return {
311
+ mode: 'form',
312
+ message: baseMessage + (ask.multi ? ' (select multiple)' : ''),
313
+ requestedSchema: {
314
+ type: 'object',
315
+ properties: {
316
+ selection: ask.multi
317
+ ? {
318
+ type: 'array',
319
+ items: { type: 'string', enum: options },
320
+ title: ask.label || 'Selection',
321
+ description: `Options: ${labels.join(', ')}`,
322
+ }
323
+ : {
324
+ type: 'string',
325
+ enum: options,
326
+ title: ask.label || 'Selection',
327
+ description: `Options: ${labels.join(', ')}`,
328
+ },
329
+ },
330
+ required: ask.required !== false ? ['selection'] : [],
331
+ },
332
+ };
333
+ case 'date':
334
+ return {
335
+ mode: 'form',
336
+ message: baseMessage,
337
+ requestedSchema: {
338
+ type: 'object',
339
+ properties: {
340
+ value: {
341
+ type: 'string',
342
+ format: 'date',
343
+ title: ask.label || 'Date',
344
+ description: ask.hint || ask.message,
345
+ default: ask.default,
346
+ },
347
+ },
348
+ required: ask.required !== false ? ['value'] : [],
349
+ },
350
+ };
351
+ default:
352
+ // Generic text input for unknown types
353
+ return {
354
+ mode: 'form',
355
+ message: baseMessage,
356
+ requestedSchema: {
357
+ type: 'object',
358
+ properties: {
359
+ value: {
360
+ type: 'string',
361
+ title: 'Input',
362
+ },
363
+ },
364
+ },
365
+ };
366
+ }
367
+ }
368
+ /**
369
+ * Extract value from elicitation response content
370
+ */
371
+ extractElicitValue(ask, content) {
372
+ switch (ask.ask) {
373
+ case 'confirm':
374
+ return content.confirmed ?? false;
375
+ case 'select':
376
+ return content.selection;
377
+ default:
378
+ return content.value;
379
+ }
380
+ }
381
+ /**
382
+ * Get default value for an ask when elicitation is not available or declined
383
+ */
384
+ getDefaultForAsk(ask) {
385
+ if ('default' in ask) {
386
+ return ask.default;
387
+ }
388
+ switch (ask.ask) {
389
+ case 'confirm':
390
+ return false;
391
+ case 'number':
392
+ return 0;
393
+ case 'select':
394
+ return ask.multi ? [] : null;
395
+ case 'date':
396
+ return new Date().toISOString().split('T')[0];
397
+ default:
398
+ return '';
399
+ }
400
+ }
38
401
  /**
39
402
  * Set up MCP protocol handlers
40
403
  */
41
404
  setupHandlers() {
42
405
  // Handle tools/list
43
406
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
407
+ // If photon is unresolved (conflict), return placeholder tools from manifest metadata
408
+ if (!this.mcp && this.options.unresolvedPhoton) {
409
+ return { tools: this.buildPlaceholderTools() };
410
+ }
44
411
  if (!this.mcp) {
45
412
  return { tools: [] };
46
413
  }
47
414
  return {
48
- tools: this.mcp.tools.map(tool => ({
49
- name: tool.name,
50
- description: tool.description,
51
- inputSchema: tool.inputSchema,
52
- })),
415
+ tools: this.mcp.tools.map((tool) => {
416
+ const toolDef = {
417
+ name: tool.name,
418
+ description: tool.description,
419
+ inputSchema: tool.inputSchema,
420
+ };
421
+ // Add _meta with UI template reference (format depends on client capabilities)
422
+ const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name);
423
+ if (linkedUI) {
424
+ toolDef._meta = this.buildUIToolMeta(linkedUI.id);
425
+ }
426
+ return toolDef;
427
+ }),
53
428
  };
54
429
  });
55
430
  // Handle tools/call
56
431
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
432
+ const { name: toolName, arguments: args } = request.params;
433
+ // Deferred conflict resolution: resolve photon on first tool call
434
+ if (!this.mcp && this.options.unresolvedPhoton) {
435
+ await this.resolveUnresolvedPhoton();
436
+ }
57
437
  if (!this.mcp) {
58
438
  throw new Error('MCP not loaded');
59
439
  }
60
- const { name: toolName, arguments: args } = request.params;
61
440
  try {
62
- const result = await this.loader.executeTool(this.mcp, toolName, args || {});
441
+ // Create MCP-aware input provider for elicitation support
442
+ const inputProvider = this.createMCPInputProvider();
443
+ // Handler for channel events - forward to daemon for cross-process pub/sub
444
+ const outputHandler = (emit) => {
445
+ if (this.daemonName && emit?.channel) {
446
+ publishToChannel(this.daemonName, emit.channel, emit).catch(() => {
447
+ // Ignore publish errors - daemon may not be running
448
+ });
449
+ }
450
+ };
451
+ const result = await this.loader.executeTool(this.mcp, toolName, args || {}, {
452
+ inputProvider,
453
+ outputHandler,
454
+ });
63
455
  // Find the tool to get its outputFormat
64
- const tool = this.mcp.tools.find(t => t.name === toolName);
456
+ const tool = this.mcp.tools.find((t) => t.name === toolName);
65
457
  const outputFormat = tool?.outputFormat;
458
+ // Check if this was a stateful workflow execution
459
+ const isStateful = result && typeof result === 'object' && result._stateful === true;
460
+ const actualResult = isStateful ? result.result : result;
66
461
  // Build content with optional mimeType annotation
67
462
  const content = {
68
463
  type: 'text',
69
- text: this.formatResult(result),
464
+ text: this.formatResult(actualResult),
70
465
  };
71
466
  // Add mimeType annotation if outputFormat is a content type
72
467
  if (outputFormat) {
@@ -76,9 +471,34 @@ export class PhotonServer {
76
471
  content.annotations = { mimeType };
77
472
  }
78
473
  }
474
+ // For stateful workflows, add run ID as a separate content block
475
+ // This allows the AI to inform the user about the workflow run
476
+ if (isStateful && result.runId) {
477
+ const workflowInfo = {
478
+ type: 'text',
479
+ text: `\n\n---\nšŸ“‹ **Workflow Run**: ${result.runId}\n` +
480
+ `Status: ${result.status}${result.resumed ? ' (resumed)' : ''}\n` +
481
+ `This is a stateful workflow. To resume if interrupted, use run ID: ${result.runId}`,
482
+ };
483
+ return { content: [content, workflowInfo] };
484
+ }
79
485
  return { content: [content] };
80
486
  }
81
487
  catch (error) {
488
+ // Check for config error — attempt elicitation to resolve missing env vars
489
+ const errorMsg = getErrorMessage(error);
490
+ if (this.mcp?.instance?._photonConfigError &&
491
+ this.clientSupportsElicitation()) {
492
+ const retryResult = await this.attemptConfigElicitation(toolName, args || {});
493
+ if (retryResult)
494
+ return retryResult;
495
+ }
496
+ // Log error with context for debugging
497
+ this.log('error', 'Tool execution failed', {
498
+ tool: toolName,
499
+ error: errorMsg,
500
+ args: this.options.devMode ? args : undefined,
501
+ });
82
502
  // Format error for AI consumption
83
503
  return this.formatError(error, toolName, args);
84
504
  }
@@ -89,12 +509,14 @@ export class PhotonServer {
89
509
  return { prompts: [] };
90
510
  }
91
511
  return {
92
- prompts: this.mcp.templates.map(template => ({
512
+ prompts: this.mcp.templates.map((template) => ({
93
513
  name: template.name,
94
514
  description: template.description,
95
515
  arguments: Object.entries(template.inputSchema.properties || {}).map(([name, schema]) => ({
96
516
  name,
97
- description: schema.description || '',
517
+ description: (typeof schema === 'object' && schema && 'description' in schema
518
+ ? schema.description
519
+ : '') || '',
98
520
  required: template.inputSchema.required?.includes(name) || false,
99
521
  })),
100
522
  })),
@@ -107,7 +529,7 @@ export class PhotonServer {
107
529
  }
108
530
  const { name: promptName, arguments: args } = request.params;
109
531
  // Find the template
110
- const template = this.mcp.templates.find(t => t.name === promptName);
532
+ const template = this.mcp.templates.find((t) => t.name === promptName);
111
533
  if (!template) {
112
534
  throw new Error(`Prompt not found: ${promptName}`);
113
535
  }
@@ -118,7 +540,11 @@ export class PhotonServer {
118
540
  return this.formatTemplateResult(result);
119
541
  }
120
542
  catch (error) {
121
- throw new Error(`Failed to get prompt: ${error.message}`);
543
+ this.log('error', 'Prompt execution failed', {
544
+ prompt: promptName,
545
+ error: getErrorMessage(error),
546
+ });
547
+ throw new Error(`Failed to get prompt: ${getErrorMessage(error)}`);
122
548
  }
123
549
  });
124
550
  // Handle resources/list (static URIs only, no parameters)
@@ -127,15 +553,49 @@ export class PhotonServer {
127
553
  return { resources: [] };
128
554
  }
129
555
  // Only return resources with static URIs (no {parameters})
130
- const staticResources = this.mcp.statics.filter(s => !this.isUriTemplate(s.uri));
131
- return {
132
- resources: staticResources.map(static_ => ({
133
- uri: static_.uri,
134
- name: static_.name,
135
- description: static_.description,
136
- mimeType: static_.mimeType || 'text/plain',
137
- })),
138
- };
556
+ const staticResources = this.mcp.statics.filter((s) => !this.isUriTemplate(s.uri));
557
+ const resources = staticResources.map((static_) => ({
558
+ uri: static_.uri,
559
+ name: static_.name,
560
+ description: static_.description,
561
+ mimeType: static_.mimeType || 'text/plain',
562
+ }));
563
+ // Add assets from asset folder (UI, prompts, resources)
564
+ if (this.mcp.assets) {
565
+ const photonName = this.mcp.name;
566
+ // Add UI assets (format depends on client capabilities)
567
+ for (const ui of this.mcp.assets.ui) {
568
+ // Use pre-generated URI from loader, or build one
569
+ const uiUri = ui.uri || this.buildUIResourceUri(ui.id);
570
+ resources.push({
571
+ uri: uiUri,
572
+ name: `ui:${ui.id}`,
573
+ description: ui.linkedTool
574
+ ? `UI template for ${ui.linkedTool} tool`
575
+ : `UI template: ${ui.id}`,
576
+ mimeType: ui.mimeType || this.getUIMimeType(),
577
+ });
578
+ }
579
+ // Add prompt assets
580
+ for (const prompt of this.mcp.assets.prompts) {
581
+ resources.push({
582
+ uri: `photon://${photonName}/prompts/${prompt.id}`,
583
+ name: `prompt:${prompt.id}`,
584
+ description: prompt.description || `Prompt template: ${prompt.id}`,
585
+ mimeType: 'text/markdown',
586
+ });
587
+ }
588
+ // Add resource assets
589
+ for (const resource of this.mcp.assets.resources) {
590
+ resources.push({
591
+ uri: `photon://${photonName}/resources/${resource.id}`,
592
+ name: `resource:${resource.id}`,
593
+ description: resource.description || `Static resource: ${resource.id}`,
594
+ mimeType: resource.mimeType || 'application/octet-stream',
595
+ });
596
+ }
597
+ }
598
+ return { resources };
139
599
  });
140
600
  // Handle resources/templates/list (parameterized URIs)
141
601
  this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
@@ -143,9 +603,9 @@ export class PhotonServer {
143
603
  return { resourceTemplates: [] };
144
604
  }
145
605
  // Only return resources with URI templates (has {parameters})
146
- const templateResources = this.mcp.statics.filter(s => this.isUriTemplate(s.uri));
606
+ const templateResources = this.mcp.statics.filter((s) => this.isUriTemplate(s.uri));
147
607
  return {
148
- resourceTemplates: templateResources.map(static_ => ({
608
+ resourceTemplates: templateResources.map((static_) => ({
149
609
  uriTemplate: static_.uri,
150
610
  name: static_.name,
151
611
  description: static_.description,
@@ -159,22 +619,19 @@ export class PhotonServer {
159
619
  throw new Error('MCP not loaded');
160
620
  }
161
621
  const { uri } = request.params;
162
- // Find the static resource by URI
163
- const static_ = this.mcp.statics.find(s => s.uri === uri || this.matchUriPattern(s.uri, uri));
164
- if (!static_) {
165
- throw new Error(`Resource not found: ${uri}`);
166
- }
167
- try {
168
- // Parse URI parameters if URI is a pattern
169
- const params = this.parseUriParams(static_.uri, uri);
170
- // Execute the static method
171
- const result = await this.loader.executeTool(this.mcp, static_.name, params);
172
- // Handle Static return type
173
- return this.formatStaticResult(result, static_.mimeType);
622
+ // Check for SEP-1865 ui:// URI format
623
+ const uiMatch = uri.match(/^ui:\/\/([^/]+)\/(.+)$/);
624
+ if (uiMatch && this.mcp.assets) {
625
+ const [, _photonName, assetId] = uiMatch;
626
+ return this.handleUIAssetRead(uri, assetId);
174
627
  }
175
- catch (error) {
176
- throw new Error(`Failed to read resource: ${error.message}`);
628
+ // Check for legacy photon:// asset URI format
629
+ const assetMatch = uri.match(/^photon:\/\/([^/]+)\/(ui|prompts|resources)\/(.+)$/);
630
+ if (assetMatch && this.mcp.assets) {
631
+ return this.handleAssetRead(uri, assetMatch);
177
632
  }
633
+ // Handle static resources
634
+ return this.handleStaticRead(uri);
178
635
  });
179
636
  }
180
637
  /**
@@ -290,12 +747,13 @@ export class PhotonServer {
290
747
  formatError(error, toolName, args) {
291
748
  // Determine error type
292
749
  let errorType = 'runtime_error';
293
- let errorMessage = error.message || String(error);
750
+ let errorMessage = getErrorMessage(error) || String(error);
294
751
  let suggestion = '';
295
752
  // Categorize common errors and provide suggestions
296
753
  if (errorMessage.includes('not a function') || errorMessage.includes('undefined')) {
297
754
  errorType = 'implementation_error';
298
- suggestion = 'The tool implementation may have an issue. Check that all methods are properly defined.';
755
+ suggestion =
756
+ 'The tool implementation may have an issue. Check that all methods are properly defined.';
299
757
  }
300
758
  else if (errorMessage.includes('required') || errorMessage.includes('validation')) {
301
759
  errorType = 'validation_error';
@@ -307,7 +765,8 @@ export class PhotonServer {
307
765
  }
308
766
  else if (errorMessage.includes('ECONNREFUSED') || errorMessage.includes('network')) {
309
767
  errorType = 'network_error';
310
- suggestion = 'Cannot connect to external service. Check network connection and service availability.';
768
+ suggestion =
769
+ 'Cannot connect to external service. Check network connection and service availability.';
311
770
  }
312
771
  else if (errorMessage.includes('permission') || errorMessage.includes('EACCES')) {
313
772
  errorType = 'permission_error';
@@ -333,9 +792,9 @@ export class PhotonServer {
333
792
  structuredMessage += `\nStack trace:\n${error.stack}\n`;
334
793
  }
335
794
  // Log to stderr for debugging
336
- console.error(`[Photon Error] ${toolName}: ${errorMessage}`);
795
+ this.log('error', `[Photon Error] ${toolName}: ${errorMessage}`);
337
796
  if (this.options.devMode && error.stack) {
338
- console.error(error.stack);
797
+ this.log('debug', error.stack);
339
798
  }
340
799
  return {
341
800
  content: [
@@ -347,29 +806,980 @@ export class PhotonServer {
347
806
  isError: true,
348
807
  };
349
808
  }
809
+ /**
810
+ * Build placeholder tools from unresolved photon manifest metadata
811
+ */
812
+ buildPlaceholderTools() {
813
+ const unresolved = this.options.unresolvedPhoton;
814
+ if (!unresolved)
815
+ return [];
816
+ // Collect tool names from all source metadata
817
+ const toolNames = new Set();
818
+ for (const source of unresolved.sources) {
819
+ if (source.metadata?.tools) {
820
+ for (const tool of source.metadata.tools) {
821
+ toolNames.add(tool);
822
+ }
823
+ }
824
+ }
825
+ // If no tools in metadata, create a single setup tool
826
+ if (toolNames.size === 0) {
827
+ return [
828
+ {
829
+ name: 'setup',
830
+ description: `Set up ${unresolved.name} — call this tool to begin.`,
831
+ inputSchema: { type: 'object', properties: {} },
832
+ },
833
+ ];
834
+ }
835
+ return Array.from(toolNames).map((name) => ({
836
+ name,
837
+ description: `Requires setup — call to begin.`,
838
+ inputSchema: { type: 'object', properties: {} },
839
+ }));
840
+ }
841
+ /**
842
+ * Resolve an unresolved photon (deferred conflict resolution)
843
+ *
844
+ * If the client supports elicitation, presents marketplace choices.
845
+ * Otherwise, auto-picks the recommendation.
846
+ */
847
+ async resolveUnresolvedPhoton() {
848
+ const unresolved = this.options.unresolvedPhoton;
849
+ if (!unresolved)
850
+ return;
851
+ let selectedSource;
852
+ if (unresolved.sources.length === 1) {
853
+ selectedSource = unresolved.sources[0];
854
+ }
855
+ else if (this.clientSupportsElicitation()) {
856
+ // Present choices via elicitation
857
+ const options = {};
858
+ const sourceLabels = [];
859
+ for (const source of unresolved.sources) {
860
+ const version = source.metadata?.version || 'unknown';
861
+ const label = `${source.marketplace.name} (v${version})`;
862
+ sourceLabels.push({ const: source.marketplace.name, title: label });
863
+ }
864
+ const result = await this.server.elicitInput({
865
+ message: `Multiple sources found for "${unresolved.name}". Which marketplace should be used?`,
866
+ requestedSchema: {
867
+ type: 'object',
868
+ properties: {
869
+ marketplace: {
870
+ type: 'string',
871
+ title: 'Marketplace',
872
+ oneOf: sourceLabels,
873
+ default: unresolved.recommendation || unresolved.sources[0].marketplace.name,
874
+ },
875
+ },
876
+ required: ['marketplace'],
877
+ },
878
+ });
879
+ const chosen = result.action === 'accept' && result.content
880
+ ? result.content.marketplace
881
+ : unresolved.recommendation;
882
+ selectedSource =
883
+ unresolved.sources.find((s) => s.marketplace.name === chosen) || unresolved.sources[0];
884
+ }
885
+ else {
886
+ // No elicitation — auto-pick recommendation
887
+ const rec = unresolved.recommendation;
888
+ selectedSource = rec
889
+ ? unresolved.sources.find((s) => s.marketplace.name === rec) || unresolved.sources[0]
890
+ : unresolved.sources[0];
891
+ this.log('info', `Auto-selected marketplace: ${selectedSource.marketplace.name}`);
892
+ }
893
+ // Download and install photon
894
+ await this.downloadAndLoadPhoton(unresolved.name, unresolved.workingDir, selectedSource);
895
+ }
896
+ /**
897
+ * Download a photon from a marketplace source, save to workingDir, and load it
898
+ */
899
+ async downloadAndLoadPhoton(photonName, workingDir, source) {
900
+ const { MarketplaceManager, calculateHash } = await import('./marketplace-manager.js');
901
+ const manager = new MarketplaceManager();
902
+ await manager.initialize();
903
+ const result = await manager.fetchMCP(photonName);
904
+ if (!result) {
905
+ throw new Error(`Failed to download photon: ${photonName}`);
906
+ }
907
+ // Save photon file
908
+ const { default: fsPromises } = await import('fs/promises');
909
+ const filePath = (await import('path')).join(workingDir, `${photonName}.photon.ts`);
910
+ const fileName = `${photonName}.photon.ts`;
911
+ // Ensure working directory exists
912
+ await fsPromises.mkdir(workingDir, { recursive: true });
913
+ await fsPromises.writeFile(filePath, result.content, 'utf-8');
914
+ // Save metadata
915
+ if (source.metadata) {
916
+ const contentHash = calculateHash(result.content);
917
+ await manager.savePhotonMetadata(fileName, source.marketplace, source.metadata, contentHash);
918
+ // Download assets if present
919
+ if (source.metadata.assets && source.metadata.assets.length > 0) {
920
+ const assets = await manager.fetchAssets(source.marketplace, source.metadata.assets);
921
+ for (const [assetPath, content] of assets) {
922
+ const targetPath = (await import('path')).join(workingDir, assetPath);
923
+ const targetDir = (await import('path')).dirname(targetPath);
924
+ await fsPromises.mkdir(targetDir, { recursive: true });
925
+ await fsPromises.writeFile(targetPath, content, 'utf-8');
926
+ }
927
+ }
928
+ }
929
+ // Update options and load
930
+ this.options.filePath = filePath;
931
+ this.options.unresolvedPhoton = undefined;
932
+ this.log('info', `Downloaded and loading ${photonName}...`);
933
+ this.mcp = await this.loader.loadFile(filePath);
934
+ // Notify clients that tools have changed
935
+ await this.notifyListsChanged();
936
+ }
937
+ /**
938
+ * Attempt config elicitation to resolve missing env vars, then retry tool call
939
+ */
940
+ async attemptConfigElicitation(toolName, args) {
941
+ try {
942
+ // Extract constructor params to build form
943
+ const params = await this.loader.extractConstructorParams(this.options.filePath);
944
+ if (params.length === 0)
945
+ return null;
946
+ const photonName = this.mcp?.name || 'photon';
947
+ const { toEnvVarName } = await import('./shared/config-docs.js');
948
+ // Build form properties from constructor params
949
+ const properties = {};
950
+ const required = [];
951
+ for (const param of params) {
952
+ const envVarName = toEnvVarName(photonName, param.name);
953
+ const existing = process.env[envVarName];
954
+ // Skip params that already have values
955
+ if (existing)
956
+ continue;
957
+ // Skip optional params with defaults
958
+ if (param.hasDefault || param.isOptional)
959
+ continue;
960
+ properties[envVarName] = {
961
+ type: param.type === 'number' ? 'number' : param.type === 'boolean' ? 'boolean' : 'string',
962
+ title: param.name,
963
+ description: `Environment variable: ${envVarName}`,
964
+ };
965
+ required.push(envVarName);
966
+ }
967
+ if (Object.keys(properties).length === 0)
968
+ return null;
969
+ const result = await this.server.elicitInput({
970
+ message: `${photonName} requires configuration. Please provide the following:`,
971
+ requestedSchema: {
972
+ type: 'object',
973
+ properties,
974
+ required,
975
+ },
976
+ });
977
+ if (result.action !== 'accept' || !result.content)
978
+ return null;
979
+ // Set env vars from elicitation response
980
+ const content = result.content;
981
+ for (const [key, value] of Object.entries(content)) {
982
+ if (value !== undefined && value !== '') {
983
+ process.env[key] = String(value);
984
+ }
985
+ }
986
+ // Reload photon with new env vars
987
+ this.log('info', 'Reloading photon with elicited configuration...');
988
+ this.mcp = await this.loader.loadFile(this.options.filePath);
989
+ await this.notifyListsChanged();
990
+ // Retry the original tool call
991
+ const inputProvider = this.createMCPInputProvider();
992
+ const outputHandler = (emit) => {
993
+ if (this.daemonName && emit?.channel) {
994
+ publishToChannel(this.daemonName, emit.channel, emit).catch(() => { });
995
+ }
996
+ };
997
+ const retryResult = await this.loader.executeTool(this.mcp, toolName, args, {
998
+ inputProvider,
999
+ outputHandler,
1000
+ });
1001
+ const isStateful = retryResult && typeof retryResult === 'object' && retryResult._stateful === true;
1002
+ const actualResult = isStateful ? retryResult.result : retryResult;
1003
+ return {
1004
+ content: [{ type: 'text', text: this.formatResult(actualResult) }],
1005
+ };
1006
+ }
1007
+ catch (error) {
1008
+ this.log('warn', `Config elicitation failed: ${getErrorMessage(error)}`);
1009
+ return null; // elicitation unsupported by client
1010
+ }
1011
+ }
350
1012
  /**
351
1013
  * Initialize and start the server
352
1014
  */
353
1015
  async start() {
354
1016
  try {
355
- // Load the Photon MCP file
356
- console.error(`Loading ${this.options.filePath}...`);
357
- this.mcp = await this.loader.loadFile(this.options.filePath);
358
- // Connect to stdio transport
359
- const transport = new StdioServerTransport();
360
- await this.server.connect(transport);
361
- console.error(`Server started: ${this.mcp.name}`);
1017
+ // If unresolvedPhoton is set, skip loading — defer to first tool call
1018
+ if (this.options.unresolvedPhoton) {
1019
+ this.log('info', `Deferred loading for ${this.options.unresolvedPhoton.name} (${this.options.unresolvedPhoton.sources.length} marketplace sources)`);
1020
+ }
1021
+ else {
1022
+ // Initialize MCP client factory for enabling this.mcp() in Photons
1023
+ // This allows Photons to call external MCPs via protocol
1024
+ try {
1025
+ this.mcpClientFactory = await createStandaloneMCPClientFactory(this.options.devMode);
1026
+ const servers = await this.mcpClientFactory.listServers();
1027
+ if (servers.length > 0) {
1028
+ this.log('info', `MCP access enabled: ${servers.join(', ')}`);
1029
+ this.loader.setMCPClientFactory(this.mcpClientFactory);
1030
+ }
1031
+ }
1032
+ catch (error) {
1033
+ this.log('warn', `Failed to load MCP config: ${getErrorMessage(error)}`);
1034
+ }
1035
+ // Check if photon is stateful (requires daemon)
1036
+ const extractor = new PhotonDocExtractor(this.options.filePath);
1037
+ const metadata = await extractor.extractFullMetadata();
1038
+ const isStateful = metadata.stateful;
1039
+ // Start daemon for stateful photons (enables cross-client communication)
1040
+ if (isStateful) {
1041
+ const photonName = metadata.name;
1042
+ this.daemonName = photonName; // Store for subscription
1043
+ this.log('info', `Stateful photon detected: ${photonName}`);
1044
+ if (!isDaemonRunning(photonName)) {
1045
+ this.log('info', `Starting daemon for ${photonName}...`);
1046
+ await startDaemon(photonName, this.options.filePath, true);
1047
+ // Wait for daemon to be ready
1048
+ for (let i = 0; i < 10; i++) {
1049
+ await new Promise((r) => setTimeout(r, 500));
1050
+ if (await pingDaemon(photonName)) {
1051
+ this.log('info', `Daemon ready for ${photonName}`);
1052
+ break;
1053
+ }
1054
+ }
1055
+ }
1056
+ else {
1057
+ this.log('info', `Daemon already running for ${photonName}`);
1058
+ }
1059
+ }
1060
+ // Load the Photon MCP file
1061
+ this.log('info', `Loading ${this.options.filePath}...`);
1062
+ this.mcp = await this.loader.loadFile(this.options.filePath);
1063
+ }
1064
+ // Subscribe to daemon channels for cross-process notifications
1065
+ await this.subscribeToChannels();
1066
+ // Start with the appropriate transport
1067
+ const transport = this.options.transport || 'stdio';
1068
+ if (transport === 'sse') {
1069
+ await this.startSSE();
1070
+ }
1071
+ else {
1072
+ await this.startStdio();
1073
+ }
362
1074
  // In dev mode, we could set up file watching here
363
1075
  if (this.options.devMode) {
364
- console.error('Dev mode enabled - hot reload active');
1076
+ this.log('info', 'Dev mode enabled - hot reload active');
365
1077
  }
366
1078
  }
367
1079
  catch (error) {
368
- console.error(`Failed to start server: ${error.message}`);
369
- console.error(error.stack);
1080
+ this.log('error', `Failed to start server: ${getErrorMessage(error)}`);
1081
+ if (error instanceof Error && error.stack) {
1082
+ this.log('debug', error.stack);
1083
+ }
370
1084
  process.exit(1);
371
1085
  }
372
1086
  }
1087
+ /**
1088
+ * Subscribe to daemon channels for cross-process notifications
1089
+ * This enables real-time updates when other processes (e.g., Beam UI, other MCP clients) modify data
1090
+ */
1091
+ async subscribeToChannels() {
1092
+ // Only subscribe if we have a daemon running (stateful photon)
1093
+ if (!this.daemonName)
1094
+ return;
1095
+ try {
1096
+ // Subscribe to wildcard channel for all events from this photon
1097
+ // E.g., "kanban:*" receives "kanban:photon", "kanban:my-board", etc.
1098
+ const unsubscribe = await subscribeChannel(this.daemonName, `${this.daemonName}:*`, (message) => {
1099
+ this.handleChannelMessage(message);
1100
+ });
1101
+ this.channelUnsubscribers.push(unsubscribe);
1102
+ this.log('info', `Subscribed to daemon channel: ${this.daemonName}:*`);
1103
+ }
1104
+ catch (error) {
1105
+ this.log('warn', `Failed to subscribe to daemon: ${getErrorMessage(error)}`);
1106
+ }
1107
+ }
1108
+ /**
1109
+ * Handle incoming channel messages and forward as MCP notifications
1110
+ */
1111
+ async handleChannelMessage(message) {
1112
+ if (!message || typeof message !== 'object')
1113
+ return;
1114
+ const msg = message;
1115
+ // Format message for display
1116
+ const displayMessage = msg.event
1117
+ ? `[${msg.event}] ${JSON.stringify(msg.data || {})}`
1118
+ : JSON.stringify(msg);
1119
+ // Forward as MCP status notification to all connected clients
1120
+ const payload = {
1121
+ method: 'notifications/status',
1122
+ params: { type: 'info', message: displayMessage },
1123
+ };
1124
+ try {
1125
+ await this.server.notification(payload);
1126
+ }
1127
+ catch {
1128
+ // ignore - client may not support notifications
1129
+ }
1130
+ // Also send to SSE sessions
1131
+ for (const session of this.sseSessions.values()) {
1132
+ try {
1133
+ await session.server.notification(payload);
1134
+ }
1135
+ catch {
1136
+ // ignore session errors
1137
+ }
1138
+ }
1139
+ }
1140
+ /**
1141
+ * Start server with stdio transport
1142
+ */
1143
+ async startStdio() {
1144
+ const transport = new StdioServerTransport();
1145
+ await this.server.connect(transport);
1146
+ this.log('info', `Server started: ${this.mcp.name}`);
1147
+ }
1148
+ /**
1149
+ * Start server with SSE transport (HTTP)
1150
+ */
1151
+ async startSSE() {
1152
+ const port = this.options.port || 3000;
1153
+ const ssePath = '/mcp';
1154
+ const messagesPath = '/mcp/messages';
1155
+ this.httpServer = createServer(async (req, res) => {
1156
+ if (!req.url) {
1157
+ res.writeHead(400).end('Missing URL');
1158
+ return;
1159
+ }
1160
+ const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
1161
+ // Handle CORS preflight
1162
+ if (req.method === 'OPTIONS') {
1163
+ res.writeHead(204, {
1164
+ 'Access-Control-Allow-Origin': '*',
1165
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
1166
+ 'Access-Control-Allow-Headers': 'Content-Type',
1167
+ });
1168
+ res.end();
1169
+ return;
1170
+ }
1171
+ // SSE connection endpoint
1172
+ if (req.method === 'GET' && url.pathname === ssePath) {
1173
+ await this.handleSSEConnection(res, messagesPath);
1174
+ return;
1175
+ }
1176
+ // Message posting endpoint
1177
+ if (req.method === 'POST' && url.pathname === messagesPath) {
1178
+ await this.handleSSEMessage(req, res, url);
1179
+ return;
1180
+ }
1181
+ // Health check / info endpoint
1182
+ if (req.method === 'GET' && url.pathname === '/') {
1183
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1184
+ const endpoints = {
1185
+ sse: `http://localhost:${port}${ssePath}`,
1186
+ messages: `http://localhost:${port}${messagesPath}`,
1187
+ };
1188
+ if (this.devMode) {
1189
+ endpoints.playground = `http://localhost:${port}/playground`;
1190
+ }
1191
+ res.end(JSON.stringify({
1192
+ name: this.mcp?.name || 'photon-mcp',
1193
+ transport: 'sse',
1194
+ endpoints,
1195
+ tools: this.mcp?.tools.length || 0,
1196
+ assets: this.mcp?.assets
1197
+ ? {
1198
+ ui: this.mcp.assets.ui.length,
1199
+ prompts: this.mcp.assets.prompts.length,
1200
+ resources: this.mcp.assets.resources.length,
1201
+ }
1202
+ : null,
1203
+ }));
1204
+ return;
1205
+ }
1206
+ // Playground and API endpoints - only in dev mode
1207
+ if (this.devMode) {
1208
+ if (req.method === 'GET' && url.pathname === '/playground') {
1209
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1210
+ res.end(await this.getPlaygroundHTML(port));
1211
+ return;
1212
+ }
1213
+ // API: List all photons
1214
+ if (req.method === 'GET' && url.pathname === '/api/photons') {
1215
+ res.writeHead(200, {
1216
+ 'Content-Type': 'application/json',
1217
+ 'Access-Control-Allow-Origin': '*',
1218
+ });
1219
+ try {
1220
+ const photons = await this.listAllPhotons();
1221
+ res.end(JSON.stringify({ photons }));
1222
+ }
1223
+ catch (error) {
1224
+ res.writeHead(500);
1225
+ res.end(JSON.stringify({ error: getErrorMessage(error) }));
1226
+ }
1227
+ return;
1228
+ }
1229
+ // API: List tools (for compatibility, now returns current photon)
1230
+ if (req.method === 'GET' && url.pathname === '/api/tools') {
1231
+ res.writeHead(200, {
1232
+ 'Content-Type': 'application/json',
1233
+ 'Access-Control-Allow-Origin': '*',
1234
+ });
1235
+ const tools = this.mcp?.tools.map((tool) => {
1236
+ const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name);
1237
+ return {
1238
+ name: tool.name,
1239
+ description: tool.description,
1240
+ inputSchema: tool.inputSchema,
1241
+ ui: linkedUI
1242
+ ? { id: linkedUI.id, uri: `photon://${this.mcp.name}/ui/${linkedUI.id}` }
1243
+ : null,
1244
+ };
1245
+ }) || [];
1246
+ res.end(JSON.stringify({ tools }));
1247
+ return;
1248
+ }
1249
+ if (req.method === 'GET' && url.pathname === '/api/status') {
1250
+ res.writeHead(200, {
1251
+ 'Content-Type': 'application/json',
1252
+ 'Access-Control-Allow-Origin': '*',
1253
+ });
1254
+ res.end(JSON.stringify(this.buildStatusSnapshot()));
1255
+ return;
1256
+ }
1257
+ if (req.method === 'GET' && url.pathname === '/api/status-stream') {
1258
+ this.handleStatusStream(req, res);
1259
+ return;
1260
+ }
1261
+ }
1262
+ // API: Call tool
1263
+ if (req.method === 'POST' && url.pathname === '/api/call') {
1264
+ res.setHeader('Access-Control-Allow-Origin', '*');
1265
+ res.setHeader('Content-Type', 'application/json');
1266
+ let body = '';
1267
+ req.on('data', (chunk) => (body += chunk));
1268
+ req.on('end', async () => {
1269
+ try {
1270
+ const { tool, args } = JSON.parse(body);
1271
+ const result = await this.loader.executeTool(this.mcp, tool, args || {});
1272
+ const isStateful = result && typeof result === 'object' && result._stateful === true;
1273
+ res.writeHead(200);
1274
+ res.end(JSON.stringify({
1275
+ success: true,
1276
+ data: isStateful ? result.result : result,
1277
+ }));
1278
+ }
1279
+ catch (error) {
1280
+ res.writeHead(500);
1281
+ res.end(JSON.stringify({ success: false, error: getErrorMessage(error) }));
1282
+ }
1283
+ });
1284
+ return;
1285
+ }
1286
+ // API: Call tool with streaming progress (SSE)
1287
+ if (req.method === 'POST' && url.pathname === '/api/call-stream') {
1288
+ res.setHeader('Access-Control-Allow-Origin', '*');
1289
+ res.setHeader('Content-Type', 'text/event-stream');
1290
+ res.setHeader('Cache-Control', 'no-cache');
1291
+ res.setHeader('Connection', 'keep-alive');
1292
+ let body = '';
1293
+ req.on('data', (chunk) => (body += chunk));
1294
+ req.on('end', async () => {
1295
+ let requestId = `run_${Date.now()}`;
1296
+ const sendMessage = (message) => {
1297
+ res.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`);
1298
+ };
1299
+ try {
1300
+ const payload = JSON.parse(body || '{}');
1301
+ const tool = payload.tool;
1302
+ if (!tool) {
1303
+ throw new Error('Tool name is required');
1304
+ }
1305
+ const args = payload.args || {};
1306
+ const progressToken = payload.progressToken ?? `progress_${Date.now()}`;
1307
+ requestId = payload.requestId || requestId;
1308
+ const sendNotification = (method, params) => {
1309
+ sendMessage({ jsonrpc: '2.0', method, params });
1310
+ };
1311
+ const reportProgress = (emit) => {
1312
+ const rawValue = typeof emit?.value === 'number' ? emit.value : 0;
1313
+ const percent = rawValue <= 1 ? rawValue * 100 : rawValue;
1314
+ sendNotification('notifications/progress', {
1315
+ progressToken,
1316
+ progress: percent,
1317
+ total: 100,
1318
+ message: emit?.message || null,
1319
+ });
1320
+ };
1321
+ const outputHandler = (emit) => {
1322
+ if (!emit)
1323
+ return;
1324
+ if (emit.emit === 'progress') {
1325
+ reportProgress(emit);
1326
+ }
1327
+ else if (emit.emit === 'status') {
1328
+ sendNotification('notifications/status', {
1329
+ type: emit.type || 'info',
1330
+ message: emit.message || '',
1331
+ });
1332
+ }
1333
+ else {
1334
+ sendNotification('notifications/emit', { event: emit });
1335
+ }
1336
+ // Forward channel events to daemon for cross-process pub/sub
1337
+ if (this.daemonName && emit.channel) {
1338
+ publishToChannel(this.daemonName, emit.channel, emit).catch(() => {
1339
+ // Ignore publish errors - daemon may not be running
1340
+ });
1341
+ }
1342
+ };
1343
+ sendNotification('notifications/status', {
1344
+ type: 'info',
1345
+ message: `Starting ${tool}`,
1346
+ });
1347
+ const result = await this.loader.executeTool(this.mcp, tool, args, { outputHandler });
1348
+ const isStateful = result && typeof result === 'object' && result._stateful === true;
1349
+ sendMessage({
1350
+ jsonrpc: '2.0',
1351
+ id: requestId,
1352
+ result: {
1353
+ success: true,
1354
+ data: isStateful ? result.result : result,
1355
+ },
1356
+ });
1357
+ res.end();
1358
+ }
1359
+ catch (error) {
1360
+ const message = getErrorMessage(error);
1361
+ const errorPayload = {
1362
+ jsonrpc: '2.0',
1363
+ error: { code: -32000, message },
1364
+ };
1365
+ if (requestId) {
1366
+ errorPayload.id = requestId;
1367
+ }
1368
+ sendMessage(errorPayload);
1369
+ res.end();
1370
+ }
1371
+ });
1372
+ return;
1373
+ }
1374
+ // API: Get UI template
1375
+ if (req.method === 'GET' && url.pathname.startsWith('/api/ui/')) {
1376
+ const uiId = url.pathname.replace('/api/ui/', '');
1377
+ const ui = this.mcp?.assets?.ui.find((u) => u.id === uiId);
1378
+ if (ui?.resolvedPath) {
1379
+ try {
1380
+ const content = await fs.readFile(ui.resolvedPath, 'utf-8');
1381
+ res.writeHead(200, {
1382
+ 'Content-Type': 'text/html',
1383
+ 'Access-Control-Allow-Origin': '*',
1384
+ });
1385
+ res.end(content);
1386
+ return;
1387
+ }
1388
+ catch {
1389
+ // Fall through to 404
1390
+ }
1391
+ }
1392
+ res.writeHead(404).end('UI not found');
1393
+ return;
1394
+ }
1395
+ res.writeHead(404).end('Not Found');
1396
+ });
1397
+ this.httpServer.on('clientError', (err, socket) => {
1398
+ this.log('warn', 'HTTP client error', { message: err.message });
1399
+ socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
1400
+ });
1401
+ await new Promise((resolve) => {
1402
+ this.httpServer.listen(port, () => {
1403
+ this.log('info', `${this.mcp.name} MCP server listening`, {
1404
+ transport: 'sse',
1405
+ port,
1406
+ devMode: this.devMode,
1407
+ });
1408
+ this.log('debug', 'SSE endpoints ready', {
1409
+ baseUrl: `http://localhost:${port}`,
1410
+ ssePath,
1411
+ messagesPath,
1412
+ playground: this.devMode ? `http://localhost:${port}/playground` : undefined,
1413
+ });
1414
+ resolve();
1415
+ });
1416
+ });
1417
+ }
1418
+ /**
1419
+ * List all photons in the .photon directory
1420
+ */
1421
+ async listAllPhotons() {
1422
+ const { listPhotonFiles, DEFAULT_PHOTON_DIR } = await import('./path-resolver.js');
1423
+ const photonFiles = await listPhotonFiles();
1424
+ const photons = await Promise.all(photonFiles.map(async (file) => {
1425
+ try {
1426
+ const loader = new PhotonLoader(this.devMode, this.logger.child({ component: 'photon-loader', scope: 'discovery' }));
1427
+ const mcp = await loader.loadFile(file);
1428
+ return {
1429
+ name: mcp.name,
1430
+ description: mcp.description,
1431
+ file: file.replace(DEFAULT_PHOTON_DIR + '/', ''),
1432
+ tools: mcp.tools.map((tool) => ({
1433
+ name: tool.name,
1434
+ description: tool.description,
1435
+ inputSchema: tool.inputSchema,
1436
+ })),
1437
+ };
1438
+ }
1439
+ catch (error) {
1440
+ this.log('warn', `Failed to load photon: ${file}`, { error: getErrorMessage(error) });
1441
+ return null; // skip unloadable photon
1442
+ }
1443
+ }));
1444
+ return photons.filter((p) => p !== null);
1445
+ }
1446
+ /**
1447
+ * Generate playground HTML for interactive testing
1448
+ */
1449
+ async getPlaygroundHTML(port) {
1450
+ const name = this.mcp?.name || 'photon-mcp';
1451
+ return generatePlaygroundHTML({ name, port });
1452
+ }
1453
+ /**
1454
+ * Handle new SSE connection
1455
+ */
1456
+ async handleSSEConnection(res, messagesPath) {
1457
+ res.setHeader('Access-Control-Allow-Origin', '*');
1458
+ // Create a new MCP server instance for this session
1459
+ const sessionServer = new Server({
1460
+ name: this.mcp?.name || 'photon-mcp',
1461
+ version: PHOTON_VERSION,
1462
+ }, {
1463
+ capabilities: {
1464
+ tools: { listChanged: true },
1465
+ prompts: { listChanged: true },
1466
+ resources: { listChanged: true },
1467
+ experimental: {
1468
+ sampling: {}, // Support elicitation via MCP sampling protocol
1469
+ },
1470
+ },
1471
+ });
1472
+ // Copy handlers to the session server
1473
+ this.setupSessionHandlers(sessionServer);
1474
+ // Create SSE transport
1475
+ const transport = new SSEServerTransport(messagesPath, res);
1476
+ const sessionId = transport.sessionId;
1477
+ // Store session
1478
+ this.sseSessions.set(sessionId, { server: sessionServer, transport });
1479
+ // Clean up on close
1480
+ transport.onclose = async () => {
1481
+ this.sseSessions.delete(sessionId);
1482
+ await sessionServer.close();
1483
+ };
1484
+ transport.onerror = (error) => {
1485
+ this.log('warn', 'SSE transport error', {
1486
+ sessionId,
1487
+ error: error instanceof Error ? getErrorMessage(error) : String(error),
1488
+ });
1489
+ };
1490
+ try {
1491
+ await sessionServer.connect(transport);
1492
+ this.log('info', 'SSE client connected', { sessionId });
1493
+ }
1494
+ catch (error) {
1495
+ this.sseSessions.delete(sessionId);
1496
+ this.log('error', 'Failed to establish SSE connection', {
1497
+ sessionId,
1498
+ error: getErrorMessage(error) ?? String(error),
1499
+ });
1500
+ if (!res.headersSent) {
1501
+ res.writeHead(500).end('Failed to establish SSE connection');
1502
+ }
1503
+ }
1504
+ }
1505
+ /**
1506
+ * Handle incoming SSE message
1507
+ */
1508
+ async handleSSEMessage(req, res, url) {
1509
+ res.setHeader('Access-Control-Allow-Origin', '*');
1510
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
1511
+ const sessionId = url.searchParams.get('sessionId');
1512
+ if (!sessionId) {
1513
+ res.writeHead(400).end('Missing sessionId query parameter');
1514
+ return;
1515
+ }
1516
+ const session = this.sseSessions.get(sessionId);
1517
+ if (!session) {
1518
+ res.writeHead(404).end('Unknown session');
1519
+ return;
1520
+ }
1521
+ try {
1522
+ await session.transport.handlePostMessage(req, res);
1523
+ }
1524
+ catch (error) {
1525
+ this.log('error', 'Failed to process SSE message', {
1526
+ sessionId,
1527
+ error: getErrorMessage(error) ?? String(error),
1528
+ });
1529
+ if (!res.headersSent) {
1530
+ res.writeHead(500).end('Failed to process message');
1531
+ }
1532
+ }
1533
+ }
1534
+ /**
1535
+ * Set up handlers for a session-specific MCP server
1536
+ * This duplicates handlers from the main server to each session
1537
+ */
1538
+ setupSessionHandlers(sessionServer) {
1539
+ // Handle tools/list
1540
+ sessionServer.setRequestHandler(ListToolsRequestSchema, async () => {
1541
+ if (!this.mcp)
1542
+ return { tools: [] };
1543
+ return {
1544
+ tools: this.mcp.tools.map((tool) => {
1545
+ const toolDef = {
1546
+ name: tool.name,
1547
+ description: tool.description,
1548
+ inputSchema: tool.inputSchema,
1549
+ };
1550
+ // Add _meta with UI template reference (format depends on client capabilities)
1551
+ const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name);
1552
+ if (linkedUI) {
1553
+ toolDef._meta = this.buildUIToolMeta(linkedUI.id, sessionServer);
1554
+ }
1555
+ return toolDef;
1556
+ }),
1557
+ };
1558
+ });
1559
+ // Handle tools/call
1560
+ sessionServer.setRequestHandler(CallToolRequestSchema, async (request) => {
1561
+ if (!this.mcp)
1562
+ throw new Error('MCP not loaded');
1563
+ const { name: toolName, arguments: args } = request.params;
1564
+ try {
1565
+ // Create MCP-aware input provider for elicitation support (use sessionServer for SSE)
1566
+ const inputProvider = this.createMCPInputProvider(sessionServer);
1567
+ // Handler for channel events - forward to daemon for cross-process pub/sub
1568
+ const outputHandler = (emit) => {
1569
+ if (this.daemonName && emit?.channel) {
1570
+ publishToChannel(this.daemonName, emit.channel, emit).catch(() => {
1571
+ // Ignore publish errors - daemon may not be running
1572
+ });
1573
+ }
1574
+ };
1575
+ const result = await this.loader.executeTool(this.mcp, toolName, args || {}, {
1576
+ inputProvider,
1577
+ outputHandler,
1578
+ });
1579
+ const tool = this.mcp.tools.find((t) => t.name === toolName);
1580
+ const outputFormat = tool?.outputFormat;
1581
+ const isStateful = result && typeof result === 'object' && result._stateful === true;
1582
+ const actualResult = isStateful ? result.result : result;
1583
+ const content = {
1584
+ type: 'text',
1585
+ text: this.formatResult(actualResult),
1586
+ };
1587
+ if (outputFormat) {
1588
+ const { formatToMimeType } = await import('./cli-formatter.js');
1589
+ const mimeType = formatToMimeType(outputFormat);
1590
+ if (mimeType) {
1591
+ content.annotations = { mimeType };
1592
+ }
1593
+ }
1594
+ const response = { content: [content] };
1595
+ if (isStateful) {
1596
+ response._meta = { runId: result.runId, status: result.status };
1597
+ }
1598
+ return response;
1599
+ }
1600
+ catch (error) {
1601
+ return this.formatError(error, toolName, args);
1602
+ }
1603
+ });
1604
+ // Handle prompts/list
1605
+ sessionServer.setRequestHandler(ListPromptsRequestSchema, async () => {
1606
+ if (!this.mcp)
1607
+ return { prompts: [] };
1608
+ return {
1609
+ prompts: this.mcp.templates.map((template) => ({
1610
+ name: template.name,
1611
+ description: template.description,
1612
+ arguments: template.inputSchema?.properties
1613
+ ? Object.entries(template.inputSchema.properties).map(([name, schema]) => ({
1614
+ name,
1615
+ description: schema.description || '',
1616
+ required: template.inputSchema?.required?.includes(name) || false,
1617
+ }))
1618
+ : [],
1619
+ })),
1620
+ };
1621
+ });
1622
+ // Handle prompts/get
1623
+ sessionServer.setRequestHandler(GetPromptRequestSchema, async (request) => {
1624
+ if (!this.mcp)
1625
+ throw new Error('MCP not loaded');
1626
+ const { name: promptName, arguments: args } = request.params;
1627
+ try {
1628
+ const result = await this.loader.executeTool(this.mcp, promptName, args || {});
1629
+ return this.formatTemplateResult(result);
1630
+ }
1631
+ catch (error) {
1632
+ throw new Error(`Failed to get prompt: ${getErrorMessage(error)}`);
1633
+ }
1634
+ });
1635
+ // Handle resources/list
1636
+ sessionServer.setRequestHandler(ListResourcesRequestSchema, async () => {
1637
+ if (!this.mcp)
1638
+ return { resources: [] };
1639
+ const resources = [];
1640
+ // Add static resources
1641
+ for (const static_ of this.mcp.statics) {
1642
+ if (!this.isUriTemplate(static_.uri)) {
1643
+ resources.push({
1644
+ uri: static_.uri,
1645
+ name: static_.name,
1646
+ description: static_.description,
1647
+ mimeType: static_.mimeType,
1648
+ });
1649
+ }
1650
+ }
1651
+ // Add asset resources (UI format depends on client capabilities)
1652
+ if (this.mcp.assets) {
1653
+ for (const ui of this.mcp.assets.ui) {
1654
+ // Use pre-generated URI from loader, or build one
1655
+ const uiUri = ui.uri || this.buildUIResourceUri(ui.id, sessionServer);
1656
+ resources.push({
1657
+ uri: uiUri,
1658
+ name: `ui:${ui.id}`,
1659
+ description: ui.linkedTool
1660
+ ? `UI template for ${ui.linkedTool} tool`
1661
+ : `UI template: ${ui.id}`,
1662
+ mimeType: ui.mimeType || this.getUIMimeType(sessionServer),
1663
+ });
1664
+ }
1665
+ for (const prompt of this.mcp.assets.prompts) {
1666
+ resources.push({
1667
+ uri: `photon://${this.mcp.name}/prompts/${prompt.id}`,
1668
+ name: `prompt:${prompt.id}`,
1669
+ description: prompt.description || `Prompt template: ${prompt.id}`,
1670
+ mimeType: 'text/markdown',
1671
+ });
1672
+ }
1673
+ for (const resource of this.mcp.assets.resources) {
1674
+ resources.push({
1675
+ uri: `photon://${this.mcp.name}/resources/${resource.id}`,
1676
+ name: `resource:${resource.id}`,
1677
+ description: `Static resource: ${resource.id}`,
1678
+ mimeType: resource.mimeType || 'application/json',
1679
+ });
1680
+ }
1681
+ }
1682
+ return { resources };
1683
+ });
1684
+ // Handle resources/templates/list
1685
+ sessionServer.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
1686
+ if (!this.mcp)
1687
+ return { resourceTemplates: [] };
1688
+ return {
1689
+ resourceTemplates: this.mcp.statics
1690
+ .filter((static_) => this.isUriTemplate(static_.uri))
1691
+ .map((static_) => ({
1692
+ uriTemplate: static_.uri,
1693
+ name: static_.name,
1694
+ description: static_.description,
1695
+ mimeType: static_.mimeType,
1696
+ })),
1697
+ };
1698
+ });
1699
+ // Handle resources/read
1700
+ sessionServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1701
+ if (!this.mcp)
1702
+ throw new Error('MCP not loaded');
1703
+ const { uri } = request.params;
1704
+ // Check for SEP-1865 ui:// URI format
1705
+ const uiMatch = uri.match(/^ui:\/\/([^/]+)\/(.+)$/);
1706
+ if (uiMatch && this.mcp.assets) {
1707
+ const [, _photonName, assetId] = uiMatch;
1708
+ return this.handleUIAssetRead(uri, assetId);
1709
+ }
1710
+ // Check for legacy photon:// asset URI format
1711
+ const assetMatch = uri.match(/^photon:\/\/([^/]+)\/(ui|prompts|resources)\/(.+)$/);
1712
+ if (assetMatch && this.mcp.assets) {
1713
+ return this.handleAssetRead(uri, assetMatch);
1714
+ }
1715
+ // Handle static resources
1716
+ return this.handleStaticRead(uri);
1717
+ });
1718
+ }
1719
+ /**
1720
+ * Handle asset read (for both stdio and SSE handlers)
1721
+ */
1722
+ /**
1723
+ * Handle SEP-1865 ui:// resource read
1724
+ */
1725
+ async handleUIAssetRead(uri, assetId) {
1726
+ const ui = this.mcp.assets.ui.find((u) => u.id === assetId);
1727
+ if (!ui || !ui.resolvedPath) {
1728
+ throw new Error(`UI asset not found: ${uri}`);
1729
+ }
1730
+ const content = await fs.readFile(ui.resolvedPath, 'utf-8');
1731
+ return {
1732
+ contents: [{ uri, mimeType: ui.mimeType || 'text/html+mcp', text: content }],
1733
+ };
1734
+ }
1735
+ /**
1736
+ * Handle legacy photon:// asset read
1737
+ */
1738
+ async handleAssetRead(uri, assetMatch) {
1739
+ const [, _photonName, assetType, assetId] = assetMatch;
1740
+ let resolvedPath;
1741
+ let mimeType = 'text/plain';
1742
+ if (assetType === 'ui') {
1743
+ const ui = this.mcp.assets.ui.find((u) => u.id === assetId);
1744
+ if (ui) {
1745
+ resolvedPath = ui.resolvedPath;
1746
+ mimeType = ui.mimeType || 'text/html';
1747
+ }
1748
+ }
1749
+ else if (assetType === 'prompts') {
1750
+ const prompt = this.mcp.assets.prompts.find((p) => p.id === assetId);
1751
+ if (prompt) {
1752
+ resolvedPath = prompt.resolvedPath;
1753
+ mimeType = 'text/markdown';
1754
+ }
1755
+ }
1756
+ else if (assetType === 'resources') {
1757
+ const resource = this.mcp.assets.resources.find((r) => r.id === assetId);
1758
+ if (resource) {
1759
+ resolvedPath = resource.resolvedPath;
1760
+ mimeType = resource.mimeType || 'application/octet-stream';
1761
+ }
1762
+ }
1763
+ if (resolvedPath) {
1764
+ const content = await fs.readFile(resolvedPath, 'utf-8');
1765
+ return {
1766
+ contents: [{ uri, mimeType, text: content }],
1767
+ };
1768
+ }
1769
+ throw new Error(`Asset not found: ${uri}`);
1770
+ }
1771
+ /**
1772
+ * Handle static resource read (for both stdio and SSE handlers)
1773
+ */
1774
+ async handleStaticRead(uri) {
1775
+ const static_ = this.mcp.statics.find((s) => s.uri === uri || this.matchUriPattern(s.uri, uri));
1776
+ if (!static_) {
1777
+ throw new Error(`Resource not found: ${uri}`);
1778
+ }
1779
+ const params = this.parseUriParams(static_.uri, uri);
1780
+ const result = await this.loader.executeTool(this.mcp, static_.name, params);
1781
+ return this.formatStaticResult(result, static_.mimeType);
1782
+ }
373
1783
  /**
374
1784
  * Stop the server
375
1785
  */
@@ -379,11 +1789,102 @@ export class PhotonServer {
379
1789
  if (this.mcp?.instance?.onShutdown) {
380
1790
  await this.mcp.instance.onShutdown();
381
1791
  }
1792
+ // Disconnect MCP clients
1793
+ if (this.mcpClientFactory) {
1794
+ await this.mcpClientFactory.disconnect();
1795
+ }
1796
+ // Close SSE sessions
1797
+ for (const [_sessionId, session] of this.sseSessions) {
1798
+ await session.server.close();
1799
+ }
1800
+ this.sseSessions.clear();
1801
+ for (const client of this.statusClients) {
1802
+ client.end();
1803
+ }
1804
+ this.statusClients.clear();
1805
+ // Close HTTP server if running
1806
+ if (this.httpServer) {
1807
+ await new Promise((resolve) => {
1808
+ this.httpServer.close(() => resolve());
1809
+ });
1810
+ this.httpServer = null;
1811
+ }
382
1812
  await this.server.close();
383
- console.error('Server stopped');
1813
+ this.log('info', 'Server stopped');
384
1814
  }
385
1815
  catch (error) {
386
- console.error(`Error stopping server: ${error.message}`);
1816
+ this.log('error', 'Error stopping server', { error: getErrorMessage(error) });
1817
+ }
1818
+ }
1819
+ buildStatusSnapshot() {
1820
+ const warnings = [];
1821
+ const instance = this.mcp?.instance;
1822
+ if (instance && '_photonConfigError' in instance && instance._photonConfigError) {
1823
+ warnings.push('Photon configuration incomplete. Check env vars and MCP credentials.');
1824
+ }
1825
+ const assets = this.mcp?.assets || { ui: [], prompts: [], resources: [] };
1826
+ const tools = this.mcp?.tools || [];
1827
+ return {
1828
+ photon: this.mcp?.name || null,
1829
+ devMode: this.devMode,
1830
+ hotReloadDisabled: this.hotReloadDisabled,
1831
+ lastReloadError: this.lastReloadError || null,
1832
+ status: this.currentStatus,
1833
+ warnings,
1834
+ summary: {
1835
+ toolCount: tools.length,
1836
+ tools: tools.map((tool) => ({
1837
+ name: tool.name,
1838
+ description: tool.description || '',
1839
+ hasUI: Boolean(assets.ui?.some((ui) => ui.linkedTool === tool.name)),
1840
+ })),
1841
+ uiAssets: (assets.ui || []).map((ui) => ({ id: ui.id, linkedTool: ui.linkedTool })),
1842
+ promptCount: assets.prompts?.length || 0,
1843
+ resourceCount: assets.resources?.length || 0,
1844
+ },
1845
+ };
1846
+ }
1847
+ handleStatusStream(_req, res) {
1848
+ res.writeHead(200, {
1849
+ 'Content-Type': 'text/event-stream',
1850
+ 'Cache-Control': 'no-cache',
1851
+ Connection: 'keep-alive',
1852
+ 'Access-Control-Allow-Origin': '*',
1853
+ });
1854
+ res.write(`data: ${JSON.stringify(this.buildStatusSnapshot())}\n\n`);
1855
+ this.statusClients.add(res);
1856
+ const cleanup = () => {
1857
+ this.statusClients.delete(res);
1858
+ };
1859
+ res.on('close', cleanup);
1860
+ res.on('error', cleanup);
1861
+ }
1862
+ async broadcastReloadStatus(type, message) {
1863
+ this.currentStatus = { type, message, timestamp: Date.now() };
1864
+ this.pushStatusUpdate();
1865
+ const payload = {
1866
+ method: 'notifications/status',
1867
+ params: { type, message },
1868
+ };
1869
+ try {
1870
+ await this.server.notification(payload);
1871
+ }
1872
+ catch {
1873
+ // ignore
1874
+ }
1875
+ for (const session of this.sseSessions.values()) {
1876
+ try {
1877
+ await session.server.notification(payload);
1878
+ }
1879
+ catch {
1880
+ // ignore session errors
1881
+ }
1882
+ }
1883
+ }
1884
+ pushStatusUpdate() {
1885
+ const frame = `data: ${JSON.stringify(this.currentStatus)}\n\n`;
1886
+ for (const client of this.statusClients) {
1887
+ client.write(frame);
387
1888
  }
388
1889
  }
389
1890
  /**
@@ -398,8 +1899,11 @@ export class PhotonServer {
398
1899
  clearTimeout(this.reloadRetryTimeout);
399
1900
  this.reloadRetryTimeout = undefined;
400
1901
  }
1902
+ if (this.hotReloadDisabled) {
1903
+ throw new HotReloadDisabledError('Hot reload temporarily disabled after repeated failures. Restart Photon or fix the errors to re-enable.');
1904
+ }
401
1905
  try {
402
- console.error('šŸ”„ Reloading...');
1906
+ this.log('info', 'Reloading Photon');
403
1907
  // Store old instance in case we need to rollback
404
1908
  const oldInstance = this.mcp;
405
1909
  // Call shutdown hook on old instance (but keep it for rollback)
@@ -408,7 +1912,7 @@ export class PhotonServer {
408
1912
  await oldInstance.instance.onShutdown();
409
1913
  }
410
1914
  catch (shutdownError) {
411
- console.error(`āš ļø Shutdown hook failed: ${shutdownError.message}`);
1915
+ this.log('warn', 'Shutdown hook failed during reload', { error: shutdownError.message });
412
1916
  // Continue with reload anyway
413
1917
  }
414
1918
  }
@@ -417,40 +1921,70 @@ export class PhotonServer {
417
1921
  // Success! Update instance and reset failure count
418
1922
  this.mcp = newMcp;
419
1923
  this.reloadFailureCount = 0;
1924
+ this.hotReloadDisabled = false;
1925
+ this.lastReloadError = undefined;
420
1926
  // Send list_changed notifications to inform client of updates
421
1927
  await this.notifyListsChanged();
422
- console.error('āœ… Reload complete');
1928
+ // If daemon is running for this photon, reload it too
1929
+ if (this.daemonName && isDaemonRunning(this.daemonName)) {
1930
+ try {
1931
+ const result = await reloadDaemon(this.daemonName, this.options.filePath);
1932
+ if (result.success) {
1933
+ this.log('info', 'Daemon reloaded', { sessionsUpdated: result.sessionsUpdated });
1934
+ }
1935
+ else {
1936
+ this.log('warn', 'Daemon reload failed', { error: result.error });
1937
+ }
1938
+ }
1939
+ catch (err) {
1940
+ this.log('warn', 'Daemon reload failed, may need manual restart', {
1941
+ error: getErrorMessage(err),
1942
+ });
1943
+ }
1944
+ }
1945
+ await this.broadcastReloadStatus('info', 'Hot reload complete');
1946
+ this.log('info', 'Reload complete');
423
1947
  }
424
1948
  catch (error) {
425
1949
  this.reloadFailureCount++;
426
- console.error(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
427
- console.error(`āŒ Reload failed (attempt ${this.reloadFailureCount}/${this.MAX_RELOAD_FAILURES})`);
428
- console.error(`Error: ${error.message}`);
429
- console.error(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
430
- if (error.name === 'PhotonInitializationError') {
431
- console.error(`\nšŸ’” The onInitialize() lifecycle hook failed.`);
432
- console.error(` Common causes:`);
433
- console.error(` - Database connection failure`);
434
- console.error(` - API authentication error`);
435
- console.error(` - Missing environment variables`);
436
- console.error(` - Invalid configuration`);
1950
+ this.log('error', 'Reload failed', {
1951
+ attempt: this.reloadFailureCount,
1952
+ maxAttempts: this.MAX_RELOAD_FAILURES,
1953
+ error: getErrorMessage(error),
1954
+ });
1955
+ if (error instanceof Error && error.name === 'PhotonInitializationError') {
1956
+ this.log('warn', 'onInitialize lifecycle hook failed', {
1957
+ hints: [
1958
+ 'Database connection failure',
1959
+ 'API authentication error',
1960
+ 'Missing environment variables',
1961
+ 'Invalid configuration',
1962
+ ],
1963
+ });
437
1964
  }
1965
+ this.lastReloadError = {
1966
+ message: getErrorMessage(error),
1967
+ stack: error instanceof Error ? error.stack : undefined,
1968
+ timestamp: Date.now(),
1969
+ attempts: this.reloadFailureCount,
1970
+ };
1971
+ await this.broadcastReloadStatus('error', `Hot reload failed: ${getErrorMessage(error)}`);
438
1972
  if (this.reloadFailureCount >= this.MAX_RELOAD_FAILURES) {
439
- console.error(`\nāš ļø Maximum reload failures reached (${this.MAX_RELOAD_FAILURES})`);
440
- console.error(` Keeping previous working version active.`);
441
- console.error(` Fix the errors and save the file again to retry.`);
442
- console.error(`\n Or restart the server: photon mcp <name> --dev`);
443
- // Reset counter after cooling off
1973
+ this.log('error', 'Maximum reload failures reached', {
1974
+ maxAttempts: this.MAX_RELOAD_FAILURES,
1975
+ action: 'keeping previous version active',
1976
+ });
1977
+ this.log('info', 'Server still running with previous version');
1978
+ this.hotReloadDisabled = true;
444
1979
  this.reloadFailureCount = 0;
1980
+ throw new HotReloadDisabledError('Hot reload disabled after repeated failures. Restart Photon dev server once the errors are resolved.');
445
1981
  }
446
- else {
447
- // Schedule automatic retry
448
- const retryDelay = Math.min(5000 * this.reloadFailureCount, 15000); // 5s, 10s, 15s max
449
- console.error(`\nšŸ”„ Will retry reload in ${retryDelay / 1000}s if file changes again...`);
450
- console.error(` Previous working version remains active.`);
451
- }
452
- // Keep the old instance running - it's still functional
453
- console.error(`\nāœ“ Server still running with previous version`);
1982
+ const retryDelay = Math.min(5000 * this.reloadFailureCount, 15000);
1983
+ this.log('warn', 'Reload failed - waiting for next change', {
1984
+ retrySeconds: retryDelay / 1000,
1985
+ });
1986
+ this.log('info', 'Server still running with previous version');
1987
+ throw error;
454
1988
  }
455
1989
  }
456
1990
  /**
@@ -471,11 +2005,13 @@ export class PhotonServer {
471
2005
  await this.server.notification({
472
2006
  method: 'notifications/resources/list_changed',
473
2007
  });
474
- console.error('Sent list_changed notifications');
2008
+ this.log('debug', 'Sent list_changed notifications');
475
2009
  }
476
2010
  catch (error) {
477
2011
  // Notification sending is best-effort - don't fail reload if it fails
478
- console.error(`Warning: Failed to send list_changed notifications: ${error.message}`);
2012
+ this.log('warn', 'Failed to send list_changed notifications', {
2013
+ error: getErrorMessage(error),
2014
+ });
479
2015
  }
480
2016
  }
481
2017
  }