@portel/photon 1.18.0 → 1.20.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/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.js +16 -4
- package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-config.js +4 -4
- package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-marketplace.js +14 -1
- package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +196 -77
- 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 +17 -0
- package/dist/auto-ui/bridge/index.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +1 -0
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +64 -16
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +12 -0
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam-form.bundle.js +49 -6
- package/dist/beam-form.bundle.js.map +2 -2
- package/dist/beam.bundle.js +2090 -512
- package/dist/beam.bundle.js.map +4 -4
- package/dist/capability-negotiator.d.ts +67 -0
- package/dist/capability-negotiator.d.ts.map +1 -0
- package/dist/capability-negotiator.js +104 -0
- package/dist/capability-negotiator.js.map +1 -0
- package/dist/channel-manager.d.ts +122 -0
- package/dist/channel-manager.d.ts.map +1 -0
- package/dist/channel-manager.js +266 -0
- package/dist/channel-manager.js.map +1 -0
- package/dist/claude-code-plugin.js +1 -1
- package/dist/cli/commands/beam.d.ts.map +1 -1
- package/dist/cli/commands/beam.js +8 -2
- package/dist/cli/commands/beam.js.map +1 -1
- package/dist/cli/commands/changelog.d.ts +9 -0
- package/dist/cli/commands/changelog.d.ts.map +1 -0
- package/dist/cli/commands/changelog.js +133 -0
- package/dist/cli/commands/changelog.js.map +1 -0
- package/dist/cli/commands/maker.d.ts.map +1 -1
- package/dist/cli/commands/maker.js +23 -2
- package/dist/cli/commands/maker.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +53 -0
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/package.d.ts.map +1 -1
- package/dist/cli/commands/package.js +43 -9
- package/dist/cli/commands/package.js.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +1 -0
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/update.d.ts +3 -2
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +50 -43
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +16 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli-alias.js +1 -1
- package/dist/cli-alias.js.map +1 -1
- package/dist/context-store.d.ts +23 -33
- package/dist/context-store.d.ts.map +1 -1
- package/dist/context-store.js +147 -97
- package/dist/context-store.js.map +1 -1
- package/dist/context.d.ts +15 -10
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +37 -13
- package/dist/context.js.map +1 -1
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +12 -0
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/server.js +34 -51
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/worker-manager.d.ts.map +1 -1
- package/dist/daemon/worker-manager.js +21 -7
- package/dist/daemon/worker-manager.js.map +1 -1
- package/dist/data-migration.d.ts +27 -0
- package/dist/data-migration.d.ts.map +1 -0
- package/dist/data-migration.js +307 -0
- package/dist/data-migration.js.map +1 -0
- package/dist/editor-support/docblock-tag-catalog.d.ts.map +1 -1
- package/dist/editor-support/docblock-tag-catalog.js +6 -0
- package/dist/editor-support/docblock-tag-catalog.js.map +1 -1
- package/dist/loader.d.ts +13 -0
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +169 -22
- package/dist/loader.js.map +1 -1
- package/dist/marketplace-manager.d.ts +6 -0
- package/dist/marketplace-manager.d.ts.map +1 -1
- package/dist/marketplace-manager.js +185 -62
- package/dist/marketplace-manager.js.map +1 -1
- package/dist/namespace-migration.d.ts +1 -0
- package/dist/namespace-migration.d.ts.map +1 -1
- package/dist/namespace-migration.js +86 -0
- package/dist/namespace-migration.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +47 -21
- 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 +6 -0
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/readme-syncer.d.ts.map +1 -1
- package/dist/readme-syncer.js +6 -1
- package/dist/readme-syncer.js.map +1 -1
- package/dist/resource-server.d.ts +105 -0
- package/dist/resource-server.d.ts.map +1 -0
- package/dist/resource-server.js +723 -0
- package/dist/resource-server.js.map +1 -0
- package/dist/serv/auth/jwt.d.ts +2 -0
- package/dist/serv/auth/jwt.d.ts.map +1 -1
- package/dist/serv/auth/jwt.js +11 -5
- package/dist/serv/auth/jwt.js.map +1 -1
- package/dist/serv/vault/token-vault.d.ts +2 -0
- package/dist/serv/vault/token-vault.d.ts.map +1 -1
- package/dist/serv/vault/token-vault.js +6 -0
- package/dist/serv/vault/token-vault.js.map +1 -1
- package/dist/server.d.ts +30 -119
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +252 -1122
- package/dist/server.js.map +1 -1
- package/dist/shared/audit.d.ts.map +1 -1
- package/dist/shared/audit.js +11 -4
- package/dist/shared/audit.js.map +1 -1
- package/dist/shared/security.d.ts +10 -0
- package/dist/shared/security.d.ts.map +1 -1
- package/dist/shared/security.js +27 -0
- package/dist/shared/security.js.map +1 -1
- package/dist/task-executor.d.ts +69 -0
- package/dist/task-executor.d.ts.map +1 -0
- package/dist/task-executor.js +182 -0
- package/dist/task-executor.js.map +1 -0
- package/dist/tasks/store.d.ts.map +1 -1
- package/dist/tasks/store.js +6 -2
- package/dist/tasks/store.js.map +1 -1
- package/dist/types/photon-instance.d.ts +50 -0
- package/dist/types/photon-instance.d.ts.map +1 -0
- package/dist/types/photon-instance.js +9 -0
- package/dist/types/photon-instance.js.map +1 -0
- package/dist/types/server-types.d.ts +61 -0
- package/dist/types/server-types.d.ts.map +1 -0
- package/dist/types/server-types.js +8 -0
- package/dist/types/server-types.js.map +1 -0
- package/dist/version-notify.d.ts +27 -0
- package/dist/version-notify.d.ts.map +1 -0
- package/dist/version-notify.js +142 -0
- package/dist/version-notify.js.map +1 -0
- package/package.json +3 -3
- package/dist/auto-ui/bridge/openai-shim.d.ts +0 -20
- package/dist/auto-ui/bridge/openai-shim.d.ts.map +0 -1
- package/dist/auto-ui/bridge/openai-shim.js +0 -231
- package/dist/auto-ui/bridge/openai-shim.js.map +0 -1
- package/dist/auto-ui/bridge/photon-app.d.ts +0 -162
- package/dist/auto-ui/bridge/photon-app.d.ts.map +0 -1
- package/dist/auto-ui/bridge/photon-app.js +0 -460
- package/dist/auto-ui/bridge/photon-app.js.map +0 -1
- package/dist/auto-ui/daemon-tools.d.ts +0 -45
- package/dist/auto-ui/daemon-tools.d.ts.map +0 -1
- package/dist/auto-ui/daemon-tools.js +0 -581
- package/dist/auto-ui/daemon-tools.js.map +0 -1
- package/dist/auto-ui/design-system/index.d.ts +0 -21
- package/dist/auto-ui/design-system/index.d.ts.map +0 -1
- package/dist/auto-ui/design-system/index.js +0 -27
- package/dist/auto-ui/design-system/index.js.map +0 -1
- package/dist/auto-ui/design-system/transaction-ui.d.ts +0 -70
- package/dist/auto-ui/design-system/transaction-ui.d.ts.map +0 -1
- package/dist/auto-ui/design-system/transaction-ui.js +0 -982
- package/dist/auto-ui/design-system/transaction-ui.js.map +0 -1
- package/dist/auto-ui/playground-server.d.ts +0 -7
- package/dist/auto-ui/playground-server.d.ts.map +0 -1
- package/dist/auto-ui/playground-server.js +0 -840
- package/dist/auto-ui/playground-server.js.map +0 -1
- package/dist/auto-ui/rendering/components.d.ts +0 -29
- package/dist/auto-ui/rendering/components.d.ts.map +0 -1
- package/dist/auto-ui/rendering/components.js +0 -1341
- package/dist/auto-ui/rendering/components.js.map +0 -1
- package/dist/auto-ui/rendering/field-analyzer.d.ts +0 -104
- package/dist/auto-ui/rendering/field-analyzer.d.ts.map +0 -1
- package/dist/auto-ui/rendering/field-analyzer.js +0 -447
- package/dist/auto-ui/rendering/field-analyzer.js.map +0 -1
- package/dist/auto-ui/rendering/field-renderers.d.ts +0 -64
- package/dist/auto-ui/rendering/field-renderers.d.ts.map +0 -1
- package/dist/auto-ui/rendering/field-renderers.js +0 -317
- package/dist/auto-ui/rendering/field-renderers.js.map +0 -1
- package/dist/auto-ui/rendering/index.d.ts +0 -28
- package/dist/auto-ui/rendering/index.d.ts.map +0 -1
- package/dist/auto-ui/rendering/index.js +0 -60
- package/dist/auto-ui/rendering/index.js.map +0 -1
- package/dist/auto-ui/rendering/layout-selector.d.ts +0 -60
- package/dist/auto-ui/rendering/layout-selector.d.ts.map +0 -1
- package/dist/auto-ui/rendering/layout-selector.js +0 -476
- package/dist/auto-ui/rendering/layout-selector.js.map +0 -1
- package/dist/markdown-utils.d.ts +0 -8
- package/dist/markdown-utils.d.ts.map +0 -1
- package/dist/markdown-utils.js +0 -64
- package/dist/markdown-utils.js.map +0 -1
- package/dist/mcp-client.d.ts +0 -9
- package/dist/mcp-client.d.ts.map +0 -1
- package/dist/mcp-client.js +0 -11
- package/dist/mcp-client.js.map +0 -1
- package/dist/mcp-elicitation.d.ts +0 -32
- package/dist/mcp-elicitation.d.ts.map +0 -1
- package/dist/mcp-elicitation.js +0 -26
- package/dist/mcp-elicitation.js.map +0 -1
- package/dist/photons/builder-compass.photon.d.ts +0 -167
- package/dist/photons/builder-compass.photon.d.ts.map +0 -1
- package/dist/photons/builder-compass.photon.js +0 -816
- package/dist/photons/builder-compass.photon.js.map +0 -1
- package/dist/photons/builder-compass.photon.ts +0 -1129
- package/dist/photons/docs/ui/docs.html +0 -441
- package/dist/photons/docs.photon.d.ts +0 -237
- package/dist/photons/docs.photon.d.ts.map +0 -1
- package/dist/photons/docs.photon.js +0 -483
- package/dist/photons/docs.photon.js.map +0 -1
- package/dist/photons/docs.photon.ts +0 -536
- package/dist/photons/slides.photon.d.ts +0 -212
- package/dist/photons/slides.photon.d.ts.map +0 -1
- package/dist/photons/slides.photon.js +0 -355
- package/dist/photons/slides.photon.js.map +0 -1
- package/dist/photons/slides.photon.ts +0 -370
- package/dist/photons/spreadsheet/ui/spreadsheet.html +0 -779
- package/dist/photons/spreadsheet.photon.d.ts +0 -554
- package/dist/photons/spreadsheet.photon.d.ts.map +0 -1
- package/dist/photons/spreadsheet.photon.js +0 -1050
- package/dist/photons/spreadsheet.photon.js.map +0 -1
- package/dist/photons/spreadsheet.photon.ts +0 -1239
- package/dist/photons/ui/builder-compass.html +0 -1199
- package/dist/photons/ui/builder-compass.photon.html +0 -380
- package/dist/security-scanner.d.ts +0 -52
- package/dist/security-scanner.d.ts.map +0 -1
- package/dist/security-scanner.js +0 -181
- package/dist/security-scanner.js.map +0 -1
- package/dist/shared/performance.d.ts +0 -65
- package/dist/shared/performance.d.ts.map +0 -1
- package/dist/shared/performance.js +0 -136
- package/dist/shared/performance.js.map +0 -1
package/dist/server.js
CHANGED
|
@@ -8,10 +8,9 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
8
8
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
9
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
10
10
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, GetTaskRequestSchema, ListTasksRequestSchema, CancelTaskRequestSchema, GetTaskPayloadRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
-
import { readFileSync } from 'node:fs';
|
|
12
11
|
import { readText } from './shared/io.js';
|
|
13
|
-
import * as path from 'node:path';
|
|
14
12
|
import { createServer } from 'node:http';
|
|
13
|
+
import * as path from 'node:path';
|
|
15
14
|
import { URL } from 'node:url';
|
|
16
15
|
import { PhotonLoader } from './loader.js';
|
|
17
16
|
import { generateExecutionId } from '@portel/photon-core';
|
|
@@ -21,14 +20,15 @@ import { createLogger } from './shared/logger.js';
|
|
|
21
20
|
import { getErrorMessage, formatToolError } from './shared/error-handler.js';
|
|
22
21
|
import { validateOrThrow, assertString, notEmpty, inRange, oneOf, hasExtension, } from './shared/validation.js';
|
|
23
22
|
import { generatePlaygroundHTML } from './auto-ui/playground-html.js';
|
|
24
|
-
import {
|
|
23
|
+
import { pingDaemon } from './daemon/client.js';
|
|
25
24
|
import { isGlobalDaemonRunning, startGlobalDaemon } from './daemon/manager.js';
|
|
25
|
+
import { ChannelManager, } from './channel-manager.js';
|
|
26
26
|
import { PhotonDocExtractor } from './photon-doc-extractor.js';
|
|
27
|
-
import { isLocalRequest, readBody, setSecurityHeaders } from './shared/security.js';
|
|
27
|
+
import { isLocalRequest, readBody, setSecurityHeaders, getCorsOrigin } from './shared/security.js';
|
|
28
28
|
import { audit } from './shared/audit.js';
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
29
|
+
import { TaskExecutor } from './task-executor.js';
|
|
30
|
+
import { CapabilityNegotiator } from './capability-negotiator.js';
|
|
31
|
+
import { ResourceServer } from './resource-server.js';
|
|
32
32
|
export class HotReloadDisabledError extends Error {
|
|
33
33
|
constructor(message) {
|
|
34
34
|
super(message);
|
|
@@ -105,20 +105,24 @@ class BeamCompatTransport {
|
|
|
105
105
|
}
|
|
106
106
|
// Otherwise push to SSE stream if connected
|
|
107
107
|
if (this.sseResponse && !this.sseResponse.writableEnded) {
|
|
108
|
-
|
|
108
|
+
const id = message.id ?? crypto.randomUUID();
|
|
109
|
+
this.sseResponse.write(`event: message\nid: ${id}\ndata: ${JSON.stringify(message)}\n\n`);
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
async handleHTTP(req, res, url) {
|
|
113
|
+
const corsOrigin = getCorsOrigin(req);
|
|
112
114
|
// GET — open SSE stream for server-to-client notifications
|
|
113
115
|
if (req.method === 'GET') {
|
|
114
116
|
this.sseResponse = res;
|
|
115
|
-
|
|
117
|
+
const sseHeaders = {
|
|
116
118
|
'Content-Type': 'text/event-stream',
|
|
117
119
|
'Cache-Control': 'no-cache',
|
|
118
120
|
Connection: 'keep-alive',
|
|
119
|
-
'Access-Control-Allow-Origin': '*',
|
|
120
121
|
'Mcp-Session-Id': this.sessionId,
|
|
121
|
-
}
|
|
122
|
+
};
|
|
123
|
+
if (corsOrigin)
|
|
124
|
+
sseHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
125
|
+
res.writeHead(200, sseHeaders);
|
|
122
126
|
// Send initial event so the client knows connection is established
|
|
123
127
|
res.write(':connected\n\n');
|
|
124
128
|
// Send keepalive every 15s
|
|
@@ -170,12 +174,14 @@ class BeamCompatTransport {
|
|
|
170
174
|
try {
|
|
171
175
|
const result = await this.subPhotonExecutor(targetPhoton, methodName, parsed.params.arguments || {});
|
|
172
176
|
const response = { jsonrpc: '2.0', id: parsed.id, result };
|
|
173
|
-
|
|
177
|
+
const subHeaders = {
|
|
174
178
|
'Content-Type': 'application/json',
|
|
175
|
-
'Access-Control-Allow-Origin': '*',
|
|
176
179
|
'Access-Control-Expose-Headers': 'Mcp-Session-Id',
|
|
177
180
|
'Mcp-Session-Id': this.sessionId,
|
|
178
|
-
}
|
|
181
|
+
};
|
|
182
|
+
if (corsOrigin)
|
|
183
|
+
subHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
184
|
+
res.writeHead(200, subHeaders);
|
|
179
185
|
res.end(JSON.stringify(response));
|
|
180
186
|
return;
|
|
181
187
|
}
|
|
@@ -188,10 +194,12 @@ class BeamCompatTransport {
|
|
|
188
194
|
isError: true,
|
|
189
195
|
},
|
|
190
196
|
};
|
|
191
|
-
|
|
197
|
+
const errHeaders = {
|
|
192
198
|
'Content-Type': 'application/json',
|
|
193
|
-
|
|
194
|
-
|
|
199
|
+
};
|
|
200
|
+
if (corsOrigin)
|
|
201
|
+
errHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
202
|
+
res.writeHead(200, errHeaders);
|
|
195
203
|
res.end(JSON.stringify(response));
|
|
196
204
|
return;
|
|
197
205
|
}
|
|
@@ -204,7 +212,12 @@ class BeamCompatTransport {
|
|
|
204
212
|
// Notifications have no id — fire-and-forget
|
|
205
213
|
if (parsed.id === undefined) {
|
|
206
214
|
this.onmessage?.(parsed, { sessionId: this.sessionId });
|
|
207
|
-
|
|
215
|
+
const notifHeaders = {
|
|
216
|
+
'Mcp-Session-Id': this.sessionId,
|
|
217
|
+
};
|
|
218
|
+
if (corsOrigin)
|
|
219
|
+
notifHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
220
|
+
res.writeHead(202, notifHeaders);
|
|
208
221
|
res.end();
|
|
209
222
|
return;
|
|
210
223
|
}
|
|
@@ -213,22 +226,30 @@ class BeamCompatTransport {
|
|
|
213
226
|
this.pendingResponse = resolve;
|
|
214
227
|
this.onmessage?.(parsed, { sessionId: this.sessionId });
|
|
215
228
|
});
|
|
216
|
-
|
|
229
|
+
const resHeaders = {
|
|
217
230
|
'Content-Type': 'application/json',
|
|
218
|
-
'Access-Control-Allow-Origin': '*',
|
|
219
231
|
'Access-Control-Expose-Headers': 'Mcp-Session-Id',
|
|
220
232
|
'Mcp-Session-Id': this.sessionId,
|
|
221
|
-
}
|
|
233
|
+
};
|
|
234
|
+
if (corsOrigin)
|
|
235
|
+
resHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
236
|
+
res.writeHead(200, resHeaders);
|
|
222
237
|
res.end(JSON.stringify(response));
|
|
223
238
|
return;
|
|
224
239
|
}
|
|
225
240
|
// DELETE — session termination (spec compliance)
|
|
226
241
|
if (req.method === 'DELETE') {
|
|
227
|
-
|
|
242
|
+
const delHeaders = {};
|
|
243
|
+
if (corsOrigin)
|
|
244
|
+
delHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
245
|
+
res.writeHead(200, delHeaders);
|
|
228
246
|
res.end();
|
|
229
247
|
return;
|
|
230
248
|
}
|
|
231
|
-
|
|
249
|
+
const methodHeaders = {};
|
|
250
|
+
if (corsOrigin)
|
|
251
|
+
methodHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
252
|
+
res.writeHead(405, methodHeaders);
|
|
232
253
|
res.end('Method not allowed');
|
|
233
254
|
}
|
|
234
255
|
}
|
|
@@ -236,6 +257,7 @@ export class PhotonServer {
|
|
|
236
257
|
loader;
|
|
237
258
|
mcp = null;
|
|
238
259
|
server;
|
|
260
|
+
taskExecutor;
|
|
239
261
|
options;
|
|
240
262
|
mcpClientFactory = null;
|
|
241
263
|
httpServer = null;
|
|
@@ -244,7 +266,7 @@ export class PhotonServer {
|
|
|
244
266
|
hotReloadDisabled = false;
|
|
245
267
|
lastReloadError;
|
|
246
268
|
statusClients = new Set();
|
|
247
|
-
|
|
269
|
+
channelManager;
|
|
248
270
|
daemonName = null;
|
|
249
271
|
/** Tracked instance name for daemon drift recovery (STDIO path) */
|
|
250
272
|
daemonInstanceName;
|
|
@@ -252,19 +274,12 @@ export class PhotonServer {
|
|
|
252
274
|
sseInstanceNames = new Map();
|
|
253
275
|
/** Whether client capabilities have been logged (one-time on first tools/list) */
|
|
254
276
|
clientCapabilitiesLogged = false;
|
|
255
|
-
/**
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
* it. Real clients like Claude Desktop and ChatGPT send UI capability under
|
|
262
|
-
* `extensions`, not `experimental`. We intercept the raw JSON-RPC message to
|
|
263
|
-
* capture the full capabilities before Zod strips them.
|
|
264
|
-
*
|
|
265
|
-
* Key: Server instance → Value: raw capabilities object from initialize request
|
|
266
|
-
*/
|
|
267
|
-
rawClientCapabilities = new WeakMap();
|
|
277
|
+
/** Client capability detection and negotiation */
|
|
278
|
+
capabilityNegotiator = new CapabilityNegotiator();
|
|
279
|
+
/** Compatibility alias for tests that seed raw capabilities directly on PhotonServer */
|
|
280
|
+
rawClientCapabilities = this.capabilityNegotiator.rawClientCapabilities;
|
|
281
|
+
/** Resource listing, reading, and asset serving */
|
|
282
|
+
resourceServer;
|
|
268
283
|
currentStatus = {
|
|
269
284
|
type: 'info',
|
|
270
285
|
message: 'Ready',
|
|
@@ -309,10 +324,36 @@ export class PhotonServer {
|
|
|
309
324
|
baseLoggerOptions.scope = this.devMode ? 'dev' : 'runtime';
|
|
310
325
|
}
|
|
311
326
|
this.logger = createLogger(baseLoggerOptions);
|
|
312
|
-
|
|
327
|
+
const loaderVerbose = (baseLoggerOptions.level ?? 'info') !== 'warn' &&
|
|
328
|
+
(baseLoggerOptions.level ?? 'info') !== 'error';
|
|
329
|
+
this.loader = new PhotonLoader(loaderVerbose, this.logger.child({ component: 'photon-loader', scope: 'loader' }), options.workingDir);
|
|
330
|
+
// Initialize ChannelManager — owns all channel/pub-sub logic
|
|
331
|
+
// The sink is wired up lazily because `this.server` doesn't exist yet
|
|
332
|
+
const channelSink = {
|
|
333
|
+
sendNotification: async (notification) => {
|
|
334
|
+
await this.server.notification(notification);
|
|
335
|
+
},
|
|
336
|
+
sendNotificationToAllSessions: async (notification) => {
|
|
337
|
+
for (const session of Array.from(this.sseSessions.values())) {
|
|
338
|
+
await session.server.notification(notification);
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
getPhotonInstance: () => this.mcp,
|
|
342
|
+
};
|
|
343
|
+
this.channelManager = new ChannelManager({
|
|
344
|
+
channelOptions: {
|
|
345
|
+
channelMode: options.channelMode,
|
|
346
|
+
channelName: options.channelName,
|
|
347
|
+
channelTargets: options.channelTargets,
|
|
348
|
+
channelInstructions: options.channelInstructions,
|
|
349
|
+
},
|
|
350
|
+
workingDir: options.workingDir,
|
|
351
|
+
sink: channelSink,
|
|
352
|
+
log: (level, message, data) => this.log(level, message, data),
|
|
353
|
+
});
|
|
313
354
|
// Create MCP server instance
|
|
314
355
|
this.server = new Server({
|
|
315
|
-
name:
|
|
356
|
+
name: this.channelManager.getServerName(),
|
|
316
357
|
version: PHOTON_VERSION,
|
|
317
358
|
}, {
|
|
318
359
|
capabilities: {
|
|
@@ -333,9 +374,24 @@ export class PhotonServer {
|
|
|
333
374
|
tools: { call: {} },
|
|
334
375
|
},
|
|
335
376
|
},
|
|
336
|
-
//
|
|
337
|
-
|
|
377
|
+
// Channel capabilities (experimental) — delegated to ChannelManager
|
|
378
|
+
...this.channelManager.getExtraCapabilities(),
|
|
338
379
|
},
|
|
380
|
+
// Channel instructions
|
|
381
|
+
...this.channelManager.getExtraServerOptions(),
|
|
382
|
+
});
|
|
383
|
+
// Task executor — handles MCP Tasks protocol (spec v2025-11-25)
|
|
384
|
+
this.taskExecutor = new TaskExecutor((level, message, meta) => this.log(level, message, meta), {
|
|
385
|
+
executeTool: (photon, toolName, args, opts) => this.loader.executeTool(photon, toolName, args, opts),
|
|
386
|
+
}, { createMCPInputProvider: (server) => this.createMCPInputProvider(server) });
|
|
387
|
+
// Resource server — handles ListResources, ReadResource, asset serving
|
|
388
|
+
this.resourceServer = new ResourceServer({
|
|
389
|
+
executeTool: (photon, toolName, args) => this.loader.executeTool(photon, toolName, args),
|
|
390
|
+
getLoadedPhotons: () => this.loader.getLoadedPhotons(),
|
|
391
|
+
}, {
|
|
392
|
+
filePath: options.filePath,
|
|
393
|
+
embeddedAssets: options.embeddedAssets,
|
|
394
|
+
embeddedUITemplates: options.embeddedUITemplates,
|
|
339
395
|
});
|
|
340
396
|
// Set up protocol handlers
|
|
341
397
|
this.setupHandlers();
|
|
@@ -350,121 +406,11 @@ export class PhotonServer {
|
|
|
350
406
|
this.logger.log(level, message, meta);
|
|
351
407
|
}
|
|
352
408
|
/**
|
|
353
|
-
*
|
|
409
|
+
* Send a permission response back to the client.
|
|
410
|
+
* Delegates to ChannelManager.
|
|
354
411
|
*/
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return `ui://${photonName}/${uiId}`;
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Build tool metadata for UI based on detected format
|
|
361
|
-
*/
|
|
362
|
-
buildUIToolMeta(uiId) {
|
|
363
|
-
const uri = this.buildUIResourceUri(uiId);
|
|
364
|
-
return { ui: { resourceUri: uri } };
|
|
365
|
-
}
|
|
366
|
-
static ICON_MIME_TYPES = {
|
|
367
|
-
'.png': 'image/png',
|
|
368
|
-
'.jpg': 'image/jpeg',
|
|
369
|
-
'.jpeg': 'image/jpeg',
|
|
370
|
-
'.gif': 'image/gif',
|
|
371
|
-
'.svg': 'image/svg+xml',
|
|
372
|
-
'.webp': 'image/webp',
|
|
373
|
-
'.ico': 'image/x-icon',
|
|
374
|
-
};
|
|
375
|
-
/** Cached resolved icons per tool name */
|
|
376
|
-
resolvedIconsCache = new Map();
|
|
377
|
-
/**
|
|
378
|
-
* Resolve raw icon image paths to MCP Icon[] format (data URIs).
|
|
379
|
-
* Results are cached so file I/O only happens once per tool.
|
|
380
|
-
*/
|
|
381
|
-
resolveIconImages(iconImages) {
|
|
382
|
-
const cacheKey = iconImages.map((i) => i.path).join('|');
|
|
383
|
-
const cached = this.resolvedIconsCache.get(cacheKey);
|
|
384
|
-
if (cached)
|
|
385
|
-
return cached;
|
|
386
|
-
const photonDir = path.dirname(this.options.filePath);
|
|
387
|
-
const icons = [];
|
|
388
|
-
for (const entry of iconImages) {
|
|
389
|
-
try {
|
|
390
|
-
const resolvedPath = path.resolve(photonDir, entry.path);
|
|
391
|
-
const ext = path.extname(resolvedPath).toLowerCase();
|
|
392
|
-
const mimeType = PhotonServer.ICON_MIME_TYPES[ext];
|
|
393
|
-
if (!mimeType)
|
|
394
|
-
continue;
|
|
395
|
-
const data = readFileSync(resolvedPath);
|
|
396
|
-
const dataUri = `data:${mimeType};base64,${data.toString('base64')}`;
|
|
397
|
-
const icon = {
|
|
398
|
-
src: dataUri,
|
|
399
|
-
mimeType,
|
|
400
|
-
};
|
|
401
|
-
if (entry.sizes)
|
|
402
|
-
icon.sizes = entry.sizes;
|
|
403
|
-
if (entry.theme)
|
|
404
|
-
icon.theme = entry.theme;
|
|
405
|
-
icons.push(icon);
|
|
406
|
-
}
|
|
407
|
-
catch {
|
|
408
|
-
// Skip unreadable icon files silently
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
this.resolvedIconsCache.set(cacheKey, icons);
|
|
412
|
-
return icons;
|
|
413
|
-
}
|
|
414
|
-
/**
|
|
415
|
-
* Get UI mimeType based on detected format and client capabilities
|
|
416
|
-
*/
|
|
417
|
-
getUIMimeType() {
|
|
418
|
-
return 'text/html;profile=mcp-app';
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Check if client supports elicitation
|
|
422
|
-
*
|
|
423
|
-
* Elicitation is a client capability declared during initialization.
|
|
424
|
-
* The server can use elicitInput() when the client supports it.
|
|
425
|
-
*/
|
|
426
|
-
clientSupportsElicitation(server) {
|
|
427
|
-
const targetServer = server || this.server;
|
|
428
|
-
const capabilities = targetServer.getClientCapabilities();
|
|
429
|
-
if (!capabilities) {
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
// Check for elicitation capability (MCP 2025-06 spec)
|
|
433
|
-
return !!capabilities.elicitation;
|
|
434
|
-
}
|
|
435
|
-
static MCP_UI_CAPABILITY = 'io.modelcontextprotocol/ui';
|
|
436
|
-
/**
|
|
437
|
-
* Check if client supports MCP Apps UI (structuredContent + _meta.ui)
|
|
438
|
-
*
|
|
439
|
-
* Looks for the "io.modelcontextprotocol/ui" capability in the client's
|
|
440
|
-
* initialize handshake. Any MCP client that advertises this capability
|
|
441
|
-
* gets rich UI responses — Claude Desktop, ChatGPT, MCPJam, etc.
|
|
442
|
-
*
|
|
443
|
-
* The capability may appear under `experimental` (older SDK types) or
|
|
444
|
-
* `extensions` (protocol version 2025-11-25+). We check both so it
|
|
445
|
-
* just works regardless of which field the client uses.
|
|
446
|
-
*
|
|
447
|
-
* Beam is special-cased because it's our own SSE transport where the
|
|
448
|
-
* capability is implicit.
|
|
449
|
-
*/
|
|
450
|
-
clientSupportsUI(server) {
|
|
451
|
-
const targetServer = server || this.server;
|
|
452
|
-
// Check SDK-parsed capabilities (works for `experimental` which is in the Zod schema)
|
|
453
|
-
const capabilities = targetServer.getClientCapabilities();
|
|
454
|
-
if (capabilities?.experimental?.[PhotonServer.MCP_UI_CAPABILITY]) {
|
|
455
|
-
return true;
|
|
456
|
-
}
|
|
457
|
-
// Check raw capabilities captured before Zod parsing (needed for `extensions`
|
|
458
|
-
// which the SDK's Zod schema strips — Claude Desktop and ChatGPT use this field)
|
|
459
|
-
const raw = this.rawClientCapabilities.get(targetServer);
|
|
460
|
-
if (raw?.extensions?.[PhotonServer.MCP_UI_CAPABILITY]) {
|
|
461
|
-
return true;
|
|
462
|
-
}
|
|
463
|
-
// Beam is our own transport — UI support is implicit
|
|
464
|
-
const clientInfo = targetServer.getClientVersion();
|
|
465
|
-
if (clientInfo?.name === 'beam')
|
|
466
|
-
return true;
|
|
467
|
-
return false;
|
|
412
|
+
respondToPermission(response) {
|
|
413
|
+
this.channelManager.respondToPermission(response);
|
|
468
414
|
}
|
|
469
415
|
/**
|
|
470
416
|
* Log client identity and capabilities for debugging tier detection
|
|
@@ -472,8 +418,8 @@ export class PhotonServer {
|
|
|
472
418
|
logClientCapabilities(server) {
|
|
473
419
|
const clientInfo = server.getClientVersion();
|
|
474
420
|
const capabilities = server.getClientCapabilities();
|
|
475
|
-
const supportsUI = this.
|
|
476
|
-
const supportsElicitation = this.
|
|
421
|
+
const supportsUI = this.capabilityNegotiator.supportsUI(server);
|
|
422
|
+
const supportsElicitation = this.capabilityNegotiator.supportsElicitation(server);
|
|
477
423
|
this.log('debug', 'Client connected', {
|
|
478
424
|
name: clientInfo?.name ?? 'unknown',
|
|
479
425
|
version: clientInfo?.version ?? 'unknown',
|
|
@@ -492,7 +438,7 @@ export class PhotonServer {
|
|
|
492
438
|
createMCPInputProvider(server) {
|
|
493
439
|
const targetServer = server || this.server;
|
|
494
440
|
const capabilities = targetServer.getClientCapabilities();
|
|
495
|
-
const supportsElicitation = this.
|
|
441
|
+
const supportsElicitation = this.capabilityNegotiator.supportsElicitation(targetServer);
|
|
496
442
|
this.log('debug', 'Creating MCP input provider', {
|
|
497
443
|
supportsElicitation,
|
|
498
444
|
capabilities: JSON.stringify(capabilities),
|
|
@@ -789,13 +735,13 @@ export class PhotonServer {
|
|
|
789
735
|
toolDef.outputSchema = schema.outputSchema;
|
|
790
736
|
// MCP tool icons (resolve image paths to data URIs)
|
|
791
737
|
if (tool.iconImages && tool.iconImages.length > 0) {
|
|
792
|
-
const icons = this.resolveIconImages(tool.iconImages);
|
|
738
|
+
const icons = this.resourceServer.resolveIconImages(tool.iconImages);
|
|
793
739
|
if (icons.length > 0)
|
|
794
740
|
toolDef.icons = icons;
|
|
795
741
|
}
|
|
796
742
|
const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name || u.linkedTools?.includes(tool.name));
|
|
797
|
-
if (linkedUI && this.
|
|
798
|
-
toolDef._meta = this.buildUIToolMeta(linkedUI.id);
|
|
743
|
+
if (linkedUI && this.capabilityNegotiator.supportsUI(ctx.server)) {
|
|
744
|
+
toolDef._meta = this.resourceServer.buildUIToolMeta(this.mcp.name, linkedUI.id);
|
|
799
745
|
}
|
|
800
746
|
return toolDef;
|
|
801
747
|
});
|
|
@@ -863,7 +809,7 @@ export class PhotonServer {
|
|
|
863
809
|
// Elicitation-based instance selection when _use called without name
|
|
864
810
|
if (toolName === '_use' &&
|
|
865
811
|
(!args || !('name' in args)) &&
|
|
866
|
-
this.
|
|
812
|
+
this.capabilityNegotiator.supportsElicitation(ctx.server)) {
|
|
867
813
|
const instancesResult = (await sendCommand(this.daemonName, '_instances', {}, sendOpts));
|
|
868
814
|
const instances = instancesResult?.instances || ['default'];
|
|
869
815
|
const options = instances.map((inst) => ({
|
|
@@ -928,11 +874,7 @@ export class PhotonServer {
|
|
|
928
874
|
// Handler for channel events - forward to daemon for cross-process pub/sub
|
|
929
875
|
const outputHandler = (emit) => {
|
|
930
876
|
// Forward channel events to daemon for cross-process pub/sub
|
|
931
|
-
|
|
932
|
-
publishToChannel(this.daemonName, emit.channel, emit, this.options.workingDir).catch(() => {
|
|
933
|
-
// Ignore publish errors - daemon may not be running
|
|
934
|
-
});
|
|
935
|
-
}
|
|
877
|
+
this.channelManager.publishIfChannel(emit);
|
|
936
878
|
// Forward emit yields as MCP progress notifications to STDIO client
|
|
937
879
|
if (emit?.emit === 'progress') {
|
|
938
880
|
const rawValue = typeof emit.value === 'number' ? emit.value : 0;
|
|
@@ -1074,12 +1016,12 @@ export class PhotonServer {
|
|
|
1074
1016
|
}
|
|
1075
1017
|
// Enrich response with structuredContent + _meta for tools with linked UIs
|
|
1076
1018
|
const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === toolName || u.linkedTools?.includes(toolName));
|
|
1077
|
-
if (linkedUI && this.
|
|
1019
|
+
if (linkedUI && this.capabilityNegotiator.supportsUI(ctx.server)) {
|
|
1078
1020
|
if (actualResult !== undefined && actualResult !== null) {
|
|
1079
1021
|
response.structuredContent =
|
|
1080
1022
|
typeof actualResult === 'string' ? { text: actualResult } : actualResult;
|
|
1081
1023
|
}
|
|
1082
|
-
const uiMeta = this.buildUIToolMeta(linkedUI.id);
|
|
1024
|
+
const uiMeta = this.resourceServer.buildUIToolMeta(this.mcp.name, linkedUI.id);
|
|
1083
1025
|
response._meta = { ...response._meta, ...uiMeta };
|
|
1084
1026
|
}
|
|
1085
1027
|
return response;
|
|
@@ -1114,122 +1056,6 @@ export class PhotonServer {
|
|
|
1114
1056
|
const result = await this.loader.executeTool(this.mcp, promptName, args || {});
|
|
1115
1057
|
return this.formatTemplateResult(result);
|
|
1116
1058
|
}
|
|
1117
|
-
handleListResources(ctx) {
|
|
1118
|
-
if (!this.mcp) {
|
|
1119
|
-
return { resources: [] };
|
|
1120
|
-
}
|
|
1121
|
-
const staticResources = this.mcp.statics.filter((s) => !this.isUriTemplate(s.uri));
|
|
1122
|
-
const resources = staticResources.map((static_) => ({
|
|
1123
|
-
uri: static_.uri,
|
|
1124
|
-
name: static_.name,
|
|
1125
|
-
description: static_.description,
|
|
1126
|
-
mimeType: static_.mimeType || 'text/plain',
|
|
1127
|
-
}));
|
|
1128
|
-
if (this.mcp.assets) {
|
|
1129
|
-
const photonName = this.mcp.name;
|
|
1130
|
-
for (const ui of this.mcp.assets.ui) {
|
|
1131
|
-
const uiUri = ui.uri || this.buildUIResourceUri(ui.id);
|
|
1132
|
-
resources.push({
|
|
1133
|
-
uri: uiUri,
|
|
1134
|
-
name: `ui:${ui.id}`,
|
|
1135
|
-
description: ui.linkedTool
|
|
1136
|
-
? `UI template for ${ui.linkedTool} tool`
|
|
1137
|
-
: `UI template: ${ui.id}`,
|
|
1138
|
-
// Always use MCP Apps mime type for UI resources — photon-core's
|
|
1139
|
-
// getMimeTypeFromPath returns plain text/html which causes Claude
|
|
1140
|
-
// Desktop to skip rendering the app UI
|
|
1141
|
-
mimeType: this.getUIMimeType(),
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1144
|
-
for (const prompt of this.mcp.assets.prompts) {
|
|
1145
|
-
resources.push({
|
|
1146
|
-
uri: `photon://${photonName}/prompts/${prompt.id}`,
|
|
1147
|
-
name: `prompt:${prompt.id}`,
|
|
1148
|
-
description: prompt.description || `Prompt template: ${prompt.id}`,
|
|
1149
|
-
mimeType: 'text/markdown',
|
|
1150
|
-
});
|
|
1151
|
-
}
|
|
1152
|
-
for (const resource of this.mcp.assets.resources) {
|
|
1153
|
-
resources.push({
|
|
1154
|
-
uri: `photon://${photonName}/resources/${resource.id}`,
|
|
1155
|
-
name: `resource:${resource.id}`,
|
|
1156
|
-
description: resource.description || `Static resource: ${resource.id}`,
|
|
1157
|
-
mimeType: resource.mimeType || 'application/octet-stream',
|
|
1158
|
-
});
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
// Include sub-photon UI resources (compiled binary mode)
|
|
1162
|
-
if (this.options.embeddedAssets) {
|
|
1163
|
-
const allLoaded = this.loader.getLoadedPhotons();
|
|
1164
|
-
for (const [, loaded] of allLoaded) {
|
|
1165
|
-
if (loaded.name === this.mcp.name)
|
|
1166
|
-
continue;
|
|
1167
|
-
if (loaded.assets?.ui) {
|
|
1168
|
-
for (const ui of loaded.assets.ui) {
|
|
1169
|
-
const uiUri = ui.uri || `ui://${loaded.name}/${ui.id}`;
|
|
1170
|
-
resources.push({
|
|
1171
|
-
uri: uiUri,
|
|
1172
|
-
name: `ui:${ui.id}`,
|
|
1173
|
-
description: ui.linkedTool
|
|
1174
|
-
? `UI template for ${loaded.name}/${ui.linkedTool}`
|
|
1175
|
-
: `UI template: ${loaded.name}/${ui.id}`,
|
|
1176
|
-
mimeType: ui.mimeType || this.getUIMimeType(),
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
return { resources };
|
|
1183
|
-
}
|
|
1184
|
-
handleListResourceTemplates() {
|
|
1185
|
-
if (!this.mcp) {
|
|
1186
|
-
return { resourceTemplates: [] };
|
|
1187
|
-
}
|
|
1188
|
-
const templateResources = this.mcp.statics.filter((s) => this.isUriTemplate(s.uri));
|
|
1189
|
-
return {
|
|
1190
|
-
resourceTemplates: templateResources.map((static_) => ({
|
|
1191
|
-
uriTemplate: static_.uri,
|
|
1192
|
-
name: static_.name,
|
|
1193
|
-
description: static_.description,
|
|
1194
|
-
mimeType: static_.mimeType || 'text/plain',
|
|
1195
|
-
})),
|
|
1196
|
-
};
|
|
1197
|
-
}
|
|
1198
|
-
async handleReadResource(request) {
|
|
1199
|
-
if (!this.mcp) {
|
|
1200
|
-
throw new Error('MCP not loaded');
|
|
1201
|
-
}
|
|
1202
|
-
const { uri: rawUri } = request.params;
|
|
1203
|
-
const uri = typeof rawUri === 'string'
|
|
1204
|
-
? rawUri.replace(/^ui:\/\/\/([^/]+)\/(.+)$/, 'ui://$1/$2')
|
|
1205
|
-
: rawUri;
|
|
1206
|
-
const uiMatch = uri.match(/^ui:\/\/([^/]+)\/(.+)$/);
|
|
1207
|
-
if (uiMatch) {
|
|
1208
|
-
const [, uiPhotonName, assetId] = uiMatch;
|
|
1209
|
-
// Check main photon first
|
|
1210
|
-
if (this.mcp.assets && uiPhotonName === this.mcp.name) {
|
|
1211
|
-
return this.handleUIAssetRead(uri, assetId);
|
|
1212
|
-
}
|
|
1213
|
-
// Check sub-photons (compiled binary mode)
|
|
1214
|
-
if (this.options.embeddedAssets) {
|
|
1215
|
-
const allLoaded = this.loader.getLoadedPhotons();
|
|
1216
|
-
for (const [, loaded] of allLoaded) {
|
|
1217
|
-
if (loaded.name === uiPhotonName && loaded.assets?.ui) {
|
|
1218
|
-
return this.handleUIAssetRead(uri, assetId, loaded);
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
// Fallback to main photon
|
|
1223
|
-
if (this.mcp.assets) {
|
|
1224
|
-
return this.handleUIAssetRead(uri, assetId);
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
const assetMatch = uri.match(/^photon:\/\/([^/]+)\/(ui|prompts|resources)\/(.+)$/);
|
|
1228
|
-
if (assetMatch && this.mcp.assets) {
|
|
1229
|
-
return this.handleAssetRead(uri, assetMatch);
|
|
1230
|
-
}
|
|
1231
|
-
return this.handleStaticRead(uri);
|
|
1232
|
-
}
|
|
1233
1059
|
// ─── Transport-specific setup ─────────────────────────────────────
|
|
1234
1060
|
/**
|
|
1235
1061
|
* Set up MCP protocol handlers (STDIO transport)
|
|
@@ -1267,9 +1093,7 @@ export class PhotonServer {
|
|
|
1267
1093
|
const executionId = generateExecutionId();
|
|
1268
1094
|
const inputProvider = this.createMCPInputProvider();
|
|
1269
1095
|
const outputHandler = (emit) => {
|
|
1270
|
-
|
|
1271
|
-
publishToChannel(this.daemonName, emit.channel, emit, this.options.workingDir).catch(() => { });
|
|
1272
|
-
}
|
|
1096
|
+
this.channelManager.publishIfChannel(emit);
|
|
1273
1097
|
// Forward emit yields as MCP notifications for async tools
|
|
1274
1098
|
if (emit?.emit === 'progress' || emit?.emit === 'status') {
|
|
1275
1099
|
const rawValue = emit?.emit === 'progress' && typeof emit.value === 'number' ? emit.value : 0;
|
|
@@ -1354,22 +1178,7 @@ export class PhotonServer {
|
|
|
1354
1178
|
const taskField = request.params?.task;
|
|
1355
1179
|
if (taskField && this.mcp) {
|
|
1356
1180
|
const { name: toolName, arguments: args } = request.params;
|
|
1357
|
-
|
|
1358
|
-
const task = createTask(this.mcp.name, toolName, args, ttl);
|
|
1359
|
-
const controller = new AbortController();
|
|
1360
|
-
registerController(task.id, controller);
|
|
1361
|
-
const executeFn = async (taskInputProvider, outputHandler) => {
|
|
1362
|
-
return this.loader.executeTool(this.mcp, toolName, args || {}, {
|
|
1363
|
-
outputHandler,
|
|
1364
|
-
inputProvider: taskInputProvider,
|
|
1365
|
-
});
|
|
1366
|
-
};
|
|
1367
|
-
runTaskExecution(task.id, executeFn, {
|
|
1368
|
-
signal: controller.signal,
|
|
1369
|
-
});
|
|
1370
|
-
return {
|
|
1371
|
-
content: [{ type: 'text', text: JSON.stringify({ task: toWireFormat(task) }, null, 2) }],
|
|
1372
|
-
};
|
|
1181
|
+
return this.taskExecutor.handleTaskModeCall(this.mcp.name, toolName, args || {}, taskField);
|
|
1373
1182
|
}
|
|
1374
1183
|
try {
|
|
1375
1184
|
return await this.handleCallTool(ctx, request);
|
|
@@ -1377,7 +1186,8 @@ export class PhotonServer {
|
|
|
1377
1186
|
catch (error) {
|
|
1378
1187
|
// STDIO-only: config elicitation retry
|
|
1379
1188
|
const { name: toolName, arguments: args } = request.params;
|
|
1380
|
-
if (this.mcp?.instance?._photonConfigError &&
|
|
1189
|
+
if (this.mcp?.instance?._photonConfigError &&
|
|
1190
|
+
this.capabilityNegotiator.supportsElicitation(this.server)) {
|
|
1381
1191
|
const retryResult = await this.attemptConfigElicitation(toolName, args || {});
|
|
1382
1192
|
if (retryResult)
|
|
1383
1193
|
return retryResult;
|
|
@@ -1409,136 +1219,26 @@ export class PhotonServer {
|
|
|
1409
1219
|
}
|
|
1410
1220
|
});
|
|
1411
1221
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
1412
|
-
return this.handleListResources(
|
|
1222
|
+
return this.resourceServer.handleListResources(this.mcp);
|
|
1413
1223
|
});
|
|
1414
1224
|
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
1415
|
-
return this.handleListResourceTemplates();
|
|
1225
|
+
return this.resourceServer.handleListResourceTemplates(this.mcp);
|
|
1416
1226
|
});
|
|
1417
1227
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
1418
|
-
return this.handleReadResource(request);
|
|
1228
|
+
return this.resourceServer.handleReadResource(request, this.mcp);
|
|
1419
1229
|
});
|
|
1420
|
-
// ── MCP Tasks handlers (2025-11-25 spec) ──
|
|
1230
|
+
// ── MCP Tasks handlers (2025-11-25 spec) — delegated to TaskExecutor ──
|
|
1421
1231
|
this.server.setRequestHandler(GetTaskRequestSchema, async (request) => {
|
|
1422
|
-
|
|
1423
|
-
const task = getTask(taskId);
|
|
1424
|
-
if (!task)
|
|
1425
|
-
throw new Error(`Task not found: ${taskId}`);
|
|
1426
|
-
return toWireFormat(task);
|
|
1232
|
+
return this.taskExecutor.handleGetTask(request.params.taskId);
|
|
1427
1233
|
});
|
|
1428
1234
|
this.server.setRequestHandler(ListTasksRequestSchema, async (request) => {
|
|
1429
|
-
|
|
1430
|
-
const cursor = request.params?.cursor;
|
|
1431
|
-
const offset = cursor ? parseInt(cursor, 10) || 0 : 0;
|
|
1432
|
-
const pageSize = 50;
|
|
1433
|
-
const page = allTasks.slice(offset, offset + pageSize);
|
|
1434
|
-
const nextCursor = offset + pageSize < allTasks.length ? String(offset + pageSize) : undefined;
|
|
1435
|
-
return {
|
|
1436
|
-
tasks: page.map(toWireFormat),
|
|
1437
|
-
...(nextCursor && { nextCursor }),
|
|
1438
|
-
};
|
|
1235
|
+
return this.taskExecutor.handleListTasks(request.params?.cursor);
|
|
1439
1236
|
});
|
|
1440
1237
|
this.server.setRequestHandler(CancelTaskRequestSchema, async (request) => {
|
|
1441
|
-
|
|
1442
|
-
const task = getTask(taskId);
|
|
1443
|
-
if (!task)
|
|
1444
|
-
throw new Error(`Task not found: ${taskId}`);
|
|
1445
|
-
if (TERMINAL_STATES.includes(task.state)) {
|
|
1446
|
-
throw new Error(`Cannot cancel task in terminal state: ${task.state}`);
|
|
1447
|
-
}
|
|
1448
|
-
const controller = getController(taskId);
|
|
1449
|
-
if (controller)
|
|
1450
|
-
controller.abort();
|
|
1451
|
-
const updated = updateTask(taskId, {
|
|
1452
|
-
state: 'cancelled',
|
|
1453
|
-
statusMessage: 'The task was cancelled by request.',
|
|
1454
|
-
});
|
|
1455
|
-
unregisterController(taskId);
|
|
1456
|
-
return toWireFormat(updated);
|
|
1238
|
+
return this.taskExecutor.handleCancelTask(request.params.taskId);
|
|
1457
1239
|
});
|
|
1458
1240
|
this.server.setRequestHandler(GetTaskPayloadRequestSchema, async (request) => {
|
|
1459
|
-
|
|
1460
|
-
const task = getTask(taskId);
|
|
1461
|
-
if (!task)
|
|
1462
|
-
throw new Error(`Task not found: ${taskId}`);
|
|
1463
|
-
// Helper to format terminal task result
|
|
1464
|
-
const formatResult = (t) => {
|
|
1465
|
-
if (t.state === 'failed') {
|
|
1466
|
-
return {
|
|
1467
|
-
content: [{ type: 'text', text: t.error || 'Task failed' }],
|
|
1468
|
-
isError: true,
|
|
1469
|
-
_meta: relatedTaskMeta(taskId),
|
|
1470
|
-
};
|
|
1471
|
-
}
|
|
1472
|
-
if (t.state === 'cancelled') {
|
|
1473
|
-
return {
|
|
1474
|
-
content: [{ type: 'text', text: 'Task was cancelled.' }],
|
|
1475
|
-
isError: false,
|
|
1476
|
-
_meta: relatedTaskMeta(taskId),
|
|
1477
|
-
};
|
|
1478
|
-
}
|
|
1479
|
-
// Completed
|
|
1480
|
-
if (t.result && typeof t.result === 'object' && 'content' in t.result) {
|
|
1481
|
-
return { ...t.result, _meta: relatedTaskMeta(taskId) };
|
|
1482
|
-
}
|
|
1483
|
-
const text = typeof t.result === 'string' ? t.result : JSON.stringify(t.result ?? null);
|
|
1484
|
-
return {
|
|
1485
|
-
content: [{ type: 'text', text }],
|
|
1486
|
-
isError: false,
|
|
1487
|
-
_meta: relatedTaskMeta(taskId),
|
|
1488
|
-
};
|
|
1489
|
-
};
|
|
1490
|
-
// Already terminal — return immediately
|
|
1491
|
-
if (TERMINAL_STATES.includes(task.state)) {
|
|
1492
|
-
return formatResult(task);
|
|
1493
|
-
}
|
|
1494
|
-
// If input_required, try to get input via elicitation
|
|
1495
|
-
if (task.state === 'input_required' && task.input) {
|
|
1496
|
-
const inputProvider = this.createMCPInputProvider(this.server);
|
|
1497
|
-
try {
|
|
1498
|
-
const value = await inputProvider(task.input);
|
|
1499
|
-
resolveTaskInput(taskId, value);
|
|
1500
|
-
}
|
|
1501
|
-
catch {
|
|
1502
|
-
resolveTaskInput(taskId, null);
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
// Block until terminal (max 5 min per call)
|
|
1506
|
-
try {
|
|
1507
|
-
const abortController = new AbortController();
|
|
1508
|
-
const timeout = setTimeout(() => abortController.abort(), 300000);
|
|
1509
|
-
try {
|
|
1510
|
-
while (true) {
|
|
1511
|
-
const current = await waitForTerminalOrInput(taskId, abortController.signal);
|
|
1512
|
-
if (TERMINAL_STATES.includes(current.state)) {
|
|
1513
|
-
return formatResult(current);
|
|
1514
|
-
}
|
|
1515
|
-
if (current.state === 'input_required' && current.input) {
|
|
1516
|
-
const inputProvider = this.createMCPInputProvider(this.server);
|
|
1517
|
-
try {
|
|
1518
|
-
const value = await inputProvider(current.input);
|
|
1519
|
-
resolveTaskInput(taskId, value);
|
|
1520
|
-
}
|
|
1521
|
-
catch {
|
|
1522
|
-
resolveTaskInput(taskId, null);
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
finally {
|
|
1528
|
-
clearTimeout(timeout);
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
catch {
|
|
1532
|
-
const current = getTask(taskId);
|
|
1533
|
-
if (current && TERMINAL_STATES.includes(current.state)) {
|
|
1534
|
-
return formatResult(current);
|
|
1535
|
-
}
|
|
1536
|
-
return {
|
|
1537
|
-
content: [{ type: 'text', text: `Task ${taskId} is still running.` }],
|
|
1538
|
-
isError: false,
|
|
1539
|
-
_meta: relatedTaskMeta(taskId),
|
|
1540
|
-
};
|
|
1541
|
-
}
|
|
1241
|
+
return this.taskExecutor.handleGetTaskPayload(request.params.taskId, this.server);
|
|
1542
1242
|
});
|
|
1543
1243
|
}
|
|
1544
1244
|
/**
|
|
@@ -1589,63 +1289,27 @@ export class PhotonServer {
|
|
|
1589
1289
|
};
|
|
1590
1290
|
}
|
|
1591
1291
|
/**
|
|
1592
|
-
*
|
|
1593
|
-
|
|
1594
|
-
formatStaticResult(result, mimeType) {
|
|
1595
|
-
const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
1596
|
-
return {
|
|
1597
|
-
contents: [
|
|
1598
|
-
{
|
|
1599
|
-
uri: '',
|
|
1600
|
-
mimeType: mimeType || 'text/plain',
|
|
1601
|
-
text,
|
|
1602
|
-
},
|
|
1603
|
-
],
|
|
1604
|
-
};
|
|
1605
|
-
}
|
|
1606
|
-
/**
|
|
1607
|
-
* Check if a URI is a template (contains {parameters})
|
|
1292
|
+
* Compatibility wrappers for tests and older internal call sites after
|
|
1293
|
+
* resource helpers moved into ResourceServer.
|
|
1608
1294
|
*/
|
|
1609
1295
|
isUriTemplate(uri) {
|
|
1610
|
-
return
|
|
1296
|
+
return this.resourceServer.isUriTemplate(uri);
|
|
1611
1297
|
}
|
|
1612
|
-
/**
|
|
1613
|
-
* Match URI pattern with actual URI
|
|
1614
|
-
* Example: github://repos/{owner}/{repo} matches github://repos/foo/bar
|
|
1615
|
-
*/
|
|
1616
1298
|
matchUriPattern(pattern, uri) {
|
|
1617
|
-
|
|
1618
|
-
// Replace {param} with capturing groups
|
|
1619
|
-
const regexPattern = pattern.replace(/\{[^}]+\}/g, '([^/]+)');
|
|
1620
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
1621
|
-
return regex.test(uri);
|
|
1299
|
+
return this.resourceServer.matchUriPattern(pattern, uri);
|
|
1622
1300
|
}
|
|
1623
|
-
/**
|
|
1624
|
-
* Parse parameters from URI based on pattern
|
|
1625
|
-
* Example: pattern="github://repos/{owner}/{repo}", uri="github://repos/foo/bar"
|
|
1626
|
-
* Returns: { owner: "foo", repo: "bar" }
|
|
1627
|
-
*/
|
|
1628
1301
|
parseUriParams(pattern, uri) {
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
const
|
|
1639
|
-
|
|
1640
|
-
// Extract values from URI
|
|
1641
|
-
const values = uri.match(regex);
|
|
1642
|
-
if (values) {
|
|
1643
|
-
// Skip first element (full match)
|
|
1644
|
-
for (let i = 0; i < paramNames.length; i++) {
|
|
1645
|
-
params[paramNames[i]] = values[i + 1];
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
return params;
|
|
1302
|
+
return this.resourceServer.parseUriParams(pattern, uri);
|
|
1303
|
+
}
|
|
1304
|
+
formatStaticResult(result, mimeType) {
|
|
1305
|
+
return this.resourceServer.formatStaticResult(result, mimeType);
|
|
1306
|
+
}
|
|
1307
|
+
clientSupportsUI(server) {
|
|
1308
|
+
return this.capabilityNegotiator.supportsUI(server);
|
|
1309
|
+
}
|
|
1310
|
+
buildUIToolMeta(uiId) {
|
|
1311
|
+
const photonName = this.mcp?.name || path.basename(this.options.filePath, path.extname(this.options.filePath));
|
|
1312
|
+
return this.resourceServer.buildUIToolMeta(photonName, uiId);
|
|
1649
1313
|
}
|
|
1650
1314
|
/**
|
|
1651
1315
|
* Format error for AI consumption
|
|
@@ -1722,7 +1386,7 @@ export class PhotonServer {
|
|
|
1722
1386
|
if (unresolved.sources.length === 1) {
|
|
1723
1387
|
selectedSource = unresolved.sources[0];
|
|
1724
1388
|
}
|
|
1725
|
-
else if (this.
|
|
1389
|
+
else if (this.capabilityNegotiator.supportsElicitation(this.server)) {
|
|
1726
1390
|
// Present choices via elicitation
|
|
1727
1391
|
const sourceLabels = [];
|
|
1728
1392
|
for (const source of unresolved.sources) {
|
|
@@ -1838,11 +1502,7 @@ export class PhotonServer {
|
|
|
1838
1502
|
// Retry the original tool call
|
|
1839
1503
|
const inputProvider = this.createMCPInputProvider();
|
|
1840
1504
|
const outputHandler = (emit) => {
|
|
1841
|
-
|
|
1842
|
-
publishToChannel(this.daemonName, emit.channel, emit, this.options.workingDir).catch((e) => {
|
|
1843
|
-
this.log('debug', 'Publish to channel failed', { error: getErrorMessage(e) });
|
|
1844
|
-
});
|
|
1845
|
-
}
|
|
1505
|
+
this.channelManager.publishIfChannel(emit);
|
|
1846
1506
|
};
|
|
1847
1507
|
const retryResult = await this.loader.executeTool(this.mcp, toolName, args, {
|
|
1848
1508
|
inputProvider,
|
|
@@ -1863,6 +1523,19 @@ export class PhotonServer {
|
|
|
1863
1523
|
* Initialize and start the server
|
|
1864
1524
|
*/
|
|
1865
1525
|
async start() {
|
|
1526
|
+
// Safety net: catch unhandled errors so a bad photon can't crash the server
|
|
1527
|
+
process.on('uncaughtException', (err) => {
|
|
1528
|
+
this.log('error', 'Uncaught exception in PhotonServer', {
|
|
1529
|
+
error: getErrorMessage(err),
|
|
1530
|
+
stack: err?.stack,
|
|
1531
|
+
});
|
|
1532
|
+
});
|
|
1533
|
+
process.on('unhandledRejection', (reason) => {
|
|
1534
|
+
this.log('error', 'Unhandled rejection in PhotonServer', {
|
|
1535
|
+
error: reason instanceof Error ? getErrorMessage(reason) : String(reason),
|
|
1536
|
+
stack: reason instanceof Error ? reason.stack : undefined,
|
|
1537
|
+
});
|
|
1538
|
+
});
|
|
1866
1539
|
try {
|
|
1867
1540
|
// If unresolvedPhoton is set, skip loading — defer to first tool call
|
|
1868
1541
|
if (this.options.unresolvedPhoton) {
|
|
@@ -1887,9 +1560,12 @@ export class PhotonServer {
|
|
|
1887
1560
|
const metadata = await extractor.extractFullMetadata();
|
|
1888
1561
|
const isStateful = metadata.stateful;
|
|
1889
1562
|
// Start daemon for stateful photons (enables cross-client communication)
|
|
1563
|
+
// Channel mode also uses the daemon — the singleton instance holds the
|
|
1564
|
+
// bot connection, and multiple MCP clients subscribe to its events
|
|
1890
1565
|
if (isStateful) {
|
|
1891
1566
|
const photonName = metadata.name;
|
|
1892
1567
|
this.daemonName = photonName; // Store for subscription
|
|
1568
|
+
this.channelManager.setDaemonName(photonName);
|
|
1893
1569
|
this.log('info', `Stateful photon detected: ${photonName}`);
|
|
1894
1570
|
if (!isGlobalDaemonRunning()) {
|
|
1895
1571
|
this.log('info', `Starting daemon for ${photonName}...`);
|
|
@@ -1921,8 +1597,10 @@ export class PhotonServer {
|
|
|
1921
1597
|
this.mcp = await this.loader.loadFile(this.options.filePath);
|
|
1922
1598
|
}
|
|
1923
1599
|
}
|
|
1924
|
-
// Subscribe to daemon channels for cross-process notifications
|
|
1925
|
-
|
|
1600
|
+
// Subscribe to daemon channels for cross-process notifications.
|
|
1601
|
+
// In channel mode, ChannelManager intercepts 'channel-push' events
|
|
1602
|
+
// and translates them to notifications/claude/channel for the connected client.
|
|
1603
|
+
await this.channelManager.subscribeToChannels();
|
|
1926
1604
|
// Start with the appropriate transport
|
|
1927
1605
|
const transport = this.options.transport || 'stdio';
|
|
1928
1606
|
if (transport === 'sse') {
|
|
@@ -1944,106 +1622,12 @@ export class PhotonServer {
|
|
|
1944
1622
|
process.exit(1);
|
|
1945
1623
|
}
|
|
1946
1624
|
}
|
|
1947
|
-
/**
|
|
1948
|
-
* Subscribe to daemon channels for cross-process notifications
|
|
1949
|
-
* This enables real-time updates when other processes (e.g., Beam UI, other MCP clients) modify data
|
|
1950
|
-
*/
|
|
1951
|
-
async subscribeToChannels() {
|
|
1952
|
-
// Only subscribe if we have a daemon running (stateful photon)
|
|
1953
|
-
if (!this.daemonName)
|
|
1954
|
-
return;
|
|
1955
|
-
try {
|
|
1956
|
-
// Subscribe to wildcard channel for all events from this photon
|
|
1957
|
-
// E.g., "kanban:*" receives "kanban:photon", "kanban:my-board", etc.
|
|
1958
|
-
const unsubscribe = await subscribeChannel(this.daemonName, `${this.daemonName}:*`, (message) => {
|
|
1959
|
-
void this.handleChannelMessage(message);
|
|
1960
|
-
}, { workingDir: this.options.workingDir });
|
|
1961
|
-
this.channelUnsubscribers.push(unsubscribe);
|
|
1962
|
-
this.log('info', `Subscribed to daemon channel: ${this.daemonName}:*`);
|
|
1963
|
-
}
|
|
1964
|
-
catch (error) {
|
|
1965
|
-
this.log('warn', `Failed to subscribe to daemon: ${getErrorMessage(error)}`);
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
/**
|
|
1969
|
-
* Handle incoming channel messages and forward as MCP notifications
|
|
1970
|
-
* This enables cross-client real-time updates (e.g., Beam updates show in Claude Desktop)
|
|
1971
|
-
*
|
|
1972
|
-
* Uses standard MCP Apps notification with embedded _photon data:
|
|
1973
|
-
* - Claude Desktop forwards standard notifications (ui/notifications/host-context-changed)
|
|
1974
|
-
* - Photon bridge extracts _photon field and routes to event listeners
|
|
1975
|
-
* - This ensures cross-client sync works without requiring custom protocol support
|
|
1976
|
-
*/
|
|
1977
|
-
async handleChannelMessage(message) {
|
|
1978
|
-
if (!message || typeof message !== 'object')
|
|
1979
|
-
return;
|
|
1980
|
-
const msg = message;
|
|
1981
|
-
// Debug logging for cross-client event transmission
|
|
1982
|
-
if (process.env.PHOTON_DEBUG_EVENTS === '1') {
|
|
1983
|
-
console.error(`[PHOTON-SERVER] Received daemon message on ${String(msg.channel)}: event=${String(msg.event)}`);
|
|
1984
|
-
}
|
|
1985
|
-
// Use STANDARD notification with embedded photon data
|
|
1986
|
-
// Claude Desktop will forward this (it's a standard notification)
|
|
1987
|
-
// Our bridge extracts _photon and routes to the appropriate event handler
|
|
1988
|
-
const payload = {
|
|
1989
|
-
method: 'ui/notifications/host-context-changed',
|
|
1990
|
-
params: {
|
|
1991
|
-
// _photon field carries our custom event data
|
|
1992
|
-
_photon: {
|
|
1993
|
-
photon: this.daemonName,
|
|
1994
|
-
channel: msg.channel,
|
|
1995
|
-
event: msg.event,
|
|
1996
|
-
data: msg.data,
|
|
1997
|
-
},
|
|
1998
|
-
},
|
|
1999
|
-
};
|
|
2000
|
-
try {
|
|
2001
|
-
if (process.env.PHOTON_DEBUG_EVENTS === '1') {
|
|
2002
|
-
console.error(`[PHOTON-SERVER] Sending notification to MCP clients...`);
|
|
2003
|
-
}
|
|
2004
|
-
await this.server.notification(payload);
|
|
2005
|
-
if (process.env.PHOTON_DEBUG_EVENTS === '1') {
|
|
2006
|
-
console.error(`[PHOTON-SERVER] Notification sent successfully`);
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
catch (e) {
|
|
2010
|
-
console.error(`[PHOTON-SERVER-ERROR] Notification send failed: ${getErrorMessage(e)}`);
|
|
2011
|
-
this.log('debug', 'Notification send failed', { error: getErrorMessage(e) });
|
|
2012
|
-
}
|
|
2013
|
-
// Also send to SSE sessions — snapshot to avoid live-iterator + await issues
|
|
2014
|
-
for (const session of Array.from(this.sseSessions.values())) {
|
|
2015
|
-
try {
|
|
2016
|
-
await session.server.notification(payload);
|
|
2017
|
-
}
|
|
2018
|
-
catch (e) {
|
|
2019
|
-
this.log('debug', 'Session notification failed', { error: getErrorMessage(e) });
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
/**
|
|
2024
|
-
* Intercept a transport to capture raw client capabilities before Zod strips them.
|
|
2025
|
-
*
|
|
2026
|
-
* The MCP SDK's Zod schema for ClientCapabilities doesn't include `extensions`
|
|
2027
|
-
* (protocol 2025-11-25+), so getClientCapabilities() returns an object without it.
|
|
2028
|
-
* We intercept the transport's onmessage to capture the raw `initialize` request
|
|
2029
|
-
* and store capabilities before Zod parsing occurs.
|
|
2030
|
-
*/
|
|
2031
|
-
interceptTransportForRawCapabilities(transport, targetServer) {
|
|
2032
|
-
const origOnMessage = transport.onmessage;
|
|
2033
|
-
transport.onmessage = (message, extra) => {
|
|
2034
|
-
// Capture raw capabilities from initialize request
|
|
2035
|
-
if (message?.method === 'initialize' && message?.params?.capabilities) {
|
|
2036
|
-
this.rawClientCapabilities.set(targetServer, message.params.capabilities);
|
|
2037
|
-
}
|
|
2038
|
-
origOnMessage?.(message, extra);
|
|
2039
|
-
};
|
|
2040
|
-
}
|
|
2041
1625
|
/**
|
|
2042
1626
|
* Start server with stdio transport
|
|
2043
1627
|
*/
|
|
2044
1628
|
async startStdio() {
|
|
2045
1629
|
const transport = new StdioServerTransport();
|
|
2046
|
-
this.interceptTransportForRawCapabilities(transport, this.server);
|
|
1630
|
+
this.capabilityNegotiator.interceptTransportForRawCapabilities(transport, this.server, (msg) => this.channelManager.interceptPermissionRequest(msg));
|
|
2047
1631
|
await this.server.connect(transport);
|
|
2048
1632
|
this.log('info', `Server started: ${this.mcp.name}`);
|
|
2049
1633
|
}
|
|
@@ -2052,15 +1636,16 @@ export class PhotonServer {
|
|
|
2052
1636
|
*/
|
|
2053
1637
|
async startSSE() {
|
|
2054
1638
|
const port = this.options.port || 3000;
|
|
2055
|
-
const useStreamableHTTP = !!this.options.embeddedAssets;
|
|
2056
1639
|
const ssePath = '/mcp';
|
|
2057
1640
|
const messagesPath = '/mcp/messages';
|
|
2058
|
-
//
|
|
2059
|
-
//
|
|
2060
|
-
//
|
|
2061
|
-
//
|
|
1641
|
+
// Always use Streamable HTTP transport for SSE mode.
|
|
1642
|
+
// The legacy SSE transport (endpoint event + /mcp/messages?sessionId=) is deprecated
|
|
1643
|
+
// in the MCP spec and not supported by modern clients (e.g. llama.cpp).
|
|
1644
|
+
// Streamable HTTP uses the standard protocol:
|
|
1645
|
+
// POST /mcp — JSON-RPC request → JSON response
|
|
1646
|
+
// GET /mcp — SSE stream for server-to-client notifications
|
|
2062
1647
|
let beamTransport = null;
|
|
2063
|
-
|
|
1648
|
+
{
|
|
2064
1649
|
const photonName = this.mcp?.name || 'photon';
|
|
2065
1650
|
beamTransport = new BeamCompatTransport(photonName, {
|
|
2066
1651
|
description: this.mcp?.description,
|
|
@@ -2068,7 +1653,7 @@ export class PhotonServer {
|
|
|
2068
1653
|
stateful: !!this.mcp?.stateful,
|
|
2069
1654
|
hasSettings: !!this.mcp?.hasSettings,
|
|
2070
1655
|
});
|
|
2071
|
-
this.interceptTransportForRawCapabilities(beamTransport, this.server);
|
|
1656
|
+
this.capabilityNegotiator.interceptTransportForRawCapabilities(beamTransport, this.server, (msg) => this.channelManager.interceptPermissionRequest(msg));
|
|
2072
1657
|
await this.server.connect(beamTransport);
|
|
2073
1658
|
// Wire sub-photons: collect all loaded photons except the main one
|
|
2074
1659
|
const mainName = this.mcp?.name || 'photon';
|
|
@@ -2127,14 +1712,17 @@ export class PhotonServer {
|
|
|
2127
1712
|
return;
|
|
2128
1713
|
}
|
|
2129
1714
|
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
1715
|
+
const corsOrigin = getCorsOrigin(req);
|
|
2130
1716
|
// Handle CORS preflight
|
|
2131
1717
|
if (req.method === 'OPTIONS') {
|
|
2132
|
-
|
|
2133
|
-
'Access-Control-Allow-Origin': '*',
|
|
1718
|
+
const preflightHeaders = {
|
|
2134
1719
|
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
2135
|
-
'Access-Control-Allow-Headers': 'Content-Type, Accept, Mcp-Session-Id',
|
|
1720
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Mcp-Session-Id, Mcp-Protocol-Version, X-Photon-Request',
|
|
2136
1721
|
'Access-Control-Expose-Headers': 'Mcp-Session-Id',
|
|
2137
|
-
}
|
|
1722
|
+
};
|
|
1723
|
+
if (corsOrigin)
|
|
1724
|
+
preflightHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
1725
|
+
res.writeHead(204, preflightHeaders);
|
|
2138
1726
|
res.end();
|
|
2139
1727
|
return;
|
|
2140
1728
|
}
|
|
@@ -2145,7 +1733,7 @@ export class PhotonServer {
|
|
|
2145
1733
|
}
|
|
2146
1734
|
// Legacy SSE transport (when not using Streamable HTTP)
|
|
2147
1735
|
if (!beamTransport && req.method === 'GET' && url.pathname === ssePath) {
|
|
2148
|
-
await this.handleSSEConnection(res, messagesPath);
|
|
1736
|
+
await this.handleSSEConnection(req, res, messagesPath);
|
|
2149
1737
|
return;
|
|
2150
1738
|
}
|
|
2151
1739
|
if (!beamTransport && req.method === 'POST' && url.pathname === messagesPath) {
|
|
@@ -2154,7 +1742,10 @@ export class PhotonServer {
|
|
|
2154
1742
|
}
|
|
2155
1743
|
// Serve embedded index.html at root when assets are available
|
|
2156
1744
|
if (req.method === 'GET' && url.pathname === '/' && this.options.embeddedAssets) {
|
|
2157
|
-
|
|
1745
|
+
const htmlHeaders = { 'Content-Type': 'text/html' };
|
|
1746
|
+
if (corsOrigin)
|
|
1747
|
+
htmlHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
1748
|
+
res.writeHead(200, htmlHeaders);
|
|
2158
1749
|
res.end(this.options.embeddedAssets.indexHtml);
|
|
2159
1750
|
return;
|
|
2160
1751
|
}
|
|
@@ -2192,10 +1783,10 @@ export class PhotonServer {
|
|
|
2192
1783
|
}
|
|
2193
1784
|
// API: List all photons
|
|
2194
1785
|
if (req.method === 'GET' && url.pathname === '/api/photons') {
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
'Access-Control-Allow-Origin'
|
|
2198
|
-
|
|
1786
|
+
const photonHeaders = { 'Content-Type': 'application/json' };
|
|
1787
|
+
if (corsOrigin)
|
|
1788
|
+
photonHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
1789
|
+
res.writeHead(200, photonHeaders);
|
|
2199
1790
|
try {
|
|
2200
1791
|
const photons = await this.listAllPhotons();
|
|
2201
1792
|
res.end(JSON.stringify({ photons }));
|
|
@@ -2208,10 +1799,10 @@ export class PhotonServer {
|
|
|
2208
1799
|
}
|
|
2209
1800
|
// API: List tools (for compatibility, now returns current photon)
|
|
2210
1801
|
if (req.method === 'GET' && url.pathname === '/api/tools') {
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
'Access-Control-Allow-Origin'
|
|
2214
|
-
|
|
1802
|
+
const toolHeaders = { 'Content-Type': 'application/json' };
|
|
1803
|
+
if (corsOrigin)
|
|
1804
|
+
toolHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
1805
|
+
res.writeHead(200, toolHeaders);
|
|
2215
1806
|
const tools = this.mcp?.tools.map((tool) => {
|
|
2216
1807
|
const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name || u.linkedTools?.includes(tool.name));
|
|
2217
1808
|
return {
|
|
@@ -2231,10 +1822,10 @@ export class PhotonServer {
|
|
|
2231
1822
|
return;
|
|
2232
1823
|
}
|
|
2233
1824
|
if (req.method === 'GET' && url.pathname === '/api/status') {
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
'Access-Control-Allow-Origin'
|
|
2237
|
-
|
|
1825
|
+
const statusHeaders = { 'Content-Type': 'application/json' };
|
|
1826
|
+
if (corsOrigin)
|
|
1827
|
+
statusHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
1828
|
+
res.writeHead(200, statusHeaders);
|
|
2238
1829
|
res.end(JSON.stringify(this.buildStatusSnapshot()));
|
|
2239
1830
|
return;
|
|
2240
1831
|
}
|
|
@@ -2246,7 +1837,8 @@ export class PhotonServer {
|
|
|
2246
1837
|
// API: Call tool
|
|
2247
1838
|
if (req.method === 'POST' && url.pathname === '/api/call') {
|
|
2248
1839
|
// Security: restrict CORS to localhost and require local request
|
|
2249
|
-
|
|
1840
|
+
if (corsOrigin)
|
|
1841
|
+
res.setHeader('Access-Control-Allow-Origin', corsOrigin);
|
|
2250
1842
|
res.setHeader('Content-Type', 'application/json');
|
|
2251
1843
|
if (!isLocalRequest(req)) {
|
|
2252
1844
|
res.writeHead(403);
|
|
@@ -2278,7 +1870,8 @@ export class PhotonServer {
|
|
|
2278
1870
|
}
|
|
2279
1871
|
// API: Call tool with streaming progress (SSE)
|
|
2280
1872
|
if (req.method === 'POST' && url.pathname === '/api/call-stream') {
|
|
2281
|
-
|
|
1873
|
+
if (corsOrigin)
|
|
1874
|
+
res.setHeader('Access-Control-Allow-Origin', corsOrigin);
|
|
2282
1875
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
2283
1876
|
res.setHeader('Cache-Control', 'no-cache');
|
|
2284
1877
|
res.setHeader('Connection', 'keep-alive');
|
|
@@ -2342,11 +1935,7 @@ export class PhotonServer {
|
|
|
2342
1935
|
sendNotification('notifications/emit', { event: emit });
|
|
2343
1936
|
}
|
|
2344
1937
|
// Forward channel events to daemon for cross-process pub/sub
|
|
2345
|
-
|
|
2346
|
-
publishToChannel(this.daemonName, emit.channel, emit, this.options.workingDir).catch(() => {
|
|
2347
|
-
// Ignore publish errors - daemon may not be running
|
|
2348
|
-
});
|
|
2349
|
-
}
|
|
1938
|
+
this.channelManager.publishIfChannel(emit);
|
|
2350
1939
|
};
|
|
2351
1940
|
sendNotification('notifications/status', {
|
|
2352
1941
|
type: 'info',
|
|
@@ -2389,10 +1978,10 @@ export class PhotonServer {
|
|
|
2389
1978
|
if (ui?.resolvedPath) {
|
|
2390
1979
|
try {
|
|
2391
1980
|
const content = await readText(ui.resolvedPath);
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
'Access-Control-Allow-Origin'
|
|
2395
|
-
|
|
1981
|
+
const uiHeaders = { 'Content-Type': 'text/html' };
|
|
1982
|
+
if (corsOrigin)
|
|
1983
|
+
uiHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
1984
|
+
res.writeHead(200, uiHeaders);
|
|
2396
1985
|
res.end(content);
|
|
2397
1986
|
return;
|
|
2398
1987
|
}
|
|
@@ -2410,11 +1999,13 @@ export class PhotonServer {
|
|
|
2410
1999
|
if (req.method === 'GET' && url.pathname === '/api/diagnostics') {
|
|
2411
2000
|
const { PHOTON_VERSION } = await import('./version.js');
|
|
2412
2001
|
const photonName = this.mcp?.name || 'photon';
|
|
2413
|
-
const tools = this.mcp
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2002
|
+
const tools = this.mcp
|
|
2003
|
+
? Object.keys(this.mcp._toolSchemas || {}).length
|
|
2004
|
+
: 0;
|
|
2005
|
+
const diagHeaders = { 'Content-Type': 'application/json' };
|
|
2006
|
+
if (corsOrigin)
|
|
2007
|
+
diagHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
2008
|
+
res.writeHead(200, diagHeaders);
|
|
2418
2009
|
res.end(JSON.stringify({
|
|
2419
2010
|
photonVersion: PHOTON_VERSION,
|
|
2420
2011
|
workingDir: process.cwd(),
|
|
@@ -2443,28 +2034,34 @@ export class PhotonServer {
|
|
|
2443
2034
|
hostVersion: '1.5.0',
|
|
2444
2035
|
injectedPhotons: [],
|
|
2445
2036
|
});
|
|
2446
|
-
|
|
2037
|
+
const bridgeHeaders = { 'Content-Type': 'text/html' };
|
|
2038
|
+
if (corsOrigin)
|
|
2039
|
+
bridgeHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
2040
|
+
res.writeHead(200, bridgeHeaders);
|
|
2447
2041
|
res.end(script);
|
|
2448
2042
|
return;
|
|
2449
2043
|
}
|
|
2450
2044
|
if (req.method === 'GET' && url.pathname === '/index.html') {
|
|
2451
|
-
|
|
2045
|
+
const indexHeaders = { 'Content-Type': 'text/html' };
|
|
2046
|
+
if (corsOrigin)
|
|
2047
|
+
indexHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
2048
|
+
res.writeHead(200, indexHeaders);
|
|
2452
2049
|
res.end(assets.indexHtml);
|
|
2453
2050
|
return;
|
|
2454
2051
|
}
|
|
2455
2052
|
if (req.method === 'GET' && url.pathname === '/beam.bundle.js') {
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
'Access-Control-Allow-Origin'
|
|
2459
|
-
|
|
2053
|
+
const jsHeaders = { 'Content-Type': 'text/javascript' };
|
|
2054
|
+
if (corsOrigin)
|
|
2055
|
+
jsHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
2056
|
+
res.writeHead(200, jsHeaders);
|
|
2460
2057
|
res.end(assets.bundleJs);
|
|
2461
2058
|
return;
|
|
2462
2059
|
}
|
|
2463
2060
|
if (req.method === 'GET' && url.pathname === '/sw.js') {
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
'Access-Control-Allow-Origin'
|
|
2467
|
-
|
|
2061
|
+
const swHeaders = { 'Content-Type': 'text/javascript' };
|
|
2062
|
+
if (corsOrigin)
|
|
2063
|
+
swHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
2064
|
+
res.writeHead(200, swHeaders);
|
|
2468
2065
|
res.end('self.addEventListener("fetch", () => {});');
|
|
2469
2066
|
return;
|
|
2470
2067
|
}
|
|
@@ -2479,14 +2076,20 @@ export class PhotonServer {
|
|
|
2479
2076
|
<script>window.PHOTON_APP_NAME="${appName}";window.PHOTON_SSE_URL=window.location.origin;</script>
|
|
2480
2077
|
<script src="/beam.bundle.js"></script>
|
|
2481
2078
|
</body></html>`;
|
|
2482
|
-
|
|
2079
|
+
const appHeaders = { 'Content-Type': 'text/html' };
|
|
2080
|
+
if (corsOrigin)
|
|
2081
|
+
appHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
2082
|
+
res.writeHead(200, appHeaders);
|
|
2483
2083
|
res.end(appHtml);
|
|
2484
2084
|
return;
|
|
2485
2085
|
}
|
|
2486
2086
|
}
|
|
2487
2087
|
// SPA fallback: serve index.html for unmatched GET requests (compiled binary with Beam UI)
|
|
2488
2088
|
if (req.method === 'GET' && this.options.embeddedAssets) {
|
|
2489
|
-
|
|
2089
|
+
const fallbackHeaders = { 'Content-Type': 'text/html' };
|
|
2090
|
+
if (corsOrigin)
|
|
2091
|
+
fallbackHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
2092
|
+
res.writeHead(200, fallbackHeaders);
|
|
2490
2093
|
res.end(this.options.embeddedAssets.indexHtml);
|
|
2491
2094
|
return;
|
|
2492
2095
|
}
|
|
@@ -2499,17 +2102,7 @@ export class PhotonServer {
|
|
|
2499
2102
|
});
|
|
2500
2103
|
await new Promise((resolve) => {
|
|
2501
2104
|
this.httpServer.listen(port, () => {
|
|
2502
|
-
|
|
2503
|
-
transport: 'sse',
|
|
2504
|
-
port,
|
|
2505
|
-
devMode: this.devMode,
|
|
2506
|
-
});
|
|
2507
|
-
this.log('debug', 'SSE endpoints ready', {
|
|
2508
|
-
baseUrl: `http://localhost:${port}`,
|
|
2509
|
-
ssePath,
|
|
2510
|
-
messagesPath,
|
|
2511
|
-
playground: this.devMode ? `http://localhost:${port}/playground` : undefined,
|
|
2512
|
-
});
|
|
2105
|
+
process.stdout.write(`⚡ ${this.mcp.name} → http://localhost:${port}${ssePath}\n`);
|
|
2513
2106
|
resolve();
|
|
2514
2107
|
});
|
|
2515
2108
|
});
|
|
@@ -2553,8 +2146,10 @@ export class PhotonServer {
|
|
|
2553
2146
|
/**
|
|
2554
2147
|
* Handle new SSE connection
|
|
2555
2148
|
*/
|
|
2556
|
-
async handleSSEConnection(res, messagesPath) {
|
|
2557
|
-
|
|
2149
|
+
async handleSSEConnection(req, res, messagesPath) {
|
|
2150
|
+
const origin = getCorsOrigin(req);
|
|
2151
|
+
if (origin)
|
|
2152
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
2558
2153
|
// Create a new MCP server instance for this session
|
|
2559
2154
|
const sessionServer = new Server({
|
|
2560
2155
|
name: this.mcp?.name || 'photon-mcp',
|
|
@@ -2574,7 +2169,7 @@ export class PhotonServer {
|
|
|
2574
2169
|
this.setupSessionHandlers(sessionServer);
|
|
2575
2170
|
// Create SSE transport
|
|
2576
2171
|
const transport = new SSEServerTransport(messagesPath, res);
|
|
2577
|
-
this.interceptTransportForRawCapabilities(transport, sessionServer);
|
|
2172
|
+
this.capabilityNegotiator.interceptTransportForRawCapabilities(transport, sessionServer, (msg) => this.channelManager.interceptPermissionRequest(msg));
|
|
2578
2173
|
const sessionId = transport.sessionId;
|
|
2579
2174
|
// Store session
|
|
2580
2175
|
this.sseSessions.set(sessionId, { server: sessionServer, transport });
|
|
@@ -2621,7 +2216,9 @@ export class PhotonServer {
|
|
|
2621
2216
|
* Handle incoming SSE message
|
|
2622
2217
|
*/
|
|
2623
2218
|
async handleSSEMessage(req, res, url) {
|
|
2624
|
-
|
|
2219
|
+
const origin = getCorsOrigin(req);
|
|
2220
|
+
if (origin)
|
|
2221
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
2625
2222
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
2626
2223
|
const sessionId = url.searchParams.get('sessionId');
|
|
2627
2224
|
if (!sessionId) {
|
|
@@ -2680,476 +2277,14 @@ export class PhotonServer {
|
|
|
2680
2277
|
return this.handleGetPrompt(request);
|
|
2681
2278
|
});
|
|
2682
2279
|
sessionServer.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
2683
|
-
return this.handleListResources(
|
|
2280
|
+
return this.resourceServer.handleListResources(this.mcp);
|
|
2684
2281
|
});
|
|
2685
2282
|
sessionServer.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
2686
|
-
return this.handleListResourceTemplates();
|
|
2283
|
+
return this.resourceServer.handleListResourceTemplates(this.mcp);
|
|
2687
2284
|
});
|
|
2688
2285
|
sessionServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
2689
|
-
return this.handleReadResource(request);
|
|
2690
|
-
});
|
|
2691
|
-
}
|
|
2692
|
-
/**
|
|
2693
|
-
* Handle asset read (for both stdio and SSE handlers)
|
|
2694
|
-
*/
|
|
2695
|
-
/**
|
|
2696
|
-
* Handle SEP-1865 ui:// resource read
|
|
2697
|
-
*/
|
|
2698
|
-
async handleUIAssetRead(uri, assetId, photon) {
|
|
2699
|
-
const target = photon || this.mcp;
|
|
2700
|
-
const photonName = target.name;
|
|
2701
|
-
let content;
|
|
2702
|
-
// Try embedded templates first (compiled binary mode)
|
|
2703
|
-
if (this.options.embeddedUITemplates) {
|
|
2704
|
-
const templates = this.options.embeddedUITemplates[photonName];
|
|
2705
|
-
if (templates && templates[assetId]) {
|
|
2706
|
-
content = templates[assetId];
|
|
2707
|
-
}
|
|
2708
|
-
}
|
|
2709
|
-
// Fall back to disk if not embedded
|
|
2710
|
-
if (!content) {
|
|
2711
|
-
if (!target.assets?.ui) {
|
|
2712
|
-
throw new Error(`UI asset not found: ${uri}`);
|
|
2713
|
-
}
|
|
2714
|
-
const ui = target.assets.ui.find((u) => u.id === assetId);
|
|
2715
|
-
if (!ui || !ui.resolvedPath) {
|
|
2716
|
-
throw new Error(`UI asset not found: ${uri}`);
|
|
2717
|
-
}
|
|
2718
|
-
content = await readText(ui.resolvedPath);
|
|
2719
|
-
}
|
|
2720
|
-
// Wrap .photon.html fragments in a full HTML document.
|
|
2721
|
-
// These files contain only <style>, markup, and <script> — no <!doctype> or <html>.
|
|
2722
|
-
// Beam wraps them automatically, but MCP clients (Claude Desktop) need a complete document.
|
|
2723
|
-
const isFragment = !content.trimStart().toLowerCase().startsWith('<!doctype') &&
|
|
2724
|
-
!content.trimStart().toLowerCase().startsWith('<html');
|
|
2725
|
-
if (isFragment) {
|
|
2726
|
-
content = `<!doctype html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n</head>\n<body>\n${content}\n</body>\n</html>`;
|
|
2727
|
-
}
|
|
2728
|
-
// Inject MCP Apps bridge script for Claude Desktop compatibility
|
|
2729
|
-
const bridgeScript = this.generateMcpAppsBridge();
|
|
2730
|
-
content = content.replace('<head>', `<head>\n${bridgeScript}`);
|
|
2731
|
-
return {
|
|
2732
|
-
contents: [{ uri, mimeType: 'text/html;profile=mcp-app', text: content }],
|
|
2733
|
-
};
|
|
2734
|
-
}
|
|
2735
|
-
/**
|
|
2736
|
-
* Generate minimal MCP Apps bridge script for Claude Desktop compatibility
|
|
2737
|
-
* This handles the ui/initialize handshake and tool result delivery
|
|
2738
|
-
*/
|
|
2739
|
-
generateMcpAppsBridge() {
|
|
2740
|
-
const photonName = this.mcp?.name || 'photon-app';
|
|
2741
|
-
const injectedPhotons = this.mcp?.injectedPhotons || [];
|
|
2742
|
-
return `<script>
|
|
2743
|
-
(function() {
|
|
2744
|
-
'use strict';
|
|
2745
|
-
var pendingCalls = {};
|
|
2746
|
-
var callIdCounter = 0;
|
|
2747
|
-
var toolResult = null;
|
|
2748
|
-
var resultListeners = [];
|
|
2749
|
-
var emitListeners = [];
|
|
2750
|
-
var themeListeners = [];
|
|
2751
|
-
var eventListeners = {}; // For specific event subscriptions (e.g., 'taskMove')
|
|
2752
|
-
var photonEventListeners = {}; // Namespaced by photon name for injected photons
|
|
2753
|
-
var currentTheme = 'dark';
|
|
2754
|
-
var injectedPhotons = ${JSON.stringify(injectedPhotons)};
|
|
2755
|
-
|
|
2756
|
-
function generateCallId() {
|
|
2757
|
-
return 'call_' + (++callIdCounter) + '_' + Math.random().toString(36).slice(2);
|
|
2758
|
-
}
|
|
2759
|
-
|
|
2760
|
-
function postToHost(msg) {
|
|
2761
|
-
window.parent.postMessage(msg, '*');
|
|
2762
|
-
}
|
|
2763
|
-
|
|
2764
|
-
// Listen for messages from host
|
|
2765
|
-
window.addEventListener('message', function(e) {
|
|
2766
|
-
var m = e.data;
|
|
2767
|
-
if (!m || typeof m !== 'object') return;
|
|
2768
|
-
|
|
2769
|
-
// Handle JSON-RPC messages
|
|
2770
|
-
if (m.jsonrpc === '2.0') {
|
|
2771
|
-
// Response to our request (has id, no method)
|
|
2772
|
-
if (m.id && !m.method && pendingCalls[m.id]) {
|
|
2773
|
-
var pending = pendingCalls[m.id];
|
|
2774
|
-
delete pendingCalls[m.id];
|
|
2775
|
-
if (m.error) {
|
|
2776
|
-
pending.reject(new Error(m.error.message));
|
|
2777
|
-
} else {
|
|
2778
|
-
// Extract clean data from MCP result format
|
|
2779
|
-
var result = m.result;
|
|
2780
|
-
var cleanData = result;
|
|
2781
|
-
if (result && result.structuredContent) {
|
|
2782
|
-
cleanData = result.structuredContent;
|
|
2783
|
-
} else if (result && result.content && Array.isArray(result.content)) {
|
|
2784
|
-
var textItem = result.content.find(function(i) { return i.type === 'text'; });
|
|
2785
|
-
if (textItem && textItem.text) {
|
|
2786
|
-
try { cleanData = JSON.parse(textItem.text); } catch(e) { cleanData = textItem.text; }
|
|
2787
|
-
}
|
|
2788
|
-
}
|
|
2789
|
-
pending.resolve(cleanData);
|
|
2790
|
-
}
|
|
2791
|
-
return;
|
|
2792
|
-
}
|
|
2793
|
-
|
|
2794
|
-
// Tool result notification
|
|
2795
|
-
if (m.method === 'ui/notifications/tool-result') {
|
|
2796
|
-
var result = m.params;
|
|
2797
|
-
// Extract data from MCP result format
|
|
2798
|
-
if (result.structuredContent) {
|
|
2799
|
-
toolResult = result.structuredContent;
|
|
2800
|
-
} else if (result.content && Array.isArray(result.content)) {
|
|
2801
|
-
var textItem = result.content.find(function(i) { return i.type === 'text'; });
|
|
2802
|
-
if (textItem && textItem.text) {
|
|
2803
|
-
try { toolResult = JSON.parse(textItem.text); } catch(e) { toolResult = textItem.text; }
|
|
2804
|
-
}
|
|
2805
|
-
} else {
|
|
2806
|
-
toolResult = result;
|
|
2807
|
-
}
|
|
2808
|
-
// Set __PHOTON_DATA__ for UIs that read it at init
|
|
2809
|
-
window.__PHOTON_DATA__ = toolResult;
|
|
2810
|
-
// Dispatch event for UIs to re-initialize with new data
|
|
2811
|
-
window.dispatchEvent(new CustomEvent('photon:data-ready', { detail: toolResult }));
|
|
2812
|
-
resultListeners.forEach(function(cb) { cb(toolResult); });
|
|
2813
|
-
}
|
|
2814
|
-
|
|
2815
|
-
// Host context changed (theme + embedded photon events)
|
|
2816
|
-
if (m.method === 'ui/notifications/host-context-changed') {
|
|
2817
|
-
// Standard theme handling
|
|
2818
|
-
if (m.params && m.params.theme) {
|
|
2819
|
-
currentTheme = m.params.theme;
|
|
2820
|
-
document.documentElement.classList.remove('light', 'dark', 'light-theme');
|
|
2821
|
-
document.documentElement.classList.add(m.params.theme);
|
|
2822
|
-
document.documentElement.setAttribute('data-theme', m.params.theme);
|
|
2823
|
-
// Apply theme token CSS variables (matching platform-compat applyThemeTokens)
|
|
2824
|
-
if (m.params.styles && m.params.styles.variables) {
|
|
2825
|
-
var root = document.documentElement;
|
|
2826
|
-
var vars = m.params.styles.variables;
|
|
2827
|
-
for (var key in vars) { root.style.setProperty(key, vars[key]); }
|
|
2828
|
-
}
|
|
2829
|
-
// Apply background/text colors to match platform-compat bridge
|
|
2830
|
-
if (m.params.theme === 'light') {
|
|
2831
|
-
document.documentElement.classList.add('light-theme');
|
|
2832
|
-
document.documentElement.style.colorScheme = 'light';
|
|
2833
|
-
document.documentElement.style.backgroundColor = '#ffffff';
|
|
2834
|
-
if (document.body) { document.body.style.backgroundColor = '#ffffff'; document.body.style.color = '#1a1a1a'; }
|
|
2835
|
-
} else {
|
|
2836
|
-
document.documentElement.style.colorScheme = 'dark';
|
|
2837
|
-
document.documentElement.style.backgroundColor = '#0d0d0d';
|
|
2838
|
-
if (document.body) { document.body.style.backgroundColor = '#0d0d0d'; document.body.style.color = '#e6e6e6'; }
|
|
2839
|
-
}
|
|
2840
|
-
themeListeners.forEach(function(cb) { cb(currentTheme); });
|
|
2841
|
-
}
|
|
2842
|
-
|
|
2843
|
-
// Extract embedded photon event data
|
|
2844
|
-
// This enables real-time sync via standard MCP protocol
|
|
2845
|
-
if (m.params && m.params._photon) {
|
|
2846
|
-
var photonData = m.params._photon;
|
|
2847
|
-
// Route to generic emit listeners
|
|
2848
|
-
emitListeners.forEach(function(cb) { cb(photonData); });
|
|
2849
|
-
|
|
2850
|
-
var eventName = photonData.event;
|
|
2851
|
-
var sourcePhoton = photonData.data && photonData.data._source;
|
|
2852
|
-
|
|
2853
|
-
// Route to photon-specific listeners if _source is specified (injected photon events)
|
|
2854
|
-
if (sourcePhoton && photonEventListeners[sourcePhoton] && photonEventListeners[sourcePhoton][eventName]) {
|
|
2855
|
-
photonEventListeners[sourcePhoton][eventName].forEach(function(cb) {
|
|
2856
|
-
cb(photonData.data);
|
|
2857
|
-
});
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
// Also route to global event listeners (main photon events, or fallback)
|
|
2861
|
-
if (eventName && eventListeners[eventName]) {
|
|
2862
|
-
eventListeners[eventName].forEach(function(cb) {
|
|
2863
|
-
cb(photonData.data);
|
|
2864
|
-
});
|
|
2865
|
-
}
|
|
2866
|
-
}
|
|
2867
|
-
}
|
|
2868
|
-
}
|
|
2869
|
-
});
|
|
2870
|
-
|
|
2871
|
-
// Mark that we're in MCP Apps context (not Beam)
|
|
2872
|
-
window.__MCP_APPS_CONTEXT__ = true;
|
|
2873
|
-
|
|
2874
|
-
// Expose photon bridge API
|
|
2875
|
-
window.photon = {
|
|
2876
|
-
get toolOutput() { return toolResult; },
|
|
2877
|
-
onResult: function(cb) {
|
|
2878
|
-
resultListeners.push(cb);
|
|
2879
|
-
if (toolResult) cb(toolResult);
|
|
2880
|
-
return function() {
|
|
2881
|
-
var i = resultListeners.indexOf(cb);
|
|
2882
|
-
if (i >= 0) resultListeners.splice(i, 1);
|
|
2883
|
-
};
|
|
2884
|
-
},
|
|
2885
|
-
callTool: function(name, args, opts) {
|
|
2886
|
-
var callId = generateCallId();
|
|
2887
|
-
return new Promise(function(resolve, reject) {
|
|
2888
|
-
pendingCalls[callId] = { resolve: resolve, reject: reject };
|
|
2889
|
-
var a = args || {};
|
|
2890
|
-
if (opts && opts.instance !== undefined) { a = Object.assign({}, a, { _targetInstance: opts.instance }); }
|
|
2891
|
-
postToHost({
|
|
2892
|
-
jsonrpc: '2.0',
|
|
2893
|
-
id: callId,
|
|
2894
|
-
method: 'tools/call',
|
|
2895
|
-
params: { name: name, arguments: a }
|
|
2286
|
+
return this.resourceServer.handleReadResource(request, this.mcp);
|
|
2896
2287
|
});
|
|
2897
|
-
setTimeout(function() {
|
|
2898
|
-
if (pendingCalls[callId]) {
|
|
2899
|
-
delete pendingCalls[callId];
|
|
2900
|
-
reject(new Error('Tool call timeout'));
|
|
2901
|
-
}
|
|
2902
|
-
}, 30000);
|
|
2903
|
-
});
|
|
2904
|
-
},
|
|
2905
|
-
invoke: function(name, args, opts) { return window.photon.callTool(name, args, opts); },
|
|
2906
|
-
onEmit: function(cb) {
|
|
2907
|
-
emitListeners.push(cb);
|
|
2908
|
-
return function() {
|
|
2909
|
-
var i = emitListeners.indexOf(cb);
|
|
2910
|
-
if (i >= 0) emitListeners.splice(i, 1);
|
|
2911
|
-
};
|
|
2912
|
-
},
|
|
2913
|
-
onThemeChange: function(cb) {
|
|
2914
|
-
themeListeners.push(cb);
|
|
2915
|
-
// Call immediately with current theme
|
|
2916
|
-
cb(currentTheme);
|
|
2917
|
-
return function() {
|
|
2918
|
-
var i = themeListeners.indexOf(cb);
|
|
2919
|
-
if (i >= 0) themeListeners.splice(i, 1);
|
|
2920
|
-
};
|
|
2921
|
-
},
|
|
2922
|
-
get theme() { return currentTheme; },
|
|
2923
|
-
|
|
2924
|
-
// Generic event subscription for real-time sync
|
|
2925
|
-
// Usage: photon.on('taskMove', function(data) { ... })
|
|
2926
|
-
on: function(eventName, cb) {
|
|
2927
|
-
if (!eventListeners[eventName]) eventListeners[eventName] = [];
|
|
2928
|
-
eventListeners[eventName].push(cb);
|
|
2929
|
-
return function() {
|
|
2930
|
-
var i = eventListeners[eventName].indexOf(cb);
|
|
2931
|
-
if (i >= 0) eventListeners[eventName].splice(i, 1);
|
|
2932
|
-
};
|
|
2933
|
-
},
|
|
2934
|
-
|
|
2935
|
-
// Photon-specific event subscription (for injected photon events)
|
|
2936
|
-
// Usage: photon.onPhoton('notifications', 'alertCreated', function(data) { ... })
|
|
2937
|
-
onPhoton: function(photonName, eventName, cb) {
|
|
2938
|
-
if (!photonEventListeners[photonName]) photonEventListeners[photonName] = {};
|
|
2939
|
-
if (!photonEventListeners[photonName][eventName]) photonEventListeners[photonName][eventName] = [];
|
|
2940
|
-
photonEventListeners[photonName][eventName].push(cb);
|
|
2941
|
-
return function() {
|
|
2942
|
-
var i = photonEventListeners[photonName][eventName].indexOf(cb);
|
|
2943
|
-
if (i >= 0) photonEventListeners[photonName][eventName].splice(i, 1);
|
|
2944
|
-
};
|
|
2945
|
-
}
|
|
2946
|
-
};
|
|
2947
|
-
|
|
2948
|
-
// Create direct window object: window.{photonName}
|
|
2949
|
-
// This provides a clean class-like API that mirrors server methods:
|
|
2950
|
-
// Server: this.emit('taskMove', data)
|
|
2951
|
-
// Client: kanban.onTaskMove(cb) - subscribe to events
|
|
2952
|
-
// Client: kanban.taskMove(args) - call server method
|
|
2953
|
-
var photonName = '${photonName}';
|
|
2954
|
-
window[photonName] = new Proxy({}, {
|
|
2955
|
-
get: function(target, prop) {
|
|
2956
|
-
if (typeof prop !== 'string') return undefined;
|
|
2957
|
-
|
|
2958
|
-
// onEventName -> subscribe to 'eventName' event
|
|
2959
|
-
// e.g., onTaskMove -> subscribe to 'taskMove'
|
|
2960
|
-
if (prop.startsWith('on') && prop.length > 2) {
|
|
2961
|
-
var eventName = prop.charAt(2).toLowerCase() + prop.slice(3);
|
|
2962
|
-
return function(cb) {
|
|
2963
|
-
return window.photon.on(eventName, cb);
|
|
2964
|
-
};
|
|
2965
|
-
}
|
|
2966
|
-
|
|
2967
|
-
// methodName -> call server tool
|
|
2968
|
-
// e.g., taskMove(args) -> photon.callTool('taskMove', args)
|
|
2969
|
-
return function(args) {
|
|
2970
|
-
return window.photon.callTool(prop, args);
|
|
2971
|
-
};
|
|
2972
|
-
}
|
|
2973
|
-
});
|
|
2974
|
-
|
|
2975
|
-
// Create proxies for injected photons (for event subscriptions)
|
|
2976
|
-
// e.g., notifications.onAlertCreated(cb) subscribes to 'alertCreated' from 'notifications' photon
|
|
2977
|
-
injectedPhotons.forEach(function(injectedName) {
|
|
2978
|
-
window[injectedName] = new Proxy({}, {
|
|
2979
|
-
get: function(target, prop) {
|
|
2980
|
-
if (typeof prop !== 'string') return undefined;
|
|
2981
|
-
|
|
2982
|
-
// onEventName -> subscribe to photon-specific event
|
|
2983
|
-
if (prop.startsWith('on') && prop.length > 2) {
|
|
2984
|
-
var eventName = prop.charAt(2).toLowerCase() + prop.slice(3);
|
|
2985
|
-
return function(cb) {
|
|
2986
|
-
return window.photon.onPhoton(injectedName, eventName, cb);
|
|
2987
|
-
};
|
|
2988
|
-
}
|
|
2989
|
-
|
|
2990
|
-
// Method calls on injected photons are not supported from client
|
|
2991
|
-
// (injected photon methods are only available server-side)
|
|
2992
|
-
return undefined;
|
|
2993
|
-
}
|
|
2994
|
-
});
|
|
2995
|
-
});
|
|
2996
|
-
|
|
2997
|
-
// Size notification helper
|
|
2998
|
-
function sendSizeChanged() {
|
|
2999
|
-
var body = document.body;
|
|
3000
|
-
var root = document.documentElement;
|
|
3001
|
-
|
|
3002
|
-
// Calculate actual content dimensions
|
|
3003
|
-
var width = Math.max(
|
|
3004
|
-
body.scrollWidth,
|
|
3005
|
-
body.offsetWidth,
|
|
3006
|
-
root.clientWidth,
|
|
3007
|
-
root.scrollWidth,
|
|
3008
|
-
root.offsetWidth
|
|
3009
|
-
);
|
|
3010
|
-
var height = Math.max(
|
|
3011
|
-
body.scrollHeight,
|
|
3012
|
-
body.offsetHeight,
|
|
3013
|
-
root.clientHeight,
|
|
3014
|
-
root.scrollHeight,
|
|
3015
|
-
root.offsetHeight
|
|
3016
|
-
);
|
|
3017
|
-
|
|
3018
|
-
// Check for scrollable containers with overflow:hidden that hide true content size
|
|
3019
|
-
var containers = document.querySelectorAll('.board, [style*="overflow"]');
|
|
3020
|
-
containers.forEach(function(el) {
|
|
3021
|
-
if (el.scrollWidth > width) width = el.scrollWidth;
|
|
3022
|
-
if (el.scrollHeight > height) height = el.scrollHeight;
|
|
3023
|
-
});
|
|
3024
|
-
|
|
3025
|
-
// For kanban-style boards, calculate from column count
|
|
3026
|
-
var columns = document.querySelectorAll('.column');
|
|
3027
|
-
if (columns.length > 0) {
|
|
3028
|
-
var columnWidth = 220; // min-width + gap
|
|
3029
|
-
var boardPadding = 48;
|
|
3030
|
-
var neededWidth = (columns.length * columnWidth) + boardPadding;
|
|
3031
|
-
if (neededWidth > width) width = neededWidth;
|
|
3032
|
-
}
|
|
3033
|
-
|
|
3034
|
-
// Reasonable minimums, maximums, and padding
|
|
3035
|
-
width = Math.max(width, 600) + 32;
|
|
3036
|
-
// Force minimum height for kanban-style boards
|
|
3037
|
-
// header(120) + column headers(50) + 3-4 cards(450) = 620
|
|
3038
|
-
if (columns.length > 0) {
|
|
3039
|
-
height = Math.max(height, 620);
|
|
3040
|
-
} else {
|
|
3041
|
-
height = Math.max(height, 400);
|
|
3042
|
-
}
|
|
3043
|
-
|
|
3044
|
-
postToHost({
|
|
3045
|
-
jsonrpc: '2.0',
|
|
3046
|
-
method: 'ui/notifications/size-changed',
|
|
3047
|
-
params: { width: width, height: height }
|
|
3048
|
-
});
|
|
3049
|
-
}
|
|
3050
|
-
|
|
3051
|
-
// MCP Apps handshake: send ui/initialize and wait for response
|
|
3052
|
-
var initId = generateCallId();
|
|
3053
|
-
pendingCalls[initId] = {
|
|
3054
|
-
resolve: function(result) {
|
|
3055
|
-
// Apply theme from host context (matching platform-compat bridge)
|
|
3056
|
-
if (result.hostContext && result.hostContext.theme) {
|
|
3057
|
-
currentTheme = result.hostContext.theme;
|
|
3058
|
-
document.documentElement.classList.remove('light', 'dark', 'light-theme');
|
|
3059
|
-
document.documentElement.classList.add(result.hostContext.theme);
|
|
3060
|
-
document.documentElement.setAttribute('data-theme', result.hostContext.theme);
|
|
3061
|
-
// Apply theme token CSS variables from host context
|
|
3062
|
-
if (result.hostContext.styles && result.hostContext.styles.variables) {
|
|
3063
|
-
var root = document.documentElement;
|
|
3064
|
-
var vars = result.hostContext.styles.variables;
|
|
3065
|
-
for (var key in vars) { root.style.setProperty(key, vars[key]); }
|
|
3066
|
-
}
|
|
3067
|
-
if (result.hostContext.theme === 'light') {
|
|
3068
|
-
document.documentElement.classList.add('light-theme');
|
|
3069
|
-
document.documentElement.style.colorScheme = 'light';
|
|
3070
|
-
} else {
|
|
3071
|
-
document.documentElement.style.colorScheme = 'dark';
|
|
3072
|
-
}
|
|
3073
|
-
}
|
|
3074
|
-
// Complete handshake
|
|
3075
|
-
postToHost({ jsonrpc: '2.0', method: 'ui/notifications/initialized', params: {} });
|
|
3076
|
-
|
|
3077
|
-
// Set up size notifications after handshake
|
|
3078
|
-
setTimeout(sendSizeChanged, 100);
|
|
3079
|
-
var resizeObserver = new ResizeObserver(function() {
|
|
3080
|
-
sendSizeChanged();
|
|
3081
|
-
});
|
|
3082
|
-
resizeObserver.observe(document.documentElement);
|
|
3083
|
-
resizeObserver.observe(document.body);
|
|
3084
|
-
},
|
|
3085
|
-
reject: function(err) { console.error('MCP Apps init failed:', err); }
|
|
3086
|
-
};
|
|
3087
|
-
|
|
3088
|
-
postToHost({
|
|
3089
|
-
jsonrpc: '2.0',
|
|
3090
|
-
id: initId,
|
|
3091
|
-
method: 'ui/initialize',
|
|
3092
|
-
params: {
|
|
3093
|
-
appInfo: { name: '${photonName}', version: '1.0.0' },
|
|
3094
|
-
appCapabilities: {},
|
|
3095
|
-
protocolVersion: '2026-01-26'
|
|
3096
|
-
}
|
|
3097
|
-
});
|
|
3098
|
-
})();
|
|
3099
|
-
</script>`;
|
|
3100
|
-
}
|
|
3101
|
-
/**
|
|
3102
|
-
* Handle photon:// asset read (Beam format)
|
|
3103
|
-
*/
|
|
3104
|
-
async handleAssetRead(uri, assetMatch) {
|
|
3105
|
-
const [, _photonName, assetType, assetId] = assetMatch;
|
|
3106
|
-
let resolvedPath;
|
|
3107
|
-
let mimeType = 'text/plain';
|
|
3108
|
-
if (assetType === 'ui') {
|
|
3109
|
-
const ui = this.mcp.assets.ui.find((u) => u.id === assetId);
|
|
3110
|
-
if (ui) {
|
|
3111
|
-
resolvedPath = ui.resolvedPath;
|
|
3112
|
-
mimeType = ui.mimeType || 'text/html;profile=mcp-app';
|
|
3113
|
-
}
|
|
3114
|
-
}
|
|
3115
|
-
else if (assetType === 'prompts') {
|
|
3116
|
-
const prompt = this.mcp.assets.prompts.find((p) => p.id === assetId);
|
|
3117
|
-
if (prompt) {
|
|
3118
|
-
resolvedPath = prompt.resolvedPath;
|
|
3119
|
-
mimeType = 'text/markdown';
|
|
3120
|
-
}
|
|
3121
|
-
}
|
|
3122
|
-
else if (assetType === 'resources') {
|
|
3123
|
-
const resource = this.mcp.assets.resources.find((r) => r.id === assetId);
|
|
3124
|
-
if (resource) {
|
|
3125
|
-
resolvedPath = resource.resolvedPath;
|
|
3126
|
-
mimeType = resource.mimeType || 'application/octet-stream';
|
|
3127
|
-
}
|
|
3128
|
-
}
|
|
3129
|
-
if (resolvedPath) {
|
|
3130
|
-
let content = await readText(resolvedPath);
|
|
3131
|
-
// Inject MCP Apps bridge for UI assets
|
|
3132
|
-
if (assetType === 'ui') {
|
|
3133
|
-
const bridgeScript = this.generateMcpAppsBridge();
|
|
3134
|
-
content = content.replace('<head>', `<head>\n${bridgeScript}`);
|
|
3135
|
-
}
|
|
3136
|
-
return {
|
|
3137
|
-
contents: [{ uri, mimeType, text: content }],
|
|
3138
|
-
};
|
|
3139
|
-
}
|
|
3140
|
-
throw new Error(`Asset not found: ${uri}`);
|
|
3141
|
-
}
|
|
3142
|
-
/**
|
|
3143
|
-
* Handle static resource read (for both stdio and SSE handlers)
|
|
3144
|
-
*/
|
|
3145
|
-
async handleStaticRead(uri) {
|
|
3146
|
-
const static_ = this.mcp.statics.find((s) => s.uri === uri || this.matchUriPattern(s.uri, uri));
|
|
3147
|
-
if (!static_) {
|
|
3148
|
-
throw new Error(`Resource not found: ${uri}`);
|
|
3149
|
-
}
|
|
3150
|
-
const params = this.parseUriParams(static_.uri, uri);
|
|
3151
|
-
const result = await this.loader.executeTool(this.mcp, static_.name, params);
|
|
3152
|
-
return this.formatStaticResult(result, static_.mimeType);
|
|
3153
2288
|
}
|
|
3154
2289
|
/**
|
|
3155
2290
|
* Stop the server
|
|
@@ -3165,15 +2300,7 @@ export class PhotonServer {
|
|
|
3165
2300
|
await this.mcpClientFactory.disconnect();
|
|
3166
2301
|
}
|
|
3167
2302
|
// Unsubscribe daemon channels
|
|
3168
|
-
|
|
3169
|
-
try {
|
|
3170
|
-
unsubscribe();
|
|
3171
|
-
}
|
|
3172
|
-
catch {
|
|
3173
|
-
/* ignore */
|
|
3174
|
-
}
|
|
3175
|
-
}
|
|
3176
|
-
this.channelUnsubscribers = [];
|
|
2303
|
+
this.channelManager.cleanup();
|
|
3177
2304
|
// Close SSE sessions — snapshot to avoid live-iterator + await issues
|
|
3178
2305
|
for (const session of Array.from(this.sseSessions.values())) {
|
|
3179
2306
|
await session.server.close();
|
|
@@ -3226,12 +2353,15 @@ export class PhotonServer {
|
|
|
3226
2353
|
};
|
|
3227
2354
|
}
|
|
3228
2355
|
handleStatusStream(_req, res) {
|
|
3229
|
-
|
|
2356
|
+
const ssHeaders = {
|
|
3230
2357
|
'Content-Type': 'text/event-stream',
|
|
3231
2358
|
'Cache-Control': 'no-cache',
|
|
3232
2359
|
Connection: 'keep-alive',
|
|
3233
|
-
|
|
3234
|
-
|
|
2360
|
+
};
|
|
2361
|
+
const origin = getCorsOrigin(_req);
|
|
2362
|
+
if (origin)
|
|
2363
|
+
ssHeaders['Access-Control-Allow-Origin'] = origin;
|
|
2364
|
+
res.writeHead(200, ssHeaders);
|
|
3235
2365
|
res.write(`data: ${JSON.stringify(this.buildStatusSnapshot())}\n\n`);
|
|
3236
2366
|
this.statusClients.add(res);
|
|
3237
2367
|
const cleanup = () => {
|