@portel/photon 1.8.4 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -210
- package/dist/async/dedup-map.d.ts +40 -0
- package/dist/async/dedup-map.d.ts.map +1 -0
- package/dist/async/dedup-map.js +80 -0
- package/dist/async/dedup-map.js.map +1 -0
- package/dist/async/index.d.ts +11 -0
- package/dist/async/index.d.ts.map +1 -0
- package/dist/async/index.js +11 -0
- package/dist/async/index.js.map +1 -0
- package/dist/async/loading-gate.d.ts +27 -0
- package/dist/async/loading-gate.d.ts.map +1 -0
- package/dist/async/loading-gate.js +48 -0
- package/dist/async/loading-gate.js.map +1 -0
- package/dist/async/with-timeout.d.ts +6 -0
- package/dist/async/with-timeout.d.ts.map +1 -0
- package/dist/async/with-timeout.js +17 -0
- package/dist/async/with-timeout.js.map +1 -0
- package/dist/auto-ui/beam/class-metadata.d.ts +52 -0
- package/dist/auto-ui/beam/class-metadata.d.ts.map +1 -0
- package/dist/auto-ui/beam/class-metadata.js +133 -0
- package/dist/auto-ui/beam/class-metadata.js.map +1 -0
- package/dist/auto-ui/beam/config.d.ts +13 -0
- package/dist/auto-ui/beam/config.d.ts.map +1 -0
- package/dist/auto-ui/beam/config.js +52 -0
- package/dist/auto-ui/beam/config.js.map +1 -0
- package/dist/auto-ui/beam/external-mcp.d.ts +37 -0
- package/dist/auto-ui/beam/external-mcp.d.ts.map +1 -0
- package/dist/auto-ui/beam/external-mcp.js +311 -0
- package/dist/auto-ui/beam/external-mcp.js.map +1 -0
- package/dist/auto-ui/beam/photon-management.d.ts +51 -0
- package/dist/auto-ui/beam/photon-management.d.ts.map +1 -0
- package/dist/auto-ui/beam/photon-management.js +310 -0
- package/dist/auto-ui/beam/photon-management.js.map +1 -0
- package/dist/auto-ui/beam/routes/api-browse.d.ts +17 -0
- package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -0
- package/dist/auto-ui/beam/routes/api-browse.js +531 -0
- package/dist/auto-ui/beam/routes/api-browse.js.map +1 -0
- package/dist/auto-ui/beam/routes/api-config.d.ts +9 -0
- package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -0
- package/dist/auto-ui/beam/routes/api-config.js +494 -0
- package/dist/auto-ui/beam/routes/api-config.js.map +1 -0
- package/dist/auto-ui/beam/routes/api-marketplace.d.ts +8 -0
- package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -0
- package/dist/auto-ui/beam/routes/api-marketplace.js +490 -0
- package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -0
- package/dist/auto-ui/beam/startup.d.ts +41 -0
- package/dist/auto-ui/beam/startup.d.ts.map +1 -0
- package/dist/auto-ui/beam/startup.js +98 -0
- package/dist/auto-ui/beam/startup.js.map +1 -0
- package/dist/auto-ui/beam/subscription.d.ts +35 -0
- package/dist/auto-ui/beam/subscription.d.ts.map +1 -0
- package/dist/auto-ui/beam/subscription.js +151 -0
- package/dist/auto-ui/beam/subscription.js.map +1 -0
- package/dist/auto-ui/beam/types.d.ts +103 -0
- package/dist/auto-ui/beam/types.d.ts.map +1 -0
- package/dist/auto-ui/beam/types.js +8 -0
- package/dist/auto-ui/beam/types.js.map +1 -0
- package/dist/auto-ui/beam.d.ts +2 -0
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +731 -2519
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/bridge/index.d.ts.map +1 -1
- package/dist/auto-ui/bridge/index.js +10 -2
- package/dist/auto-ui/bridge/index.js.map +1 -1
- package/dist/auto-ui/components/card.d.ts.map +1 -1
- package/dist/auto-ui/components/card.js +3 -1
- package/dist/auto-ui/components/card.js.map +1 -1
- package/dist/auto-ui/components/progress.d.ts.map +1 -1
- package/dist/auto-ui/components/progress.js.map +1 -1
- package/dist/auto-ui/daemon-tools.d.ts +1 -1
- package/dist/auto-ui/daemon-tools.d.ts.map +1 -1
- package/dist/auto-ui/daemon-tools.js +4 -3
- package/dist/auto-ui/daemon-tools.js.map +1 -1
- package/dist/auto-ui/photon-bridge.d.ts +6 -2
- package/dist/auto-ui/photon-bridge.d.ts.map +1 -1
- package/dist/auto-ui/photon-bridge.js +20 -8
- package/dist/auto-ui/photon-bridge.js.map +1 -1
- package/dist/auto-ui/platform-compat.d.ts.map +1 -1
- package/dist/auto-ui/platform-compat.js +4 -0
- package/dist/auto-ui/platform-compat.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +4 -2
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +120 -30
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +4 -2
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js +3 -0
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +9218 -4539
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli/commands/alias.d.ts +14 -0
- package/dist/cli/commands/alias.d.ts.map +1 -0
- package/dist/cli/commands/alias.js +41 -0
- package/dist/cli/commands/alias.js.map +1 -0
- package/dist/cli/commands/audit.d.ts +9 -0
- package/dist/cli/commands/audit.d.ts.map +1 -0
- package/dist/cli/commands/audit.js +377 -0
- package/dist/cli/commands/audit.js.map +1 -0
- package/dist/cli/commands/beam.d.ts +20 -0
- package/dist/cli/commands/beam.d.ts.map +1 -0
- package/dist/cli/commands/beam.js +256 -0
- package/dist/cli/commands/beam.js.map +1 -0
- package/dist/cli/commands/config.d.ts +14 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +165 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/daemon.d.ts +11 -0
- package/dist/cli/commands/daemon.d.ts.map +1 -0
- package/dist/cli/commands/daemon.js +108 -0
- package/dist/cli/commands/daemon.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +257 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/host.d.ts +11 -0
- package/dist/cli/commands/host.d.ts.map +1 -0
- package/dist/cli/commands/host.js +96 -0
- package/dist/cli/commands/host.js.map +1 -0
- package/dist/cli/commands/info.d.ts +1 -1
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +16 -15
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +774 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/maker.d.ts +12 -0
- package/dist/cli/commands/maker.d.ts.map +1 -0
- package/dist/cli/commands/maker.js +605 -0
- package/dist/cli/commands/maker.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +27 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +390 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/package-app.d.ts +1 -1
- package/dist/cli/commands/package-app.d.ts.map +1 -1
- package/dist/cli/commands/package-app.js +5 -4
- package/dist/cli/commands/package-app.js.map +1 -1
- package/dist/cli/commands/package.d.ts +1 -1
- package/dist/cli/commands/package.d.ts.map +1 -1
- package/dist/cli/commands/package.js +134 -32
- package/dist/cli/commands/package.js.map +1 -1
- package/dist/cli/commands/run.d.ts +34 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +334 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/search.d.ts +11 -0
- package/dist/cli/commands/search.d.ts.map +1 -0
- package/dist/cli/commands/search.js +60 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +11 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +138 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/test.d.ts +14 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/test.js +51 -0
- package/dist/cli/commands/test.js.map +1 -0
- package/dist/cli/commands/update.d.ts +11 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +72 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +139 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli-alias.js +2 -2
- package/dist/cli-alias.js.map +1 -1
- package/dist/cli.d.ts +3 -16
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -2724
- package/dist/cli.js.map +1 -1
- package/dist/context-store.d.ts +13 -12
- package/dist/context-store.d.ts.map +1 -1
- package/dist/context-store.js +47 -23
- package/dist/context-store.js.map +1 -1
- package/dist/context.d.ts +35 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +38 -0
- package/dist/context.js.map +1 -0
- package/dist/daemon/client.d.ts +25 -13
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +183 -135
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +58 -26
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +348 -157
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/protocol.d.ts +9 -3
- package/dist/daemon/protocol.d.ts.map +1 -1
- package/dist/daemon/protocol.js +2 -0
- package/dist/daemon/protocol.js.map +1 -1
- package/dist/daemon/server.js +850 -200
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/session-manager.d.ts +16 -2
- package/dist/daemon/session-manager.d.ts.map +1 -1
- package/dist/daemon/session-manager.js +65 -7
- package/dist/daemon/session-manager.js.map +1 -1
- package/dist/daemon/state-machine.d.ts +22 -0
- package/dist/daemon/state-machine.d.ts.map +1 -0
- package/dist/daemon/state-machine.js +48 -0
- package/dist/daemon/state-machine.js.map +1 -0
- package/dist/deploy/cloudflare.d.ts.map +1 -1
- package/dist/deploy/cloudflare.js +5 -5
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/loader.d.ts +65 -7
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +587 -63
- package/dist/loader.js.map +1 -1
- package/dist/marketplace-manager.d.ts +87 -13
- package/dist/marketplace-manager.d.ts.map +1 -1
- package/dist/marketplace-manager.js +476 -30
- package/dist/marketplace-manager.js.map +1 -1
- package/dist/path-resolver.d.ts +3 -1
- package/dist/path-resolver.d.ts.map +1 -1
- package/dist/path-resolver.js +4 -3
- package/dist/path-resolver.js.map +1 -1
- package/dist/photon-cli-runner.d.ts +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +34 -44
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts +1 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +62 -19
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/photons/maker.photon.d.ts.map +1 -1
- package/dist/photons/maker.photon.js +4 -4
- package/dist/photons/maker.photon.js.map +1 -1
- package/dist/photons/maker.photon.ts +4 -3
- package/dist/photons/marketplace.photon.d.ts.map +1 -1
- package/dist/photons/marketplace.photon.js +10 -27
- package/dist/photons/marketplace.photon.js.map +1 -1
- package/dist/photons/marketplace.photon.ts +14 -33
- package/dist/photons/tunnel.photon.d.ts.map +1 -1
- package/dist/photons/tunnel.photon.js +4 -8
- package/dist/photons/tunnel.photon.js.map +1 -1
- package/dist/photons/tunnel.photon.ts +4 -7
- package/dist/serv/session/kv-store.d.ts +1 -1
- package/dist/serv/session/kv-store.d.ts.map +1 -1
- package/dist/serv/session/store.d.ts.map +1 -1
- package/dist/serv/session/store.js +16 -14
- package/dist/serv/session/store.js.map +1 -1
- package/dist/serv/vault/token-vault.js +1 -1
- package/dist/serv/vault/token-vault.js.map +1 -1
- package/dist/server.d.ts +34 -12
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +364 -313
- package/dist/server.js.map +1 -1
- package/dist/shared/audit.d.ts +30 -0
- package/dist/shared/audit.d.ts.map +1 -0
- package/dist/shared/audit.js +89 -0
- package/dist/shared/audit.js.map +1 -0
- package/dist/shared/cli-sections.d.ts +0 -4
- package/dist/shared/cli-sections.d.ts.map +1 -1
- package/dist/shared/cli-sections.js +0 -6
- package/dist/shared/cli-sections.js.map +1 -1
- package/dist/shared/cli-utils.d.ts +2 -56
- package/dist/shared/cli-utils.d.ts.map +1 -1
- package/dist/shared/cli-utils.js +1 -87
- package/dist/shared/cli-utils.js.map +1 -1
- package/dist/shared/error-handler.d.ts +6 -72
- package/dist/shared/error-handler.d.ts.map +1 -1
- package/dist/shared/error-handler.js +22 -213
- package/dist/shared/error-handler.js.map +1 -1
- package/dist/shared/security.d.ts +0 -9
- package/dist/shared/security.d.ts.map +1 -1
- package/dist/shared/security.js +0 -30
- package/dist/shared/security.js.map +1 -1
- package/dist/shared-utils.d.ts +0 -26
- package/dist/shared-utils.d.ts.map +1 -1
- package/dist/shared-utils.js +0 -44
- package/dist/shared-utils.js.map +1 -1
- package/dist/shell-completions.d.ts +1 -1
- package/dist/shell-completions.d.ts.map +1 -1
- package/dist/shell-completions.js +5 -5
- package/dist/shell-completions.js.map +1 -1
- package/dist/template-manager.d.ts.map +1 -1
- package/dist/template-manager.js +14 -1
- package/dist/template-manager.js.map +1 -1
- package/dist/test-runner.d.ts +0 -12
- package/dist/test-runner.d.ts.map +1 -1
- package/dist/test-runner.js +4 -39
- package/dist/test-runner.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/dist/version-checker.d.ts +4 -4
- package/dist/version-checker.d.ts.map +1 -1
- package/dist/version-checker.js +33 -4
- package/dist/version-checker.js.map +1 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +14 -12
- package/dist/watcher.js.map +1 -1
- package/package.json +24 -17
package/dist/server.js
CHANGED
|
@@ -12,7 +12,7 @@ import * as fs from 'fs/promises';
|
|
|
12
12
|
import { createServer } from 'node:http';
|
|
13
13
|
import { URL } from 'node:url';
|
|
14
14
|
import { PhotonLoader } from './loader.js';
|
|
15
|
-
import { generateExecutionId
|
|
15
|
+
import { generateExecutionId } from '@portel/photon-core';
|
|
16
16
|
import { createSDKMCPClientFactory } from '@portel/photon-core';
|
|
17
17
|
import { PHOTON_VERSION } from './version.js';
|
|
18
18
|
import { createLogger } from './shared/logger.js';
|
|
@@ -22,7 +22,8 @@ import { generatePlaygroundHTML } from './auto-ui/playground-html.js';
|
|
|
22
22
|
import { subscribeChannel, pingDaemon, publishToChannel } from './daemon/client.js';
|
|
23
23
|
import { isGlobalDaemonRunning, startGlobalDaemon } from './daemon/manager.js';
|
|
24
24
|
import { PhotonDocExtractor } from './photon-doc-extractor.js';
|
|
25
|
-
import { isLocalRequest, readBody, setSecurityHeaders
|
|
25
|
+
import { isLocalRequest, readBody, setSecurityHeaders } from './shared/security.js';
|
|
26
|
+
import { audit } from './shared/audit.js';
|
|
26
27
|
export class HotReloadDisabledError extends Error {
|
|
27
28
|
constructor(message) {
|
|
28
29
|
super(message);
|
|
@@ -49,6 +50,19 @@ export class PhotonServer {
|
|
|
49
50
|
sseInstanceNames = new Map();
|
|
50
51
|
/** Whether client capabilities have been logged (one-time on first tools/list) */
|
|
51
52
|
clientCapabilitiesLogged = false;
|
|
53
|
+
/**
|
|
54
|
+
* Raw client capabilities captured from the initialize request BEFORE Zod parsing.
|
|
55
|
+
*
|
|
56
|
+
* The MCP SDK uses Zod to validate incoming requests, which strips unknown fields
|
|
57
|
+
* from ClientCapabilities. Notably, `extensions` (protocol 2025-11-25+) is not in
|
|
58
|
+
* the SDK's Zod schema yet, so `getClientCapabilities()` returns an object without
|
|
59
|
+
* it. Real clients like Claude Desktop and ChatGPT send UI capability under
|
|
60
|
+
* `extensions`, not `experimental`. We intercept the raw JSON-RPC message to
|
|
61
|
+
* capture the full capabilities before Zod strips them.
|
|
62
|
+
*
|
|
63
|
+
* Key: Server instance → Value: raw capabilities object from initialize request
|
|
64
|
+
*/
|
|
65
|
+
rawClientCapabilities = new WeakMap();
|
|
52
66
|
currentStatus = {
|
|
53
67
|
type: 'info',
|
|
54
68
|
message: 'Ready',
|
|
@@ -85,7 +99,7 @@ export class PhotonServer {
|
|
|
85
99
|
baseLoggerOptions.scope = this.devMode ? 'dev' : 'runtime';
|
|
86
100
|
}
|
|
87
101
|
this.logger = createLogger(baseLoggerOptions);
|
|
88
|
-
this.loader = new PhotonLoader(true, this.logger.child({ component: 'photon-loader', scope: 'loader' }));
|
|
102
|
+
this.loader = new PhotonLoader(true, this.logger.child({ component: 'photon-loader', scope: 'loader' }), options.workingDir);
|
|
89
103
|
// Create MCP server instance
|
|
90
104
|
this.server = new Server({
|
|
91
105
|
name: 'photon-mcp',
|
|
@@ -117,15 +131,6 @@ export class PhotonServer {
|
|
|
117
131
|
log(level, message, meta) {
|
|
118
132
|
this.logger.log(level, message, meta);
|
|
119
133
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Detect UI format based on client capabilities
|
|
122
|
-
*
|
|
123
|
-
* All clients use the MCP Apps standard (SEP-1865) ui:// format.
|
|
124
|
-
* Text-only clients have no UI support.
|
|
125
|
-
*/
|
|
126
|
-
getUIFormat() {
|
|
127
|
-
return 'sep-1865';
|
|
128
|
-
}
|
|
129
134
|
/**
|
|
130
135
|
* Build UI resource URI based on detected format
|
|
131
136
|
*/
|
|
@@ -161,32 +166,38 @@ export class PhotonServer {
|
|
|
161
166
|
// Check for elicitation capability (MCP 2025-06 spec)
|
|
162
167
|
return !!capabilities.elicitation;
|
|
163
168
|
}
|
|
164
|
-
|
|
165
|
-
static UI_CAPABLE_CLIENTS = new Set(['chatgpt', 'mcpjam', 'mcp-inspector']);
|
|
169
|
+
static MCP_UI_CAPABILITY = 'io.modelcontextprotocol/ui';
|
|
166
170
|
/**
|
|
167
171
|
* Check if client supports MCP Apps UI (structuredContent + _meta.ui)
|
|
168
172
|
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
173
|
+
* Looks for the "io.modelcontextprotocol/ui" capability in the client's
|
|
174
|
+
* initialize handshake. Any MCP client that advertises this capability
|
|
175
|
+
* gets rich UI responses — Claude Desktop, ChatGPT, MCPJam, etc.
|
|
176
|
+
*
|
|
177
|
+
* The capability may appear under `experimental` (older SDK types) or
|
|
178
|
+
* `extensions` (protocol version 2025-11-25+). We check both so it
|
|
179
|
+
* just works regardless of which field the client uses.
|
|
172
180
|
*
|
|
173
|
-
*
|
|
181
|
+
* Beam is special-cased because it's our own SSE transport where the
|
|
182
|
+
* capability is implicit.
|
|
174
183
|
*/
|
|
175
184
|
clientSupportsUI(server) {
|
|
176
185
|
const targetServer = server || this.server;
|
|
177
|
-
//
|
|
186
|
+
// Check SDK-parsed capabilities (works for `experimental` which is in the Zod schema)
|
|
178
187
|
const capabilities = targetServer.getClientCapabilities();
|
|
179
|
-
if (capabilities?.experimental?.[
|
|
188
|
+
if (capabilities?.experimental?.[PhotonServer.MCP_UI_CAPABILITY]) {
|
|
180
189
|
return true;
|
|
181
190
|
}
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (PhotonServer.UI_CAPABLE_CLIENTS.has(clientInfo.name))
|
|
188
|
-
return true;
|
|
191
|
+
// Check raw capabilities captured before Zod parsing (needed for `extensions`
|
|
192
|
+
// which the SDK's Zod schema strips — Claude Desktop and ChatGPT use this field)
|
|
193
|
+
const raw = this.rawClientCapabilities.get(targetServer);
|
|
194
|
+
if (raw?.extensions?.[PhotonServer.MCP_UI_CAPABILITY]) {
|
|
195
|
+
return true;
|
|
189
196
|
}
|
|
197
|
+
// Beam is our own transport — UI support is implicit
|
|
198
|
+
const clientInfo = targetServer.getClientVersion();
|
|
199
|
+
if (clientInfo?.name === 'beam')
|
|
200
|
+
return true;
|
|
190
201
|
return false;
|
|
191
202
|
}
|
|
192
203
|
/**
|
|
@@ -415,9 +426,16 @@ export class PhotonServer {
|
|
|
415
426
|
return { tools: [] };
|
|
416
427
|
}
|
|
417
428
|
const tools = this.mcp.tools.map((tool) => {
|
|
429
|
+
// Append deprecation notice to tool description if tagged
|
|
430
|
+
let description = tool.description;
|
|
431
|
+
const deprecated = tool.deprecated;
|
|
432
|
+
if (deprecated) {
|
|
433
|
+
const notice = typeof deprecated === 'string' ? deprecated : 'This tool is deprecated.';
|
|
434
|
+
description = `[DEPRECATED: ${notice}] ${description}`;
|
|
435
|
+
}
|
|
418
436
|
const toolDef = {
|
|
419
437
|
name: tool.name,
|
|
420
|
-
description
|
|
438
|
+
description,
|
|
421
439
|
inputSchema: tool.inputSchema,
|
|
422
440
|
};
|
|
423
441
|
const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name);
|
|
@@ -461,6 +479,7 @@ export class PhotonServer {
|
|
|
461
479
|
photonPath: this.options.filePath,
|
|
462
480
|
sessionId: ctx.sessionId,
|
|
463
481
|
instanceName: ctx.getInstanceName(),
|
|
482
|
+
workingDir: this.options.workingDir,
|
|
464
483
|
};
|
|
465
484
|
// Elicitation-based instance selection when _use called without name
|
|
466
485
|
if (toolName === '_use' &&
|
|
@@ -518,7 +537,8 @@ export class PhotonServer {
|
|
|
518
537
|
const result = await sendCommand(this.daemonName, toolName, (args || {}), sendOpts);
|
|
519
538
|
// Track instance name after successful _use
|
|
520
539
|
if (toolName === '_use') {
|
|
521
|
-
|
|
540
|
+
const nameVal = args?.name;
|
|
541
|
+
ctx.setInstanceName(typeof nameVal === 'string' ? nameVal : '');
|
|
522
542
|
}
|
|
523
543
|
return {
|
|
524
544
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
@@ -529,17 +549,33 @@ export class PhotonServer {
|
|
|
529
549
|
// Handler for channel events - forward to daemon for cross-process pub/sub
|
|
530
550
|
const outputHandler = (emit) => {
|
|
531
551
|
if (this.daemonName && emit?.channel) {
|
|
532
|
-
publishToChannel(this.daemonName, emit.channel, emit).catch(() => {
|
|
552
|
+
publishToChannel(this.daemonName, emit.channel, emit, this.options.workingDir).catch(() => {
|
|
533
553
|
// Ignore publish errors - daemon may not be running
|
|
534
554
|
});
|
|
535
555
|
}
|
|
536
556
|
};
|
|
537
557
|
const tool = this.mcp.tools.find((t) => t.name === toolName);
|
|
538
558
|
const outputFormat = tool?.outputFormat;
|
|
559
|
+
const startTime = Date.now();
|
|
539
560
|
const result = await this.loader.executeTool(this.mcp, toolName, args || {}, {
|
|
540
561
|
inputProvider,
|
|
541
562
|
outputHandler,
|
|
542
563
|
});
|
|
564
|
+
const durationMs = Date.now() - startTime;
|
|
565
|
+
const transport = this.options.transport || 'stdio';
|
|
566
|
+
this.log('info', `${toolName} completed in ${durationMs}ms`, {
|
|
567
|
+
durationMs,
|
|
568
|
+
photon: this.mcp?.name,
|
|
569
|
+
transport,
|
|
570
|
+
});
|
|
571
|
+
audit({
|
|
572
|
+
ts: new Date().toISOString(),
|
|
573
|
+
event: 'tool_call',
|
|
574
|
+
photon: this.mcp?.name,
|
|
575
|
+
method: toolName,
|
|
576
|
+
client: transport,
|
|
577
|
+
durationMs,
|
|
578
|
+
});
|
|
543
579
|
const isStateful = result && typeof result === 'object' && result._stateful === true;
|
|
544
580
|
const actualResult = isStateful ? result.result : result;
|
|
545
581
|
// Build content with optional mimeType annotation
|
|
@@ -716,7 +752,7 @@ export class PhotonServer {
|
|
|
716
752
|
const inputProvider = this.createMCPInputProvider();
|
|
717
753
|
const outputHandler = (emit) => {
|
|
718
754
|
if (this.daemonName && emit?.channel) {
|
|
719
|
-
publishToChannel(this.daemonName, emit.channel, emit).catch(() => { });
|
|
755
|
+
publishToChannel(this.daemonName, emit.channel, emit, this.options.workingDir).catch(() => { });
|
|
720
756
|
}
|
|
721
757
|
};
|
|
722
758
|
this.loader
|
|
@@ -1052,41 +1088,14 @@ export class PhotonServer {
|
|
|
1052
1088
|
* Download a photon from a marketplace source, save to workingDir, and load it
|
|
1053
1089
|
*/
|
|
1054
1090
|
async downloadAndLoadPhoton(photonName, workingDir, source) {
|
|
1055
|
-
const { MarketplaceManager
|
|
1091
|
+
const { MarketplaceManager } = await import('./marketplace-manager.js');
|
|
1056
1092
|
const manager = new MarketplaceManager();
|
|
1057
1093
|
await manager.initialize();
|
|
1058
1094
|
const result = await manager.fetchMCP(photonName);
|
|
1059
1095
|
if (!result) {
|
|
1060
1096
|
throw new Error(`Failed to download photon: ${photonName}`);
|
|
1061
1097
|
}
|
|
1062
|
-
|
|
1063
|
-
const { default: fsPromises } = await import('fs/promises');
|
|
1064
|
-
const filePath = (await import('path')).join(workingDir, `${photonName}.photon.ts`);
|
|
1065
|
-
const fileName = `${photonName}.photon.ts`;
|
|
1066
|
-
// Ensure working directory exists
|
|
1067
|
-
await fsPromises.mkdir(workingDir, { recursive: true });
|
|
1068
|
-
await fsPromises.writeFile(filePath, result.content, 'utf-8');
|
|
1069
|
-
// Save metadata
|
|
1070
|
-
if (source.metadata) {
|
|
1071
|
-
const contentHash = calculateHash(result.content);
|
|
1072
|
-
await manager.savePhotonMetadata(fileName, source.marketplace, source.metadata, contentHash);
|
|
1073
|
-
// Download assets if present
|
|
1074
|
-
if (source.metadata.assets && source.metadata.assets.length > 0) {
|
|
1075
|
-
const assets = await manager.fetchAssets(source.marketplace, source.metadata.assets);
|
|
1076
|
-
for (const [assetPath, content] of assets) {
|
|
1077
|
-
// Security: validate asset path to prevent traversal
|
|
1078
|
-
const safePath = validateAssetPath(assetPath);
|
|
1079
|
-
const targetPath = (await import('path')).join(workingDir, safePath);
|
|
1080
|
-
if (!isPathWithin(targetPath, workingDir)) {
|
|
1081
|
-
this.log('warn', `Skipping unsafe asset path: ${assetPath}`);
|
|
1082
|
-
continue;
|
|
1083
|
-
}
|
|
1084
|
-
const targetDir = (await import('path')).dirname(targetPath);
|
|
1085
|
-
await fsPromises.mkdir(targetDir, { recursive: true });
|
|
1086
|
-
await fsPromises.writeFile(targetPath, content, 'utf-8');
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1098
|
+
const { photonPath: filePath } = await manager.installPhoton(result, photonName, workingDir);
|
|
1090
1099
|
// Update options and load
|
|
1091
1100
|
this.options.filePath = filePath;
|
|
1092
1101
|
this.options.unresolvedPhoton = undefined;
|
|
@@ -1152,7 +1161,7 @@ export class PhotonServer {
|
|
|
1152
1161
|
const inputProvider = this.createMCPInputProvider();
|
|
1153
1162
|
const outputHandler = (emit) => {
|
|
1154
1163
|
if (this.daemonName && emit?.channel) {
|
|
1155
|
-
publishToChannel(this.daemonName, emit.channel, emit).catch((e) => {
|
|
1164
|
+
publishToChannel(this.daemonName, emit.channel, emit, this.options.workingDir).catch((e) => {
|
|
1156
1165
|
this.log('debug', 'Publish to channel failed', { error: getErrorMessage(e) });
|
|
1157
1166
|
});
|
|
1158
1167
|
}
|
|
@@ -1259,8 +1268,8 @@ export class PhotonServer {
|
|
|
1259
1268
|
// Subscribe to wildcard channel for all events from this photon
|
|
1260
1269
|
// E.g., "kanban:*" receives "kanban:photon", "kanban:my-board", etc.
|
|
1261
1270
|
const unsubscribe = await subscribeChannel(this.daemonName, `${this.daemonName}:*`, (message) => {
|
|
1262
|
-
this.handleChannelMessage(message);
|
|
1263
|
-
});
|
|
1271
|
+
void this.handleChannelMessage(message);
|
|
1272
|
+
}, { workingDir: this.options.workingDir });
|
|
1264
1273
|
this.channelUnsubscribers.push(unsubscribe);
|
|
1265
1274
|
this.log('info', `Subscribed to daemon channel: ${this.daemonName}:*`);
|
|
1266
1275
|
}
|
|
@@ -1302,8 +1311,8 @@ export class PhotonServer {
|
|
|
1302
1311
|
catch (e) {
|
|
1303
1312
|
this.log('debug', 'Notification send failed', { error: getErrorMessage(e) });
|
|
1304
1313
|
}
|
|
1305
|
-
// Also send to SSE sessions
|
|
1306
|
-
for (const session of this.sseSessions.values()) {
|
|
1314
|
+
// Also send to SSE sessions — snapshot to avoid live-iterator + await issues
|
|
1315
|
+
for (const session of Array.from(this.sseSessions.values())) {
|
|
1307
1316
|
try {
|
|
1308
1317
|
await session.server.notification(payload);
|
|
1309
1318
|
}
|
|
@@ -1312,11 +1321,30 @@ export class PhotonServer {
|
|
|
1312
1321
|
}
|
|
1313
1322
|
}
|
|
1314
1323
|
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Intercept a transport to capture raw client capabilities before Zod strips them.
|
|
1326
|
+
*
|
|
1327
|
+
* The MCP SDK's Zod schema for ClientCapabilities doesn't include `extensions`
|
|
1328
|
+
* (protocol 2025-11-25+), so getClientCapabilities() returns an object without it.
|
|
1329
|
+
* We intercept the transport's onmessage to capture the raw `initialize` request
|
|
1330
|
+
* and store capabilities before Zod parsing occurs.
|
|
1331
|
+
*/
|
|
1332
|
+
interceptTransportForRawCapabilities(transport, targetServer) {
|
|
1333
|
+
const origOnMessage = transport.onmessage;
|
|
1334
|
+
transport.onmessage = (message, extra) => {
|
|
1335
|
+
// Capture raw capabilities from initialize request
|
|
1336
|
+
if (message?.method === 'initialize' && message?.params?.capabilities) {
|
|
1337
|
+
this.rawClientCapabilities.set(targetServer, message.params.capabilities);
|
|
1338
|
+
}
|
|
1339
|
+
origOnMessage?.(message, extra);
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1315
1342
|
/**
|
|
1316
1343
|
* Start server with stdio transport
|
|
1317
1344
|
*/
|
|
1318
1345
|
async startStdio() {
|
|
1319
1346
|
const transport = new StdioServerTransport();
|
|
1347
|
+
this.interceptTransportForRawCapabilities(transport, this.server);
|
|
1320
1348
|
await this.server.connect(transport);
|
|
1321
1349
|
this.log('info', `Server started: ${this.mcp.name}`);
|
|
1322
1350
|
}
|
|
@@ -1327,263 +1355,269 @@ export class PhotonServer {
|
|
|
1327
1355
|
const port = this.options.port || 3000;
|
|
1328
1356
|
const ssePath = '/mcp';
|
|
1329
1357
|
const messagesPath = '/mcp/messages';
|
|
1330
|
-
this.httpServer = createServer(
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
}
|
|
1337
|
-
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
1338
|
-
// Handle CORS preflight
|
|
1339
|
-
if (req.method === 'OPTIONS') {
|
|
1340
|
-
res.writeHead(204, {
|
|
1341
|
-
'Access-Control-Allow-Origin': '*',
|
|
1342
|
-
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
1343
|
-
'Access-Control-Allow-Headers': 'Content-Type',
|
|
1344
|
-
});
|
|
1345
|
-
res.end();
|
|
1346
|
-
return;
|
|
1347
|
-
}
|
|
1348
|
-
// SSE connection endpoint
|
|
1349
|
-
if (req.method === 'GET' && url.pathname === ssePath) {
|
|
1350
|
-
await this.handleSSEConnection(res, messagesPath);
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
// Message posting endpoint
|
|
1354
|
-
if (req.method === 'POST' && url.pathname === messagesPath) {
|
|
1355
|
-
await this.handleSSEMessage(req, res, url);
|
|
1356
|
-
return;
|
|
1357
|
-
}
|
|
1358
|
-
// Health check / info endpoint
|
|
1359
|
-
if (req.method === 'GET' && url.pathname === '/') {
|
|
1360
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1361
|
-
const endpoints = {
|
|
1362
|
-
sse: `http://localhost:${port}${ssePath}`,
|
|
1363
|
-
messages: `http://localhost:${port}${messagesPath}`,
|
|
1364
|
-
};
|
|
1365
|
-
if (this.devMode) {
|
|
1366
|
-
endpoints.playground = `http://localhost:${port}/playground`;
|
|
1367
|
-
}
|
|
1368
|
-
res.end(JSON.stringify({
|
|
1369
|
-
name: this.mcp?.name || 'photon-mcp',
|
|
1370
|
-
transport: 'sse',
|
|
1371
|
-
endpoints,
|
|
1372
|
-
tools: this.mcp?.tools.length || 0,
|
|
1373
|
-
assets: this.mcp?.assets
|
|
1374
|
-
? {
|
|
1375
|
-
ui: this.mcp.assets.ui.length,
|
|
1376
|
-
prompts: this.mcp.assets.prompts.length,
|
|
1377
|
-
resources: this.mcp.assets.resources.length,
|
|
1378
|
-
}
|
|
1379
|
-
: null,
|
|
1380
|
-
}));
|
|
1381
|
-
return;
|
|
1382
|
-
}
|
|
1383
|
-
// Playground and API endpoints - only in dev mode
|
|
1384
|
-
if (this.devMode) {
|
|
1385
|
-
if (req.method === 'GET' && url.pathname === '/playground') {
|
|
1386
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1387
|
-
res.end(await this.getPlaygroundHTML(port));
|
|
1388
|
-
return;
|
|
1389
|
-
}
|
|
1390
|
-
// API: List all photons
|
|
1391
|
-
if (req.method === 'GET' && url.pathname === '/api/photons') {
|
|
1392
|
-
res.writeHead(200, {
|
|
1393
|
-
'Content-Type': 'application/json',
|
|
1394
|
-
'Access-Control-Allow-Origin': '*',
|
|
1395
|
-
});
|
|
1396
|
-
try {
|
|
1397
|
-
const photons = await this.listAllPhotons();
|
|
1398
|
-
res.end(JSON.stringify({ photons }));
|
|
1399
|
-
}
|
|
1400
|
-
catch (error) {
|
|
1401
|
-
res.writeHead(500);
|
|
1402
|
-
res.end(JSON.stringify({ error: getErrorMessage(error) }));
|
|
1403
|
-
}
|
|
1358
|
+
this.httpServer = createServer((req, res) => {
|
|
1359
|
+
void (async () => {
|
|
1360
|
+
// Security: set standard security headers on all responses
|
|
1361
|
+
setSecurityHeaders(res);
|
|
1362
|
+
if (!req.url) {
|
|
1363
|
+
res.writeHead(400).end('Missing URL');
|
|
1404
1364
|
return;
|
|
1405
1365
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1366
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
1367
|
+
// Handle CORS preflight
|
|
1368
|
+
if (req.method === 'OPTIONS') {
|
|
1369
|
+
res.writeHead(204, {
|
|
1410
1370
|
'Access-Control-Allow-Origin': '*',
|
|
1371
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
1372
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
1411
1373
|
});
|
|
1412
|
-
|
|
1413
|
-
const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name);
|
|
1414
|
-
return {
|
|
1415
|
-
name: tool.name,
|
|
1416
|
-
description: tool.description,
|
|
1417
|
-
inputSchema: tool.inputSchema,
|
|
1418
|
-
ui: linkedUI
|
|
1419
|
-
? { id: linkedUI.id, uri: `ui://${this.mcp.name}/${linkedUI.id}` }
|
|
1420
|
-
: null,
|
|
1421
|
-
};
|
|
1422
|
-
}) || [];
|
|
1423
|
-
res.end(JSON.stringify({ tools }));
|
|
1374
|
+
res.end();
|
|
1424
1375
|
return;
|
|
1425
1376
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
'Access-Control-Allow-Origin': '*',
|
|
1430
|
-
});
|
|
1431
|
-
res.end(JSON.stringify(this.buildStatusSnapshot()));
|
|
1377
|
+
// SSE connection endpoint
|
|
1378
|
+
if (req.method === 'GET' && url.pathname === ssePath) {
|
|
1379
|
+
await this.handleSSEConnection(res, messagesPath);
|
|
1432
1380
|
return;
|
|
1433
1381
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1382
|
+
// Message posting endpoint
|
|
1383
|
+
if (req.method === 'POST' && url.pathname === messagesPath) {
|
|
1384
|
+
await this.handleSSEMessage(req, res, url);
|
|
1436
1385
|
return;
|
|
1437
1386
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
}
|
|
1449
|
-
if (!this.mcp) {
|
|
1450
|
-
res.writeHead(503);
|
|
1451
|
-
res.end(JSON.stringify({ success: false, error: 'Photon not loaded' }));
|
|
1452
|
-
return;
|
|
1453
|
-
}
|
|
1454
|
-
try {
|
|
1455
|
-
const body = await readBody(req);
|
|
1456
|
-
const { tool, args } = JSON.parse(body);
|
|
1457
|
-
const result = await this.loader.executeTool(this.mcp, tool, args || {});
|
|
1458
|
-
const isStateful = result && typeof result === 'object' && result._stateful === true;
|
|
1459
|
-
res.writeHead(200);
|
|
1387
|
+
// Health check / info endpoint
|
|
1388
|
+
if (req.method === 'GET' && url.pathname === '/') {
|
|
1389
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1390
|
+
const endpoints = {
|
|
1391
|
+
sse: `http://localhost:${port}${ssePath}`,
|
|
1392
|
+
messages: `http://localhost:${port}${messagesPath}`,
|
|
1393
|
+
};
|
|
1394
|
+
if (this.devMode) {
|
|
1395
|
+
endpoints.playground = `http://localhost:${port}/playground`;
|
|
1396
|
+
}
|
|
1460
1397
|
res.end(JSON.stringify({
|
|
1461
|
-
|
|
1462
|
-
|
|
1398
|
+
name: this.mcp?.name || 'photon-mcp',
|
|
1399
|
+
transport: 'sse',
|
|
1400
|
+
endpoints,
|
|
1401
|
+
tools: this.mcp?.tools.length || 0,
|
|
1402
|
+
assets: this.mcp?.assets
|
|
1403
|
+
? {
|
|
1404
|
+
ui: this.mcp.assets.ui.length,
|
|
1405
|
+
prompts: this.mcp.assets.prompts.length,
|
|
1406
|
+
resources: this.mcp.assets.resources.length,
|
|
1407
|
+
}
|
|
1408
|
+
: null,
|
|
1463
1409
|
}));
|
|
1464
|
-
}
|
|
1465
|
-
catch (error) {
|
|
1466
|
-
const status = error.message?.includes('too large') ? 413 : 500;
|
|
1467
|
-
res.writeHead(status);
|
|
1468
|
-
res.end(JSON.stringify({ success: false, error: getErrorMessage(error) }));
|
|
1469
|
-
}
|
|
1470
|
-
return;
|
|
1471
|
-
}
|
|
1472
|
-
// API: Call tool with streaming progress (SSE)
|
|
1473
|
-
if (req.method === 'POST' && url.pathname === '/api/call-stream') {
|
|
1474
|
-
res.setHeader('Access-Control-Allow-Origin', `http://localhost:${this.options.port || 3000}`);
|
|
1475
|
-
res.setHeader('Content-Type', 'text/event-stream');
|
|
1476
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
1477
|
-
res.setHeader('Connection', 'keep-alive');
|
|
1478
|
-
if (!this.mcp) {
|
|
1479
|
-
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
1480
|
-
res.end(JSON.stringify({ success: false, error: 'Photon not loaded' }));
|
|
1481
1410
|
return;
|
|
1482
1411
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
};
|
|
1490
|
-
try {
|
|
1491
|
-
const payload = JSON.parse(body || '{}');
|
|
1492
|
-
const tool = payload.tool;
|
|
1493
|
-
if (!tool) {
|
|
1494
|
-
throw new Error('Tool name is required');
|
|
1495
|
-
}
|
|
1496
|
-
const args = payload.args || {};
|
|
1497
|
-
const progressToken = payload.progressToken ?? `progress_${Date.now()}`;
|
|
1498
|
-
requestId = payload.requestId || requestId;
|
|
1499
|
-
const sendNotification = (method, params) => {
|
|
1500
|
-
sendMessage({ jsonrpc: '2.0', method, params });
|
|
1501
|
-
};
|
|
1502
|
-
const reportProgress = (emit) => {
|
|
1503
|
-
const rawValue = typeof emit?.value === 'number' ? emit.value : 0;
|
|
1504
|
-
const percent = rawValue <= 1 ? rawValue * 100 : rawValue;
|
|
1505
|
-
sendNotification('notifications/progress', {
|
|
1506
|
-
progressToken,
|
|
1507
|
-
progress: percent,
|
|
1508
|
-
total: 100,
|
|
1509
|
-
message: emit?.message || null,
|
|
1510
|
-
});
|
|
1511
|
-
};
|
|
1512
|
-
const outputHandler = (emit) => {
|
|
1513
|
-
if (!emit)
|
|
1514
|
-
return;
|
|
1515
|
-
if (emit.emit === 'progress') {
|
|
1516
|
-
reportProgress(emit);
|
|
1517
|
-
}
|
|
1518
|
-
else if (emit.emit === 'status') {
|
|
1519
|
-
sendNotification('notifications/status', {
|
|
1520
|
-
type: emit.type || 'info',
|
|
1521
|
-
message: emit.message || '',
|
|
1522
|
-
});
|
|
1523
|
-
}
|
|
1524
|
-
else {
|
|
1525
|
-
sendNotification('notifications/emit', { event: emit });
|
|
1526
|
-
}
|
|
1527
|
-
// Forward channel events to daemon for cross-process pub/sub
|
|
1528
|
-
if (this.daemonName && emit.channel) {
|
|
1529
|
-
publishToChannel(this.daemonName, emit.channel, emit).catch(() => {
|
|
1530
|
-
// Ignore publish errors - daemon may not be running
|
|
1531
|
-
});
|
|
1532
|
-
}
|
|
1533
|
-
};
|
|
1534
|
-
sendNotification('notifications/status', {
|
|
1535
|
-
type: 'info',
|
|
1536
|
-
message: `Starting ${tool}`,
|
|
1537
|
-
});
|
|
1538
|
-
const result = await this.loader.executeTool(this.mcp, tool, args, { outputHandler });
|
|
1539
|
-
const isStateful = result && typeof result === 'object' && result._stateful === true;
|
|
1540
|
-
sendMessage({
|
|
1541
|
-
jsonrpc: '2.0',
|
|
1542
|
-
id: requestId,
|
|
1543
|
-
result: {
|
|
1544
|
-
success: true,
|
|
1545
|
-
data: isStateful ? result.result : result,
|
|
1546
|
-
},
|
|
1547
|
-
});
|
|
1548
|
-
res.end();
|
|
1412
|
+
// Playground and API endpoints - only in dev mode
|
|
1413
|
+
if (this.devMode) {
|
|
1414
|
+
if (req.method === 'GET' && url.pathname === '/playground') {
|
|
1415
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1416
|
+
res.end(await this.getPlaygroundHTML(port));
|
|
1417
|
+
return;
|
|
1549
1418
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
};
|
|
1556
|
-
|
|
1557
|
-
|
|
1419
|
+
// API: List all photons
|
|
1420
|
+
if (req.method === 'GET' && url.pathname === '/api/photons') {
|
|
1421
|
+
res.writeHead(200, {
|
|
1422
|
+
'Content-Type': 'application/json',
|
|
1423
|
+
'Access-Control-Allow-Origin': '*',
|
|
1424
|
+
});
|
|
1425
|
+
try {
|
|
1426
|
+
const photons = await this.listAllPhotons();
|
|
1427
|
+
res.end(JSON.stringify({ photons }));
|
|
1428
|
+
}
|
|
1429
|
+
catch (error) {
|
|
1430
|
+
res.writeHead(500);
|
|
1431
|
+
res.end(JSON.stringify({ error: getErrorMessage(error) }));
|
|
1558
1432
|
}
|
|
1559
|
-
|
|
1560
|
-
res.end();
|
|
1433
|
+
return;
|
|
1561
1434
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1435
|
+
// API: List tools (for compatibility, now returns current photon)
|
|
1436
|
+
if (req.method === 'GET' && url.pathname === '/api/tools') {
|
|
1437
|
+
res.writeHead(200, {
|
|
1438
|
+
'Content-Type': 'application/json',
|
|
1439
|
+
'Access-Control-Allow-Origin': '*',
|
|
1440
|
+
});
|
|
1441
|
+
const tools = this.mcp?.tools.map((tool) => {
|
|
1442
|
+
const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name);
|
|
1443
|
+
return {
|
|
1444
|
+
name: tool.name,
|
|
1445
|
+
description: tool.description,
|
|
1446
|
+
inputSchema: tool.inputSchema,
|
|
1447
|
+
ui: linkedUI
|
|
1448
|
+
? { id: linkedUI.id, uri: `ui://${this.mcp.name}/${linkedUI.id}` }
|
|
1449
|
+
: null,
|
|
1450
|
+
};
|
|
1451
|
+
}) || [];
|
|
1452
|
+
res.end(JSON.stringify({ tools }));
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (req.method === 'GET' && url.pathname === '/api/status') {
|
|
1572
1456
|
res.writeHead(200, {
|
|
1573
|
-
'Content-Type': '
|
|
1457
|
+
'Content-Type': 'application/json',
|
|
1574
1458
|
'Access-Control-Allow-Origin': '*',
|
|
1575
1459
|
});
|
|
1576
|
-
res.end(
|
|
1460
|
+
res.end(JSON.stringify(this.buildStatusSnapshot()));
|
|
1577
1461
|
return;
|
|
1578
1462
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1463
|
+
if (req.method === 'GET' && url.pathname === '/api/status-stream') {
|
|
1464
|
+
this.handleStatusStream(req, res);
|
|
1465
|
+
return;
|
|
1581
1466
|
}
|
|
1582
1467
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1468
|
+
// API: Call tool
|
|
1469
|
+
if (req.method === 'POST' && url.pathname === '/api/call') {
|
|
1470
|
+
// Security: restrict CORS to localhost and require local request
|
|
1471
|
+
res.setHeader('Access-Control-Allow-Origin', `http://localhost:${this.options.port || 3000}`);
|
|
1472
|
+
res.setHeader('Content-Type', 'application/json');
|
|
1473
|
+
if (!isLocalRequest(req)) {
|
|
1474
|
+
res.writeHead(403);
|
|
1475
|
+
res.end(JSON.stringify({ success: false, error: 'Forbidden: non-local request' }));
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
if (!this.mcp) {
|
|
1479
|
+
res.writeHead(503);
|
|
1480
|
+
res.end(JSON.stringify({ success: false, error: 'Photon not loaded' }));
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
try {
|
|
1484
|
+
const body = await readBody(req);
|
|
1485
|
+
const { tool, args } = JSON.parse(body);
|
|
1486
|
+
const result = await this.loader.executeTool(this.mcp, tool, args || {});
|
|
1487
|
+
const isStateful = result && typeof result === 'object' && result._stateful === true;
|
|
1488
|
+
res.writeHead(200);
|
|
1489
|
+
res.end(JSON.stringify({
|
|
1490
|
+
success: true,
|
|
1491
|
+
data: isStateful ? result.result : result,
|
|
1492
|
+
}));
|
|
1493
|
+
}
|
|
1494
|
+
catch (error) {
|
|
1495
|
+
const status = error.message?.includes('too large') ? 413 : 500;
|
|
1496
|
+
res.writeHead(status);
|
|
1497
|
+
res.end(JSON.stringify({ success: false, error: getErrorMessage(error) }));
|
|
1498
|
+
}
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
// API: Call tool with streaming progress (SSE)
|
|
1502
|
+
if (req.method === 'POST' && url.pathname === '/api/call-stream') {
|
|
1503
|
+
res.setHeader('Access-Control-Allow-Origin', `http://localhost:${this.options.port || 3000}`);
|
|
1504
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
1505
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1506
|
+
res.setHeader('Connection', 'keep-alive');
|
|
1507
|
+
if (!this.mcp) {
|
|
1508
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
1509
|
+
res.end(JSON.stringify({ success: false, error: 'Photon not loaded' }));
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
let body = '';
|
|
1513
|
+
req.on('data', (chunk) => (body += chunk));
|
|
1514
|
+
req.on('end', () => {
|
|
1515
|
+
void (async () => {
|
|
1516
|
+
let requestId = `run_${Date.now()}`;
|
|
1517
|
+
const sendMessage = (message) => {
|
|
1518
|
+
res.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`);
|
|
1519
|
+
};
|
|
1520
|
+
try {
|
|
1521
|
+
const payload = JSON.parse(body || '{}');
|
|
1522
|
+
const tool = payload.tool;
|
|
1523
|
+
if (!tool) {
|
|
1524
|
+
throw new Error('Tool name is required');
|
|
1525
|
+
}
|
|
1526
|
+
const args = payload.args || {};
|
|
1527
|
+
const progressToken = payload.progressToken ?? `progress_${Date.now()}`;
|
|
1528
|
+
requestId = payload.requestId || requestId;
|
|
1529
|
+
const sendNotification = (method, params) => {
|
|
1530
|
+
sendMessage({ jsonrpc: '2.0', method, params });
|
|
1531
|
+
};
|
|
1532
|
+
const reportProgress = (emit) => {
|
|
1533
|
+
const rawValue = typeof emit?.value === 'number' ? emit.value : 0;
|
|
1534
|
+
const percent = rawValue <= 1 ? rawValue * 100 : rawValue;
|
|
1535
|
+
sendNotification('notifications/progress', {
|
|
1536
|
+
progressToken,
|
|
1537
|
+
progress: percent,
|
|
1538
|
+
total: 100,
|
|
1539
|
+
message: emit?.message || null,
|
|
1540
|
+
});
|
|
1541
|
+
};
|
|
1542
|
+
const outputHandler = (emit) => {
|
|
1543
|
+
if (!emit)
|
|
1544
|
+
return;
|
|
1545
|
+
if (emit.emit === 'progress') {
|
|
1546
|
+
reportProgress(emit);
|
|
1547
|
+
}
|
|
1548
|
+
else if (emit.emit === 'status') {
|
|
1549
|
+
sendNotification('notifications/status', {
|
|
1550
|
+
type: emit.type || 'info',
|
|
1551
|
+
message: emit.message || '',
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
else {
|
|
1555
|
+
sendNotification('notifications/emit', { event: emit });
|
|
1556
|
+
}
|
|
1557
|
+
// Forward channel events to daemon for cross-process pub/sub
|
|
1558
|
+
if (this.daemonName && emit.channel) {
|
|
1559
|
+
publishToChannel(this.daemonName, emit.channel, emit, this.options.workingDir).catch(() => {
|
|
1560
|
+
// Ignore publish errors - daemon may not be running
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
sendNotification('notifications/status', {
|
|
1565
|
+
type: 'info',
|
|
1566
|
+
message: `Starting ${tool}`,
|
|
1567
|
+
});
|
|
1568
|
+
const result = await this.loader.executeTool(this.mcp, tool, args, {
|
|
1569
|
+
outputHandler,
|
|
1570
|
+
});
|
|
1571
|
+
const isStateful = result && typeof result === 'object' && result._stateful === true;
|
|
1572
|
+
sendMessage({
|
|
1573
|
+
jsonrpc: '2.0',
|
|
1574
|
+
id: requestId,
|
|
1575
|
+
result: {
|
|
1576
|
+
success: true,
|
|
1577
|
+
data: isStateful ? result.result : result,
|
|
1578
|
+
},
|
|
1579
|
+
});
|
|
1580
|
+
res.end();
|
|
1581
|
+
}
|
|
1582
|
+
catch (error) {
|
|
1583
|
+
const message = getErrorMessage(error);
|
|
1584
|
+
const errorPayload = {
|
|
1585
|
+
jsonrpc: '2.0',
|
|
1586
|
+
error: { code: -32000, message },
|
|
1587
|
+
};
|
|
1588
|
+
if (requestId) {
|
|
1589
|
+
errorPayload.id = requestId;
|
|
1590
|
+
}
|
|
1591
|
+
sendMessage(errorPayload);
|
|
1592
|
+
res.end();
|
|
1593
|
+
}
|
|
1594
|
+
})();
|
|
1595
|
+
});
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
// API: Get UI template
|
|
1599
|
+
if (req.method === 'GET' && url.pathname.startsWith('/api/ui/')) {
|
|
1600
|
+
const uiId = url.pathname.replace('/api/ui/', '');
|
|
1601
|
+
const ui = this.mcp?.assets?.ui.find((u) => u.id === uiId);
|
|
1602
|
+
if (ui?.resolvedPath) {
|
|
1603
|
+
try {
|
|
1604
|
+
const content = await fs.readFile(ui.resolvedPath, 'utf-8');
|
|
1605
|
+
res.writeHead(200, {
|
|
1606
|
+
'Content-Type': 'text/html',
|
|
1607
|
+
'Access-Control-Allow-Origin': '*',
|
|
1608
|
+
});
|
|
1609
|
+
res.end(content);
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
catch {
|
|
1613
|
+
// Fall through to 404
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
res.writeHead(404).end('UI not found');
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
res.writeHead(404).end('Not Found');
|
|
1620
|
+
})();
|
|
1587
1621
|
});
|
|
1588
1622
|
this.httpServer.on('clientError', (err, socket) => {
|
|
1589
1623
|
this.log('warn', 'HTTP client error', { message: err.message });
|
|
@@ -1610,7 +1644,8 @@ export class PhotonServer {
|
|
|
1610
1644
|
* List all photons in the .photon directory
|
|
1611
1645
|
*/
|
|
1612
1646
|
async listAllPhotons() {
|
|
1613
|
-
const { listPhotonFiles
|
|
1647
|
+
const { listPhotonFiles } = await import('./path-resolver.js');
|
|
1648
|
+
const { getDefaultContext } = await import('./context.js');
|
|
1614
1649
|
const photonFiles = await listPhotonFiles();
|
|
1615
1650
|
const photons = await Promise.all(photonFiles.map(async (file) => {
|
|
1616
1651
|
try {
|
|
@@ -1619,7 +1654,7 @@ export class PhotonServer {
|
|
|
1619
1654
|
return {
|
|
1620
1655
|
name: mcp.name,
|
|
1621
1656
|
description: mcp.description,
|
|
1622
|
-
file: file.replace(
|
|
1657
|
+
file: file.replace(getDefaultContext().baseDir + '/', ''),
|
|
1623
1658
|
tools: mcp.tools.map((tool) => ({
|
|
1624
1659
|
name: tool.name,
|
|
1625
1660
|
description: tool.description,
|
|
@@ -1664,24 +1699,27 @@ export class PhotonServer {
|
|
|
1664
1699
|
this.setupSessionHandlers(sessionServer);
|
|
1665
1700
|
// Create SSE transport
|
|
1666
1701
|
const transport = new SSEServerTransport(messagesPath, res);
|
|
1702
|
+
this.interceptTransportForRawCapabilities(transport, sessionServer);
|
|
1667
1703
|
const sessionId = transport.sessionId;
|
|
1668
1704
|
// Store session
|
|
1669
1705
|
this.sseSessions.set(sessionId, { server: sessionServer, transport });
|
|
1670
1706
|
// Clean up on close (guard against recursive close:
|
|
1671
1707
|
// onclose → sessionServer.close() → transport.close() → onclose)
|
|
1672
1708
|
let closing = false;
|
|
1673
|
-
transport.onclose =
|
|
1709
|
+
transport.onclose = () => {
|
|
1674
1710
|
if (closing)
|
|
1675
1711
|
return;
|
|
1676
1712
|
closing = true;
|
|
1677
1713
|
this.sseSessions.delete(sessionId);
|
|
1678
1714
|
this.log('info', 'SSE client disconnected', { sessionId });
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1715
|
+
void (async () => {
|
|
1716
|
+
try {
|
|
1717
|
+
await sessionServer.close();
|
|
1718
|
+
}
|
|
1719
|
+
catch {
|
|
1720
|
+
// Ignore errors during cleanup (transport already closed)
|
|
1721
|
+
}
|
|
1722
|
+
})();
|
|
1685
1723
|
};
|
|
1686
1724
|
transport.onerror = (error) => {
|
|
1687
1725
|
this.log('warn', 'SSE transport error', {
|
|
@@ -1945,15 +1983,17 @@ export class PhotonServer {
|
|
|
1945
1983
|
if (i >= 0) resultListeners.splice(i, 1);
|
|
1946
1984
|
};
|
|
1947
1985
|
},
|
|
1948
|
-
callTool: function(name, args) {
|
|
1986
|
+
callTool: function(name, args, opts) {
|
|
1949
1987
|
var callId = generateCallId();
|
|
1950
1988
|
return new Promise(function(resolve, reject) {
|
|
1951
1989
|
pendingCalls[callId] = { resolve: resolve, reject: reject };
|
|
1990
|
+
var a = args || {};
|
|
1991
|
+
if (opts && opts.instance !== undefined) { a = Object.assign({}, a, { _targetInstance: opts.instance }); }
|
|
1952
1992
|
postToHost({
|
|
1953
1993
|
jsonrpc: '2.0',
|
|
1954
1994
|
id: callId,
|
|
1955
1995
|
method: 'tools/call',
|
|
1956
|
-
params: { name: name, arguments:
|
|
1996
|
+
params: { name: name, arguments: a }
|
|
1957
1997
|
});
|
|
1958
1998
|
setTimeout(function() {
|
|
1959
1999
|
if (pendingCalls[callId]) {
|
|
@@ -1963,7 +2003,7 @@ export class PhotonServer {
|
|
|
1963
2003
|
}, 30000);
|
|
1964
2004
|
});
|
|
1965
2005
|
},
|
|
1966
|
-
invoke: function(name, args) { return window.photon.callTool(name, args); },
|
|
2006
|
+
invoke: function(name, args, opts) { return window.photon.callTool(name, args, opts); },
|
|
1967
2007
|
onEmit: function(cb) {
|
|
1968
2008
|
emitListeners.push(cb);
|
|
1969
2009
|
return function() {
|
|
@@ -2225,8 +2265,18 @@ export class PhotonServer {
|
|
|
2225
2265
|
if (this.mcpClientFactory) {
|
|
2226
2266
|
await this.mcpClientFactory.disconnect();
|
|
2227
2267
|
}
|
|
2228
|
-
//
|
|
2229
|
-
for (const
|
|
2268
|
+
// Unsubscribe daemon channels
|
|
2269
|
+
for (const unsubscribe of this.channelUnsubscribers) {
|
|
2270
|
+
try {
|
|
2271
|
+
unsubscribe();
|
|
2272
|
+
}
|
|
2273
|
+
catch {
|
|
2274
|
+
/* ignore */
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
this.channelUnsubscribers = [];
|
|
2278
|
+
// Close SSE sessions — snapshot to avoid live-iterator + await issues
|
|
2279
|
+
for (const session of Array.from(this.sseSessions.values())) {
|
|
2230
2280
|
await session.server.close();
|
|
2231
2281
|
}
|
|
2232
2282
|
this.sseSessions.clear();
|
|
@@ -2304,7 +2354,8 @@ export class PhotonServer {
|
|
|
2304
2354
|
catch (e) {
|
|
2305
2355
|
this.log('debug', 'Notification send failed', { error: getErrorMessage(e) });
|
|
2306
2356
|
}
|
|
2307
|
-
|
|
2357
|
+
// Snapshot to avoid live-iterator + await issues
|
|
2358
|
+
for (const session of Array.from(this.sseSessions.values())) {
|
|
2308
2359
|
try {
|
|
2309
2360
|
await session.server.notification(payload);
|
|
2310
2361
|
}
|