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