@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
|
@@ -0,0 +1,1314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streamable HTTP Transport for MCP
|
|
3
|
+
*
|
|
4
|
+
* Implements the MCP Streamable HTTP transport specification (2025-03-26).
|
|
5
|
+
* This allows standard MCP clients (like Claude Desktop) to connect to Beam.
|
|
6
|
+
*
|
|
7
|
+
* Endpoint: /mcp
|
|
8
|
+
* - POST: Client sends JSON-RPC requests, server responds with JSON or SSE
|
|
9
|
+
* - GET: Opens SSE stream for server-initiated messages
|
|
10
|
+
*
|
|
11
|
+
* Configuration Schema (SEP-1596 inspired):
|
|
12
|
+
* - Returns configurationSchema in initialize response
|
|
13
|
+
* - Uses JSON Schema for rich UI generation (dropdowns, file pickers, etc.)
|
|
14
|
+
* - beam/configure tool for submitting configuration
|
|
15
|
+
* - beam/browse tool for server filesystem browsing
|
|
16
|
+
*
|
|
17
|
+
* @see https://modelcontextprotocol.io/specification/2025-03-26/basic/transports
|
|
18
|
+
*/
|
|
19
|
+
import { randomUUID } from 'crypto';
|
|
20
|
+
import { readdir, stat } from 'fs/promises';
|
|
21
|
+
import { join, dirname } from 'path';
|
|
22
|
+
import { homedir } from 'os';
|
|
23
|
+
import { PHOTON_VERSION } from '../version.js';
|
|
24
|
+
import { buildToolMetadataExtensions } from './types.js';
|
|
25
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
26
|
+
// SESSION MANAGEMENT
|
|
27
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
28
|
+
const sessions = new Map();
|
|
29
|
+
const pendingElicitations = new Map();
|
|
30
|
+
// Clean up old sessions periodically (30 min timeout)
|
|
31
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
32
|
+
setInterval(() => {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
for (const [id, session] of sessions) {
|
|
35
|
+
if (now - session.lastActivity.getTime() > SESSION_TIMEOUT_MS) {
|
|
36
|
+
sessions.delete(id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}, 60 * 1000);
|
|
40
|
+
function getOrCreateSession(sessionId) {
|
|
41
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
42
|
+
const session = sessions.get(sessionId);
|
|
43
|
+
session.lastActivity = new Date();
|
|
44
|
+
return session;
|
|
45
|
+
}
|
|
46
|
+
const newSession = {
|
|
47
|
+
id: randomUUID(),
|
|
48
|
+
initialized: false,
|
|
49
|
+
createdAt: new Date(),
|
|
50
|
+
lastActivity: new Date(),
|
|
51
|
+
};
|
|
52
|
+
sessions.set(newSession.id, newSession);
|
|
53
|
+
return newSession;
|
|
54
|
+
}
|
|
55
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
56
|
+
// CONFIGURATION SCHEMA GENERATION
|
|
57
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
58
|
+
/**
|
|
59
|
+
* Convert ConfigParam to JSON Schema property
|
|
60
|
+
*/
|
|
61
|
+
function configParamToJsonSchema(param) {
|
|
62
|
+
const schema = {
|
|
63
|
+
description: `Environment variable: ${param.envVar}`,
|
|
64
|
+
'x-env-var': param.envVar,
|
|
65
|
+
};
|
|
66
|
+
// Map TypeScript types to JSON Schema types
|
|
67
|
+
switch (param.type.toLowerCase()) {
|
|
68
|
+
case 'number':
|
|
69
|
+
schema.type = 'number';
|
|
70
|
+
break;
|
|
71
|
+
case 'boolean':
|
|
72
|
+
schema.type = 'boolean';
|
|
73
|
+
break;
|
|
74
|
+
case 'string':
|
|
75
|
+
default:
|
|
76
|
+
schema.type = 'string';
|
|
77
|
+
// Check for common sensitive parameter names - use OpenAPI standard
|
|
78
|
+
if (/password|secret|token|key|credential/i.test(param.name)) {
|
|
79
|
+
schema.format = 'password';
|
|
80
|
+
schema.writeOnly = true;
|
|
81
|
+
}
|
|
82
|
+
// Check for path-like parameter names
|
|
83
|
+
else if (/path|file|dir|directory|folder/i.test(param.name)) {
|
|
84
|
+
schema.format = 'path';
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
// Add default value if present
|
|
89
|
+
if (param.hasDefault && param.defaultValue !== undefined) {
|
|
90
|
+
schema.default = param.defaultValue;
|
|
91
|
+
}
|
|
92
|
+
return schema;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Generate configurationSchema for all unconfigured photons
|
|
96
|
+
* Uses JSON Schema format for rich UI generation
|
|
97
|
+
*/
|
|
98
|
+
function generateConfigurationSchema(photons) {
|
|
99
|
+
const schema = {};
|
|
100
|
+
for (const photon of photons) {
|
|
101
|
+
// Only include unconfigured photons with params
|
|
102
|
+
if (photon.configured)
|
|
103
|
+
continue;
|
|
104
|
+
const unconfigured = photon;
|
|
105
|
+
if (!unconfigured.requiredParams || unconfigured.requiredParams.length === 0)
|
|
106
|
+
continue;
|
|
107
|
+
const properties = {};
|
|
108
|
+
const required = [];
|
|
109
|
+
for (const param of unconfigured.requiredParams) {
|
|
110
|
+
properties[param.name] = configParamToJsonSchema(param);
|
|
111
|
+
// Mark as required if not optional and no default
|
|
112
|
+
if (!param.isOptional && !param.hasDefault) {
|
|
113
|
+
required.push(param.name);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
schema[photon.name] = {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties,
|
|
119
|
+
required: required.length > 0 ? required : undefined,
|
|
120
|
+
'x-error-message': unconfigured.errorMessage,
|
|
121
|
+
'x-internal': unconfigured.internal,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return schema;
|
|
125
|
+
}
|
|
126
|
+
const handlers = {
|
|
127
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
// Lifecycle
|
|
129
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
initialize: async (req, session, ctx) => {
|
|
131
|
+
session.initialized = true;
|
|
132
|
+
// Capture client info and detect Beam clients
|
|
133
|
+
const clientInfo = req.params?.clientInfo;
|
|
134
|
+
if (clientInfo) {
|
|
135
|
+
session.clientInfo = clientInfo;
|
|
136
|
+
session.isBeam = clientInfo.name === 'beam';
|
|
137
|
+
}
|
|
138
|
+
// Generate configuration schema for unconfigured photons
|
|
139
|
+
const configurationSchema = generateConfigurationSchema(ctx.photons);
|
|
140
|
+
return {
|
|
141
|
+
jsonrpc: '2.0',
|
|
142
|
+
id: req.id,
|
|
143
|
+
result: {
|
|
144
|
+
protocolVersion: '2025-03-26',
|
|
145
|
+
serverInfo: {
|
|
146
|
+
name: 'beam-mcp',
|
|
147
|
+
version: PHOTON_VERSION,
|
|
148
|
+
},
|
|
149
|
+
capabilities: {
|
|
150
|
+
tools: { listChanged: true },
|
|
151
|
+
resources: { listChanged: true },
|
|
152
|
+
},
|
|
153
|
+
// SEP-1596 inspired: configuration schema for unconfigured photons
|
|
154
|
+
// Uses JSON Schema for rich UI generation
|
|
155
|
+
configurationSchema: Object.keys(configurationSchema).length > 0 ? configurationSchema : undefined,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
'notifications/initialized': async (req, session) => {
|
|
160
|
+
// Notification - no response needed
|
|
161
|
+
return { jsonrpc: '2.0' };
|
|
162
|
+
},
|
|
163
|
+
// Handle elicitation response from frontend
|
|
164
|
+
'beam/elicitation-response': async (req, session) => {
|
|
165
|
+
const params = req.params;
|
|
166
|
+
const elicitationId = params?.elicitationId;
|
|
167
|
+
if (!elicitationId) {
|
|
168
|
+
return {
|
|
169
|
+
jsonrpc: '2.0',
|
|
170
|
+
id: req.id,
|
|
171
|
+
error: { code: -32602, message: 'Missing elicitationId' },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const pending = pendingElicitations.get(elicitationId);
|
|
175
|
+
if (!pending) {
|
|
176
|
+
return {
|
|
177
|
+
jsonrpc: '2.0',
|
|
178
|
+
id: req.id,
|
|
179
|
+
error: { code: -32602, message: 'Unknown elicitationId' },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
pendingElicitations.delete(elicitationId);
|
|
183
|
+
if (params?.cancelled) {
|
|
184
|
+
pending.reject(new Error('Elicitation cancelled by user'));
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
pending.resolve(params?.value);
|
|
188
|
+
}
|
|
189
|
+
return { jsonrpc: '2.0', id: req.id, result: { success: true } };
|
|
190
|
+
},
|
|
191
|
+
// Client notifies what resource they're viewing (for on-demand subscriptions)
|
|
192
|
+
// photonId: hash of photon path (unique across servers)
|
|
193
|
+
// itemId: whatever the photon uses to identify the item (e.g., board name)
|
|
194
|
+
// lastEventId: optional - for replay of missed events on reconnect
|
|
195
|
+
'beam/viewing': async (req, session, ctx) => {
|
|
196
|
+
const params = req.params;
|
|
197
|
+
const photonId = params?.photonId;
|
|
198
|
+
const itemId = params?.itemId;
|
|
199
|
+
const lastEventId = params?.lastEventId;
|
|
200
|
+
if (photonId && itemId && ctx.subscriptionManager) {
|
|
201
|
+
ctx.subscriptionManager.onClientViewingBoard(session.id, photonId, itemId, lastEventId);
|
|
202
|
+
}
|
|
203
|
+
// Notification - no response needed
|
|
204
|
+
return { jsonrpc: '2.0' };
|
|
205
|
+
},
|
|
206
|
+
ping: async (req) => {
|
|
207
|
+
return { jsonrpc: '2.0', id: req.id, result: {} };
|
|
208
|
+
},
|
|
209
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
210
|
+
// Tools
|
|
211
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
212
|
+
'tools/list': async (req, session, ctx) => {
|
|
213
|
+
const tools = [];
|
|
214
|
+
// Add configured photon methods as tools
|
|
215
|
+
for (const photon of ctx.photons) {
|
|
216
|
+
if (!photon.configured || !photon.methods)
|
|
217
|
+
continue;
|
|
218
|
+
for (const method of photon.methods) {
|
|
219
|
+
tools.push({
|
|
220
|
+
name: `${photon.name}/${method.name}`,
|
|
221
|
+
description: method.description || `Execute ${method.name}`,
|
|
222
|
+
inputSchema: method.params || { type: 'object', properties: {} },
|
|
223
|
+
'x-photon-id': photon.id, // Unique ID (hash of path) for subscriptions
|
|
224
|
+
'x-photon-path': photon.path, // File path for View Source
|
|
225
|
+
'x-photon-description': photon.description,
|
|
226
|
+
'x-photon-icon': photon.icon,
|
|
227
|
+
'x-photon-internal': photon.internal,
|
|
228
|
+
'x-photon-prompt-count': photon.promptCount ?? 0,
|
|
229
|
+
'x-photon-resource-count': photon.resourceCount ?? 0,
|
|
230
|
+
...buildToolMetadataExtensions(method),
|
|
231
|
+
// MCP Apps standard: _meta.ui for linked UI resources and visibility
|
|
232
|
+
...(method.linkedUi || method.visibility
|
|
233
|
+
? {
|
|
234
|
+
_meta: {
|
|
235
|
+
ui: {
|
|
236
|
+
...(method.linkedUi
|
|
237
|
+
? { resourceUri: `ui://${photon.name}/${method.linkedUi}` }
|
|
238
|
+
: {}),
|
|
239
|
+
...(method.visibility ? { visibility: method.visibility } : {}),
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
: {}),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Add beam system tools (internal — hidden from sidebar)
|
|
248
|
+
tools.push({
|
|
249
|
+
name: 'beam/configure',
|
|
250
|
+
'x-photon-internal': true,
|
|
251
|
+
description: 'Configure a photon with required parameters. Use initialize response configurationSchema to get required fields.',
|
|
252
|
+
inputSchema: {
|
|
253
|
+
type: 'object',
|
|
254
|
+
properties: {
|
|
255
|
+
photon: {
|
|
256
|
+
type: 'string',
|
|
257
|
+
description: 'Name of the photon to configure',
|
|
258
|
+
},
|
|
259
|
+
config: {
|
|
260
|
+
type: 'object',
|
|
261
|
+
description: 'Configuration values (key-value pairs matching the configurationSchema)',
|
|
262
|
+
additionalProperties: true,
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
required: ['photon', 'config'],
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
tools.push({
|
|
269
|
+
name: 'beam/browse',
|
|
270
|
+
description: 'Browse server filesystem for file/directory selection',
|
|
271
|
+
inputSchema: {
|
|
272
|
+
type: 'object',
|
|
273
|
+
properties: {
|
|
274
|
+
path: {
|
|
275
|
+
type: 'string',
|
|
276
|
+
description: 'Directory path to list (defaults to home directory)',
|
|
277
|
+
},
|
|
278
|
+
filter: {
|
|
279
|
+
type: 'string',
|
|
280
|
+
description: 'File extension filter (e.g., ".pem,.crt" or "*.photon.ts")',
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
tools.push({
|
|
286
|
+
name: 'beam/reload',
|
|
287
|
+
description: 'Reload a photon to pick up file changes',
|
|
288
|
+
inputSchema: {
|
|
289
|
+
type: 'object',
|
|
290
|
+
properties: {
|
|
291
|
+
photon: {
|
|
292
|
+
type: 'string',
|
|
293
|
+
description: 'Name of the photon to reload',
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
required: ['photon'],
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
tools.push({
|
|
300
|
+
name: 'beam/remove',
|
|
301
|
+
description: 'Remove a photon from the workspace',
|
|
302
|
+
inputSchema: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
properties: {
|
|
305
|
+
photon: {
|
|
306
|
+
type: 'string',
|
|
307
|
+
description: 'Name of the photon to remove',
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
required: ['photon'],
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
tools.push({
|
|
314
|
+
name: 'beam/photon-help',
|
|
315
|
+
description: 'Get rich documentation for a photon',
|
|
316
|
+
inputSchema: {
|
|
317
|
+
type: 'object',
|
|
318
|
+
properties: {
|
|
319
|
+
photon: {
|
|
320
|
+
type: 'string',
|
|
321
|
+
description: 'Name of the photon to get help for',
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
required: ['photon'],
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
tools.push({
|
|
328
|
+
name: 'beam/update-metadata',
|
|
329
|
+
description: 'Update photon or method metadata (icon, description)',
|
|
330
|
+
inputSchema: {
|
|
331
|
+
type: 'object',
|
|
332
|
+
properties: {
|
|
333
|
+
photon: {
|
|
334
|
+
type: 'string',
|
|
335
|
+
description: 'Name of the photon',
|
|
336
|
+
},
|
|
337
|
+
method: {
|
|
338
|
+
type: 'string',
|
|
339
|
+
description: 'Name of the method (optional, for method metadata)',
|
|
340
|
+
},
|
|
341
|
+
metadata: {
|
|
342
|
+
type: 'object',
|
|
343
|
+
description: 'Metadata to update (icon, description)',
|
|
344
|
+
properties: {
|
|
345
|
+
icon: { type: 'string' },
|
|
346
|
+
description: { type: 'string' },
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
required: ['photon', 'metadata'],
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
// Filter out app-only tools for external (non-Beam) MCP clients
|
|
354
|
+
const visibleTools = session.isBeam
|
|
355
|
+
? tools
|
|
356
|
+
: tools.filter((t) => {
|
|
357
|
+
const vis = t._meta?.ui?.visibility;
|
|
358
|
+
if (vis && Array.isArray(vis) && vis.includes('app') && !vis.includes('model')) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
return true;
|
|
362
|
+
});
|
|
363
|
+
return { jsonrpc: '2.0', id: req.id, result: { tools: visibleTools } };
|
|
364
|
+
},
|
|
365
|
+
'tools/call': async (req, session, ctx) => {
|
|
366
|
+
const { name, arguments: args } = req.params;
|
|
367
|
+
// Handle beam system tools
|
|
368
|
+
if (name === 'beam/configure') {
|
|
369
|
+
return handleBeamConfigure(req, ctx, args || {});
|
|
370
|
+
}
|
|
371
|
+
if (name === 'beam/browse') {
|
|
372
|
+
return handleBeamBrowse(req, args || {});
|
|
373
|
+
}
|
|
374
|
+
if (name === 'beam/reload') {
|
|
375
|
+
return handleBeamReload(req, ctx, args || {});
|
|
376
|
+
}
|
|
377
|
+
if (name === 'beam/remove') {
|
|
378
|
+
return handleBeamRemove(req, ctx, args || {});
|
|
379
|
+
}
|
|
380
|
+
if (name === 'beam/update-metadata') {
|
|
381
|
+
return handleBeamUpdateMetadata(req, ctx, args || {});
|
|
382
|
+
}
|
|
383
|
+
if (name === 'beam/photon-help') {
|
|
384
|
+
return handleBeamPhotonHelp(req, ctx, args || {});
|
|
385
|
+
}
|
|
386
|
+
// Parse tool name: photon-name/method-name
|
|
387
|
+
const slashIndex = name.indexOf('/');
|
|
388
|
+
if (slashIndex === -1) {
|
|
389
|
+
return {
|
|
390
|
+
jsonrpc: '2.0',
|
|
391
|
+
id: req.id,
|
|
392
|
+
result: {
|
|
393
|
+
content: [{ type: 'text', text: `Invalid tool name: ${name}` }],
|
|
394
|
+
isError: true,
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const photonName = name.slice(0, slashIndex);
|
|
399
|
+
const methodName = name.slice(slashIndex + 1);
|
|
400
|
+
// Find photon info for UI metadata
|
|
401
|
+
const photonInfo = ctx.photons.find((p) => p.name === photonName);
|
|
402
|
+
const methodInfo = photonInfo?.configured
|
|
403
|
+
? photonInfo.methods?.find((m) => m.name === methodName)
|
|
404
|
+
: undefined;
|
|
405
|
+
// Build UI metadata
|
|
406
|
+
const uiMetadata = {};
|
|
407
|
+
if (methodInfo?.outputFormat) {
|
|
408
|
+
uiMetadata['x-output-format'] = methodInfo.outputFormat;
|
|
409
|
+
}
|
|
410
|
+
const mcp = ctx.photonMCPs.get(photonName);
|
|
411
|
+
if (!mcp?.instance) {
|
|
412
|
+
return {
|
|
413
|
+
jsonrpc: '2.0',
|
|
414
|
+
id: req.id,
|
|
415
|
+
result: {
|
|
416
|
+
content: [{ type: 'text', text: `Photon not found: ${photonName}` }],
|
|
417
|
+
isError: true,
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
// Check instance first, then prototype, then static methods on class
|
|
422
|
+
let method = mcp.instance[methodName];
|
|
423
|
+
let isStatic = false;
|
|
424
|
+
if (typeof method !== 'function') {
|
|
425
|
+
method = Object.getPrototypeOf(mcp.instance)?.[methodName];
|
|
426
|
+
}
|
|
427
|
+
// Check for static method on class constructor
|
|
428
|
+
if (typeof method !== 'function' && mcp.classConstructor) {
|
|
429
|
+
method = mcp.classConstructor[methodName];
|
|
430
|
+
isStatic = true;
|
|
431
|
+
}
|
|
432
|
+
if (typeof method !== 'function') {
|
|
433
|
+
return {
|
|
434
|
+
jsonrpc: '2.0',
|
|
435
|
+
id: req.id,
|
|
436
|
+
result: {
|
|
437
|
+
content: [{ type: 'text', text: `Method not found: ${methodName}` }],
|
|
438
|
+
isError: true,
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
// Create outputHandler to capture emits for real-time UI updates
|
|
444
|
+
const outputHandler = (yieldValue) => {
|
|
445
|
+
if (!ctx.broadcast)
|
|
446
|
+
return;
|
|
447
|
+
// Forward progress events as MCP notifications
|
|
448
|
+
if (yieldValue?.emit === 'progress') {
|
|
449
|
+
const rawValue = typeof yieldValue.value === 'number' ? yieldValue.value : 0;
|
|
450
|
+
const progress = rawValue <= 1 ? rawValue * 100 : rawValue;
|
|
451
|
+
ctx.broadcast({
|
|
452
|
+
jsonrpc: '2.0',
|
|
453
|
+
method: 'notifications/progress',
|
|
454
|
+
params: {
|
|
455
|
+
progressToken: `progress_${photonName}_${methodName}`,
|
|
456
|
+
progress,
|
|
457
|
+
total: 100,
|
|
458
|
+
message: yieldValue.message || null,
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
// Forward status events as MCP notifications
|
|
464
|
+
if (yieldValue?.emit === 'status') {
|
|
465
|
+
ctx.broadcast({
|
|
466
|
+
jsonrpc: '2.0',
|
|
467
|
+
method: 'notifications/progress',
|
|
468
|
+
params: {
|
|
469
|
+
progressToken: `progress_${photonName}_${methodName}`,
|
|
470
|
+
progress: 0,
|
|
471
|
+
total: 100,
|
|
472
|
+
message: yieldValue.message || '',
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
// Forward toast events as beam notifications
|
|
478
|
+
if (yieldValue?.emit === 'toast') {
|
|
479
|
+
ctx.broadcast({
|
|
480
|
+
jsonrpc: '2.0',
|
|
481
|
+
method: 'beam/toast',
|
|
482
|
+
params: {
|
|
483
|
+
message: yieldValue.message || '',
|
|
484
|
+
type: yieldValue.type || 'info',
|
|
485
|
+
duration: yieldValue.duration,
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
// Forward thinking events as beam notifications
|
|
491
|
+
if (yieldValue?.emit === 'thinking') {
|
|
492
|
+
ctx.broadcast({
|
|
493
|
+
jsonrpc: '2.0',
|
|
494
|
+
method: 'beam/thinking',
|
|
495
|
+
params: {
|
|
496
|
+
active: yieldValue.active ?? true,
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
// Forward log events as beam notifications
|
|
502
|
+
if (yieldValue?.emit === 'log') {
|
|
503
|
+
ctx.broadcast({
|
|
504
|
+
jsonrpc: '2.0',
|
|
505
|
+
method: 'beam/log',
|
|
506
|
+
params: {
|
|
507
|
+
message: yieldValue.message || '',
|
|
508
|
+
level: yieldValue.level || 'info',
|
|
509
|
+
data: yieldValue.data,
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
// Forward channel events (task-moved, task-updated, etc.) with full delta
|
|
515
|
+
// These contain specific event type + data for efficient UI updates
|
|
516
|
+
if (yieldValue?.channel && yieldValue?.event) {
|
|
517
|
+
ctx.broadcast({
|
|
518
|
+
type: 'channel-event',
|
|
519
|
+
photon: photonName,
|
|
520
|
+
channel: yieldValue.channel,
|
|
521
|
+
event: yieldValue.event,
|
|
522
|
+
data: yieldValue.data,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
// Note: board-update emits are intentionally not forwarded here
|
|
526
|
+
// Channel events provide more specific info for real-time updates
|
|
527
|
+
};
|
|
528
|
+
// Create inputProvider to handle ask yields (elicitation)
|
|
529
|
+
const inputProvider = async (ask) => {
|
|
530
|
+
if (!ctx.broadcast) {
|
|
531
|
+
throw new Error('No broadcast connection for elicitation');
|
|
532
|
+
}
|
|
533
|
+
// Generate unique elicitation ID
|
|
534
|
+
const elicitationId = randomUUID();
|
|
535
|
+
return new Promise((resolve, reject) => {
|
|
536
|
+
// Store pending elicitation
|
|
537
|
+
pendingElicitations.set(elicitationId, {
|
|
538
|
+
resolve,
|
|
539
|
+
reject,
|
|
540
|
+
sessionId: session?.id || '',
|
|
541
|
+
});
|
|
542
|
+
// Broadcast elicitation request to frontend
|
|
543
|
+
ctx.broadcast({
|
|
544
|
+
jsonrpc: '2.0',
|
|
545
|
+
method: 'beam/elicitation',
|
|
546
|
+
params: {
|
|
547
|
+
elicitationId,
|
|
548
|
+
...ask,
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
// Timeout after 5 minutes
|
|
552
|
+
setTimeout(() => {
|
|
553
|
+
if (pendingElicitations.has(elicitationId)) {
|
|
554
|
+
pendingElicitations.delete(elicitationId);
|
|
555
|
+
reject(new Error('Elicitation timeout - no response received'));
|
|
556
|
+
}
|
|
557
|
+
}, 300000);
|
|
558
|
+
});
|
|
559
|
+
};
|
|
560
|
+
// Use loader.executeTool if available (sets up execution context for this.emit())
|
|
561
|
+
// Fall back to direct method call for backward compatibility
|
|
562
|
+
let result;
|
|
563
|
+
if (ctx.loader) {
|
|
564
|
+
result = await ctx.loader.executeTool(mcp, methodName, args || {}, {
|
|
565
|
+
outputHandler,
|
|
566
|
+
inputProvider,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
// For static methods, don't bind to instance
|
|
571
|
+
result = isStatic ? await method(args || {}) : await method.call(mcp.instance, args || {});
|
|
572
|
+
}
|
|
573
|
+
// Handle async generators (when not using loader)
|
|
574
|
+
if (result && typeof result[Symbol.asyncIterator] === 'function') {
|
|
575
|
+
const chunks = [];
|
|
576
|
+
for await (const chunk of result) {
|
|
577
|
+
if (chunk.emit === 'result') {
|
|
578
|
+
chunks.push(chunk.data);
|
|
579
|
+
}
|
|
580
|
+
else if (chunk.emit === 'board-update' && ctx.broadcast) {
|
|
581
|
+
// Forward board-update from generator
|
|
582
|
+
ctx.broadcast({
|
|
583
|
+
type: 'board-update',
|
|
584
|
+
photon: photonName,
|
|
585
|
+
board: chunk.board,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
else if (chunk.emit !== 'progress') {
|
|
589
|
+
chunks.push(chunk);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const finalResult = chunks.length === 1 ? chunks[0] : chunks;
|
|
593
|
+
const genResponse = {
|
|
594
|
+
jsonrpc: '2.0',
|
|
595
|
+
id: req.id,
|
|
596
|
+
result: {
|
|
597
|
+
content: [{ type: 'text', text: JSON.stringify(finalResult, null, 2) }],
|
|
598
|
+
isError: false,
|
|
599
|
+
...uiMetadata,
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
// Broadcast tool result as MCP Apps notification for linked-UI methods
|
|
603
|
+
if (ctx.broadcast && methodInfo?.linkedUi) {
|
|
604
|
+
ctx.broadcast({
|
|
605
|
+
jsonrpc: '2.0',
|
|
606
|
+
method: 'ui/notifications/tool-result',
|
|
607
|
+
params: {
|
|
608
|
+
toolName: `${photonName}/${methodName}`,
|
|
609
|
+
result: genResponse.result,
|
|
610
|
+
isError: false,
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
return genResponse;
|
|
615
|
+
}
|
|
616
|
+
const toolResponse = {
|
|
617
|
+
jsonrpc: '2.0',
|
|
618
|
+
id: req.id,
|
|
619
|
+
result: {
|
|
620
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
621
|
+
isError: false,
|
|
622
|
+
...uiMetadata,
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
// Broadcast tool result as MCP Apps notification for linked-UI methods
|
|
626
|
+
if (ctx.broadcast && methodInfo?.linkedUi) {
|
|
627
|
+
ctx.broadcast({
|
|
628
|
+
jsonrpc: '2.0',
|
|
629
|
+
method: 'ui/notifications/tool-result',
|
|
630
|
+
params: {
|
|
631
|
+
toolName: `${photonName}/${methodName}`,
|
|
632
|
+
result: toolResponse.result,
|
|
633
|
+
isError: false,
|
|
634
|
+
},
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
return toolResponse;
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
641
|
+
return {
|
|
642
|
+
jsonrpc: '2.0',
|
|
643
|
+
id: req.id,
|
|
644
|
+
result: {
|
|
645
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
646
|
+
isError: true,
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
652
|
+
// Resources (MCP Apps ui:// scheme)
|
|
653
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
654
|
+
'resources/list': async (req, session, ctx) => {
|
|
655
|
+
const resources = [];
|
|
656
|
+
for (const photon of ctx.photons) {
|
|
657
|
+
if (!photon.configured || !photon.assets?.ui)
|
|
658
|
+
continue;
|
|
659
|
+
for (const uiAsset of photon.assets.ui) {
|
|
660
|
+
const uri = uiAsset.uri || `ui://${photon.name}/${uiAsset.id}`;
|
|
661
|
+
resources.push({
|
|
662
|
+
uri,
|
|
663
|
+
name: uiAsset.id,
|
|
664
|
+
mimeType: uiAsset.mimeType || 'text/html;profile=mcp-app',
|
|
665
|
+
description: uiAsset.linkedTool
|
|
666
|
+
? `UI template for ${photon.name}/${uiAsset.linkedTool}`
|
|
667
|
+
: `UI template: ${uiAsset.id}`,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return { jsonrpc: '2.0', id: req.id, result: { resources } };
|
|
672
|
+
},
|
|
673
|
+
'resources/read': async (req, session, ctx) => {
|
|
674
|
+
const { uri } = req.params;
|
|
675
|
+
// Parse ui:// URI
|
|
676
|
+
const match = uri.match(/^ui:\/\/([^/]+)\/(.+)$/);
|
|
677
|
+
if (!match) {
|
|
678
|
+
return {
|
|
679
|
+
jsonrpc: '2.0',
|
|
680
|
+
id: req.id,
|
|
681
|
+
error: { code: -32602, message: `Invalid URI: ${uri}` },
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
const [, photonName, uiId] = match;
|
|
685
|
+
const content = await ctx.loadUIAsset(photonName, uiId);
|
|
686
|
+
if (!content) {
|
|
687
|
+
return {
|
|
688
|
+
jsonrpc: '2.0',
|
|
689
|
+
id: req.id,
|
|
690
|
+
error: { code: -32602, message: `Resource not found: ${uri}` },
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
jsonrpc: '2.0',
|
|
695
|
+
id: req.id,
|
|
696
|
+
result: {
|
|
697
|
+
contents: [{ uri, mimeType: 'text/html;profile=mcp-app', text: content }],
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
703
|
+
// BEAM SYSTEM TOOLS
|
|
704
|
+
// ════════════════════════════════════════════════════════════════════════════════
|
|
705
|
+
/**
|
|
706
|
+
* Handle beam/configure tool - configure a photon with provided values
|
|
707
|
+
*/
|
|
708
|
+
async function handleBeamConfigure(req, ctx, args) {
|
|
709
|
+
const { photon: photonName, config } = args;
|
|
710
|
+
if (!photonName) {
|
|
711
|
+
return {
|
|
712
|
+
jsonrpc: '2.0',
|
|
713
|
+
id: req.id,
|
|
714
|
+
result: {
|
|
715
|
+
content: [{ type: 'text', text: 'Error: photon name is required' }],
|
|
716
|
+
isError: true,
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
if (!config || typeof config !== 'object') {
|
|
721
|
+
return {
|
|
722
|
+
jsonrpc: '2.0',
|
|
723
|
+
id: req.id,
|
|
724
|
+
result: {
|
|
725
|
+
content: [{ type: 'text', text: 'Error: config object is required' }],
|
|
726
|
+
isError: true,
|
|
727
|
+
},
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
// Check if configurePhoton callback is available
|
|
731
|
+
if (!ctx.configurePhoton) {
|
|
732
|
+
return {
|
|
733
|
+
jsonrpc: '2.0',
|
|
734
|
+
id: req.id,
|
|
735
|
+
result: {
|
|
736
|
+
content: [{ type: 'text', text: 'Error: Configuration not supported in this context' }],
|
|
737
|
+
isError: true,
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
const result = await ctx.configurePhoton(photonName, config);
|
|
743
|
+
if (result.success) {
|
|
744
|
+
return {
|
|
745
|
+
jsonrpc: '2.0',
|
|
746
|
+
id: req.id,
|
|
747
|
+
result: {
|
|
748
|
+
content: [
|
|
749
|
+
{
|
|
750
|
+
type: 'text',
|
|
751
|
+
text: `Successfully configured ${photonName}. Tools list will be updated.`,
|
|
752
|
+
},
|
|
753
|
+
],
|
|
754
|
+
isError: false,
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
return {
|
|
760
|
+
jsonrpc: '2.0',
|
|
761
|
+
id: req.id,
|
|
762
|
+
result: {
|
|
763
|
+
content: [{ type: 'text', text: `Failed to configure ${photonName}: ${result.error}` }],
|
|
764
|
+
isError: true,
|
|
765
|
+
},
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
771
|
+
return {
|
|
772
|
+
jsonrpc: '2.0',
|
|
773
|
+
id: req.id,
|
|
774
|
+
result: {
|
|
775
|
+
content: [{ type: 'text', text: `Error configuring ${photonName}: ${message}` }],
|
|
776
|
+
isError: true,
|
|
777
|
+
},
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Handle beam/browse tool - browse server filesystem
|
|
783
|
+
*/
|
|
784
|
+
async function handleBeamBrowse(req, args) {
|
|
785
|
+
const { path: requestedPath, filter } = args;
|
|
786
|
+
// Default to home directory
|
|
787
|
+
let targetPath = requestedPath || homedir();
|
|
788
|
+
// Handle relative navigation (.. for parent)
|
|
789
|
+
if (targetPath.endsWith('/..') || targetPath === '..') {
|
|
790
|
+
targetPath = dirname(targetPath.replace(/\/?\.\.$/, ''));
|
|
791
|
+
}
|
|
792
|
+
try {
|
|
793
|
+
const stats = await stat(targetPath);
|
|
794
|
+
if (!stats.isDirectory()) {
|
|
795
|
+
targetPath = dirname(targetPath);
|
|
796
|
+
}
|
|
797
|
+
const entries = await readdir(targetPath, { withFileTypes: true });
|
|
798
|
+
// Parse filter
|
|
799
|
+
const filters = filter ? filter.split(',').map((f) => f.trim().toLowerCase()) : [];
|
|
800
|
+
const items = entries
|
|
801
|
+
.filter((entry) => {
|
|
802
|
+
// Always show directories
|
|
803
|
+
if (entry.isDirectory())
|
|
804
|
+
return true;
|
|
805
|
+
// No filter = show all
|
|
806
|
+
if (filters.length === 0)
|
|
807
|
+
return true;
|
|
808
|
+
const fileName = entry.name.toLowerCase();
|
|
809
|
+
return filters.some((f) => {
|
|
810
|
+
// Handle glob patterns like "*.photon.ts"
|
|
811
|
+
if (f.startsWith('*.')) {
|
|
812
|
+
const suffix = f.slice(1);
|
|
813
|
+
return fileName.endsWith(suffix);
|
|
814
|
+
}
|
|
815
|
+
// Handle extension patterns like ".ts" or "ts"
|
|
816
|
+
const ext = f.startsWith('.') ? f : `.${f}`;
|
|
817
|
+
return fileName.endsWith(ext);
|
|
818
|
+
});
|
|
819
|
+
})
|
|
820
|
+
.map((entry) => ({
|
|
821
|
+
name: entry.name,
|
|
822
|
+
path: join(targetPath, entry.name),
|
|
823
|
+
isDirectory: entry.isDirectory(),
|
|
824
|
+
}))
|
|
825
|
+
.sort((a, b) => {
|
|
826
|
+
// Directories first, then alphabetical
|
|
827
|
+
if (a.isDirectory !== b.isDirectory) {
|
|
828
|
+
return a.isDirectory ? -1 : 1;
|
|
829
|
+
}
|
|
830
|
+
return a.name.localeCompare(b.name);
|
|
831
|
+
});
|
|
832
|
+
// Calculate parent path
|
|
833
|
+
const parent = dirname(targetPath);
|
|
834
|
+
return {
|
|
835
|
+
jsonrpc: '2.0',
|
|
836
|
+
id: req.id,
|
|
837
|
+
result: {
|
|
838
|
+
content: [
|
|
839
|
+
{
|
|
840
|
+
type: 'text',
|
|
841
|
+
text: JSON.stringify({
|
|
842
|
+
path: targetPath,
|
|
843
|
+
parent: parent !== targetPath ? parent : null,
|
|
844
|
+
items,
|
|
845
|
+
}, null, 2),
|
|
846
|
+
},
|
|
847
|
+
],
|
|
848
|
+
isError: false,
|
|
849
|
+
},
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
catch (error) {
|
|
853
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
854
|
+
return {
|
|
855
|
+
jsonrpc: '2.0',
|
|
856
|
+
id: req.id,
|
|
857
|
+
result: {
|
|
858
|
+
content: [{ type: 'text', text: `Error browsing ${targetPath}: ${message}` }],
|
|
859
|
+
isError: true,
|
|
860
|
+
},
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Handle beam/reload tool - reload a photon
|
|
866
|
+
*/
|
|
867
|
+
async function handleBeamReload(req, ctx, args) {
|
|
868
|
+
const { photon: photonName } = args;
|
|
869
|
+
if (!photonName) {
|
|
870
|
+
return {
|
|
871
|
+
jsonrpc: '2.0',
|
|
872
|
+
id: req.id,
|
|
873
|
+
result: {
|
|
874
|
+
content: [{ type: 'text', text: 'Error: photon name is required' }],
|
|
875
|
+
isError: true,
|
|
876
|
+
},
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
if (!ctx.reloadPhoton) {
|
|
880
|
+
return {
|
|
881
|
+
jsonrpc: '2.0',
|
|
882
|
+
id: req.id,
|
|
883
|
+
result: {
|
|
884
|
+
content: [{ type: 'text', text: 'Error: Reload not supported in this context' }],
|
|
885
|
+
isError: true,
|
|
886
|
+
},
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
const result = await ctx.reloadPhoton(photonName);
|
|
891
|
+
if (result.success) {
|
|
892
|
+
// Notify Beam clients about the reload
|
|
893
|
+
broadcastToBeam('beam/hot-reload', { photon: result.photon });
|
|
894
|
+
return {
|
|
895
|
+
jsonrpc: '2.0',
|
|
896
|
+
id: req.id,
|
|
897
|
+
result: {
|
|
898
|
+
content: [{ type: 'text', text: `Successfully reloaded ${photonName}` }],
|
|
899
|
+
isError: false,
|
|
900
|
+
},
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
return {
|
|
905
|
+
jsonrpc: '2.0',
|
|
906
|
+
id: req.id,
|
|
907
|
+
result: {
|
|
908
|
+
content: [{ type: 'text', text: `Failed to reload ${photonName}: ${result.error}` }],
|
|
909
|
+
isError: true,
|
|
910
|
+
},
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
catch (error) {
|
|
915
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
916
|
+
return {
|
|
917
|
+
jsonrpc: '2.0',
|
|
918
|
+
id: req.id,
|
|
919
|
+
result: {
|
|
920
|
+
content: [{ type: 'text', text: `Error reloading ${photonName}: ${message}` }],
|
|
921
|
+
isError: true,
|
|
922
|
+
},
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Handle beam/remove tool - remove a photon from the workspace
|
|
928
|
+
*/
|
|
929
|
+
async function handleBeamRemove(req, ctx, args) {
|
|
930
|
+
const { photon: photonName } = args;
|
|
931
|
+
if (!photonName) {
|
|
932
|
+
return {
|
|
933
|
+
jsonrpc: '2.0',
|
|
934
|
+
id: req.id,
|
|
935
|
+
result: {
|
|
936
|
+
content: [{ type: 'text', text: 'Error: photon name is required' }],
|
|
937
|
+
isError: true,
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
if (!ctx.removePhoton) {
|
|
942
|
+
return {
|
|
943
|
+
jsonrpc: '2.0',
|
|
944
|
+
id: req.id,
|
|
945
|
+
result: {
|
|
946
|
+
content: [{ type: 'text', text: 'Error: Remove not supported in this context' }],
|
|
947
|
+
isError: true,
|
|
948
|
+
},
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
try {
|
|
952
|
+
const result = await ctx.removePhoton(photonName);
|
|
953
|
+
if (result.success) {
|
|
954
|
+
return {
|
|
955
|
+
jsonrpc: '2.0',
|
|
956
|
+
id: req.id,
|
|
957
|
+
result: {
|
|
958
|
+
content: [{ type: 'text', text: `Successfully removed ${photonName}` }],
|
|
959
|
+
isError: false,
|
|
960
|
+
},
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
return {
|
|
965
|
+
jsonrpc: '2.0',
|
|
966
|
+
id: req.id,
|
|
967
|
+
result: {
|
|
968
|
+
content: [{ type: 'text', text: `Failed to remove ${photonName}: ${result.error}` }],
|
|
969
|
+
isError: true,
|
|
970
|
+
},
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
976
|
+
return {
|
|
977
|
+
jsonrpc: '2.0',
|
|
978
|
+
id: req.id,
|
|
979
|
+
result: {
|
|
980
|
+
content: [{ type: 'text', text: `Error removing ${photonName}: ${message}` }],
|
|
981
|
+
isError: true,
|
|
982
|
+
},
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Handle beam/update-metadata tool - update photon or method metadata
|
|
988
|
+
*/
|
|
989
|
+
async function handleBeamUpdateMetadata(req, ctx, args) {
|
|
990
|
+
const { photon: photonName, method: methodName, metadata, } = args;
|
|
991
|
+
if (!photonName) {
|
|
992
|
+
return {
|
|
993
|
+
jsonrpc: '2.0',
|
|
994
|
+
id: req.id,
|
|
995
|
+
result: {
|
|
996
|
+
content: [{ type: 'text', text: 'Error: photon name is required' }],
|
|
997
|
+
isError: true,
|
|
998
|
+
},
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
1002
|
+
return {
|
|
1003
|
+
jsonrpc: '2.0',
|
|
1004
|
+
id: req.id,
|
|
1005
|
+
result: {
|
|
1006
|
+
content: [{ type: 'text', text: 'Error: metadata object is required' }],
|
|
1007
|
+
isError: true,
|
|
1008
|
+
},
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
if (!ctx.updateMetadata) {
|
|
1012
|
+
return {
|
|
1013
|
+
jsonrpc: '2.0',
|
|
1014
|
+
id: req.id,
|
|
1015
|
+
result: {
|
|
1016
|
+
content: [{ type: 'text', text: 'Error: Update metadata not supported in this context' }],
|
|
1017
|
+
isError: true,
|
|
1018
|
+
},
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
const result = await ctx.updateMetadata(photonName, methodName || null, metadata);
|
|
1023
|
+
if (result.success) {
|
|
1024
|
+
return {
|
|
1025
|
+
jsonrpc: '2.0',
|
|
1026
|
+
id: req.id,
|
|
1027
|
+
result: {
|
|
1028
|
+
content: [
|
|
1029
|
+
{
|
|
1030
|
+
type: 'text',
|
|
1031
|
+
text: `Successfully updated metadata for ${methodName ? `${photonName}/${methodName}` : photonName}`,
|
|
1032
|
+
},
|
|
1033
|
+
],
|
|
1034
|
+
isError: false,
|
|
1035
|
+
},
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
return {
|
|
1040
|
+
jsonrpc: '2.0',
|
|
1041
|
+
id: req.id,
|
|
1042
|
+
result: {
|
|
1043
|
+
content: [{ type: 'text', text: `Failed to update metadata: ${result.error}` }],
|
|
1044
|
+
isError: true,
|
|
1045
|
+
},
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
catch (error) {
|
|
1050
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1051
|
+
return {
|
|
1052
|
+
jsonrpc: '2.0',
|
|
1053
|
+
id: req.id,
|
|
1054
|
+
result: {
|
|
1055
|
+
content: [{ type: 'text', text: `Error updating metadata: ${message}` }],
|
|
1056
|
+
isError: true,
|
|
1057
|
+
},
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Handle beam/photon-help tool - get rich documentation for a photon
|
|
1063
|
+
*/
|
|
1064
|
+
async function handleBeamPhotonHelp(req, ctx, args) {
|
|
1065
|
+
const { photon: photonName } = args;
|
|
1066
|
+
if (!photonName) {
|
|
1067
|
+
return {
|
|
1068
|
+
jsonrpc: '2.0',
|
|
1069
|
+
id: req.id,
|
|
1070
|
+
result: {
|
|
1071
|
+
content: [{ type: 'text', text: 'Error: photon name is required' }],
|
|
1072
|
+
isError: true,
|
|
1073
|
+
},
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
if (!ctx.generatePhotonHelp) {
|
|
1077
|
+
return {
|
|
1078
|
+
jsonrpc: '2.0',
|
|
1079
|
+
id: req.id,
|
|
1080
|
+
result: {
|
|
1081
|
+
content: [{ type: 'text', text: 'Error: Help generation not supported in this context' }],
|
|
1082
|
+
isError: true,
|
|
1083
|
+
},
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
try {
|
|
1087
|
+
const markdown = await ctx.generatePhotonHelp(photonName);
|
|
1088
|
+
return {
|
|
1089
|
+
jsonrpc: '2.0',
|
|
1090
|
+
id: req.id,
|
|
1091
|
+
result: {
|
|
1092
|
+
content: [{ type: 'text', text: markdown }],
|
|
1093
|
+
isError: false,
|
|
1094
|
+
},
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
catch (error) {
|
|
1098
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1099
|
+
return {
|
|
1100
|
+
jsonrpc: '2.0',
|
|
1101
|
+
id: req.id,
|
|
1102
|
+
result: {
|
|
1103
|
+
content: [{ type: 'text', text: `Error generating help: ${message}` }],
|
|
1104
|
+
isError: true,
|
|
1105
|
+
},
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Handle MCP Streamable HTTP requests
|
|
1111
|
+
*/
|
|
1112
|
+
export async function handleStreamableHTTP(req, res, options) {
|
|
1113
|
+
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
1114
|
+
// Only handle /mcp endpoint
|
|
1115
|
+
if (url.pathname !== '/mcp') {
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
// CORS headers
|
|
1119
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1120
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
1121
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id');
|
|
1122
|
+
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
|
|
1123
|
+
// Handle preflight
|
|
1124
|
+
if (req.method === 'OPTIONS') {
|
|
1125
|
+
res.writeHead(204);
|
|
1126
|
+
res.end();
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
// Get or create session
|
|
1130
|
+
// Check header first, then query parameter (for SSE which can't set headers)
|
|
1131
|
+
let sessionId = req.headers['mcp-session-id'];
|
|
1132
|
+
if (!sessionId) {
|
|
1133
|
+
sessionId = url.searchParams.get('sessionId') || undefined;
|
|
1134
|
+
}
|
|
1135
|
+
const session = getOrCreateSession(sessionId);
|
|
1136
|
+
// GET - Open SSE stream for server notifications
|
|
1137
|
+
if (req.method === 'GET') {
|
|
1138
|
+
const accept = req.headers.accept || '';
|
|
1139
|
+
if (!accept.includes('text/event-stream')) {
|
|
1140
|
+
res.writeHead(406);
|
|
1141
|
+
res.end('Accept header must include text/event-stream');
|
|
1142
|
+
return true;
|
|
1143
|
+
}
|
|
1144
|
+
res.writeHead(200, {
|
|
1145
|
+
'Content-Type': 'text/event-stream',
|
|
1146
|
+
'Cache-Control': 'no-cache',
|
|
1147
|
+
Connection: 'keep-alive',
|
|
1148
|
+
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
|
1149
|
+
'Mcp-Session-Id': session.id,
|
|
1150
|
+
});
|
|
1151
|
+
// Disable Nagle's algorithm for immediate writes
|
|
1152
|
+
res.socket?.setNoDelay(true);
|
|
1153
|
+
// Store SSE response for server-initiated messages
|
|
1154
|
+
session.sseResponse = res;
|
|
1155
|
+
// Keep connection alive
|
|
1156
|
+
const keepAlive = setInterval(() => {
|
|
1157
|
+
res.write(': keepalive\n\n');
|
|
1158
|
+
}, 30000);
|
|
1159
|
+
req.on('close', () => {
|
|
1160
|
+
clearInterval(keepAlive);
|
|
1161
|
+
session.sseResponse = undefined;
|
|
1162
|
+
// Clean up subscriptions when client disconnects
|
|
1163
|
+
if (options.subscriptionManager) {
|
|
1164
|
+
options.subscriptionManager.onClientDisconnect(session.id);
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1169
|
+
// POST - Handle JSON-RPC requests
|
|
1170
|
+
if (req.method === 'POST') {
|
|
1171
|
+
const accept = req.headers.accept || '';
|
|
1172
|
+
const wantsSSE = accept.includes('text/event-stream');
|
|
1173
|
+
// Read body
|
|
1174
|
+
let body = '';
|
|
1175
|
+
for await (const chunk of req) {
|
|
1176
|
+
body += chunk;
|
|
1177
|
+
}
|
|
1178
|
+
let requests;
|
|
1179
|
+
try {
|
|
1180
|
+
const parsed = JSON.parse(body);
|
|
1181
|
+
requests = Array.isArray(parsed) ? parsed : [parsed];
|
|
1182
|
+
}
|
|
1183
|
+
catch {
|
|
1184
|
+
res.writeHead(400);
|
|
1185
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
1186
|
+
return true;
|
|
1187
|
+
}
|
|
1188
|
+
const context = {
|
|
1189
|
+
photons: options.photons,
|
|
1190
|
+
photonMCPs: options.photonMCPs,
|
|
1191
|
+
loadUIAsset: options.loadUIAsset,
|
|
1192
|
+
configurePhoton: options.configurePhoton,
|
|
1193
|
+
reloadPhoton: options.reloadPhoton,
|
|
1194
|
+
removePhoton: options.removePhoton,
|
|
1195
|
+
updateMetadata: options.updateMetadata,
|
|
1196
|
+
generatePhotonHelp: options.generatePhotonHelp,
|
|
1197
|
+
loader: options.loader,
|
|
1198
|
+
broadcast: options.broadcast,
|
|
1199
|
+
subscriptionManager: options.subscriptionManager,
|
|
1200
|
+
};
|
|
1201
|
+
// Process requests
|
|
1202
|
+
const responses = [];
|
|
1203
|
+
for (const request of requests) {
|
|
1204
|
+
const handler = handlers[request.method];
|
|
1205
|
+
if (!handler) {
|
|
1206
|
+
if (request.id !== undefined) {
|
|
1207
|
+
responses.push({
|
|
1208
|
+
jsonrpc: '2.0',
|
|
1209
|
+
id: request.id,
|
|
1210
|
+
error: { code: -32601, message: `Method not found: ${request.method}` },
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
const response = await handler(request, session, context);
|
|
1216
|
+
// Only include responses for requests (not notifications)
|
|
1217
|
+
if (request.id !== undefined && response.id !== undefined) {
|
|
1218
|
+
responses.push(response);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
// Send response
|
|
1222
|
+
if (responses.length === 0) {
|
|
1223
|
+
// All were notifications
|
|
1224
|
+
res.writeHead(202);
|
|
1225
|
+
res.end();
|
|
1226
|
+
}
|
|
1227
|
+
else if (wantsSSE) {
|
|
1228
|
+
// SSE response
|
|
1229
|
+
res.writeHead(200, {
|
|
1230
|
+
'Content-Type': 'text/event-stream',
|
|
1231
|
+
'Cache-Control': 'no-cache',
|
|
1232
|
+
'Mcp-Session-Id': session.id,
|
|
1233
|
+
});
|
|
1234
|
+
for (const response of responses) {
|
|
1235
|
+
res.write(`data: ${JSON.stringify(response)}\n\n`);
|
|
1236
|
+
}
|
|
1237
|
+
res.end();
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
// JSON response
|
|
1241
|
+
res.writeHead(200, {
|
|
1242
|
+
'Content-Type': 'application/json',
|
|
1243
|
+
'Mcp-Session-Id': session.id,
|
|
1244
|
+
});
|
|
1245
|
+
const result = responses.length === 1 ? responses[0] : responses;
|
|
1246
|
+
res.end(JSON.stringify(result));
|
|
1247
|
+
}
|
|
1248
|
+
return true;
|
|
1249
|
+
}
|
|
1250
|
+
// Method not allowed
|
|
1251
|
+
res.writeHead(405);
|
|
1252
|
+
res.end('Method not allowed');
|
|
1253
|
+
return true;
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Send a notification to all connected SSE clients
|
|
1257
|
+
* @param method - The notification method name
|
|
1258
|
+
* @param params - Optional parameters for the notification
|
|
1259
|
+
* @param beamOnly - If true, only send to Beam clients (clientInfo.name === "beam")
|
|
1260
|
+
*/
|
|
1261
|
+
export function broadcastNotification(method, params, beamOnly = false) {
|
|
1262
|
+
const notification = {
|
|
1263
|
+
jsonrpc: '2.0',
|
|
1264
|
+
method,
|
|
1265
|
+
params,
|
|
1266
|
+
};
|
|
1267
|
+
for (const session of sessions.values()) {
|
|
1268
|
+
if (session.sseResponse && !session.sseResponse.writableEnded) {
|
|
1269
|
+
// Skip non-Beam clients if beamOnly is true
|
|
1270
|
+
if (beamOnly && !session.isBeam)
|
|
1271
|
+
continue;
|
|
1272
|
+
session.sseResponse.write(`data: ${JSON.stringify(notification)}\n\n`);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Send a notification to Beam clients only
|
|
1278
|
+
*/
|
|
1279
|
+
export function broadcastToBeam(method, params) {
|
|
1280
|
+
broadcastNotification(method, params, true);
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Get count of active sessions (for debugging)
|
|
1284
|
+
*/
|
|
1285
|
+
export function getActiveSessionCount() {
|
|
1286
|
+
let total = 0;
|
|
1287
|
+
let beam = 0;
|
|
1288
|
+
for (const session of sessions.values()) {
|
|
1289
|
+
if (session.sseResponse && !session.sseResponse.writableEnded) {
|
|
1290
|
+
total++;
|
|
1291
|
+
if (session.isBeam)
|
|
1292
|
+
beam++;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
return { total, beam };
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Send a notification to a specific session by ID
|
|
1299
|
+
* Used for replaying missed events on reconnect
|
|
1300
|
+
*/
|
|
1301
|
+
export function sendToSession(sessionId, method, params) {
|
|
1302
|
+
const session = sessions.get(sessionId);
|
|
1303
|
+
if (!session?.sseResponse || session.sseResponse.writableEnded) {
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
const notification = {
|
|
1307
|
+
jsonrpc: '2.0',
|
|
1308
|
+
method,
|
|
1309
|
+
params,
|
|
1310
|
+
};
|
|
1311
|
+
session.sseResponse.write(`data: ${JSON.stringify(notification)}\n\n`);
|
|
1312
|
+
return true;
|
|
1313
|
+
}
|
|
1314
|
+
//# sourceMappingURL=streamable-http-transport.js.map
|