@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.
- package/README.md +326 -1177
- package/dist/auto-ui/beam.d.ts +14 -0
- package/dist/auto-ui/beam.d.ts.map +1 -0
- package/dist/auto-ui/beam.js +3057 -0
- package/dist/auto-ui/beam.js.map +1 -0
- package/dist/auto-ui/bridge/index.d.ts +37 -0
- package/dist/auto-ui/bridge/index.d.ts.map +1 -0
- package/dist/auto-ui/bridge/index.js +555 -0
- package/dist/auto-ui/bridge/index.js.map +1 -0
- package/dist/auto-ui/bridge/openai-shim.d.ts +20 -0
- package/dist/auto-ui/bridge/openai-shim.d.ts.map +1 -0
- package/dist/auto-ui/bridge/openai-shim.js +231 -0
- package/dist/auto-ui/bridge/openai-shim.js.map +1 -0
- package/dist/auto-ui/bridge/photon-app.d.ts +162 -0
- package/dist/auto-ui/bridge/photon-app.d.ts.map +1 -0
- package/dist/auto-ui/bridge/photon-app.js +460 -0
- package/dist/auto-ui/bridge/photon-app.js.map +1 -0
- package/dist/auto-ui/bridge/types.d.ts +128 -0
- package/dist/auto-ui/bridge/types.d.ts.map +1 -0
- package/dist/auto-ui/bridge/types.js +7 -0
- package/dist/auto-ui/bridge/types.js.map +1 -0
- package/dist/auto-ui/components/card.d.ts +13 -0
- package/dist/auto-ui/components/card.d.ts.map +1 -0
- package/dist/auto-ui/components/card.js +64 -0
- package/dist/auto-ui/components/card.js.map +1 -0
- package/dist/auto-ui/components/form.d.ts +15 -0
- package/dist/auto-ui/components/form.d.ts.map +1 -0
- package/dist/auto-ui/components/form.js +72 -0
- package/dist/auto-ui/components/form.js.map +1 -0
- package/dist/auto-ui/components/list.d.ts +13 -0
- package/dist/auto-ui/components/list.d.ts.map +1 -0
- package/dist/auto-ui/components/list.js +58 -0
- package/dist/auto-ui/components/list.js.map +1 -0
- package/dist/auto-ui/components/progress.d.ts +18 -0
- package/dist/auto-ui/components/progress.d.ts.map +1 -0
- package/dist/auto-ui/components/progress.js +125 -0
- package/dist/auto-ui/components/progress.js.map +1 -0
- package/dist/auto-ui/components/table.d.ts +13 -0
- package/dist/auto-ui/components/table.d.ts.map +1 -0
- package/dist/auto-ui/components/table.js +82 -0
- package/dist/auto-ui/components/table.js.map +1 -0
- package/dist/auto-ui/components/tree.d.ts +13 -0
- package/dist/auto-ui/components/tree.d.ts.map +1 -0
- package/dist/auto-ui/components/tree.js +61 -0
- package/dist/auto-ui/components/tree.js.map +1 -0
- package/dist/auto-ui/daemon-tools.d.ts +45 -0
- package/dist/auto-ui/daemon-tools.d.ts.map +1 -0
- package/dist/auto-ui/daemon-tools.js +580 -0
- package/dist/auto-ui/daemon-tools.js.map +1 -0
- package/dist/auto-ui/design-system/index.d.ts +21 -0
- package/dist/auto-ui/design-system/index.d.ts.map +1 -0
- package/dist/auto-ui/design-system/index.js +27 -0
- package/dist/auto-ui/design-system/index.js.map +1 -0
- package/dist/auto-ui/design-system/tokens.d.ts +9 -0
- package/dist/auto-ui/design-system/tokens.d.ts.map +1 -0
- package/dist/auto-ui/design-system/tokens.js +27 -0
- package/dist/auto-ui/design-system/tokens.js.map +1 -0
- package/dist/auto-ui/design-system/transaction-ui.d.ts +70 -0
- package/dist/auto-ui/design-system/transaction-ui.d.ts.map +1 -0
- package/dist/auto-ui/design-system/transaction-ui.js +982 -0
- package/dist/auto-ui/design-system/transaction-ui.js.map +1 -0
- package/dist/auto-ui/frontend/index.html +84 -0
- package/dist/auto-ui/index.d.ts +23 -0
- package/dist/auto-ui/index.d.ts.map +1 -0
- package/dist/auto-ui/index.js +28 -0
- package/dist/auto-ui/index.js.map +1 -0
- package/dist/auto-ui/openapi-generator.d.ts +71 -0
- package/dist/auto-ui/openapi-generator.d.ts.map +1 -0
- package/dist/auto-ui/openapi-generator.js +223 -0
- package/dist/auto-ui/openapi-generator.js.map +1 -0
- package/dist/auto-ui/photon-bridge.d.ts +159 -0
- package/dist/auto-ui/photon-bridge.d.ts.map +1 -0
- package/dist/auto-ui/photon-bridge.js +262 -0
- package/dist/auto-ui/photon-bridge.js.map +1 -0
- package/dist/auto-ui/photon-host.d.ts +113 -0
- package/dist/auto-ui/photon-host.d.ts.map +1 -0
- package/dist/auto-ui/photon-host.js +284 -0
- package/dist/auto-ui/photon-host.js.map +1 -0
- package/dist/auto-ui/platform-compat.d.ts +71 -0
- package/dist/auto-ui/platform-compat.d.ts.map +1 -0
- package/dist/auto-ui/platform-compat.js +628 -0
- package/dist/auto-ui/platform-compat.js.map +1 -0
- package/dist/auto-ui/playground-html.d.ts +15 -0
- package/dist/auto-ui/playground-html.d.ts.map +1 -0
- package/dist/auto-ui/playground-html.js +1113 -0
- package/dist/auto-ui/playground-html.js.map +1 -0
- package/dist/auto-ui/playground-server.d.ts +7 -0
- package/dist/auto-ui/playground-server.d.ts.map +1 -0
- package/dist/auto-ui/playground-server.js +840 -0
- package/dist/auto-ui/playground-server.js.map +1 -0
- package/dist/auto-ui/registry.d.ts +13 -0
- package/dist/auto-ui/registry.d.ts.map +1 -0
- package/dist/auto-ui/registry.js +62 -0
- package/dist/auto-ui/registry.js.map +1 -0
- package/dist/auto-ui/renderer.d.ts +14 -0
- package/dist/auto-ui/renderer.d.ts.map +1 -0
- package/dist/auto-ui/renderer.js +88 -0
- package/dist/auto-ui/renderer.js.map +1 -0
- package/dist/auto-ui/rendering/components.d.ts +29 -0
- package/dist/auto-ui/rendering/components.d.ts.map +1 -0
- package/dist/auto-ui/rendering/components.js +773 -0
- package/dist/auto-ui/rendering/components.js.map +1 -0
- package/dist/auto-ui/rendering/field-analyzer.d.ts +48 -0
- package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -0
- package/dist/auto-ui/rendering/field-analyzer.js +270 -0
- package/dist/auto-ui/rendering/field-analyzer.js.map +1 -0
- package/dist/auto-ui/rendering/field-renderers.d.ts +64 -0
- package/dist/auto-ui/rendering/field-renderers.d.ts.map +1 -0
- package/dist/auto-ui/rendering/field-renderers.js +317 -0
- package/dist/auto-ui/rendering/field-renderers.js.map +1 -0
- package/dist/auto-ui/rendering/index.d.ts +28 -0
- package/dist/auto-ui/rendering/index.d.ts.map +1 -0
- package/dist/auto-ui/rendering/index.js +60 -0
- package/dist/auto-ui/rendering/index.js.map +1 -0
- package/dist/auto-ui/rendering/layout-selector.d.ts +48 -0
- package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -0
- package/dist/auto-ui/rendering/layout-selector.js +352 -0
- package/dist/auto-ui/rendering/layout-selector.js.map +1 -0
- package/dist/auto-ui/rendering/template-engine.d.ts +41 -0
- package/dist/auto-ui/rendering/template-engine.d.ts.map +1 -0
- package/dist/auto-ui/rendering/template-engine.js +238 -0
- package/dist/auto-ui/rendering/template-engine.js.map +1 -0
- package/dist/auto-ui/streamable-http-transport.d.ts +103 -0
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -0
- package/dist/auto-ui/streamable-http-transport.js +1875 -0
- package/dist/auto-ui/streamable-http-transport.js.map +1 -0
- package/dist/auto-ui/types.d.ts +384 -0
- package/dist/auto-ui/types.d.ts.map +1 -0
- package/dist/auto-ui/types.js +92 -0
- package/dist/auto-ui/types.js.map +1 -0
- package/dist/beam.bundle.js +63137 -0
- package/dist/beam.bundle.js.map +7 -0
- package/dist/claude-code-plugin.d.ts.map +1 -1
- package/dist/claude-code-plugin.js +30 -30
- package/dist/claude-code-plugin.js.map +1 -1
- package/dist/cli/commands/info.d.ts +11 -0
- package/dist/cli/commands/info.d.ts.map +1 -0
- package/dist/cli/commands/info.js +313 -0
- package/dist/cli/commands/info.js.map +1 -0
- package/dist/cli/commands/marketplace.d.ts +11 -0
- package/dist/cli/commands/marketplace.d.ts.map +1 -0
- package/dist/cli/commands/marketplace.js +198 -0
- package/dist/cli/commands/marketplace.js.map +1 -0
- package/dist/cli/commands/package-app.d.ts +9 -0
- package/dist/cli/commands/package-app.d.ts.map +1 -0
- package/dist/cli/commands/package-app.js +191 -0
- package/dist/cli/commands/package-app.js.map +1 -0
- package/dist/cli/commands/package.d.ts +11 -0
- package/dist/cli/commands/package.d.ts.map +1 -0
- package/dist/cli/commands/package.js +573 -0
- package/dist/cli/commands/package.js.map +1 -0
- package/dist/cli-alias.d.ts.map +1 -1
- package/dist/cli-alias.js +30 -28
- package/dist/cli-alias.js.map +1 -1
- package/dist/cli-formatter.d.ts +8 -24
- package/dist/cli-formatter.d.ts.map +1 -1
- package/dist/cli-formatter.js +8 -325
- package/dist/cli-formatter.js.map +1 -1
- package/dist/cli.d.ts +15 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1166 -1131
- package/dist/cli.js.map +1 -1
- package/dist/daemon/client.d.ts +84 -3
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +561 -11
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +51 -12
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +122 -61
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/protocol.d.ts +62 -6
- package/dist/daemon/protocol.d.ts.map +1 -1
- package/dist/daemon/protocol.js +76 -1
- package/dist/daemon/protocol.js.map +1 -1
- package/dist/daemon/server.d.ts +6 -6
- package/dist/daemon/server.js +743 -133
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/session-manager.d.ts +8 -1
- package/dist/daemon/session-manager.d.ts.map +1 -1
- package/dist/daemon/session-manager.js +32 -9
- package/dist/daemon/session-manager.js.map +1 -1
- package/dist/deploy/cloudflare.d.ts +12 -0
- package/dist/deploy/cloudflare.d.ts.map +1 -0
- package/dist/deploy/cloudflare.js +216 -0
- package/dist/deploy/cloudflare.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +191 -21
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +1186 -319
- package/dist/loader.js.map +1 -1
- package/dist/markdown-utils.d.ts +8 -0
- package/dist/markdown-utils.d.ts.map +1 -0
- package/dist/markdown-utils.js +63 -0
- package/dist/markdown-utils.js.map +1 -0
- package/dist/marketplace-manager.d.ts +10 -0
- package/dist/marketplace-manager.d.ts.map +1 -1
- package/dist/marketplace-manager.js +112 -28
- package/dist/marketplace-manager.js.map +1 -1
- package/dist/mcp-client.d.ts +9 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +11 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/mcp-elicitation.d.ts +32 -0
- package/dist/mcp-elicitation.d.ts.map +1 -0
- package/dist/mcp-elicitation.js +26 -0
- package/dist/mcp-elicitation.js.map +1 -0
- package/dist/path-resolver.d.ts +9 -12
- package/dist/path-resolver.d.ts.map +1 -1
- package/dist/path-resolver.js +13 -43
- package/dist/path-resolver.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +204 -77
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts +89 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +560 -32
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/photons/maker.photon.d.ts +182 -0
- package/dist/photons/maker.photon.d.ts.map +1 -0
- package/dist/photons/maker.photon.js +504 -0
- package/dist/photons/maker.photon.js.map +1 -0
- package/dist/photons/maker.photon.ts +626 -0
- package/dist/photons/marketplace.photon.d.ts +110 -0
- package/dist/photons/marketplace.photon.d.ts.map +1 -0
- package/dist/photons/marketplace.photon.js +260 -0
- package/dist/photons/marketplace.photon.js.map +1 -0
- package/dist/photons/marketplace.photon.ts +378 -0
- package/dist/photons/tunnel.photon.d.ts +80 -0
- package/dist/photons/tunnel.photon.d.ts.map +1 -0
- package/dist/photons/tunnel.photon.js +269 -0
- package/dist/photons/tunnel.photon.js.map +1 -0
- package/dist/photons/tunnel.photon.ts +345 -0
- package/dist/security-scanner.d.ts.map +1 -1
- package/dist/security-scanner.js +18 -15
- package/dist/security-scanner.js.map +1 -1
- package/dist/serv/auth/jwt.d.ts +89 -0
- package/dist/serv/auth/jwt.d.ts.map +1 -0
- package/dist/serv/auth/jwt.js +239 -0
- package/dist/serv/auth/jwt.js.map +1 -0
- package/dist/serv/auth/oauth.d.ts +117 -0
- package/dist/serv/auth/oauth.d.ts.map +1 -0
- package/dist/serv/auth/oauth.js +395 -0
- package/dist/serv/auth/oauth.js.map +1 -0
- package/dist/serv/auth/well-known.d.ts +60 -0
- package/dist/serv/auth/well-known.d.ts.map +1 -0
- package/dist/serv/auth/well-known.js +154 -0
- package/dist/serv/auth/well-known.js.map +1 -0
- package/dist/serv/db/d1-client.d.ts +65 -0
- package/dist/serv/db/d1-client.d.ts.map +1 -0
- package/dist/serv/db/d1-client.js +137 -0
- package/dist/serv/db/d1-client.js.map +1 -0
- package/dist/serv/db/d1-stores.d.ts +62 -0
- package/dist/serv/db/d1-stores.d.ts.map +1 -0
- package/dist/serv/db/d1-stores.js +307 -0
- package/dist/serv/db/d1-stores.js.map +1 -0
- package/dist/serv/index.d.ts +114 -0
- package/dist/serv/index.d.ts.map +1 -0
- package/dist/serv/index.js +172 -0
- package/dist/serv/index.js.map +1 -0
- package/dist/serv/local.d.ts +118 -0
- package/dist/serv/local.d.ts.map +1 -0
- package/dist/serv/local.js +392 -0
- package/dist/serv/local.js.map +1 -0
- package/dist/serv/middleware/auth.d.ts +66 -0
- package/dist/serv/middleware/auth.d.ts.map +1 -0
- package/dist/serv/middleware/auth.js +178 -0
- package/dist/serv/middleware/auth.js.map +1 -0
- package/dist/serv/middleware/tenant.d.ts +94 -0
- package/dist/serv/middleware/tenant.d.ts.map +1 -0
- package/dist/serv/middleware/tenant.js +152 -0
- package/dist/serv/middleware/tenant.js.map +1 -0
- package/dist/serv/runtime/executor.d.ts +76 -0
- package/dist/serv/runtime/executor.d.ts.map +1 -0
- package/dist/serv/runtime/executor.js +105 -0
- package/dist/serv/runtime/executor.js.map +1 -0
- package/dist/serv/runtime/index.d.ts +8 -0
- package/dist/serv/runtime/index.d.ts.map +1 -0
- package/dist/serv/runtime/index.js +10 -0
- package/dist/serv/runtime/index.js.map +1 -0
- package/dist/serv/runtime/oauth-context.d.ts +121 -0
- package/dist/serv/runtime/oauth-context.d.ts.map +1 -0
- package/dist/serv/runtime/oauth-context.js +153 -0
- package/dist/serv/runtime/oauth-context.js.map +1 -0
- package/dist/serv/session/kv-store.d.ts +54 -0
- package/dist/serv/session/kv-store.d.ts.map +1 -0
- package/dist/serv/session/kv-store.js +149 -0
- package/dist/serv/session/kv-store.js.map +1 -0
- package/dist/serv/session/store.d.ts +113 -0
- package/dist/serv/session/store.d.ts.map +1 -0
- package/dist/serv/session/store.js +284 -0
- package/dist/serv/session/store.js.map +1 -0
- package/dist/serv/types/index.d.ts +147 -0
- package/dist/serv/types/index.d.ts.map +1 -0
- package/dist/serv/types/index.js +8 -0
- package/dist/serv/types/index.js.map +1 -0
- package/dist/serv/vault/token-vault.d.ts +102 -0
- package/dist/serv/vault/token-vault.d.ts.map +1 -0
- package/dist/serv/vault/token-vault.js +177 -0
- package/dist/serv/vault/token-vault.js.map +1 -0
- package/dist/server.d.ts +184 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1995 -86
- package/dist/server.js.map +1 -1
- package/dist/shared/cli-sections.d.ts +6 -0
- package/dist/shared/cli-sections.d.ts.map +1 -0
- package/dist/shared/cli-sections.js +16 -0
- package/dist/shared/cli-sections.js.map +1 -0
- package/dist/shared/cli-utils.d.ts +81 -0
- package/dist/shared/cli-utils.d.ts.map +1 -0
- package/dist/shared/cli-utils.js +174 -0
- package/dist/shared/cli-utils.js.map +1 -0
- package/dist/shared/config-docs.d.ts +6 -0
- package/dist/shared/config-docs.d.ts.map +1 -0
- package/dist/shared/config-docs.js +6 -0
- package/dist/shared/config-docs.js.map +1 -0
- package/dist/shared/error-handler.d.ts +128 -0
- package/dist/shared/error-handler.d.ts.map +1 -0
- package/dist/shared/error-handler.js +342 -0
- package/dist/shared/error-handler.js.map +1 -0
- package/dist/shared/logger.d.ts +42 -0
- package/dist/shared/logger.d.ts.map +1 -0
- package/dist/shared/logger.js +123 -0
- package/dist/shared/logger.js.map +1 -0
- package/dist/shared/performance.d.ts +65 -0
- package/dist/shared/performance.d.ts.map +1 -0
- package/dist/shared/performance.js +136 -0
- package/dist/shared/performance.js.map +1 -0
- package/dist/shared/task-runner.d.ts +2 -0
- package/dist/shared/task-runner.d.ts.map +1 -0
- package/dist/shared/task-runner.js +16 -0
- package/dist/shared/task-runner.js.map +1 -0
- package/dist/shared/validation.d.ts +6 -0
- package/dist/shared/validation.d.ts.map +1 -0
- package/dist/shared/validation.js +6 -0
- package/dist/shared/validation.js.map +1 -0
- package/dist/shared-utils.d.ts +63 -0
- package/dist/shared-utils.d.ts.map +1 -0
- package/dist/shared-utils.js +123 -0
- package/dist/shared-utils.js.map +1 -0
- package/dist/template-manager.d.ts +23 -2
- package/dist/template-manager.d.ts.map +1 -1
- package/dist/template-manager.js +176 -87
- package/dist/template-manager.js.map +1 -1
- package/dist/test-client.d.ts.map +1 -1
- package/dist/test-client.js +10 -8
- package/dist/test-client.js.map +1 -1
- package/dist/test-runner.d.ts +52 -0
- package/dist/test-runner.d.ts.map +1 -0
- package/dist/test-runner.js +785 -0
- package/dist/test-runner.js.map +1 -0
- package/dist/testing.d.ts +103 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +163 -0
- package/dist/testing.js.map +1 -0
- package/dist/version-checker.d.ts.map +1 -1
- package/dist/version-checker.js +2 -2
- package/dist/version-checker.js.map +1 -1
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +21 -0
- package/dist/version.js.map +1 -0
- package/dist/watcher.d.ts +6 -3
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +49 -10
- package/dist/watcher.js.map +1 -1
- package/package.json +57 -7
- package/templates/cloudflare/worker.ts.template +381 -0
- package/templates/cloudflare/wrangler.toml.template +9 -0
- package/dist/base.d.ts +0 -58
- package/dist/base.d.ts.map +0 -1
- package/dist/base.js +0 -92
- package/dist/base.js.map +0 -1
- package/dist/dependency-manager.d.ts +0 -49
- package/dist/dependency-manager.d.ts.map +0 -1
- package/dist/dependency-manager.js +0 -165
- package/dist/dependency-manager.js.map +0 -1
- package/dist/registry-manager.d.ts +0 -76
- package/dist/registry-manager.d.ts.map +0 -1
- package/dist/registry-manager.js +0 -220
- package/dist/registry-manager.js.map +0 -1
- package/dist/schema-extractor.d.ts +0 -110
- package/dist/schema-extractor.d.ts.map +0 -1
- package/dist/schema-extractor.js +0 -727
- package/dist/schema-extractor.js.map +0 -1
- package/dist/test-marketplace-sources.d.ts +0 -5
- package/dist/test-marketplace-sources.d.ts.map +0 -1
- package/dist/test-marketplace-sources.js +0 -53
- package/dist/test-marketplace-sources.js.map +0 -1
- package/dist/types.d.ts +0 -109
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -12
- 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.
|
|
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:
|
|
84
|
+
version: PHOTON_VERSION,
|
|
22
85
|
}, {
|
|
23
86
|
capabilities: {
|
|
24
87
|
tools: {
|
|
@@ -30,43 +93,395 @@ 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 MCP Apps extension (io.modelcontextprotocol/ui)
|
|
129
|
+
// Claude Desktop and other MCP Apps clients use this format
|
|
130
|
+
const extensions = capabilities.extensions;
|
|
131
|
+
if (extensions?.['io.modelcontextprotocol/ui']) {
|
|
132
|
+
return 'sep-1865';
|
|
133
|
+
}
|
|
134
|
+
// Check for SEP-1865 UI capability
|
|
135
|
+
// SEP-1865 clients advertise: { experimental: { ui: {} } } or { ui: {} }
|
|
136
|
+
const experimental = capabilities.experimental;
|
|
137
|
+
if (experimental?.ui || capabilities.ui) {
|
|
138
|
+
return 'sep-1865';
|
|
139
|
+
}
|
|
140
|
+
// Check client info for known SEP-1865 compatible clients
|
|
141
|
+
const clientInfo = targetServer._clientVersion;
|
|
142
|
+
if (clientInfo?.name) {
|
|
143
|
+
const name = clientInfo.name.toLowerCase();
|
|
144
|
+
// Known SEP-1865 compatible clients
|
|
145
|
+
if (name.includes('claude') || name.includes('chatgpt') || name.includes('openai')) {
|
|
146
|
+
return 'sep-1865';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Default to Photon format for backward compatibility
|
|
150
|
+
return 'photon';
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Build UI resource URI based on detected format
|
|
154
|
+
*
|
|
155
|
+
* @param uiId - UI template identifier
|
|
156
|
+
* @param server - Optional server instance (for SSE sessions)
|
|
157
|
+
*/
|
|
158
|
+
buildUIResourceUri(uiId, server) {
|
|
159
|
+
const format = this.getUIFormat(server);
|
|
160
|
+
const photonName = this.mcp?.name || 'unknown';
|
|
161
|
+
switch (format) {
|
|
162
|
+
case 'sep-1865':
|
|
163
|
+
return `ui://${photonName}/${uiId}`;
|
|
164
|
+
case 'photon':
|
|
165
|
+
default:
|
|
166
|
+
return `photon://${photonName}/ui/${uiId}`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Build tool metadata for UI based on detected format
|
|
171
|
+
*
|
|
172
|
+
* @param uiId - UI template identifier
|
|
173
|
+
* @param server - Optional server instance (for SSE sessions)
|
|
174
|
+
*/
|
|
175
|
+
buildUIToolMeta(uiId, server) {
|
|
176
|
+
const format = this.getUIFormat(server);
|
|
177
|
+
const uri = this.buildUIResourceUri(uiId, server);
|
|
178
|
+
switch (format) {
|
|
179
|
+
case 'sep-1865':
|
|
180
|
+
// Official MCP Apps spec: _meta.ui.resourceUri
|
|
181
|
+
return { ui: { resourceUri: uri } };
|
|
182
|
+
case 'photon':
|
|
183
|
+
default:
|
|
184
|
+
return { outputTemplate: uri };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get UI mimeType based on detected format and client capabilities
|
|
189
|
+
*
|
|
190
|
+
* @param server - Optional server instance (for SSE sessions)
|
|
191
|
+
*/
|
|
192
|
+
getUIMimeType(server) {
|
|
193
|
+
const targetServer = server || this.server;
|
|
194
|
+
const capabilities = targetServer.getClientCapabilities();
|
|
195
|
+
// Check for MCP Apps extension with declared mimeTypes
|
|
196
|
+
// Claude Desktop uses: { extensions: { "io.modelcontextprotocol/ui": { mimeTypes: ["text/html;profile=mcp-app"] } } }
|
|
197
|
+
const extensions = capabilities?.extensions;
|
|
198
|
+
const mcpUI = extensions?.['io.modelcontextprotocol/ui'];
|
|
199
|
+
if (mcpUI?.mimeTypes?.[0]) {
|
|
200
|
+
return mcpUI.mimeTypes[0];
|
|
201
|
+
}
|
|
202
|
+
const format = this.getUIFormat(server);
|
|
203
|
+
return format === 'sep-1865' ? 'text/html;profile=mcp-app' : 'text/html';
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Check if client supports elicitation
|
|
207
|
+
*
|
|
208
|
+
* Elicitation is a client capability declared during initialization.
|
|
209
|
+
* The server can use elicitInput() when the client supports it.
|
|
210
|
+
*/
|
|
211
|
+
clientSupportsElicitation(server) {
|
|
212
|
+
const targetServer = server || this.server;
|
|
213
|
+
const capabilities = targetServer.getClientCapabilities();
|
|
214
|
+
if (!capabilities) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
// Check for elicitation capability (MCP 2025-06 spec)
|
|
218
|
+
return !!capabilities.elicitation;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Create an MCP-aware input provider for generator ask yields
|
|
222
|
+
*
|
|
223
|
+
* Uses MCP elicitInput() when client supports elicitation,
|
|
224
|
+
* otherwise falls back to readline prompts.
|
|
225
|
+
*/
|
|
226
|
+
createMCPInputProvider(server) {
|
|
227
|
+
const targetServer = server || this.server;
|
|
228
|
+
const capabilities = targetServer.getClientCapabilities();
|
|
229
|
+
const supportsElicitation = this.clientSupportsElicitation(server);
|
|
230
|
+
this.log('debug', 'Creating MCP input provider', {
|
|
231
|
+
supportsElicitation,
|
|
232
|
+
capabilities: JSON.stringify(capabilities),
|
|
233
|
+
});
|
|
234
|
+
return async (ask) => {
|
|
235
|
+
// If client doesn't support elicitation, fall back to logging the ask
|
|
236
|
+
// (MCP servers can't use readline - they communicate via protocol)
|
|
237
|
+
if (!supportsElicitation) {
|
|
238
|
+
this.log('warn', `Client doesn't support elicitation, ask will be skipped`, {
|
|
239
|
+
ask: ask.ask,
|
|
240
|
+
message: ask.message,
|
|
241
|
+
});
|
|
242
|
+
// Return default values for non-elicitation clients
|
|
243
|
+
return this.getDefaultForAsk(ask);
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
// Build elicitation request based on ask type
|
|
247
|
+
const elicitParams = this.buildElicitParams(ask);
|
|
248
|
+
// Call server.elicitInput() to request user input from the client
|
|
249
|
+
const result = await targetServer.elicitInput(elicitParams);
|
|
250
|
+
if (result.action === 'accept' && result.content) {
|
|
251
|
+
// Extract the value from the response content
|
|
252
|
+
return this.extractElicitValue(ask, result.content);
|
|
253
|
+
}
|
|
254
|
+
else if (result.action === 'decline' || result.action === 'cancel') {
|
|
255
|
+
this.log('info', `User ${result.action}ed elicitation`, { ask: ask.ask });
|
|
256
|
+
return this.getDefaultForAsk(ask);
|
|
257
|
+
}
|
|
258
|
+
return this.getDefaultForAsk(ask);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
this.log('error', `Elicitation failed`, { ask: ask.ask, error: getErrorMessage(error) });
|
|
262
|
+
return this.getDefaultForAsk(ask);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Build MCP elicit request params from a Photon ask yield
|
|
268
|
+
*/
|
|
269
|
+
buildElicitParams(ask) {
|
|
270
|
+
const baseMessage = ask.message || 'Please provide input';
|
|
271
|
+
switch (ask.ask) {
|
|
272
|
+
case 'text':
|
|
273
|
+
case 'password':
|
|
274
|
+
return {
|
|
275
|
+
mode: 'form',
|
|
276
|
+
message: baseMessage,
|
|
277
|
+
requestedSchema: {
|
|
278
|
+
type: 'object',
|
|
279
|
+
properties: {
|
|
280
|
+
value: {
|
|
281
|
+
type: 'string',
|
|
282
|
+
title: ask.label || 'Input',
|
|
283
|
+
description: ask.hint || ask.message,
|
|
284
|
+
default: ask.default,
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
required: ask.required !== false ? ['value'] : [],
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
case 'confirm':
|
|
291
|
+
return {
|
|
292
|
+
mode: 'form',
|
|
293
|
+
message: baseMessage,
|
|
294
|
+
requestedSchema: {
|
|
295
|
+
type: 'object',
|
|
296
|
+
properties: {
|
|
297
|
+
confirmed: {
|
|
298
|
+
type: 'boolean',
|
|
299
|
+
title: 'Confirm',
|
|
300
|
+
description: ask.message,
|
|
301
|
+
default: ask.default ?? false,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
required: ['confirmed'],
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
case 'number':
|
|
308
|
+
return {
|
|
309
|
+
mode: 'form',
|
|
310
|
+
message: baseMessage,
|
|
311
|
+
requestedSchema: {
|
|
312
|
+
type: 'object',
|
|
313
|
+
properties: {
|
|
314
|
+
value: {
|
|
315
|
+
type: 'number',
|
|
316
|
+
title: ask.label || 'Number',
|
|
317
|
+
description: ask.hint || ask.message,
|
|
318
|
+
default: ask.default,
|
|
319
|
+
minimum: ask.min,
|
|
320
|
+
maximum: ask.max,
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
required: ask.required !== false ? ['value'] : [],
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
case 'select':
|
|
327
|
+
// For select, we use enum in the schema
|
|
328
|
+
const options = (ask.options || []).map((o) => (typeof o === 'string' ? o : o.value));
|
|
329
|
+
const labels = (ask.options || []).map((o) => (typeof o === 'string' ? o : o.label));
|
|
330
|
+
return {
|
|
331
|
+
mode: 'form',
|
|
332
|
+
message: baseMessage + (ask.multi ? ' (select multiple)' : ''),
|
|
333
|
+
requestedSchema: {
|
|
334
|
+
type: 'object',
|
|
335
|
+
properties: {
|
|
336
|
+
selection: ask.multi
|
|
337
|
+
? {
|
|
338
|
+
type: 'array',
|
|
339
|
+
items: { type: 'string', enum: options },
|
|
340
|
+
title: ask.label || 'Selection',
|
|
341
|
+
description: `Options: ${labels.join(', ')}`,
|
|
342
|
+
}
|
|
343
|
+
: {
|
|
344
|
+
type: 'string',
|
|
345
|
+
enum: options,
|
|
346
|
+
title: ask.label || 'Selection',
|
|
347
|
+
description: `Options: ${labels.join(', ')}`,
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
required: ask.required !== false ? ['selection'] : [],
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
case 'date':
|
|
354
|
+
return {
|
|
355
|
+
mode: 'form',
|
|
356
|
+
message: baseMessage,
|
|
357
|
+
requestedSchema: {
|
|
358
|
+
type: 'object',
|
|
359
|
+
properties: {
|
|
360
|
+
value: {
|
|
361
|
+
type: 'string',
|
|
362
|
+
format: 'date',
|
|
363
|
+
title: ask.label || 'Date',
|
|
364
|
+
description: ask.hint || ask.message,
|
|
365
|
+
default: ask.default,
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
required: ask.required !== false ? ['value'] : [],
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
default:
|
|
372
|
+
// Generic text input for unknown types
|
|
373
|
+
return {
|
|
374
|
+
mode: 'form',
|
|
375
|
+
message: baseMessage,
|
|
376
|
+
requestedSchema: {
|
|
377
|
+
type: 'object',
|
|
378
|
+
properties: {
|
|
379
|
+
value: {
|
|
380
|
+
type: 'string',
|
|
381
|
+
title: 'Input',
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Extract value from elicitation response content
|
|
390
|
+
*/
|
|
391
|
+
extractElicitValue(ask, content) {
|
|
392
|
+
switch (ask.ask) {
|
|
393
|
+
case 'confirm':
|
|
394
|
+
return content.confirmed ?? false;
|
|
395
|
+
case 'select':
|
|
396
|
+
return content.selection;
|
|
397
|
+
default:
|
|
398
|
+
return content.value;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Get default value for an ask when elicitation is not available or declined
|
|
403
|
+
*/
|
|
404
|
+
getDefaultForAsk(ask) {
|
|
405
|
+
if ('default' in ask) {
|
|
406
|
+
return ask.default;
|
|
407
|
+
}
|
|
408
|
+
switch (ask.ask) {
|
|
409
|
+
case 'confirm':
|
|
410
|
+
return false;
|
|
411
|
+
case 'number':
|
|
412
|
+
return 0;
|
|
413
|
+
case 'select':
|
|
414
|
+
return ask.multi ? [] : null;
|
|
415
|
+
case 'date':
|
|
416
|
+
return new Date().toISOString().split('T')[0];
|
|
417
|
+
default:
|
|
418
|
+
return '';
|
|
419
|
+
}
|
|
420
|
+
}
|
|
38
421
|
/**
|
|
39
422
|
* Set up MCP protocol handlers
|
|
40
423
|
*/
|
|
41
424
|
setupHandlers() {
|
|
42
425
|
// Handle tools/list
|
|
43
426
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
427
|
+
// If photon is unresolved (conflict), return placeholder tools from manifest metadata
|
|
428
|
+
if (!this.mcp && this.options.unresolvedPhoton) {
|
|
429
|
+
return { tools: this.buildPlaceholderTools() };
|
|
430
|
+
}
|
|
44
431
|
if (!this.mcp) {
|
|
45
432
|
return { tools: [] };
|
|
46
433
|
}
|
|
47
434
|
return {
|
|
48
|
-
tools: this.mcp.tools.map(tool =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
435
|
+
tools: this.mcp.tools.map((tool) => {
|
|
436
|
+
const toolDef = {
|
|
437
|
+
name: tool.name,
|
|
438
|
+
description: tool.description,
|
|
439
|
+
inputSchema: tool.inputSchema,
|
|
440
|
+
};
|
|
441
|
+
// Add _meta with UI template reference (format depends on client capabilities)
|
|
442
|
+
const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name);
|
|
443
|
+
if (linkedUI) {
|
|
444
|
+
toolDef._meta = this.buildUIToolMeta(linkedUI.id);
|
|
445
|
+
}
|
|
446
|
+
return toolDef;
|
|
447
|
+
}),
|
|
53
448
|
};
|
|
54
449
|
});
|
|
55
450
|
// Handle tools/call
|
|
56
451
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
452
|
+
const { name: toolName, arguments: args } = request.params;
|
|
453
|
+
// Deferred conflict resolution: resolve photon on first tool call
|
|
454
|
+
if (!this.mcp && this.options.unresolvedPhoton) {
|
|
455
|
+
await this.resolveUnresolvedPhoton();
|
|
456
|
+
}
|
|
57
457
|
if (!this.mcp) {
|
|
58
458
|
throw new Error('MCP not loaded');
|
|
59
459
|
}
|
|
60
|
-
const { name: toolName, arguments: args } = request.params;
|
|
61
460
|
try {
|
|
62
|
-
|
|
461
|
+
// Create MCP-aware input provider for elicitation support
|
|
462
|
+
const inputProvider = this.createMCPInputProvider();
|
|
463
|
+
// Handler for channel events - forward to daemon for cross-process pub/sub
|
|
464
|
+
const outputHandler = (emit) => {
|
|
465
|
+
if (this.daemonName && emit?.channel) {
|
|
466
|
+
publishToChannel(this.daemonName, emit.channel, emit).catch(() => {
|
|
467
|
+
// Ignore publish errors - daemon may not be running
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
const result = await this.loader.executeTool(this.mcp, toolName, args || {}, {
|
|
472
|
+
inputProvider,
|
|
473
|
+
outputHandler,
|
|
474
|
+
});
|
|
63
475
|
// Find the tool to get its outputFormat
|
|
64
|
-
const tool = this.mcp.tools.find(t => t.name === toolName);
|
|
476
|
+
const tool = this.mcp.tools.find((t) => t.name === toolName);
|
|
65
477
|
const outputFormat = tool?.outputFormat;
|
|
478
|
+
// Check if this was a stateful workflow execution
|
|
479
|
+
const isStateful = result && typeof result === 'object' && result._stateful === true;
|
|
480
|
+
const actualResult = isStateful ? result.result : result;
|
|
66
481
|
// Build content with optional mimeType annotation
|
|
67
482
|
const content = {
|
|
68
483
|
type: 'text',
|
|
69
|
-
text: this.formatResult(
|
|
484
|
+
text: this.formatResult(actualResult),
|
|
70
485
|
};
|
|
71
486
|
// Add mimeType annotation if outputFormat is a content type
|
|
72
487
|
if (outputFormat) {
|
|
@@ -76,9 +491,34 @@ export class PhotonServer {
|
|
|
76
491
|
content.annotations = { mimeType };
|
|
77
492
|
}
|
|
78
493
|
}
|
|
494
|
+
// For stateful workflows, add run ID as a separate content block
|
|
495
|
+
// This allows the AI to inform the user about the workflow run
|
|
496
|
+
if (isStateful && result.runId) {
|
|
497
|
+
const workflowInfo = {
|
|
498
|
+
type: 'text',
|
|
499
|
+
text: `\n\n---\nš **Workflow Run**: ${result.runId}\n` +
|
|
500
|
+
`Status: ${result.status}${result.resumed ? ' (resumed)' : ''}\n` +
|
|
501
|
+
`This is a stateful workflow. To resume if interrupted, use run ID: ${result.runId}`,
|
|
502
|
+
};
|
|
503
|
+
return { content: [content, workflowInfo] };
|
|
504
|
+
}
|
|
79
505
|
return { content: [content] };
|
|
80
506
|
}
|
|
81
507
|
catch (error) {
|
|
508
|
+
// Check for config error ā attempt elicitation to resolve missing env vars
|
|
509
|
+
const errorMsg = getErrorMessage(error);
|
|
510
|
+
if (this.mcp?.instance?._photonConfigError &&
|
|
511
|
+
this.clientSupportsElicitation()) {
|
|
512
|
+
const retryResult = await this.attemptConfigElicitation(toolName, args || {});
|
|
513
|
+
if (retryResult)
|
|
514
|
+
return retryResult;
|
|
515
|
+
}
|
|
516
|
+
// Log error with context for debugging
|
|
517
|
+
this.log('error', 'Tool execution failed', {
|
|
518
|
+
tool: toolName,
|
|
519
|
+
error: errorMsg,
|
|
520
|
+
args: this.options.devMode ? args : undefined,
|
|
521
|
+
});
|
|
82
522
|
// Format error for AI consumption
|
|
83
523
|
return this.formatError(error, toolName, args);
|
|
84
524
|
}
|
|
@@ -89,12 +529,14 @@ export class PhotonServer {
|
|
|
89
529
|
return { prompts: [] };
|
|
90
530
|
}
|
|
91
531
|
return {
|
|
92
|
-
prompts: this.mcp.templates.map(template => ({
|
|
532
|
+
prompts: this.mcp.templates.map((template) => ({
|
|
93
533
|
name: template.name,
|
|
94
534
|
description: template.description,
|
|
95
535
|
arguments: Object.entries(template.inputSchema.properties || {}).map(([name, schema]) => ({
|
|
96
536
|
name,
|
|
97
|
-
description: schema
|
|
537
|
+
description: (typeof schema === 'object' && schema && 'description' in schema
|
|
538
|
+
? schema.description
|
|
539
|
+
: '') || '',
|
|
98
540
|
required: template.inputSchema.required?.includes(name) || false,
|
|
99
541
|
})),
|
|
100
542
|
})),
|
|
@@ -107,7 +549,7 @@ export class PhotonServer {
|
|
|
107
549
|
}
|
|
108
550
|
const { name: promptName, arguments: args } = request.params;
|
|
109
551
|
// Find the template
|
|
110
|
-
const template = this.mcp.templates.find(t => t.name === promptName);
|
|
552
|
+
const template = this.mcp.templates.find((t) => t.name === promptName);
|
|
111
553
|
if (!template) {
|
|
112
554
|
throw new Error(`Prompt not found: ${promptName}`);
|
|
113
555
|
}
|
|
@@ -118,7 +560,11 @@ export class PhotonServer {
|
|
|
118
560
|
return this.formatTemplateResult(result);
|
|
119
561
|
}
|
|
120
562
|
catch (error) {
|
|
121
|
-
|
|
563
|
+
this.log('error', 'Prompt execution failed', {
|
|
564
|
+
prompt: promptName,
|
|
565
|
+
error: getErrorMessage(error),
|
|
566
|
+
});
|
|
567
|
+
throw new Error(`Failed to get prompt: ${getErrorMessage(error)}`);
|
|
122
568
|
}
|
|
123
569
|
});
|
|
124
570
|
// Handle resources/list (static URIs only, no parameters)
|
|
@@ -127,15 +573,49 @@ export class PhotonServer {
|
|
|
127
573
|
return { resources: [] };
|
|
128
574
|
}
|
|
129
575
|
// Only return resources with static URIs (no {parameters})
|
|
130
|
-
const staticResources = this.mcp.statics.filter(s => !this.isUriTemplate(s.uri));
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
576
|
+
const staticResources = this.mcp.statics.filter((s) => !this.isUriTemplate(s.uri));
|
|
577
|
+
const resources = staticResources.map((static_) => ({
|
|
578
|
+
uri: static_.uri,
|
|
579
|
+
name: static_.name,
|
|
580
|
+
description: static_.description,
|
|
581
|
+
mimeType: static_.mimeType || 'text/plain',
|
|
582
|
+
}));
|
|
583
|
+
// Add assets from asset folder (UI, prompts, resources)
|
|
584
|
+
if (this.mcp.assets) {
|
|
585
|
+
const photonName = this.mcp.name;
|
|
586
|
+
// Add UI assets (format depends on client capabilities)
|
|
587
|
+
for (const ui of this.mcp.assets.ui) {
|
|
588
|
+
// Use pre-generated URI from loader, or build one
|
|
589
|
+
const uiUri = ui.uri || this.buildUIResourceUri(ui.id);
|
|
590
|
+
resources.push({
|
|
591
|
+
uri: uiUri,
|
|
592
|
+
name: `ui:${ui.id}`,
|
|
593
|
+
description: ui.linkedTool
|
|
594
|
+
? `UI template for ${ui.linkedTool} tool`
|
|
595
|
+
: `UI template: ${ui.id}`,
|
|
596
|
+
mimeType: ui.mimeType || this.getUIMimeType(),
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
// Add prompt assets
|
|
600
|
+
for (const prompt of this.mcp.assets.prompts) {
|
|
601
|
+
resources.push({
|
|
602
|
+
uri: `photon://${photonName}/prompts/${prompt.id}`,
|
|
603
|
+
name: `prompt:${prompt.id}`,
|
|
604
|
+
description: prompt.description || `Prompt template: ${prompt.id}`,
|
|
605
|
+
mimeType: 'text/markdown',
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
// Add resource assets
|
|
609
|
+
for (const resource of this.mcp.assets.resources) {
|
|
610
|
+
resources.push({
|
|
611
|
+
uri: `photon://${photonName}/resources/${resource.id}`,
|
|
612
|
+
name: `resource:${resource.id}`,
|
|
613
|
+
description: resource.description || `Static resource: ${resource.id}`,
|
|
614
|
+
mimeType: resource.mimeType || 'application/octet-stream',
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return { resources };
|
|
139
619
|
});
|
|
140
620
|
// Handle resources/templates/list (parameterized URIs)
|
|
141
621
|
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
@@ -143,9 +623,9 @@ export class PhotonServer {
|
|
|
143
623
|
return { resourceTemplates: [] };
|
|
144
624
|
}
|
|
145
625
|
// Only return resources with URI templates (has {parameters})
|
|
146
|
-
const templateResources = this.mcp.statics.filter(s => this.isUriTemplate(s.uri));
|
|
626
|
+
const templateResources = this.mcp.statics.filter((s) => this.isUriTemplate(s.uri));
|
|
147
627
|
return {
|
|
148
|
-
resourceTemplates: templateResources.map(static_ => ({
|
|
628
|
+
resourceTemplates: templateResources.map((static_) => ({
|
|
149
629
|
uriTemplate: static_.uri,
|
|
150
630
|
name: static_.name,
|
|
151
631
|
description: static_.description,
|
|
@@ -159,22 +639,19 @@ export class PhotonServer {
|
|
|
159
639
|
throw new Error('MCP not loaded');
|
|
160
640
|
}
|
|
161
641
|
const { uri } = request.params;
|
|
162
|
-
//
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
|
|
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);
|
|
642
|
+
// Check for SEP-1865 ui:// URI format
|
|
643
|
+
const uiMatch = uri.match(/^ui:\/\/([^/]+)\/(.+)$/);
|
|
644
|
+
if (uiMatch && this.mcp.assets) {
|
|
645
|
+
const [, _photonName, assetId] = uiMatch;
|
|
646
|
+
return this.handleUIAssetRead(uri, assetId);
|
|
174
647
|
}
|
|
175
|
-
|
|
176
|
-
|
|
648
|
+
// Check for legacy photon:// asset URI format
|
|
649
|
+
const assetMatch = uri.match(/^photon:\/\/([^/]+)\/(ui|prompts|resources)\/(.+)$/);
|
|
650
|
+
if (assetMatch && this.mcp.assets) {
|
|
651
|
+
return this.handleAssetRead(uri, assetMatch);
|
|
177
652
|
}
|
|
653
|
+
// Handle static resources
|
|
654
|
+
return this.handleStaticRead(uri);
|
|
178
655
|
});
|
|
179
656
|
}
|
|
180
657
|
/**
|
|
@@ -290,12 +767,13 @@ export class PhotonServer {
|
|
|
290
767
|
formatError(error, toolName, args) {
|
|
291
768
|
// Determine error type
|
|
292
769
|
let errorType = 'runtime_error';
|
|
293
|
-
let errorMessage = error
|
|
770
|
+
let errorMessage = getErrorMessage(error) || String(error);
|
|
294
771
|
let suggestion = '';
|
|
295
772
|
// Categorize common errors and provide suggestions
|
|
296
773
|
if (errorMessage.includes('not a function') || errorMessage.includes('undefined')) {
|
|
297
774
|
errorType = 'implementation_error';
|
|
298
|
-
suggestion =
|
|
775
|
+
suggestion =
|
|
776
|
+
'The tool implementation may have an issue. Check that all methods are properly defined.';
|
|
299
777
|
}
|
|
300
778
|
else if (errorMessage.includes('required') || errorMessage.includes('validation')) {
|
|
301
779
|
errorType = 'validation_error';
|
|
@@ -307,7 +785,8 @@ export class PhotonServer {
|
|
|
307
785
|
}
|
|
308
786
|
else if (errorMessage.includes('ECONNREFUSED') || errorMessage.includes('network')) {
|
|
309
787
|
errorType = 'network_error';
|
|
310
|
-
suggestion =
|
|
788
|
+
suggestion =
|
|
789
|
+
'Cannot connect to external service. Check network connection and service availability.';
|
|
311
790
|
}
|
|
312
791
|
else if (errorMessage.includes('permission') || errorMessage.includes('EACCES')) {
|
|
313
792
|
errorType = 'permission_error';
|
|
@@ -333,9 +812,9 @@ export class PhotonServer {
|
|
|
333
812
|
structuredMessage += `\nStack trace:\n${error.stack}\n`;
|
|
334
813
|
}
|
|
335
814
|
// Log to stderr for debugging
|
|
336
|
-
|
|
815
|
+
this.log('error', `[Photon Error] ${toolName}: ${errorMessage}`);
|
|
337
816
|
if (this.options.devMode && error.stack) {
|
|
338
|
-
|
|
817
|
+
this.log('debug', error.stack);
|
|
339
818
|
}
|
|
340
819
|
return {
|
|
341
820
|
content: [
|
|
@@ -347,29 +826,1333 @@ export class PhotonServer {
|
|
|
347
826
|
isError: true,
|
|
348
827
|
};
|
|
349
828
|
}
|
|
829
|
+
/**
|
|
830
|
+
* Build placeholder tools from unresolved photon manifest metadata
|
|
831
|
+
*/
|
|
832
|
+
buildPlaceholderTools() {
|
|
833
|
+
const unresolved = this.options.unresolvedPhoton;
|
|
834
|
+
if (!unresolved)
|
|
835
|
+
return [];
|
|
836
|
+
// Collect tool names from all source metadata
|
|
837
|
+
const toolNames = new Set();
|
|
838
|
+
for (const source of unresolved.sources) {
|
|
839
|
+
if (source.metadata?.tools) {
|
|
840
|
+
for (const tool of source.metadata.tools) {
|
|
841
|
+
toolNames.add(tool);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
// If no tools in metadata, create a single setup tool
|
|
846
|
+
if (toolNames.size === 0) {
|
|
847
|
+
return [
|
|
848
|
+
{
|
|
849
|
+
name: 'setup',
|
|
850
|
+
description: `Set up ${unresolved.name} ā call this tool to begin.`,
|
|
851
|
+
inputSchema: { type: 'object', properties: {} },
|
|
852
|
+
},
|
|
853
|
+
];
|
|
854
|
+
}
|
|
855
|
+
return Array.from(toolNames).map((name) => ({
|
|
856
|
+
name,
|
|
857
|
+
description: `Requires setup ā call to begin.`,
|
|
858
|
+
inputSchema: { type: 'object', properties: {} },
|
|
859
|
+
}));
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Resolve an unresolved photon (deferred conflict resolution)
|
|
863
|
+
*
|
|
864
|
+
* If the client supports elicitation, presents marketplace choices.
|
|
865
|
+
* Otherwise, auto-picks the recommendation.
|
|
866
|
+
*/
|
|
867
|
+
async resolveUnresolvedPhoton() {
|
|
868
|
+
const unresolved = this.options.unresolvedPhoton;
|
|
869
|
+
if (!unresolved)
|
|
870
|
+
return;
|
|
871
|
+
let selectedSource;
|
|
872
|
+
if (unresolved.sources.length === 1) {
|
|
873
|
+
selectedSource = unresolved.sources[0];
|
|
874
|
+
}
|
|
875
|
+
else if (this.clientSupportsElicitation()) {
|
|
876
|
+
// Present choices via elicitation
|
|
877
|
+
const options = {};
|
|
878
|
+
const sourceLabels = [];
|
|
879
|
+
for (const source of unresolved.sources) {
|
|
880
|
+
const version = source.metadata?.version || 'unknown';
|
|
881
|
+
const label = `${source.marketplace.name} (v${version})`;
|
|
882
|
+
sourceLabels.push({ const: source.marketplace.name, title: label });
|
|
883
|
+
}
|
|
884
|
+
const result = await this.server.elicitInput({
|
|
885
|
+
message: `Multiple sources found for "${unresolved.name}". Which marketplace should be used?`,
|
|
886
|
+
requestedSchema: {
|
|
887
|
+
type: 'object',
|
|
888
|
+
properties: {
|
|
889
|
+
marketplace: {
|
|
890
|
+
type: 'string',
|
|
891
|
+
title: 'Marketplace',
|
|
892
|
+
oneOf: sourceLabels,
|
|
893
|
+
default: unresolved.recommendation || unresolved.sources[0].marketplace.name,
|
|
894
|
+
},
|
|
895
|
+
},
|
|
896
|
+
required: ['marketplace'],
|
|
897
|
+
},
|
|
898
|
+
});
|
|
899
|
+
const chosen = result.action === 'accept' && result.content
|
|
900
|
+
? result.content.marketplace
|
|
901
|
+
: unresolved.recommendation;
|
|
902
|
+
selectedSource =
|
|
903
|
+
unresolved.sources.find((s) => s.marketplace.name === chosen) || unresolved.sources[0];
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
// No elicitation ā auto-pick recommendation
|
|
907
|
+
const rec = unresolved.recommendation;
|
|
908
|
+
selectedSource = rec
|
|
909
|
+
? unresolved.sources.find((s) => s.marketplace.name === rec) || unresolved.sources[0]
|
|
910
|
+
: unresolved.sources[0];
|
|
911
|
+
this.log('info', `Auto-selected marketplace: ${selectedSource.marketplace.name}`);
|
|
912
|
+
}
|
|
913
|
+
// Download and install photon
|
|
914
|
+
await this.downloadAndLoadPhoton(unresolved.name, unresolved.workingDir, selectedSource);
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Download a photon from a marketplace source, save to workingDir, and load it
|
|
918
|
+
*/
|
|
919
|
+
async downloadAndLoadPhoton(photonName, workingDir, source) {
|
|
920
|
+
const { MarketplaceManager, calculateHash } = await import('./marketplace-manager.js');
|
|
921
|
+
const manager = new MarketplaceManager();
|
|
922
|
+
await manager.initialize();
|
|
923
|
+
const result = await manager.fetchMCP(photonName);
|
|
924
|
+
if (!result) {
|
|
925
|
+
throw new Error(`Failed to download photon: ${photonName}`);
|
|
926
|
+
}
|
|
927
|
+
// Save photon file
|
|
928
|
+
const { default: fsPromises } = await import('fs/promises');
|
|
929
|
+
const filePath = (await import('path')).join(workingDir, `${photonName}.photon.ts`);
|
|
930
|
+
const fileName = `${photonName}.photon.ts`;
|
|
931
|
+
// Ensure working directory exists
|
|
932
|
+
await fsPromises.mkdir(workingDir, { recursive: true });
|
|
933
|
+
await fsPromises.writeFile(filePath, result.content, 'utf-8');
|
|
934
|
+
// Save metadata
|
|
935
|
+
if (source.metadata) {
|
|
936
|
+
const contentHash = calculateHash(result.content);
|
|
937
|
+
await manager.savePhotonMetadata(fileName, source.marketplace, source.metadata, contentHash);
|
|
938
|
+
// Download assets if present
|
|
939
|
+
if (source.metadata.assets && source.metadata.assets.length > 0) {
|
|
940
|
+
const assets = await manager.fetchAssets(source.marketplace, source.metadata.assets);
|
|
941
|
+
for (const [assetPath, content] of assets) {
|
|
942
|
+
const targetPath = (await import('path')).join(workingDir, assetPath);
|
|
943
|
+
const targetDir = (await import('path')).dirname(targetPath);
|
|
944
|
+
await fsPromises.mkdir(targetDir, { recursive: true });
|
|
945
|
+
await fsPromises.writeFile(targetPath, content, 'utf-8');
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
// Update options and load
|
|
950
|
+
this.options.filePath = filePath;
|
|
951
|
+
this.options.unresolvedPhoton = undefined;
|
|
952
|
+
this.log('info', `Downloaded and loading ${photonName}...`);
|
|
953
|
+
this.mcp = await this.loader.loadFile(filePath);
|
|
954
|
+
// Notify clients that tools have changed
|
|
955
|
+
await this.notifyListsChanged();
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Attempt config elicitation to resolve missing env vars, then retry tool call
|
|
959
|
+
*/
|
|
960
|
+
async attemptConfigElicitation(toolName, args) {
|
|
961
|
+
try {
|
|
962
|
+
// Extract constructor params to build form
|
|
963
|
+
const params = await this.loader.extractConstructorParams(this.options.filePath);
|
|
964
|
+
if (params.length === 0)
|
|
965
|
+
return null;
|
|
966
|
+
const photonName = this.mcp?.name || 'photon';
|
|
967
|
+
const { toEnvVarName } = await import('./shared/config-docs.js');
|
|
968
|
+
// Build form properties from constructor params
|
|
969
|
+
const properties = {};
|
|
970
|
+
const required = [];
|
|
971
|
+
for (const param of params) {
|
|
972
|
+
const envVarName = toEnvVarName(photonName, param.name);
|
|
973
|
+
const existing = process.env[envVarName];
|
|
974
|
+
// Skip params that already have values
|
|
975
|
+
if (existing)
|
|
976
|
+
continue;
|
|
977
|
+
// Skip optional params with defaults
|
|
978
|
+
if (param.hasDefault || param.isOptional)
|
|
979
|
+
continue;
|
|
980
|
+
properties[envVarName] = {
|
|
981
|
+
type: param.type === 'number' ? 'number' : param.type === 'boolean' ? 'boolean' : 'string',
|
|
982
|
+
title: param.name,
|
|
983
|
+
description: `Environment variable: ${envVarName}`,
|
|
984
|
+
};
|
|
985
|
+
required.push(envVarName);
|
|
986
|
+
}
|
|
987
|
+
if (Object.keys(properties).length === 0)
|
|
988
|
+
return null;
|
|
989
|
+
const result = await this.server.elicitInput({
|
|
990
|
+
message: `${photonName} requires configuration. Please provide the following:`,
|
|
991
|
+
requestedSchema: {
|
|
992
|
+
type: 'object',
|
|
993
|
+
properties,
|
|
994
|
+
required,
|
|
995
|
+
},
|
|
996
|
+
});
|
|
997
|
+
if (result.action !== 'accept' || !result.content)
|
|
998
|
+
return null;
|
|
999
|
+
// Set env vars from elicitation response
|
|
1000
|
+
const content = result.content;
|
|
1001
|
+
for (const [key, value] of Object.entries(content)) {
|
|
1002
|
+
if (value !== undefined && value !== '') {
|
|
1003
|
+
process.env[key] = String(value);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
// Reload photon with new env vars
|
|
1007
|
+
this.log('info', 'Reloading photon with elicited configuration...');
|
|
1008
|
+
this.mcp = await this.loader.loadFile(this.options.filePath);
|
|
1009
|
+
await this.notifyListsChanged();
|
|
1010
|
+
// Retry the original tool call
|
|
1011
|
+
const inputProvider = this.createMCPInputProvider();
|
|
1012
|
+
const outputHandler = (emit) => {
|
|
1013
|
+
if (this.daemonName && emit?.channel) {
|
|
1014
|
+
publishToChannel(this.daemonName, emit.channel, emit).catch(() => { });
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
const retryResult = await this.loader.executeTool(this.mcp, toolName, args, {
|
|
1018
|
+
inputProvider,
|
|
1019
|
+
outputHandler,
|
|
1020
|
+
});
|
|
1021
|
+
const isStateful = retryResult && typeof retryResult === 'object' && retryResult._stateful === true;
|
|
1022
|
+
const actualResult = isStateful ? retryResult.result : retryResult;
|
|
1023
|
+
return {
|
|
1024
|
+
content: [{ type: 'text', text: this.formatResult(actualResult) }],
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
this.log('warn', `Config elicitation failed: ${getErrorMessage(error)}`);
|
|
1029
|
+
return null; // elicitation unsupported by client
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
350
1032
|
/**
|
|
351
1033
|
* Initialize and start the server
|
|
352
1034
|
*/
|
|
353
1035
|
async start() {
|
|
354
1036
|
try {
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
1037
|
+
// If unresolvedPhoton is set, skip loading ā defer to first tool call
|
|
1038
|
+
if (this.options.unresolvedPhoton) {
|
|
1039
|
+
this.log('info', `Deferred loading for ${this.options.unresolvedPhoton.name} (${this.options.unresolvedPhoton.sources.length} marketplace sources)`);
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
// Initialize MCP client factory for enabling this.mcp() in Photons
|
|
1043
|
+
// This allows Photons to call external MCPs via protocol
|
|
1044
|
+
try {
|
|
1045
|
+
this.mcpClientFactory = await createStandaloneMCPClientFactory(this.options.devMode);
|
|
1046
|
+
const servers = await this.mcpClientFactory.listServers();
|
|
1047
|
+
if (servers.length > 0) {
|
|
1048
|
+
this.log('info', `MCP access enabled: ${servers.join(', ')}`);
|
|
1049
|
+
this.loader.setMCPClientFactory(this.mcpClientFactory);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
catch (error) {
|
|
1053
|
+
this.log('warn', `Failed to load MCP config: ${getErrorMessage(error)}`);
|
|
1054
|
+
}
|
|
1055
|
+
// Check if photon is stateful (requires daemon)
|
|
1056
|
+
const extractor = new PhotonDocExtractor(this.options.filePath);
|
|
1057
|
+
const metadata = await extractor.extractFullMetadata();
|
|
1058
|
+
const isStateful = metadata.stateful;
|
|
1059
|
+
// Start daemon for stateful photons (enables cross-client communication)
|
|
1060
|
+
if (isStateful) {
|
|
1061
|
+
const photonName = metadata.name;
|
|
1062
|
+
this.daemonName = photonName; // Store for subscription
|
|
1063
|
+
this.log('info', `Stateful photon detected: ${photonName}`);
|
|
1064
|
+
if (!isDaemonRunning(photonName)) {
|
|
1065
|
+
this.log('info', `Starting daemon for ${photonName}...`);
|
|
1066
|
+
await startDaemon(photonName, this.options.filePath, true);
|
|
1067
|
+
// Wait for daemon to be ready
|
|
1068
|
+
for (let i = 0; i < 10; i++) {
|
|
1069
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1070
|
+
if (await pingDaemon(photonName)) {
|
|
1071
|
+
this.log('info', `Daemon ready for ${photonName}`);
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
this.log('info', `Daemon already running for ${photonName}`);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
// Load the Photon MCP file
|
|
1081
|
+
this.log('info', `Loading ${this.options.filePath}...`);
|
|
1082
|
+
this.mcp = await this.loader.loadFile(this.options.filePath);
|
|
1083
|
+
}
|
|
1084
|
+
// Subscribe to daemon channels for cross-process notifications
|
|
1085
|
+
await this.subscribeToChannels();
|
|
1086
|
+
// Start with the appropriate transport
|
|
1087
|
+
const transport = this.options.transport || 'stdio';
|
|
1088
|
+
if (transport === 'sse') {
|
|
1089
|
+
await this.startSSE();
|
|
1090
|
+
}
|
|
1091
|
+
else {
|
|
1092
|
+
await this.startStdio();
|
|
1093
|
+
}
|
|
362
1094
|
// In dev mode, we could set up file watching here
|
|
363
1095
|
if (this.options.devMode) {
|
|
364
|
-
|
|
1096
|
+
this.log('info', 'Dev mode enabled - hot reload active');
|
|
365
1097
|
}
|
|
366
1098
|
}
|
|
367
1099
|
catch (error) {
|
|
368
|
-
|
|
369
|
-
|
|
1100
|
+
this.log('error', `Failed to start server: ${getErrorMessage(error)}`);
|
|
1101
|
+
if (error instanceof Error && error.stack) {
|
|
1102
|
+
this.log('debug', error.stack);
|
|
1103
|
+
}
|
|
370
1104
|
process.exit(1);
|
|
371
1105
|
}
|
|
372
1106
|
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Subscribe to daemon channels for cross-process notifications
|
|
1109
|
+
* This enables real-time updates when other processes (e.g., Beam UI, other MCP clients) modify data
|
|
1110
|
+
*/
|
|
1111
|
+
async subscribeToChannels() {
|
|
1112
|
+
// Only subscribe if we have a daemon running (stateful photon)
|
|
1113
|
+
if (!this.daemonName)
|
|
1114
|
+
return;
|
|
1115
|
+
try {
|
|
1116
|
+
// Subscribe to wildcard channel for all events from this photon
|
|
1117
|
+
// E.g., "kanban:*" receives "kanban:photon", "kanban:my-board", etc.
|
|
1118
|
+
const unsubscribe = await subscribeChannel(this.daemonName, `${this.daemonName}:*`, (message) => {
|
|
1119
|
+
this.handleChannelMessage(message);
|
|
1120
|
+
});
|
|
1121
|
+
this.channelUnsubscribers.push(unsubscribe);
|
|
1122
|
+
this.log('info', `Subscribed to daemon channel: ${this.daemonName}:*`);
|
|
1123
|
+
}
|
|
1124
|
+
catch (error) {
|
|
1125
|
+
this.log('warn', `Failed to subscribe to daemon: ${getErrorMessage(error)}`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Handle incoming channel messages and forward as MCP notifications
|
|
1130
|
+
* This enables cross-client real-time updates (e.g., Beam updates show in Claude Desktop)
|
|
1131
|
+
*
|
|
1132
|
+
* Uses standard MCP Apps notification with embedded _photon data:
|
|
1133
|
+
* - Claude Desktop forwards standard notifications (ui/notifications/host-context-changed)
|
|
1134
|
+
* - Photon bridge extracts _photon field and routes to event listeners
|
|
1135
|
+
* - This ensures cross-client sync works without requiring custom protocol support
|
|
1136
|
+
*/
|
|
1137
|
+
async handleChannelMessage(message) {
|
|
1138
|
+
if (!message || typeof message !== 'object')
|
|
1139
|
+
return;
|
|
1140
|
+
const msg = message;
|
|
1141
|
+
// Use STANDARD notification with embedded photon data
|
|
1142
|
+
// Claude Desktop will forward this (it's a standard notification)
|
|
1143
|
+
// Our bridge extracts _photon and routes to the appropriate event handler
|
|
1144
|
+
const payload = {
|
|
1145
|
+
method: 'ui/notifications/host-context-changed',
|
|
1146
|
+
params: {
|
|
1147
|
+
// _photon field carries our custom event data
|
|
1148
|
+
_photon: {
|
|
1149
|
+
photon: this.daemonName,
|
|
1150
|
+
channel: msg.channel,
|
|
1151
|
+
event: msg.event,
|
|
1152
|
+
data: msg.data,
|
|
1153
|
+
},
|
|
1154
|
+
},
|
|
1155
|
+
};
|
|
1156
|
+
try {
|
|
1157
|
+
await this.server.notification(payload);
|
|
1158
|
+
}
|
|
1159
|
+
catch {
|
|
1160
|
+
// ignore - client may not support notifications
|
|
1161
|
+
}
|
|
1162
|
+
// Also send to SSE sessions
|
|
1163
|
+
for (const session of this.sseSessions.values()) {
|
|
1164
|
+
try {
|
|
1165
|
+
await session.server.notification(payload);
|
|
1166
|
+
}
|
|
1167
|
+
catch {
|
|
1168
|
+
// ignore session errors
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Start server with stdio transport
|
|
1174
|
+
*/
|
|
1175
|
+
async startStdio() {
|
|
1176
|
+
const transport = new StdioServerTransport();
|
|
1177
|
+
await this.server.connect(transport);
|
|
1178
|
+
this.log('info', `Server started: ${this.mcp.name}`);
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Start server with SSE transport (HTTP)
|
|
1182
|
+
*/
|
|
1183
|
+
async startSSE() {
|
|
1184
|
+
const port = this.options.port || 3000;
|
|
1185
|
+
const ssePath = '/mcp';
|
|
1186
|
+
const messagesPath = '/mcp/messages';
|
|
1187
|
+
this.httpServer = createServer(async (req, res) => {
|
|
1188
|
+
if (!req.url) {
|
|
1189
|
+
res.writeHead(400).end('Missing URL');
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
1193
|
+
// Handle CORS preflight
|
|
1194
|
+
if (req.method === 'OPTIONS') {
|
|
1195
|
+
res.writeHead(204, {
|
|
1196
|
+
'Access-Control-Allow-Origin': '*',
|
|
1197
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
1198
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
1199
|
+
});
|
|
1200
|
+
res.end();
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
// SSE connection endpoint
|
|
1204
|
+
if (req.method === 'GET' && url.pathname === ssePath) {
|
|
1205
|
+
await this.handleSSEConnection(res, messagesPath);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
// Message posting endpoint
|
|
1209
|
+
if (req.method === 'POST' && url.pathname === messagesPath) {
|
|
1210
|
+
await this.handleSSEMessage(req, res, url);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
// Health check / info endpoint
|
|
1214
|
+
if (req.method === 'GET' && url.pathname === '/') {
|
|
1215
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1216
|
+
const endpoints = {
|
|
1217
|
+
sse: `http://localhost:${port}${ssePath}`,
|
|
1218
|
+
messages: `http://localhost:${port}${messagesPath}`,
|
|
1219
|
+
};
|
|
1220
|
+
if (this.devMode) {
|
|
1221
|
+
endpoints.playground = `http://localhost:${port}/playground`;
|
|
1222
|
+
}
|
|
1223
|
+
res.end(JSON.stringify({
|
|
1224
|
+
name: this.mcp?.name || 'photon-mcp',
|
|
1225
|
+
transport: 'sse',
|
|
1226
|
+
endpoints,
|
|
1227
|
+
tools: this.mcp?.tools.length || 0,
|
|
1228
|
+
assets: this.mcp?.assets
|
|
1229
|
+
? {
|
|
1230
|
+
ui: this.mcp.assets.ui.length,
|
|
1231
|
+
prompts: this.mcp.assets.prompts.length,
|
|
1232
|
+
resources: this.mcp.assets.resources.length,
|
|
1233
|
+
}
|
|
1234
|
+
: null,
|
|
1235
|
+
}));
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
// Playground and API endpoints - only in dev mode
|
|
1239
|
+
if (this.devMode) {
|
|
1240
|
+
if (req.method === 'GET' && url.pathname === '/playground') {
|
|
1241
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1242
|
+
res.end(await this.getPlaygroundHTML(port));
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
// API: List all photons
|
|
1246
|
+
if (req.method === 'GET' && url.pathname === '/api/photons') {
|
|
1247
|
+
res.writeHead(200, {
|
|
1248
|
+
'Content-Type': 'application/json',
|
|
1249
|
+
'Access-Control-Allow-Origin': '*',
|
|
1250
|
+
});
|
|
1251
|
+
try {
|
|
1252
|
+
const photons = await this.listAllPhotons();
|
|
1253
|
+
res.end(JSON.stringify({ photons }));
|
|
1254
|
+
}
|
|
1255
|
+
catch (error) {
|
|
1256
|
+
res.writeHead(500);
|
|
1257
|
+
res.end(JSON.stringify({ error: getErrorMessage(error) }));
|
|
1258
|
+
}
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
// API: List tools (for compatibility, now returns current photon)
|
|
1262
|
+
if (req.method === 'GET' && url.pathname === '/api/tools') {
|
|
1263
|
+
res.writeHead(200, {
|
|
1264
|
+
'Content-Type': 'application/json',
|
|
1265
|
+
'Access-Control-Allow-Origin': '*',
|
|
1266
|
+
});
|
|
1267
|
+
const tools = this.mcp?.tools.map((tool) => {
|
|
1268
|
+
const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name);
|
|
1269
|
+
return {
|
|
1270
|
+
name: tool.name,
|
|
1271
|
+
description: tool.description,
|
|
1272
|
+
inputSchema: tool.inputSchema,
|
|
1273
|
+
ui: linkedUI
|
|
1274
|
+
? { id: linkedUI.id, uri: `photon://${this.mcp.name}/ui/${linkedUI.id}` }
|
|
1275
|
+
: null,
|
|
1276
|
+
};
|
|
1277
|
+
}) || [];
|
|
1278
|
+
res.end(JSON.stringify({ tools }));
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
if (req.method === 'GET' && url.pathname === '/api/status') {
|
|
1282
|
+
res.writeHead(200, {
|
|
1283
|
+
'Content-Type': 'application/json',
|
|
1284
|
+
'Access-Control-Allow-Origin': '*',
|
|
1285
|
+
});
|
|
1286
|
+
res.end(JSON.stringify(this.buildStatusSnapshot()));
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (req.method === 'GET' && url.pathname === '/api/status-stream') {
|
|
1290
|
+
this.handleStatusStream(req, res);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
// API: Call tool
|
|
1295
|
+
if (req.method === 'POST' && url.pathname === '/api/call') {
|
|
1296
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1297
|
+
res.setHeader('Content-Type', 'application/json');
|
|
1298
|
+
let body = '';
|
|
1299
|
+
req.on('data', (chunk) => (body += chunk));
|
|
1300
|
+
req.on('end', async () => {
|
|
1301
|
+
try {
|
|
1302
|
+
const { tool, args } = JSON.parse(body);
|
|
1303
|
+
const result = await this.loader.executeTool(this.mcp, tool, args || {});
|
|
1304
|
+
const isStateful = result && typeof result === 'object' && result._stateful === true;
|
|
1305
|
+
res.writeHead(200);
|
|
1306
|
+
res.end(JSON.stringify({
|
|
1307
|
+
success: true,
|
|
1308
|
+
data: isStateful ? result.result : result,
|
|
1309
|
+
}));
|
|
1310
|
+
}
|
|
1311
|
+
catch (error) {
|
|
1312
|
+
res.writeHead(500);
|
|
1313
|
+
res.end(JSON.stringify({ success: false, error: getErrorMessage(error) }));
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
// API: Call tool with streaming progress (SSE)
|
|
1319
|
+
if (req.method === 'POST' && url.pathname === '/api/call-stream') {
|
|
1320
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1321
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
1322
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1323
|
+
res.setHeader('Connection', 'keep-alive');
|
|
1324
|
+
let body = '';
|
|
1325
|
+
req.on('data', (chunk) => (body += chunk));
|
|
1326
|
+
req.on('end', async () => {
|
|
1327
|
+
let requestId = `run_${Date.now()}`;
|
|
1328
|
+
const sendMessage = (message) => {
|
|
1329
|
+
res.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`);
|
|
1330
|
+
};
|
|
1331
|
+
try {
|
|
1332
|
+
const payload = JSON.parse(body || '{}');
|
|
1333
|
+
const tool = payload.tool;
|
|
1334
|
+
if (!tool) {
|
|
1335
|
+
throw new Error('Tool name is required');
|
|
1336
|
+
}
|
|
1337
|
+
const args = payload.args || {};
|
|
1338
|
+
const progressToken = payload.progressToken ?? `progress_${Date.now()}`;
|
|
1339
|
+
requestId = payload.requestId || requestId;
|
|
1340
|
+
const sendNotification = (method, params) => {
|
|
1341
|
+
sendMessage({ jsonrpc: '2.0', method, params });
|
|
1342
|
+
};
|
|
1343
|
+
const reportProgress = (emit) => {
|
|
1344
|
+
const rawValue = typeof emit?.value === 'number' ? emit.value : 0;
|
|
1345
|
+
const percent = rawValue <= 1 ? rawValue * 100 : rawValue;
|
|
1346
|
+
sendNotification('notifications/progress', {
|
|
1347
|
+
progressToken,
|
|
1348
|
+
progress: percent,
|
|
1349
|
+
total: 100,
|
|
1350
|
+
message: emit?.message || null,
|
|
1351
|
+
});
|
|
1352
|
+
};
|
|
1353
|
+
const outputHandler = (emit) => {
|
|
1354
|
+
if (!emit)
|
|
1355
|
+
return;
|
|
1356
|
+
if (emit.emit === 'progress') {
|
|
1357
|
+
reportProgress(emit);
|
|
1358
|
+
}
|
|
1359
|
+
else if (emit.emit === 'status') {
|
|
1360
|
+
sendNotification('notifications/status', {
|
|
1361
|
+
type: emit.type || 'info',
|
|
1362
|
+
message: emit.message || '',
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
else {
|
|
1366
|
+
sendNotification('notifications/emit', { event: emit });
|
|
1367
|
+
}
|
|
1368
|
+
// Forward channel events to daemon for cross-process pub/sub
|
|
1369
|
+
if (this.daemonName && emit.channel) {
|
|
1370
|
+
publishToChannel(this.daemonName, emit.channel, emit).catch(() => {
|
|
1371
|
+
// Ignore publish errors - daemon may not be running
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
sendNotification('notifications/status', {
|
|
1376
|
+
type: 'info',
|
|
1377
|
+
message: `Starting ${tool}`,
|
|
1378
|
+
});
|
|
1379
|
+
const result = await this.loader.executeTool(this.mcp, tool, args, { outputHandler });
|
|
1380
|
+
const isStateful = result && typeof result === 'object' && result._stateful === true;
|
|
1381
|
+
sendMessage({
|
|
1382
|
+
jsonrpc: '2.0',
|
|
1383
|
+
id: requestId,
|
|
1384
|
+
result: {
|
|
1385
|
+
success: true,
|
|
1386
|
+
data: isStateful ? result.result : result,
|
|
1387
|
+
},
|
|
1388
|
+
});
|
|
1389
|
+
res.end();
|
|
1390
|
+
}
|
|
1391
|
+
catch (error) {
|
|
1392
|
+
const message = getErrorMessage(error);
|
|
1393
|
+
const errorPayload = {
|
|
1394
|
+
jsonrpc: '2.0',
|
|
1395
|
+
error: { code: -32000, message },
|
|
1396
|
+
};
|
|
1397
|
+
if (requestId) {
|
|
1398
|
+
errorPayload.id = requestId;
|
|
1399
|
+
}
|
|
1400
|
+
sendMessage(errorPayload);
|
|
1401
|
+
res.end();
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
// API: Get UI template
|
|
1407
|
+
if (req.method === 'GET' && url.pathname.startsWith('/api/ui/')) {
|
|
1408
|
+
const uiId = url.pathname.replace('/api/ui/', '');
|
|
1409
|
+
const ui = this.mcp?.assets?.ui.find((u) => u.id === uiId);
|
|
1410
|
+
if (ui?.resolvedPath) {
|
|
1411
|
+
try {
|
|
1412
|
+
const content = await fs.readFile(ui.resolvedPath, 'utf-8');
|
|
1413
|
+
res.writeHead(200, {
|
|
1414
|
+
'Content-Type': 'text/html',
|
|
1415
|
+
'Access-Control-Allow-Origin': '*',
|
|
1416
|
+
});
|
|
1417
|
+
res.end(content);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
catch {
|
|
1421
|
+
// Fall through to 404
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
res.writeHead(404).end('UI not found');
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
res.writeHead(404).end('Not Found');
|
|
1428
|
+
});
|
|
1429
|
+
this.httpServer.on('clientError', (err, socket) => {
|
|
1430
|
+
this.log('warn', 'HTTP client error', { message: err.message });
|
|
1431
|
+
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
1432
|
+
});
|
|
1433
|
+
await new Promise((resolve) => {
|
|
1434
|
+
this.httpServer.listen(port, () => {
|
|
1435
|
+
this.log('info', `${this.mcp.name} MCP server listening`, {
|
|
1436
|
+
transport: 'sse',
|
|
1437
|
+
port,
|
|
1438
|
+
devMode: this.devMode,
|
|
1439
|
+
});
|
|
1440
|
+
this.log('debug', 'SSE endpoints ready', {
|
|
1441
|
+
baseUrl: `http://localhost:${port}`,
|
|
1442
|
+
ssePath,
|
|
1443
|
+
messagesPath,
|
|
1444
|
+
playground: this.devMode ? `http://localhost:${port}/playground` : undefined,
|
|
1445
|
+
});
|
|
1446
|
+
resolve();
|
|
1447
|
+
});
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* List all photons in the .photon directory
|
|
1452
|
+
*/
|
|
1453
|
+
async listAllPhotons() {
|
|
1454
|
+
const { listPhotonFiles, DEFAULT_PHOTON_DIR } = await import('./path-resolver.js');
|
|
1455
|
+
const photonFiles = await listPhotonFiles();
|
|
1456
|
+
const photons = await Promise.all(photonFiles.map(async (file) => {
|
|
1457
|
+
try {
|
|
1458
|
+
const loader = new PhotonLoader(this.devMode, this.logger.child({ component: 'photon-loader', scope: 'discovery' }));
|
|
1459
|
+
const mcp = await loader.loadFile(file);
|
|
1460
|
+
return {
|
|
1461
|
+
name: mcp.name,
|
|
1462
|
+
description: mcp.description,
|
|
1463
|
+
file: file.replace(DEFAULT_PHOTON_DIR + '/', ''),
|
|
1464
|
+
tools: mcp.tools.map((tool) => ({
|
|
1465
|
+
name: tool.name,
|
|
1466
|
+
description: tool.description,
|
|
1467
|
+
inputSchema: tool.inputSchema,
|
|
1468
|
+
})),
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
catch (error) {
|
|
1472
|
+
this.log('warn', `Failed to load photon: ${file}`, { error: getErrorMessage(error) });
|
|
1473
|
+
return null; // skip unloadable photon
|
|
1474
|
+
}
|
|
1475
|
+
}));
|
|
1476
|
+
return photons.filter((p) => p !== null);
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Generate playground HTML for interactive testing
|
|
1480
|
+
*/
|
|
1481
|
+
async getPlaygroundHTML(port) {
|
|
1482
|
+
const name = this.mcp?.name || 'photon-mcp';
|
|
1483
|
+
return generatePlaygroundHTML({ name, port });
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Handle new SSE connection
|
|
1487
|
+
*/
|
|
1488
|
+
async handleSSEConnection(res, messagesPath) {
|
|
1489
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1490
|
+
// Create a new MCP server instance for this session
|
|
1491
|
+
const sessionServer = new Server({
|
|
1492
|
+
name: this.mcp?.name || 'photon-mcp',
|
|
1493
|
+
version: PHOTON_VERSION,
|
|
1494
|
+
}, {
|
|
1495
|
+
capabilities: {
|
|
1496
|
+
tools: { listChanged: true },
|
|
1497
|
+
prompts: { listChanged: true },
|
|
1498
|
+
resources: { listChanged: true },
|
|
1499
|
+
experimental: {
|
|
1500
|
+
sampling: {}, // Support elicitation via MCP sampling protocol
|
|
1501
|
+
},
|
|
1502
|
+
},
|
|
1503
|
+
});
|
|
1504
|
+
// Copy handlers to the session server
|
|
1505
|
+
this.setupSessionHandlers(sessionServer);
|
|
1506
|
+
// Create SSE transport
|
|
1507
|
+
const transport = new SSEServerTransport(messagesPath, res);
|
|
1508
|
+
const sessionId = transport.sessionId;
|
|
1509
|
+
// Store session
|
|
1510
|
+
this.sseSessions.set(sessionId, { server: sessionServer, transport });
|
|
1511
|
+
// Clean up on close
|
|
1512
|
+
transport.onclose = async () => {
|
|
1513
|
+
this.sseSessions.delete(sessionId);
|
|
1514
|
+
await sessionServer.close();
|
|
1515
|
+
};
|
|
1516
|
+
transport.onerror = (error) => {
|
|
1517
|
+
this.log('warn', 'SSE transport error', {
|
|
1518
|
+
sessionId,
|
|
1519
|
+
error: error instanceof Error ? getErrorMessage(error) : String(error),
|
|
1520
|
+
});
|
|
1521
|
+
};
|
|
1522
|
+
try {
|
|
1523
|
+
await sessionServer.connect(transport);
|
|
1524
|
+
this.log('info', 'SSE client connected', { sessionId });
|
|
1525
|
+
}
|
|
1526
|
+
catch (error) {
|
|
1527
|
+
this.sseSessions.delete(sessionId);
|
|
1528
|
+
this.log('error', 'Failed to establish SSE connection', {
|
|
1529
|
+
sessionId,
|
|
1530
|
+
error: getErrorMessage(error) ?? String(error),
|
|
1531
|
+
});
|
|
1532
|
+
if (!res.headersSent) {
|
|
1533
|
+
res.writeHead(500).end('Failed to establish SSE connection');
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Handle incoming SSE message
|
|
1539
|
+
*/
|
|
1540
|
+
async handleSSEMessage(req, res, url) {
|
|
1541
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1542
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
1543
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
1544
|
+
if (!sessionId) {
|
|
1545
|
+
res.writeHead(400).end('Missing sessionId query parameter');
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
const session = this.sseSessions.get(sessionId);
|
|
1549
|
+
if (!session) {
|
|
1550
|
+
res.writeHead(404).end('Unknown session');
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
try {
|
|
1554
|
+
await session.transport.handlePostMessage(req, res);
|
|
1555
|
+
}
|
|
1556
|
+
catch (error) {
|
|
1557
|
+
this.log('error', 'Failed to process SSE message', {
|
|
1558
|
+
sessionId,
|
|
1559
|
+
error: getErrorMessage(error) ?? String(error),
|
|
1560
|
+
});
|
|
1561
|
+
if (!res.headersSent) {
|
|
1562
|
+
res.writeHead(500).end('Failed to process message');
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Set up handlers for a session-specific MCP server
|
|
1568
|
+
* This duplicates handlers from the main server to each session
|
|
1569
|
+
*/
|
|
1570
|
+
setupSessionHandlers(sessionServer) {
|
|
1571
|
+
// Handle tools/list
|
|
1572
|
+
sessionServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1573
|
+
if (!this.mcp)
|
|
1574
|
+
return { tools: [] };
|
|
1575
|
+
return {
|
|
1576
|
+
tools: this.mcp.tools.map((tool) => {
|
|
1577
|
+
const toolDef = {
|
|
1578
|
+
name: tool.name,
|
|
1579
|
+
description: tool.description,
|
|
1580
|
+
inputSchema: tool.inputSchema,
|
|
1581
|
+
};
|
|
1582
|
+
// Add _meta with UI template reference (format depends on client capabilities)
|
|
1583
|
+
const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name);
|
|
1584
|
+
if (linkedUI) {
|
|
1585
|
+
toolDef._meta = this.buildUIToolMeta(linkedUI.id, sessionServer);
|
|
1586
|
+
}
|
|
1587
|
+
return toolDef;
|
|
1588
|
+
}),
|
|
1589
|
+
};
|
|
1590
|
+
});
|
|
1591
|
+
// Handle tools/call
|
|
1592
|
+
sessionServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1593
|
+
if (!this.mcp)
|
|
1594
|
+
throw new Error('MCP not loaded');
|
|
1595
|
+
const { name: toolName, arguments: args } = request.params;
|
|
1596
|
+
try {
|
|
1597
|
+
// Create MCP-aware input provider for elicitation support (use sessionServer for SSE)
|
|
1598
|
+
const inputProvider = this.createMCPInputProvider(sessionServer);
|
|
1599
|
+
// Handler for channel events - forward to daemon for cross-process pub/sub
|
|
1600
|
+
const outputHandler = (emit) => {
|
|
1601
|
+
if (this.daemonName && emit?.channel) {
|
|
1602
|
+
publishToChannel(this.daemonName, emit.channel, emit).catch(() => {
|
|
1603
|
+
// Ignore publish errors - daemon may not be running
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
const result = await this.loader.executeTool(this.mcp, toolName, args || {}, {
|
|
1608
|
+
inputProvider,
|
|
1609
|
+
outputHandler,
|
|
1610
|
+
});
|
|
1611
|
+
const tool = this.mcp.tools.find((t) => t.name === toolName);
|
|
1612
|
+
const outputFormat = tool?.outputFormat;
|
|
1613
|
+
const isStateful = result && typeof result === 'object' && result._stateful === true;
|
|
1614
|
+
const actualResult = isStateful ? result.result : result;
|
|
1615
|
+
const content = {
|
|
1616
|
+
type: 'text',
|
|
1617
|
+
text: this.formatResult(actualResult),
|
|
1618
|
+
};
|
|
1619
|
+
if (outputFormat) {
|
|
1620
|
+
const { formatToMimeType } = await import('./cli-formatter.js');
|
|
1621
|
+
const mimeType = formatToMimeType(outputFormat);
|
|
1622
|
+
if (mimeType) {
|
|
1623
|
+
content.annotations = { mimeType };
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
const response = { content: [content] };
|
|
1627
|
+
if (isStateful) {
|
|
1628
|
+
response._meta = { runId: result.runId, status: result.status };
|
|
1629
|
+
}
|
|
1630
|
+
return response;
|
|
1631
|
+
}
|
|
1632
|
+
catch (error) {
|
|
1633
|
+
return this.formatError(error, toolName, args);
|
|
1634
|
+
}
|
|
1635
|
+
});
|
|
1636
|
+
// Handle prompts/list
|
|
1637
|
+
sessionServer.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
1638
|
+
if (!this.mcp)
|
|
1639
|
+
return { prompts: [] };
|
|
1640
|
+
return {
|
|
1641
|
+
prompts: this.mcp.templates.map((template) => ({
|
|
1642
|
+
name: template.name,
|
|
1643
|
+
description: template.description,
|
|
1644
|
+
arguments: template.inputSchema?.properties
|
|
1645
|
+
? Object.entries(template.inputSchema.properties).map(([name, schema]) => ({
|
|
1646
|
+
name,
|
|
1647
|
+
description: schema.description || '',
|
|
1648
|
+
required: template.inputSchema?.required?.includes(name) || false,
|
|
1649
|
+
}))
|
|
1650
|
+
: [],
|
|
1651
|
+
})),
|
|
1652
|
+
};
|
|
1653
|
+
});
|
|
1654
|
+
// Handle prompts/get
|
|
1655
|
+
sessionServer.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1656
|
+
if (!this.mcp)
|
|
1657
|
+
throw new Error('MCP not loaded');
|
|
1658
|
+
const { name: promptName, arguments: args } = request.params;
|
|
1659
|
+
try {
|
|
1660
|
+
const result = await this.loader.executeTool(this.mcp, promptName, args || {});
|
|
1661
|
+
return this.formatTemplateResult(result);
|
|
1662
|
+
}
|
|
1663
|
+
catch (error) {
|
|
1664
|
+
throw new Error(`Failed to get prompt: ${getErrorMessage(error)}`);
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
// Handle resources/list
|
|
1668
|
+
sessionServer.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
1669
|
+
if (!this.mcp)
|
|
1670
|
+
return { resources: [] };
|
|
1671
|
+
const resources = [];
|
|
1672
|
+
// Add static resources
|
|
1673
|
+
for (const static_ of this.mcp.statics) {
|
|
1674
|
+
if (!this.isUriTemplate(static_.uri)) {
|
|
1675
|
+
resources.push({
|
|
1676
|
+
uri: static_.uri,
|
|
1677
|
+
name: static_.name,
|
|
1678
|
+
description: static_.description,
|
|
1679
|
+
mimeType: static_.mimeType,
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
// Add asset resources (UI format depends on client capabilities)
|
|
1684
|
+
if (this.mcp.assets) {
|
|
1685
|
+
for (const ui of this.mcp.assets.ui) {
|
|
1686
|
+
// Use pre-generated URI from loader, or build one
|
|
1687
|
+
const uiUri = ui.uri || this.buildUIResourceUri(ui.id, sessionServer);
|
|
1688
|
+
resources.push({
|
|
1689
|
+
uri: uiUri,
|
|
1690
|
+
name: `ui:${ui.id}`,
|
|
1691
|
+
description: ui.linkedTool
|
|
1692
|
+
? `UI template for ${ui.linkedTool} tool`
|
|
1693
|
+
: `UI template: ${ui.id}`,
|
|
1694
|
+
mimeType: ui.mimeType || this.getUIMimeType(sessionServer),
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
for (const prompt of this.mcp.assets.prompts) {
|
|
1698
|
+
resources.push({
|
|
1699
|
+
uri: `photon://${this.mcp.name}/prompts/${prompt.id}`,
|
|
1700
|
+
name: `prompt:${prompt.id}`,
|
|
1701
|
+
description: prompt.description || `Prompt template: ${prompt.id}`,
|
|
1702
|
+
mimeType: 'text/markdown',
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
for (const resource of this.mcp.assets.resources) {
|
|
1706
|
+
resources.push({
|
|
1707
|
+
uri: `photon://${this.mcp.name}/resources/${resource.id}`,
|
|
1708
|
+
name: `resource:${resource.id}`,
|
|
1709
|
+
description: `Static resource: ${resource.id}`,
|
|
1710
|
+
mimeType: resource.mimeType || 'application/json',
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
return { resources };
|
|
1715
|
+
});
|
|
1716
|
+
// Handle resources/templates/list
|
|
1717
|
+
sessionServer.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
1718
|
+
if (!this.mcp)
|
|
1719
|
+
return { resourceTemplates: [] };
|
|
1720
|
+
return {
|
|
1721
|
+
resourceTemplates: this.mcp.statics
|
|
1722
|
+
.filter((static_) => this.isUriTemplate(static_.uri))
|
|
1723
|
+
.map((static_) => ({
|
|
1724
|
+
uriTemplate: static_.uri,
|
|
1725
|
+
name: static_.name,
|
|
1726
|
+
description: static_.description,
|
|
1727
|
+
mimeType: static_.mimeType,
|
|
1728
|
+
})),
|
|
1729
|
+
};
|
|
1730
|
+
});
|
|
1731
|
+
// Handle resources/read
|
|
1732
|
+
sessionServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
1733
|
+
if (!this.mcp)
|
|
1734
|
+
throw new Error('MCP not loaded');
|
|
1735
|
+
const { uri } = request.params;
|
|
1736
|
+
// Check for SEP-1865 ui:// URI format
|
|
1737
|
+
const uiMatch = uri.match(/^ui:\/\/([^/]+)\/(.+)$/);
|
|
1738
|
+
if (uiMatch && this.mcp.assets) {
|
|
1739
|
+
const [, _photonName, assetId] = uiMatch;
|
|
1740
|
+
return this.handleUIAssetRead(uri, assetId);
|
|
1741
|
+
}
|
|
1742
|
+
// Check for legacy photon:// asset URI format
|
|
1743
|
+
const assetMatch = uri.match(/^photon:\/\/([^/]+)\/(ui|prompts|resources)\/(.+)$/);
|
|
1744
|
+
if (assetMatch && this.mcp.assets) {
|
|
1745
|
+
return this.handleAssetRead(uri, assetMatch);
|
|
1746
|
+
}
|
|
1747
|
+
// Handle static resources
|
|
1748
|
+
return this.handleStaticRead(uri);
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Handle asset read (for both stdio and SSE handlers)
|
|
1753
|
+
*/
|
|
1754
|
+
/**
|
|
1755
|
+
* Handle SEP-1865 ui:// resource read
|
|
1756
|
+
*/
|
|
1757
|
+
async handleUIAssetRead(uri, assetId) {
|
|
1758
|
+
const ui = this.mcp.assets.ui.find((u) => u.id === assetId);
|
|
1759
|
+
if (!ui || !ui.resolvedPath) {
|
|
1760
|
+
throw new Error(`UI asset not found: ${uri}`);
|
|
1761
|
+
}
|
|
1762
|
+
let content = await fs.readFile(ui.resolvedPath, 'utf-8');
|
|
1763
|
+
// Inject MCP Apps bridge script for Claude Desktop compatibility
|
|
1764
|
+
const bridgeScript = this.generateMcpAppsBridge();
|
|
1765
|
+
content = content.replace('<head>', `<head>\n${bridgeScript}`);
|
|
1766
|
+
return {
|
|
1767
|
+
contents: [{ uri, mimeType: ui.mimeType || 'text/html;profile=mcp-app', text: content }],
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Generate minimal MCP Apps bridge script for Claude Desktop compatibility
|
|
1772
|
+
* This handles the ui/initialize handshake and tool result delivery
|
|
1773
|
+
*/
|
|
1774
|
+
generateMcpAppsBridge() {
|
|
1775
|
+
const photonName = this.mcp?.name || 'photon-app';
|
|
1776
|
+
const injectedPhotons = this.mcp?.injectedPhotons || [];
|
|
1777
|
+
return `<script>
|
|
1778
|
+
(function() {
|
|
1779
|
+
'use strict';
|
|
1780
|
+
var pendingCalls = {};
|
|
1781
|
+
var callIdCounter = 0;
|
|
1782
|
+
var toolResult = null;
|
|
1783
|
+
var resultListeners = [];
|
|
1784
|
+
var emitListeners = [];
|
|
1785
|
+
var themeListeners = [];
|
|
1786
|
+
var eventListeners = {}; // For specific event subscriptions (e.g., 'taskMove')
|
|
1787
|
+
var photonEventListeners = {}; // Namespaced by photon name for injected photons
|
|
1788
|
+
var currentTheme = 'dark';
|
|
1789
|
+
var injectedPhotons = ${JSON.stringify(injectedPhotons)};
|
|
1790
|
+
|
|
1791
|
+
function generateCallId() {
|
|
1792
|
+
return 'call_' + (++callIdCounter) + '_' + Math.random().toString(36).slice(2);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function postToHost(msg) {
|
|
1796
|
+
window.parent.postMessage(msg, '*');
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// Listen for messages from host
|
|
1800
|
+
window.addEventListener('message', function(e) {
|
|
1801
|
+
var m = e.data;
|
|
1802
|
+
if (!m || typeof m !== 'object') return;
|
|
1803
|
+
|
|
1804
|
+
// Handle JSON-RPC messages
|
|
1805
|
+
if (m.jsonrpc === '2.0') {
|
|
1806
|
+
// Response to our request (has id, no method)
|
|
1807
|
+
if (m.id && !m.method && pendingCalls[m.id]) {
|
|
1808
|
+
var pending = pendingCalls[m.id];
|
|
1809
|
+
delete pendingCalls[m.id];
|
|
1810
|
+
if (m.error) {
|
|
1811
|
+
pending.reject(new Error(m.error.message));
|
|
1812
|
+
} else {
|
|
1813
|
+
// Extract clean data from MCP result format
|
|
1814
|
+
var result = m.result;
|
|
1815
|
+
var cleanData = result;
|
|
1816
|
+
if (result && result.structuredContent) {
|
|
1817
|
+
cleanData = result.structuredContent;
|
|
1818
|
+
} else if (result && result.content && Array.isArray(result.content)) {
|
|
1819
|
+
var textItem = result.content.find(function(i) { return i.type === 'text'; });
|
|
1820
|
+
if (textItem && textItem.text) {
|
|
1821
|
+
try { cleanData = JSON.parse(textItem.text); } catch(e) { cleanData = textItem.text; }
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
pending.resolve(cleanData);
|
|
1825
|
+
}
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// Tool result notification
|
|
1830
|
+
if (m.method === 'ui/notifications/tool-result') {
|
|
1831
|
+
var result = m.params;
|
|
1832
|
+
// Extract data from MCP result format
|
|
1833
|
+
if (result.structuredContent) {
|
|
1834
|
+
toolResult = result.structuredContent;
|
|
1835
|
+
} else if (result.content && Array.isArray(result.content)) {
|
|
1836
|
+
var textItem = result.content.find(function(i) { return i.type === 'text'; });
|
|
1837
|
+
if (textItem && textItem.text) {
|
|
1838
|
+
try { toolResult = JSON.parse(textItem.text); } catch(e) { toolResult = textItem.text; }
|
|
1839
|
+
}
|
|
1840
|
+
} else {
|
|
1841
|
+
toolResult = result;
|
|
1842
|
+
}
|
|
1843
|
+
// Set __PHOTON_DATA__ for UIs that read it at init
|
|
1844
|
+
window.__PHOTON_DATA__ = toolResult;
|
|
1845
|
+
// Dispatch event for UIs to re-initialize with new data
|
|
1846
|
+
window.dispatchEvent(new CustomEvent('photon:data-ready', { detail: toolResult }));
|
|
1847
|
+
resultListeners.forEach(function(cb) { cb(toolResult); });
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// Host context changed (theme + embedded photon events)
|
|
1851
|
+
if (m.method === 'ui/notifications/host-context-changed') {
|
|
1852
|
+
// Standard theme handling
|
|
1853
|
+
if (m.params && m.params.theme) {
|
|
1854
|
+
currentTheme = m.params.theme;
|
|
1855
|
+
document.documentElement.classList.remove('light', 'dark');
|
|
1856
|
+
document.documentElement.classList.add(m.params.theme);
|
|
1857
|
+
document.documentElement.setAttribute('data-theme', m.params.theme);
|
|
1858
|
+
themeListeners.forEach(function(cb) { cb(currentTheme); });
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// Extract embedded photon event data
|
|
1862
|
+
// This enables real-time sync via standard MCP protocol
|
|
1863
|
+
if (m.params && m.params._photon) {
|
|
1864
|
+
var photonData = m.params._photon;
|
|
1865
|
+
// Route to generic emit listeners
|
|
1866
|
+
emitListeners.forEach(function(cb) { cb(photonData); });
|
|
1867
|
+
|
|
1868
|
+
var eventName = photonData.event;
|
|
1869
|
+
var sourcePhoton = photonData.data && photonData.data._source;
|
|
1870
|
+
|
|
1871
|
+
// Route to photon-specific listeners if _source is specified (injected photon events)
|
|
1872
|
+
if (sourcePhoton && photonEventListeners[sourcePhoton] && photonEventListeners[sourcePhoton][eventName]) {
|
|
1873
|
+
photonEventListeners[sourcePhoton][eventName].forEach(function(cb) {
|
|
1874
|
+
cb(photonData.data);
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// Also route to global event listeners (main photon events, or fallback)
|
|
1879
|
+
if (eventName && eventListeners[eventName]) {
|
|
1880
|
+
eventListeners[eventName].forEach(function(cb) {
|
|
1881
|
+
cb(photonData.data);
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
// Mark that we're in MCP Apps context (not Beam)
|
|
1890
|
+
window.__MCP_APPS_CONTEXT__ = true;
|
|
1891
|
+
|
|
1892
|
+
// Expose photon bridge API
|
|
1893
|
+
window.photon = {
|
|
1894
|
+
get toolOutput() { return toolResult; },
|
|
1895
|
+
onResult: function(cb) {
|
|
1896
|
+
resultListeners.push(cb);
|
|
1897
|
+
if (toolResult) cb(toolResult);
|
|
1898
|
+
return function() {
|
|
1899
|
+
var i = resultListeners.indexOf(cb);
|
|
1900
|
+
if (i >= 0) resultListeners.splice(i, 1);
|
|
1901
|
+
};
|
|
1902
|
+
},
|
|
1903
|
+
callTool: function(name, args) {
|
|
1904
|
+
var callId = generateCallId();
|
|
1905
|
+
return new Promise(function(resolve, reject) {
|
|
1906
|
+
pendingCalls[callId] = { resolve: resolve, reject: reject };
|
|
1907
|
+
postToHost({
|
|
1908
|
+
jsonrpc: '2.0',
|
|
1909
|
+
id: callId,
|
|
1910
|
+
method: 'tools/call',
|
|
1911
|
+
params: { name: name, arguments: args || {} }
|
|
1912
|
+
});
|
|
1913
|
+
setTimeout(function() {
|
|
1914
|
+
if (pendingCalls[callId]) {
|
|
1915
|
+
delete pendingCalls[callId];
|
|
1916
|
+
reject(new Error('Tool call timeout'));
|
|
1917
|
+
}
|
|
1918
|
+
}, 30000);
|
|
1919
|
+
});
|
|
1920
|
+
},
|
|
1921
|
+
invoke: function(name, args) { return window.photon.callTool(name, args); },
|
|
1922
|
+
onEmit: function(cb) {
|
|
1923
|
+
emitListeners.push(cb);
|
|
1924
|
+
return function() {
|
|
1925
|
+
var i = emitListeners.indexOf(cb);
|
|
1926
|
+
if (i >= 0) emitListeners.splice(i, 1);
|
|
1927
|
+
};
|
|
1928
|
+
},
|
|
1929
|
+
onThemeChange: function(cb) {
|
|
1930
|
+
themeListeners.push(cb);
|
|
1931
|
+
// Call immediately with current theme
|
|
1932
|
+
cb(currentTheme);
|
|
1933
|
+
return function() {
|
|
1934
|
+
var i = themeListeners.indexOf(cb);
|
|
1935
|
+
if (i >= 0) themeListeners.splice(i, 1);
|
|
1936
|
+
};
|
|
1937
|
+
},
|
|
1938
|
+
get theme() { return currentTheme; },
|
|
1939
|
+
|
|
1940
|
+
// Generic event subscription for real-time sync
|
|
1941
|
+
// Usage: photon.on('taskMove', function(data) { ... })
|
|
1942
|
+
on: function(eventName, cb) {
|
|
1943
|
+
if (!eventListeners[eventName]) eventListeners[eventName] = [];
|
|
1944
|
+
eventListeners[eventName].push(cb);
|
|
1945
|
+
return function() {
|
|
1946
|
+
var i = eventListeners[eventName].indexOf(cb);
|
|
1947
|
+
if (i >= 0) eventListeners[eventName].splice(i, 1);
|
|
1948
|
+
};
|
|
1949
|
+
},
|
|
1950
|
+
|
|
1951
|
+
// Photon-specific event subscription (for injected photon events)
|
|
1952
|
+
// Usage: photon.onPhoton('notifications', 'alertCreated', function(data) { ... })
|
|
1953
|
+
onPhoton: function(photonName, eventName, cb) {
|
|
1954
|
+
if (!photonEventListeners[photonName]) photonEventListeners[photonName] = {};
|
|
1955
|
+
if (!photonEventListeners[photonName][eventName]) photonEventListeners[photonName][eventName] = [];
|
|
1956
|
+
photonEventListeners[photonName][eventName].push(cb);
|
|
1957
|
+
return function() {
|
|
1958
|
+
var i = photonEventListeners[photonName][eventName].indexOf(cb);
|
|
1959
|
+
if (i >= 0) photonEventListeners[photonName][eventName].splice(i, 1);
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
|
|
1964
|
+
// Create direct window object: window.{photonName}
|
|
1965
|
+
// This provides a clean class-like API that mirrors server methods:
|
|
1966
|
+
// Server: this.emit('taskMove', data)
|
|
1967
|
+
// Client: kanban.onTaskMove(cb) - subscribe to events
|
|
1968
|
+
// Client: kanban.taskMove(args) - call server method
|
|
1969
|
+
var photonName = '${photonName}';
|
|
1970
|
+
window[photonName] = new Proxy({}, {
|
|
1971
|
+
get: function(target, prop) {
|
|
1972
|
+
if (typeof prop !== 'string') return undefined;
|
|
1973
|
+
|
|
1974
|
+
// onEventName -> subscribe to 'eventName' event
|
|
1975
|
+
// e.g., onTaskMove -> subscribe to 'taskMove'
|
|
1976
|
+
if (prop.startsWith('on') && prop.length > 2) {
|
|
1977
|
+
var eventName = prop.charAt(2).toLowerCase() + prop.slice(3);
|
|
1978
|
+
return function(cb) {
|
|
1979
|
+
return window.photon.on(eventName, cb);
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// methodName -> call server tool
|
|
1984
|
+
// e.g., taskMove(args) -> photon.callTool('taskMove', args)
|
|
1985
|
+
return function(args) {
|
|
1986
|
+
return window.photon.callTool(prop, args);
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
// Create proxies for injected photons (for event subscriptions)
|
|
1992
|
+
// e.g., notifications.onAlertCreated(cb) subscribes to 'alertCreated' from 'notifications' photon
|
|
1993
|
+
injectedPhotons.forEach(function(injectedName) {
|
|
1994
|
+
window[injectedName] = new Proxy({}, {
|
|
1995
|
+
get: function(target, prop) {
|
|
1996
|
+
if (typeof prop !== 'string') return undefined;
|
|
1997
|
+
|
|
1998
|
+
// onEventName -> subscribe to photon-specific event
|
|
1999
|
+
if (prop.startsWith('on') && prop.length > 2) {
|
|
2000
|
+
var eventName = prop.charAt(2).toLowerCase() + prop.slice(3);
|
|
2001
|
+
return function(cb) {
|
|
2002
|
+
return window.photon.onPhoton(injectedName, eventName, cb);
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// Method calls on injected photons are not supported from client
|
|
2007
|
+
// (injected photon methods are only available server-side)
|
|
2008
|
+
return undefined;
|
|
2009
|
+
}
|
|
2010
|
+
});
|
|
2011
|
+
});
|
|
2012
|
+
|
|
2013
|
+
// Size notification helper
|
|
2014
|
+
function sendSizeChanged() {
|
|
2015
|
+
var body = document.body;
|
|
2016
|
+
var root = document.documentElement;
|
|
2017
|
+
|
|
2018
|
+
// Calculate actual content dimensions
|
|
2019
|
+
var width = Math.max(
|
|
2020
|
+
body.scrollWidth,
|
|
2021
|
+
body.offsetWidth,
|
|
2022
|
+
root.clientWidth,
|
|
2023
|
+
root.scrollWidth,
|
|
2024
|
+
root.offsetWidth
|
|
2025
|
+
);
|
|
2026
|
+
var height = Math.max(
|
|
2027
|
+
body.scrollHeight,
|
|
2028
|
+
body.offsetHeight,
|
|
2029
|
+
root.clientHeight,
|
|
2030
|
+
root.scrollHeight,
|
|
2031
|
+
root.offsetHeight
|
|
2032
|
+
);
|
|
2033
|
+
|
|
2034
|
+
// Check for scrollable containers with overflow:hidden that hide true content size
|
|
2035
|
+
var containers = document.querySelectorAll('.board, [style*="overflow"]');
|
|
2036
|
+
containers.forEach(function(el) {
|
|
2037
|
+
if (el.scrollWidth > width) width = el.scrollWidth;
|
|
2038
|
+
if (el.scrollHeight > height) height = el.scrollHeight;
|
|
2039
|
+
});
|
|
2040
|
+
|
|
2041
|
+
// For kanban-style boards, calculate from column count
|
|
2042
|
+
var columns = document.querySelectorAll('.column');
|
|
2043
|
+
if (columns.length > 0) {
|
|
2044
|
+
var columnWidth = 220; // min-width + gap
|
|
2045
|
+
var boardPadding = 48;
|
|
2046
|
+
var neededWidth = (columns.length * columnWidth) + boardPadding;
|
|
2047
|
+
if (neededWidth > width) width = neededWidth;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// Reasonable minimums, maximums, and padding
|
|
2051
|
+
width = Math.max(width, 600) + 32;
|
|
2052
|
+
// Force minimum height for kanban-style boards
|
|
2053
|
+
// header(120) + column headers(50) + 3-4 cards(450) = 620
|
|
2054
|
+
if (columns.length > 0) {
|
|
2055
|
+
height = Math.max(height, 620);
|
|
2056
|
+
} else {
|
|
2057
|
+
height = Math.max(height, 400);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
postToHost({
|
|
2061
|
+
jsonrpc: '2.0',
|
|
2062
|
+
method: 'ui/notifications/size-changed',
|
|
2063
|
+
params: { width: width, height: height }
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
// MCP Apps handshake: send ui/initialize and wait for response
|
|
2068
|
+
var initId = generateCallId();
|
|
2069
|
+
pendingCalls[initId] = {
|
|
2070
|
+
resolve: function(result) {
|
|
2071
|
+
// Apply theme from host context
|
|
2072
|
+
if (result.hostContext && result.hostContext.theme) {
|
|
2073
|
+
document.documentElement.classList.add(result.hostContext.theme);
|
|
2074
|
+
document.documentElement.setAttribute('data-theme', result.hostContext.theme);
|
|
2075
|
+
}
|
|
2076
|
+
// Complete handshake
|
|
2077
|
+
postToHost({ jsonrpc: '2.0', method: 'ui/notifications/initialized', params: {} });
|
|
2078
|
+
|
|
2079
|
+
// Set up size notifications after handshake
|
|
2080
|
+
setTimeout(sendSizeChanged, 100);
|
|
2081
|
+
var resizeObserver = new ResizeObserver(function() {
|
|
2082
|
+
sendSizeChanged();
|
|
2083
|
+
});
|
|
2084
|
+
resizeObserver.observe(document.documentElement);
|
|
2085
|
+
resizeObserver.observe(document.body);
|
|
2086
|
+
},
|
|
2087
|
+
reject: function(err) { console.error('MCP Apps init failed:', err); }
|
|
2088
|
+
};
|
|
2089
|
+
|
|
2090
|
+
postToHost({
|
|
2091
|
+
jsonrpc: '2.0',
|
|
2092
|
+
id: initId,
|
|
2093
|
+
method: 'ui/initialize',
|
|
2094
|
+
params: {
|
|
2095
|
+
appInfo: { name: '${photonName}', version: '1.0.0' },
|
|
2096
|
+
appCapabilities: {},
|
|
2097
|
+
protocolVersion: '2026-01-26'
|
|
2098
|
+
}
|
|
2099
|
+
});
|
|
2100
|
+
})();
|
|
2101
|
+
</script>`;
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* Handle legacy photon:// asset read
|
|
2105
|
+
*/
|
|
2106
|
+
async handleAssetRead(uri, assetMatch) {
|
|
2107
|
+
const [, _photonName, assetType, assetId] = assetMatch;
|
|
2108
|
+
let resolvedPath;
|
|
2109
|
+
let mimeType = 'text/plain';
|
|
2110
|
+
if (assetType === 'ui') {
|
|
2111
|
+
const ui = this.mcp.assets.ui.find((u) => u.id === assetId);
|
|
2112
|
+
if (ui) {
|
|
2113
|
+
resolvedPath = ui.resolvedPath;
|
|
2114
|
+
mimeType = ui.mimeType || 'text/html;profile=mcp-app';
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
else if (assetType === 'prompts') {
|
|
2118
|
+
const prompt = this.mcp.assets.prompts.find((p) => p.id === assetId);
|
|
2119
|
+
if (prompt) {
|
|
2120
|
+
resolvedPath = prompt.resolvedPath;
|
|
2121
|
+
mimeType = 'text/markdown';
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
else if (assetType === 'resources') {
|
|
2125
|
+
const resource = this.mcp.assets.resources.find((r) => r.id === assetId);
|
|
2126
|
+
if (resource) {
|
|
2127
|
+
resolvedPath = resource.resolvedPath;
|
|
2128
|
+
mimeType = resource.mimeType || 'application/octet-stream';
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
if (resolvedPath) {
|
|
2132
|
+
let content = await fs.readFile(resolvedPath, 'utf-8');
|
|
2133
|
+
// Inject MCP Apps bridge for UI assets
|
|
2134
|
+
if (assetType === 'ui') {
|
|
2135
|
+
const bridgeScript = this.generateMcpAppsBridge();
|
|
2136
|
+
content = content.replace('<head>', `<head>\n${bridgeScript}`);
|
|
2137
|
+
}
|
|
2138
|
+
return {
|
|
2139
|
+
contents: [{ uri, mimeType, text: content }],
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
throw new Error(`Asset not found: ${uri}`);
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Handle static resource read (for both stdio and SSE handlers)
|
|
2146
|
+
*/
|
|
2147
|
+
async handleStaticRead(uri) {
|
|
2148
|
+
const static_ = this.mcp.statics.find((s) => s.uri === uri || this.matchUriPattern(s.uri, uri));
|
|
2149
|
+
if (!static_) {
|
|
2150
|
+
throw new Error(`Resource not found: ${uri}`);
|
|
2151
|
+
}
|
|
2152
|
+
const params = this.parseUriParams(static_.uri, uri);
|
|
2153
|
+
const result = await this.loader.executeTool(this.mcp, static_.name, params);
|
|
2154
|
+
return this.formatStaticResult(result, static_.mimeType);
|
|
2155
|
+
}
|
|
373
2156
|
/**
|
|
374
2157
|
* Stop the server
|
|
375
2158
|
*/
|
|
@@ -379,11 +2162,102 @@ export class PhotonServer {
|
|
|
379
2162
|
if (this.mcp?.instance?.onShutdown) {
|
|
380
2163
|
await this.mcp.instance.onShutdown();
|
|
381
2164
|
}
|
|
2165
|
+
// Disconnect MCP clients
|
|
2166
|
+
if (this.mcpClientFactory) {
|
|
2167
|
+
await this.mcpClientFactory.disconnect();
|
|
2168
|
+
}
|
|
2169
|
+
// Close SSE sessions
|
|
2170
|
+
for (const [_sessionId, session] of this.sseSessions) {
|
|
2171
|
+
await session.server.close();
|
|
2172
|
+
}
|
|
2173
|
+
this.sseSessions.clear();
|
|
2174
|
+
for (const client of this.statusClients) {
|
|
2175
|
+
client.end();
|
|
2176
|
+
}
|
|
2177
|
+
this.statusClients.clear();
|
|
2178
|
+
// Close HTTP server if running
|
|
2179
|
+
if (this.httpServer) {
|
|
2180
|
+
await new Promise((resolve) => {
|
|
2181
|
+
this.httpServer.close(() => resolve());
|
|
2182
|
+
});
|
|
2183
|
+
this.httpServer = null;
|
|
2184
|
+
}
|
|
382
2185
|
await this.server.close();
|
|
383
|
-
|
|
2186
|
+
this.log('info', 'Server stopped');
|
|
384
2187
|
}
|
|
385
2188
|
catch (error) {
|
|
386
|
-
|
|
2189
|
+
this.log('error', 'Error stopping server', { error: getErrorMessage(error) });
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
buildStatusSnapshot() {
|
|
2193
|
+
const warnings = [];
|
|
2194
|
+
const instance = this.mcp?.instance;
|
|
2195
|
+
if (instance && '_photonConfigError' in instance && instance._photonConfigError) {
|
|
2196
|
+
warnings.push('Photon configuration incomplete. Check env vars and MCP credentials.');
|
|
2197
|
+
}
|
|
2198
|
+
const assets = this.mcp?.assets || { ui: [], prompts: [], resources: [] };
|
|
2199
|
+
const tools = this.mcp?.tools || [];
|
|
2200
|
+
return {
|
|
2201
|
+
photon: this.mcp?.name || null,
|
|
2202
|
+
devMode: this.devMode,
|
|
2203
|
+
hotReloadDisabled: this.hotReloadDisabled,
|
|
2204
|
+
lastReloadError: this.lastReloadError || null,
|
|
2205
|
+
status: this.currentStatus,
|
|
2206
|
+
warnings,
|
|
2207
|
+
summary: {
|
|
2208
|
+
toolCount: tools.length,
|
|
2209
|
+
tools: tools.map((tool) => ({
|
|
2210
|
+
name: tool.name,
|
|
2211
|
+
description: tool.description || '',
|
|
2212
|
+
hasUI: Boolean(assets.ui?.some((ui) => ui.linkedTool === tool.name)),
|
|
2213
|
+
})),
|
|
2214
|
+
uiAssets: (assets.ui || []).map((ui) => ({ id: ui.id, linkedTool: ui.linkedTool })),
|
|
2215
|
+
promptCount: assets.prompts?.length || 0,
|
|
2216
|
+
resourceCount: assets.resources?.length || 0,
|
|
2217
|
+
},
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
handleStatusStream(_req, res) {
|
|
2221
|
+
res.writeHead(200, {
|
|
2222
|
+
'Content-Type': 'text/event-stream',
|
|
2223
|
+
'Cache-Control': 'no-cache',
|
|
2224
|
+
Connection: 'keep-alive',
|
|
2225
|
+
'Access-Control-Allow-Origin': '*',
|
|
2226
|
+
});
|
|
2227
|
+
res.write(`data: ${JSON.stringify(this.buildStatusSnapshot())}\n\n`);
|
|
2228
|
+
this.statusClients.add(res);
|
|
2229
|
+
const cleanup = () => {
|
|
2230
|
+
this.statusClients.delete(res);
|
|
2231
|
+
};
|
|
2232
|
+
res.on('close', cleanup);
|
|
2233
|
+
res.on('error', cleanup);
|
|
2234
|
+
}
|
|
2235
|
+
async broadcastReloadStatus(type, message) {
|
|
2236
|
+
this.currentStatus = { type, message, timestamp: Date.now() };
|
|
2237
|
+
this.pushStatusUpdate();
|
|
2238
|
+
const payload = {
|
|
2239
|
+
method: 'notifications/status',
|
|
2240
|
+
params: { type, message },
|
|
2241
|
+
};
|
|
2242
|
+
try {
|
|
2243
|
+
await this.server.notification(payload);
|
|
2244
|
+
}
|
|
2245
|
+
catch {
|
|
2246
|
+
// ignore
|
|
2247
|
+
}
|
|
2248
|
+
for (const session of this.sseSessions.values()) {
|
|
2249
|
+
try {
|
|
2250
|
+
await session.server.notification(payload);
|
|
2251
|
+
}
|
|
2252
|
+
catch {
|
|
2253
|
+
// ignore session errors
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
pushStatusUpdate() {
|
|
2258
|
+
const frame = `data: ${JSON.stringify(this.currentStatus)}\n\n`;
|
|
2259
|
+
for (const client of this.statusClients) {
|
|
2260
|
+
client.write(frame);
|
|
387
2261
|
}
|
|
388
2262
|
}
|
|
389
2263
|
/**
|
|
@@ -398,8 +2272,11 @@ export class PhotonServer {
|
|
|
398
2272
|
clearTimeout(this.reloadRetryTimeout);
|
|
399
2273
|
this.reloadRetryTimeout = undefined;
|
|
400
2274
|
}
|
|
2275
|
+
if (this.hotReloadDisabled) {
|
|
2276
|
+
throw new HotReloadDisabledError('Hot reload temporarily disabled after repeated failures. Restart Photon or fix the errors to re-enable.');
|
|
2277
|
+
}
|
|
401
2278
|
try {
|
|
402
|
-
|
|
2279
|
+
this.log('info', 'Reloading Photon');
|
|
403
2280
|
// Store old instance in case we need to rollback
|
|
404
2281
|
const oldInstance = this.mcp;
|
|
405
2282
|
// Call shutdown hook on old instance (but keep it for rollback)
|
|
@@ -408,7 +2285,7 @@ export class PhotonServer {
|
|
|
408
2285
|
await oldInstance.instance.onShutdown();
|
|
409
2286
|
}
|
|
410
2287
|
catch (shutdownError) {
|
|
411
|
-
|
|
2288
|
+
this.log('warn', 'Shutdown hook failed during reload', { error: shutdownError.message });
|
|
412
2289
|
// Continue with reload anyway
|
|
413
2290
|
}
|
|
414
2291
|
}
|
|
@@ -417,40 +2294,70 @@ export class PhotonServer {
|
|
|
417
2294
|
// Success! Update instance and reset failure count
|
|
418
2295
|
this.mcp = newMcp;
|
|
419
2296
|
this.reloadFailureCount = 0;
|
|
2297
|
+
this.hotReloadDisabled = false;
|
|
2298
|
+
this.lastReloadError = undefined;
|
|
420
2299
|
// Send list_changed notifications to inform client of updates
|
|
421
2300
|
await this.notifyListsChanged();
|
|
422
|
-
|
|
2301
|
+
// If daemon is running for this photon, reload it too
|
|
2302
|
+
if (this.daemonName && isDaemonRunning(this.daemonName)) {
|
|
2303
|
+
try {
|
|
2304
|
+
const result = await reloadDaemon(this.daemonName, this.options.filePath);
|
|
2305
|
+
if (result.success) {
|
|
2306
|
+
this.log('info', 'Daemon reloaded', { sessionsUpdated: result.sessionsUpdated });
|
|
2307
|
+
}
|
|
2308
|
+
else {
|
|
2309
|
+
this.log('warn', 'Daemon reload failed', { error: result.error });
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
catch (err) {
|
|
2313
|
+
this.log('warn', 'Daemon reload failed, may need manual restart', {
|
|
2314
|
+
error: getErrorMessage(err),
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
await this.broadcastReloadStatus('info', 'Hot reload complete');
|
|
2319
|
+
this.log('info', 'Reload complete');
|
|
423
2320
|
}
|
|
424
2321
|
catch (error) {
|
|
425
2322
|
this.reloadFailureCount++;
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
2323
|
+
this.log('error', 'Reload failed', {
|
|
2324
|
+
attempt: this.reloadFailureCount,
|
|
2325
|
+
maxAttempts: this.MAX_RELOAD_FAILURES,
|
|
2326
|
+
error: getErrorMessage(error),
|
|
2327
|
+
});
|
|
2328
|
+
if (error instanceof Error && error.name === 'PhotonInitializationError') {
|
|
2329
|
+
this.log('warn', 'onInitialize lifecycle hook failed', {
|
|
2330
|
+
hints: [
|
|
2331
|
+
'Database connection failure',
|
|
2332
|
+
'API authentication error',
|
|
2333
|
+
'Missing environment variables',
|
|
2334
|
+
'Invalid configuration',
|
|
2335
|
+
],
|
|
2336
|
+
});
|
|
437
2337
|
}
|
|
2338
|
+
this.lastReloadError = {
|
|
2339
|
+
message: getErrorMessage(error),
|
|
2340
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
2341
|
+
timestamp: Date.now(),
|
|
2342
|
+
attempts: this.reloadFailureCount,
|
|
2343
|
+
};
|
|
2344
|
+
await this.broadcastReloadStatus('error', `Hot reload failed: ${getErrorMessage(error)}`);
|
|
438
2345
|
if (this.reloadFailureCount >= this.MAX_RELOAD_FAILURES) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
2346
|
+
this.log('error', 'Maximum reload failures reached', {
|
|
2347
|
+
maxAttempts: this.MAX_RELOAD_FAILURES,
|
|
2348
|
+
action: 'keeping previous version active',
|
|
2349
|
+
});
|
|
2350
|
+
this.log('info', 'Server still running with previous version');
|
|
2351
|
+
this.hotReloadDisabled = true;
|
|
444
2352
|
this.reloadFailureCount = 0;
|
|
2353
|
+
throw new HotReloadDisabledError('Hot reload disabled after repeated failures. Restart Photon dev server once the errors are resolved.');
|
|
445
2354
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
// Keep the old instance running - it's still functional
|
|
453
|
-
console.error(`\nā Server still running with previous version`);
|
|
2355
|
+
const retryDelay = Math.min(5000 * this.reloadFailureCount, 15000);
|
|
2356
|
+
this.log('warn', 'Reload failed - waiting for next change', {
|
|
2357
|
+
retrySeconds: retryDelay / 1000,
|
|
2358
|
+
});
|
|
2359
|
+
this.log('info', 'Server still running with previous version');
|
|
2360
|
+
throw error;
|
|
454
2361
|
}
|
|
455
2362
|
}
|
|
456
2363
|
/**
|
|
@@ -471,11 +2378,13 @@ export class PhotonServer {
|
|
|
471
2378
|
await this.server.notification({
|
|
472
2379
|
method: 'notifications/resources/list_changed',
|
|
473
2380
|
});
|
|
474
|
-
|
|
2381
|
+
this.log('debug', 'Sent list_changed notifications');
|
|
475
2382
|
}
|
|
476
2383
|
catch (error) {
|
|
477
2384
|
// Notification sending is best-effort - don't fail reload if it fails
|
|
478
|
-
|
|
2385
|
+
this.log('warn', 'Failed to send list_changed notifications', {
|
|
2386
|
+
error: getErrorMessage(error),
|
|
2387
|
+
});
|
|
479
2388
|
}
|
|
480
2389
|
}
|
|
481
2390
|
}
|