@portel/photon 1.33.0 → 1.33.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -6
- package/dist/auth/mcp-jwt.d.ts +59 -0
- package/dist/auth/mcp-jwt.d.ts.map +1 -0
- package/dist/auth/mcp-jwt.js +177 -0
- package/dist/auth/mcp-jwt.js.map +1 -0
- package/dist/auto-ui/beam.d.ts +1 -0
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +35 -1
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/frontend/pure-view.html +5 -2
- package/dist/auto-ui/playground-html.d.ts.map +1 -1
- package/dist/auto-ui/playground-html.js +28 -38
- package/dist/auto-ui/playground-html.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +62 -11
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/beam.bundle.js +25 -17
- package/dist/beam.bundle.js.map +2 -2
- package/dist/capability-negotiator.d.ts +11 -0
- package/dist/capability-negotiator.d.ts.map +1 -1
- package/dist/capability-negotiator.js +20 -0
- package/dist/capability-negotiator.js.map +1 -1
- package/dist/cli/commands/auth.d.ts +15 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +105 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/host.d.ts.map +1 -1
- package/dist/cli/commands/host.js +9 -0
- package/dist/cli/commands/host.js.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +3 -0
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +6 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/worker-dep-proxy.d.ts +17 -0
- package/dist/daemon/worker-dep-proxy.d.ts.map +1 -0
- package/dist/daemon/worker-dep-proxy.js +92 -0
- package/dist/daemon/worker-dep-proxy.js.map +1 -0
- package/dist/daemon/worker-host.js +8 -28
- package/dist/daemon/worker-host.js.map +1 -1
- package/dist/deploy/cloudflare.d.ts +2 -0
- package/dist/deploy/cloudflare.d.ts.map +1 -1
- package/dist/deploy/cloudflare.js +135 -13
- package/dist/deploy/cloudflare.js.map +1 -1
- 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 +3 -0
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +49 -0
- package/dist/loader.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 +13 -0
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/resource-server.d.ts +15 -0
- package/dist/resource-server.d.ts.map +1 -1
- package/dist/resource-server.js +86 -5
- package/dist/resource-server.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +168 -176
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/templates/cloudflare/worker.ts.template +340 -55
package/dist/server.js
CHANGED
|
@@ -26,11 +26,27 @@ import { pingDaemon } from './daemon/client.js';
|
|
|
26
26
|
import { ensureDaemon } from './daemon/manager.js';
|
|
27
27
|
import { ChannelManager, } from './channel-manager.js';
|
|
28
28
|
import { PhotonDocExtractor } from './photon-doc-extractor.js';
|
|
29
|
-
import { isLocalRequest,
|
|
29
|
+
import { isLocalRequest, setSecurityHeaders, getCorsOrigin } from './shared/security.js';
|
|
30
30
|
import { audit } from './shared/audit.js';
|
|
31
31
|
import { TaskExecutor } from './task-executor.js';
|
|
32
32
|
import { CapabilityNegotiator } from './capability-negotiator.js';
|
|
33
33
|
import { ResourceServer, SubscriptionRegistry, } from './resource-server.js';
|
|
34
|
+
import { verifyPhotonAuthToken } from './auth/mcp-jwt.js';
|
|
35
|
+
import { loadPhotonAuth } from './cli/commands/auth.js';
|
|
36
|
+
let cachedJwtProfile = null;
|
|
37
|
+
async function loadJwtProfile(name) {
|
|
38
|
+
if (cachedJwtProfile && cachedJwtProfile.name === name) {
|
|
39
|
+
return { issuer: cachedJwtProfile.issuer, jwks: cachedJwtProfile.jwks };
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const loaded = await loadPhotonAuth(name);
|
|
43
|
+
cachedJwtProfile = { name, issuer: loaded.issuer.issuer, jwks: loaded.jwks };
|
|
44
|
+
return { issuer: loaded.issuer.issuer, jwks: loaded.jwks };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
34
50
|
export class HotReloadDisabledError extends Error {
|
|
35
51
|
constructor(message) {
|
|
36
52
|
super(message);
|
|
@@ -209,6 +225,38 @@ function findFreePort(preferred = 0) {
|
|
|
209
225
|
srv.on('error', reject);
|
|
210
226
|
});
|
|
211
227
|
}
|
|
228
|
+
function localMcpAuthMode() {
|
|
229
|
+
return process.env.PHOTON_MCP_AUTH_MODE || (process.env.PHOTON_MCP_BEARER ? 'bearer' : 'legacy');
|
|
230
|
+
}
|
|
231
|
+
function authHeaderToken(req) {
|
|
232
|
+
const header = req.headers.authorization ?? '';
|
|
233
|
+
const match = Array.isArray(header)
|
|
234
|
+
? header[0]?.match(/^Bearer\s+(.+)$/i)
|
|
235
|
+
: header.match(/^Bearer\s+(.+)$/i);
|
|
236
|
+
return match ? match[1].trim() : null;
|
|
237
|
+
}
|
|
238
|
+
function unauthorizedJson(res, id, status, code, message, reason, wwwAuthenticate, corsOrigin) {
|
|
239
|
+
const headers = {
|
|
240
|
+
'Content-Type': 'application/json',
|
|
241
|
+
'WWW-Authenticate': wwwAuthenticate,
|
|
242
|
+
};
|
|
243
|
+
if (corsOrigin)
|
|
244
|
+
headers['Access-Control-Allow-Origin'] = corsOrigin;
|
|
245
|
+
res.writeHead(status, headers);
|
|
246
|
+
res.end(JSON.stringify({
|
|
247
|
+
jsonrpc: '2.0',
|
|
248
|
+
id: id ?? null,
|
|
249
|
+
error: { code, message, data: { reason } },
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
function mcpTokenMatches(actual, expected) {
|
|
253
|
+
if (!actual || !expected)
|
|
254
|
+
return false;
|
|
255
|
+
const actualBytes = Buffer.from(actual);
|
|
256
|
+
const expectedBytes = Buffer.from(expected);
|
|
257
|
+
return (actualBytes.length === expectedBytes.length &&
|
|
258
|
+
crypto.timingSafeEqual(actualBytes, expectedBytes));
|
|
259
|
+
}
|
|
212
260
|
class BeamCompatTransport {
|
|
213
261
|
photonName;
|
|
214
262
|
photonMeta;
|
|
@@ -220,6 +268,8 @@ class BeamCompatTransport {
|
|
|
220
268
|
pendingResponse = null;
|
|
221
269
|
/** Sub-photons whose tools are injected into tools/list alongside the main photon. */
|
|
222
270
|
subPhotons = [];
|
|
271
|
+
/** Main photon tools, mirrored so auth can enforce scopes before dispatch. */
|
|
272
|
+
mainTools = [];
|
|
223
273
|
/** Callback to execute a tool on a sub-photon by name. */
|
|
224
274
|
subPhotonExecutor;
|
|
225
275
|
constructor(photonName, photonMeta) {
|
|
@@ -237,12 +287,14 @@ class BeamCompatTransport {
|
|
|
237
287
|
this.onclose?.();
|
|
238
288
|
}
|
|
239
289
|
async send(message) {
|
|
240
|
-
// Transform outgoing tools/list responses
|
|
290
|
+
// Transform outgoing tools/list responses with Photon metadata. Main photon
|
|
291
|
+
// tools stay slashless for broad client compatibility; aggregated tools use
|
|
292
|
+
// dot names (`photon.method`) while tools/call still accepts legacy slashes.
|
|
241
293
|
if (message?.result?.tools && Array.isArray(message.result.tools)) {
|
|
242
294
|
// Main photon tools
|
|
243
295
|
message.result.tools = message.result.tools.map((tool) => ({
|
|
244
296
|
...tool,
|
|
245
|
-
name:
|
|
297
|
+
name: tool.name,
|
|
246
298
|
'x-photon-id': this.photonName,
|
|
247
299
|
'x-photon-description': this.photonMeta.description || '',
|
|
248
300
|
'x-photon-icon': this.photonMeta.icon || '⚡',
|
|
@@ -260,7 +312,7 @@ class BeamCompatTransport {
|
|
|
260
312
|
const subTools = sub.tools.map((tool) => {
|
|
261
313
|
const def = {
|
|
262
314
|
...tool,
|
|
263
|
-
name: `${sub.name}
|
|
315
|
+
name: `${sub.name}.${tool.name}`,
|
|
264
316
|
'x-photon-id': sub.name,
|
|
265
317
|
'x-photon-description': sub.description,
|
|
266
318
|
'x-photon-icon': sub.icon,
|
|
@@ -300,6 +352,20 @@ class BeamCompatTransport {
|
|
|
300
352
|
this.sseResponse.write(`event: message\nid: ${id}\ndata: ${JSON.stringify(message)}\n\n`);
|
|
301
353
|
}
|
|
302
354
|
}
|
|
355
|
+
requiredScopesForTool(fullToolName) {
|
|
356
|
+
const dotIdx = fullToolName.indexOf('.');
|
|
357
|
+
const slashIdx = fullToolName.indexOf('/');
|
|
358
|
+
const separatorIdx = dotIdx !== -1 ? dotIdx : slashIdx;
|
|
359
|
+
const targetPhoton = separatorIdx === -1 ? this.photonName : fullToolName.slice(0, separatorIdx);
|
|
360
|
+
const methodName = separatorIdx === -1 ? fullToolName : fullToolName.slice(separatorIdx + 1);
|
|
361
|
+
const tools = targetPhoton === this.photonName
|
|
362
|
+
? this.mainTools
|
|
363
|
+
: this.subPhotons.find((sub) => sub.name === targetPhoton)?.tools;
|
|
364
|
+
const scopes = tools?.find((tool) => tool.name === methodName)?.scopes;
|
|
365
|
+
return Array.isArray(scopes)
|
|
366
|
+
? scopes.filter((scope) => typeof scope === 'string')
|
|
367
|
+
: [];
|
|
368
|
+
}
|
|
303
369
|
async handleHTTP(req, res, url) {
|
|
304
370
|
const corsOrigin = getCorsOrigin(req);
|
|
305
371
|
// GET — open SSE stream for server-to-client notifications
|
|
@@ -357,13 +423,75 @@ class BeamCompatTransport {
|
|
|
357
423
|
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
358
424
|
return;
|
|
359
425
|
}
|
|
360
|
-
|
|
426
|
+
const authMode = localMcpAuthMode();
|
|
427
|
+
const dispatchesUserCode = parsed?.method === 'tools/call';
|
|
428
|
+
const requiredScopes = dispatchesUserCode && typeof parsed?.params?.name === 'string'
|
|
429
|
+
? this.requiredScopesForTool(parsed.params.name)
|
|
430
|
+
: [];
|
|
431
|
+
let jwtClaims;
|
|
432
|
+
if (dispatchesUserCode && authMode === 'jwt') {
|
|
433
|
+
let jwks = null;
|
|
434
|
+
let issuer;
|
|
435
|
+
const profileName = process.env.PHOTON_MCP_JWT_PROFILE;
|
|
436
|
+
if (profileName) {
|
|
437
|
+
const profile = await loadJwtProfile(profileName);
|
|
438
|
+
if (profile) {
|
|
439
|
+
issuer = profile.issuer;
|
|
440
|
+
jwks = profile.jwks;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
try {
|
|
445
|
+
jwks = process.env.PHOTON_MCP_JWT_JWKS
|
|
446
|
+
? JSON.parse(process.env.PHOTON_MCP_JWT_JWKS)
|
|
447
|
+
: null;
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
jwks = null;
|
|
451
|
+
}
|
|
452
|
+
issuer = process.env.PHOTON_MCP_JWT_ISSUER;
|
|
453
|
+
}
|
|
454
|
+
const audience = process.env.PHOTON_MCP_JWT_AUDIENCE;
|
|
455
|
+
if (!issuer || !audience || !jwks) {
|
|
456
|
+
unauthorizedJson(res, parsed.id, 401, -32001, 'Unauthorized', 'missing_token', 'Bearer realm="photon", error="invalid_token"', corsOrigin);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const result = verifyPhotonAuthToken(authHeaderToken(req), {
|
|
460
|
+
issuer,
|
|
461
|
+
audience,
|
|
462
|
+
jwks,
|
|
463
|
+
requiredScopes,
|
|
464
|
+
});
|
|
465
|
+
if (!result.ok) {
|
|
466
|
+
const insufficientScope = result.reason === 'insufficient_scope';
|
|
467
|
+
unauthorizedJson(res, parsed.id, insufficientScope ? 403 : 401, insufficientScope ? -32003 : -32001, insufficientScope ? 'Forbidden' : 'Unauthorized', result.reason, insufficientScope
|
|
468
|
+
? `Bearer realm="photon", error="insufficient_scope", scope="${requiredScopes.join(' ')}"`
|
|
469
|
+
: 'Bearer realm="photon", error="invalid_token"', corsOrigin);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
jwtClaims = result.claims;
|
|
473
|
+
}
|
|
474
|
+
else if (dispatchesUserCode && authMode === 'bearer') {
|
|
475
|
+
const expected = process.env.PHOTON_MCP_BEARER;
|
|
476
|
+
const token = authHeaderToken(req);
|
|
477
|
+
if (!mcpTokenMatches(token, expected)) {
|
|
478
|
+
unauthorizedJson(res, parsed.id, 401, -32001, 'Unauthorized', !expected
|
|
479
|
+
? 'Authorization: Bearer <token> header missing'
|
|
480
|
+
: token
|
|
481
|
+
? 'bearer token does not match PHOTON_MCP_BEARER'
|
|
482
|
+
: 'Authorization: Bearer <token> header missing', 'Bearer realm="photon"', corsOrigin);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Transform incoming tools/call: strip photonName.method prefix from tool name
|
|
361
487
|
// and route sub-photon calls directly
|
|
362
488
|
if (parsed.method === 'tools/call' && parsed.params?.name) {
|
|
489
|
+
const dotIdx = parsed.params.name.indexOf('.');
|
|
363
490
|
const slashIdx = parsed.params.name.indexOf('/');
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const
|
|
491
|
+
const separatorIdx = dotIdx !== -1 ? dotIdx : slashIdx;
|
|
492
|
+
if (separatorIdx !== -1) {
|
|
493
|
+
const targetPhoton = parsed.params.name.slice(0, separatorIdx);
|
|
494
|
+
const methodName = parsed.params.name.slice(separatorIdx + 1);
|
|
367
495
|
// Check if this is a sub-photon call
|
|
368
496
|
if (targetPhoton !== this.photonName && this.subPhotonExecutor) {
|
|
369
497
|
const sub = this.subPhotons.find((s) => s.name === targetPhoton);
|
|
@@ -411,11 +539,16 @@ class BeamCompatTransport {
|
|
|
411
539
|
// claims ride on the SDK's MessageExtraInfo.authInfo.extra, which
|
|
412
540
|
// the protocol layer propagates to setRequestHandler's `extra` arg.
|
|
413
541
|
const { extractClaimsFromHeaders } = await import('./shared/extract-claims.js');
|
|
414
|
-
const claims = extractClaimsFromHeaders(req.headers);
|
|
542
|
+
const claims = jwtClaims ?? extractClaimsFromHeaders(req.headers);
|
|
415
543
|
const messageExtra = claims
|
|
416
544
|
? {
|
|
417
545
|
sessionId: this.sessionId,
|
|
418
|
-
authInfo: {
|
|
546
|
+
authInfo: {
|
|
547
|
+
token: '',
|
|
548
|
+
clientId: typeof claims.client_id === 'string' ? claims.client_id : '',
|
|
549
|
+
scopes: typeof claims.scope === 'string' ? claims.scope.split(/\s+/) : [],
|
|
550
|
+
extra: claims,
|
|
551
|
+
},
|
|
419
552
|
}
|
|
420
553
|
: { sessionId: this.sessionId };
|
|
421
554
|
// Notifications have no id — fire-and-forget
|
|
@@ -1011,8 +1144,12 @@ export class PhotonServer {
|
|
|
1011
1144
|
const notice = typeof deprecated === 'string' ? deprecated : 'This tool is deprecated.';
|
|
1012
1145
|
description = `[DEPRECATED: ${notice}] ${description}`;
|
|
1013
1146
|
}
|
|
1147
|
+
const slashlessName = tool.name.includes('/') ? tool.name.split('/').pop() : tool.name;
|
|
1148
|
+
const toolName = slashlessName.includes('.')
|
|
1149
|
+
? slashlessName.split('.').pop()
|
|
1150
|
+
: slashlessName;
|
|
1014
1151
|
const toolDef = {
|
|
1015
|
-
name:
|
|
1152
|
+
name: toolName,
|
|
1016
1153
|
description,
|
|
1017
1154
|
inputSchema: JSON.parse(JSON.stringify(tool.inputSchema)),
|
|
1018
1155
|
};
|
|
@@ -1045,6 +1182,8 @@ export class PhotonServer {
|
|
|
1045
1182
|
toolDef['x-output-format'] = schema.outputFormat;
|
|
1046
1183
|
if (schema.layoutHints)
|
|
1047
1184
|
toolDef['x-layout-hints'] = schema.layoutHints;
|
|
1185
|
+
if (schema.scopes)
|
|
1186
|
+
toolDef.scopes = schema.scopes;
|
|
1048
1187
|
if (schema.buttonLabel)
|
|
1049
1188
|
toolDef['x-button-label'] = schema.buttonLabel;
|
|
1050
1189
|
if (schema.icon)
|
|
@@ -1307,11 +1446,22 @@ export class PhotonServer {
|
|
|
1307
1446
|
const tool = targetMcp.tools.find((t) => t.name === toolName);
|
|
1308
1447
|
const outputFormat = tool?.outputFormat;
|
|
1309
1448
|
const startTime = Date.now();
|
|
1449
|
+
const claims = extra?.authInfo?.extra;
|
|
1450
|
+
const caller = claims && typeof claims.sub === 'string'
|
|
1451
|
+
? {
|
|
1452
|
+
id: claims.sub,
|
|
1453
|
+
name: typeof claims.name === 'string' ? claims.name : undefined,
|
|
1454
|
+
anonymous: false,
|
|
1455
|
+
scope: typeof claims.scope === 'string' ? claims.scope : undefined,
|
|
1456
|
+
claims,
|
|
1457
|
+
}
|
|
1458
|
+
: undefined;
|
|
1310
1459
|
const result = await this.loader.executeTool(targetMcp, toolName, args || {}, {
|
|
1311
1460
|
inputProvider,
|
|
1312
1461
|
outputHandler,
|
|
1313
1462
|
samplingProvider,
|
|
1314
1463
|
roots: this.rootsByServer.get(ctx.server),
|
|
1464
|
+
caller,
|
|
1315
1465
|
});
|
|
1316
1466
|
const durationMs = Date.now() - startTime;
|
|
1317
1467
|
const transport = this.options.transport || 'stdio';
|
|
@@ -2313,6 +2463,12 @@ export class PhotonServer {
|
|
|
2313
2463
|
});
|
|
2314
2464
|
this.capabilityNegotiator.interceptTransportForRawCapabilities(beamTransport, this.server, (msg) => this.channelManager.interceptPermissionRequest(msg));
|
|
2315
2465
|
await this.server.connect(beamTransport);
|
|
2466
|
+
beamTransport.mainTools = (this.mcp?.tools || [])
|
|
2467
|
+
.filter((t) => !t.internal)
|
|
2468
|
+
.map((t) => ({
|
|
2469
|
+
name: t.name,
|
|
2470
|
+
scopes: Array.isArray(t.scopes) ? t.scopes : [],
|
|
2471
|
+
}));
|
|
2316
2472
|
// Wire sub-photons: collect all loaded photons except the main one
|
|
2317
2473
|
const mainName = this.mcp?.name || 'photon';
|
|
2318
2474
|
const allLoaded = this.loader.getLoadedPhotons();
|
|
@@ -2336,6 +2492,7 @@ export class PhotonServer {
|
|
|
2336
2492
|
name: t.name,
|
|
2337
2493
|
description: t.description || '',
|
|
2338
2494
|
inputSchema: t.inputSchema,
|
|
2495
|
+
...(Array.isArray(t.scopes) ? { scopes: t.scopes } : {}),
|
|
2339
2496
|
...(linkedUI ? { linkedUi: linkedUI.id } : {}),
|
|
2340
2497
|
};
|
|
2341
2498
|
});
|
|
@@ -2465,30 +2622,6 @@ export class PhotonServer {
|
|
|
2465
2622
|
}
|
|
2466
2623
|
return;
|
|
2467
2624
|
}
|
|
2468
|
-
// API: List tools (for compatibility, now returns current photon)
|
|
2469
|
-
if (req.method === 'GET' && url.pathname === '/api/tools') {
|
|
2470
|
-
const toolHeaders = { 'Content-Type': 'application/json' };
|
|
2471
|
-
if (corsOrigin)
|
|
2472
|
-
toolHeaders['Access-Control-Allow-Origin'] = corsOrigin;
|
|
2473
|
-
res.writeHead(200, toolHeaders);
|
|
2474
|
-
const tools = this.mcp?.tools.map((tool) => {
|
|
2475
|
-
const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name || u.linkedTools?.includes(tool.name));
|
|
2476
|
-
return {
|
|
2477
|
-
name: tool.name,
|
|
2478
|
-
description: tool.description,
|
|
2479
|
-
inputSchema: JSON.parse(JSON.stringify(tool.inputSchema)),
|
|
2480
|
-
ui: linkedUI
|
|
2481
|
-
? { id: linkedUI.id, uri: `ui://${this.mcp.name}/${linkedUI.id}` }
|
|
2482
|
-
: null,
|
|
2483
|
-
};
|
|
2484
|
-
}) || [];
|
|
2485
|
-
// Resolve @choice-from fields
|
|
2486
|
-
for (const tool of tools) {
|
|
2487
|
-
await this.resolveChoiceFromFields(tool);
|
|
2488
|
-
}
|
|
2489
|
-
res.end(JSON.stringify({ tools }));
|
|
2490
|
-
return;
|
|
2491
|
-
}
|
|
2492
2625
|
if (req.method === 'GET' && url.pathname === '/api/status') {
|
|
2493
2626
|
const statusHeaders = { 'Content-Type': 'application/json' };
|
|
2494
2627
|
if (corsOrigin)
|
|
@@ -2502,147 +2635,6 @@ export class PhotonServer {
|
|
|
2502
2635
|
return;
|
|
2503
2636
|
}
|
|
2504
2637
|
}
|
|
2505
|
-
// API: Call tool
|
|
2506
|
-
if (req.method === 'POST' && url.pathname === '/api/call') {
|
|
2507
|
-
// Security: restrict CORS to localhost and require local request
|
|
2508
|
-
if (corsOrigin)
|
|
2509
|
-
res.setHeader('Access-Control-Allow-Origin', corsOrigin);
|
|
2510
|
-
res.setHeader('Content-Type', 'application/json');
|
|
2511
|
-
if (!isLocalRequest(req)) {
|
|
2512
|
-
res.writeHead(403);
|
|
2513
|
-
res.end(JSON.stringify({ success: false, error: 'Forbidden: non-local request' }));
|
|
2514
|
-
return;
|
|
2515
|
-
}
|
|
2516
|
-
if (!this.mcp) {
|
|
2517
|
-
res.writeHead(503);
|
|
2518
|
-
res.end(JSON.stringify({ success: false, error: 'Photon not loaded' }));
|
|
2519
|
-
return;
|
|
2520
|
-
}
|
|
2521
|
-
try {
|
|
2522
|
-
const body = await readBody(req);
|
|
2523
|
-
const { tool, args } = JSON.parse(body);
|
|
2524
|
-
const result = await this.loader.executeTool(this.mcp, tool, args || {});
|
|
2525
|
-
const isStateful = result && typeof result === 'object' && result._stateful === true;
|
|
2526
|
-
res.writeHead(200);
|
|
2527
|
-
res.end(JSON.stringify({
|
|
2528
|
-
success: true,
|
|
2529
|
-
data: isStateful ? result.result : result,
|
|
2530
|
-
}));
|
|
2531
|
-
}
|
|
2532
|
-
catch (error) {
|
|
2533
|
-
const status = error.message?.includes('too large') ? 413 : 500;
|
|
2534
|
-
res.writeHead(status);
|
|
2535
|
-
res.end(JSON.stringify({ success: false, error: getErrorMessage(error) }));
|
|
2536
|
-
}
|
|
2537
|
-
return;
|
|
2538
|
-
}
|
|
2539
|
-
// API: Call tool with streaming progress (SSE)
|
|
2540
|
-
if (req.method === 'POST' && url.pathname === '/api/call-stream') {
|
|
2541
|
-
if (corsOrigin)
|
|
2542
|
-
res.setHeader('Access-Control-Allow-Origin', corsOrigin);
|
|
2543
|
-
res.setHeader('Content-Type', 'text/event-stream');
|
|
2544
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
2545
|
-
res.setHeader('Connection', 'keep-alive');
|
|
2546
|
-
if (!this.mcp) {
|
|
2547
|
-
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
2548
|
-
res.end(JSON.stringify({ success: false, error: 'Photon not loaded' }));
|
|
2549
|
-
return;
|
|
2550
|
-
}
|
|
2551
|
-
let body = '';
|
|
2552
|
-
req.on('data', (chunk) => (body += chunk));
|
|
2553
|
-
req.on('end', () => {
|
|
2554
|
-
void (async () => {
|
|
2555
|
-
let requestId = `run_${Date.now()}`;
|
|
2556
|
-
const sendMessage = (message) => {
|
|
2557
|
-
res.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`);
|
|
2558
|
-
};
|
|
2559
|
-
try {
|
|
2560
|
-
const payload = JSON.parse(body || '{}');
|
|
2561
|
-
const tool = payload.tool;
|
|
2562
|
-
if (!tool) {
|
|
2563
|
-
throw new Error('Tool name is required');
|
|
2564
|
-
}
|
|
2565
|
-
const args = payload.args || {};
|
|
2566
|
-
const progressToken = payload.progressToken ?? `progress_${Date.now()}`;
|
|
2567
|
-
requestId = payload.requestId || requestId;
|
|
2568
|
-
const sendNotification = (method, params) => {
|
|
2569
|
-
sendMessage({ jsonrpc: '2.0', method, params });
|
|
2570
|
-
};
|
|
2571
|
-
const reportProgress = (emit) => {
|
|
2572
|
-
const rawValue = typeof emit?.value === 'number' ? emit.value : 0;
|
|
2573
|
-
const percent = rawValue <= 1 ? rawValue * 100 : rawValue;
|
|
2574
|
-
const payload = emit?.value ?? emit?.data;
|
|
2575
|
-
sendNotification('notifications/progress', {
|
|
2576
|
-
progressToken,
|
|
2577
|
-
progress: percent,
|
|
2578
|
-
total: 100,
|
|
2579
|
-
message: emit?.message || null,
|
|
2580
|
-
...(payload !== undefined && typeof payload !== 'number'
|
|
2581
|
-
? { data: payload }
|
|
2582
|
-
: {}),
|
|
2583
|
-
});
|
|
2584
|
-
};
|
|
2585
|
-
const outputHandler = (emit) => {
|
|
2586
|
-
if (!emit)
|
|
2587
|
-
return;
|
|
2588
|
-
if (emit.emit === 'progress') {
|
|
2589
|
-
reportProgress(emit);
|
|
2590
|
-
}
|
|
2591
|
-
else if (emit.emit === 'status') {
|
|
2592
|
-
sendNotification('notifications/status', {
|
|
2593
|
-
type: emit.type || 'info',
|
|
2594
|
-
message: emit.message || '',
|
|
2595
|
-
});
|
|
2596
|
-
}
|
|
2597
|
-
else if (emit.emit === 'render') {
|
|
2598
|
-
sendNotification('notifications/render', {
|
|
2599
|
-
format: emit.format,
|
|
2600
|
-
value: emit.value,
|
|
2601
|
-
});
|
|
2602
|
-
}
|
|
2603
|
-
else if (emit.emit === 'render:clear') {
|
|
2604
|
-
sendNotification('notifications/render', { clear: true });
|
|
2605
|
-
}
|
|
2606
|
-
else {
|
|
2607
|
-
sendNotification('notifications/emit', { event: emit });
|
|
2608
|
-
}
|
|
2609
|
-
// Forward channel events to daemon for cross-process pub/sub
|
|
2610
|
-
this.channelManager.publishIfChannel(emit);
|
|
2611
|
-
};
|
|
2612
|
-
sendNotification('notifications/status', {
|
|
2613
|
-
type: 'info',
|
|
2614
|
-
message: `Starting ${tool}`,
|
|
2615
|
-
});
|
|
2616
|
-
const result = await this.loader.executeTool(this.mcp, tool, args, {
|
|
2617
|
-
outputHandler,
|
|
2618
|
-
});
|
|
2619
|
-
const isStateful = result && typeof result === 'object' && result._stateful === true;
|
|
2620
|
-
sendMessage({
|
|
2621
|
-
jsonrpc: '2.0',
|
|
2622
|
-
id: requestId,
|
|
2623
|
-
result: {
|
|
2624
|
-
success: true,
|
|
2625
|
-
data: isStateful ? result.result : result,
|
|
2626
|
-
},
|
|
2627
|
-
});
|
|
2628
|
-
res.end();
|
|
2629
|
-
}
|
|
2630
|
-
catch (error) {
|
|
2631
|
-
const message = getErrorMessage(error);
|
|
2632
|
-
const errorPayload = {
|
|
2633
|
-
jsonrpc: '2.0',
|
|
2634
|
-
error: { code: -32000, message },
|
|
2635
|
-
};
|
|
2636
|
-
if (requestId) {
|
|
2637
|
-
errorPayload.id = requestId;
|
|
2638
|
-
}
|
|
2639
|
-
sendMessage(errorPayload);
|
|
2640
|
-
res.end();
|
|
2641
|
-
}
|
|
2642
|
-
})();
|
|
2643
|
-
});
|
|
2644
|
-
return;
|
|
2645
|
-
}
|
|
2646
2638
|
// API: Get UI template (and directory-style siblings for SPA bundles).
|
|
2647
2639
|
//
|
|
2648
2640
|
// Two shapes:
|