@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.
Files changed (65) hide show
  1. package/README.md +43 -6
  2. package/dist/auth/mcp-jwt.d.ts +59 -0
  3. package/dist/auth/mcp-jwt.d.ts.map +1 -0
  4. package/dist/auth/mcp-jwt.js +177 -0
  5. package/dist/auth/mcp-jwt.js.map +1 -0
  6. package/dist/auto-ui/beam.d.ts +1 -0
  7. package/dist/auto-ui/beam.d.ts.map +1 -1
  8. package/dist/auto-ui/beam.js +35 -1
  9. package/dist/auto-ui/beam.js.map +1 -1
  10. package/dist/auto-ui/frontend/pure-view.html +5 -2
  11. package/dist/auto-ui/playground-html.d.ts.map +1 -1
  12. package/dist/auto-ui/playground-html.js +28 -38
  13. package/dist/auto-ui/playground-html.js.map +1 -1
  14. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  15. package/dist/auto-ui/streamable-http-transport.js +62 -11
  16. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  17. package/dist/beam.bundle.js +25 -17
  18. package/dist/beam.bundle.js.map +2 -2
  19. package/dist/capability-negotiator.d.ts +11 -0
  20. package/dist/capability-negotiator.d.ts.map +1 -1
  21. package/dist/capability-negotiator.js +20 -0
  22. package/dist/capability-negotiator.js.map +1 -1
  23. package/dist/cli/commands/auth.d.ts +15 -0
  24. package/dist/cli/commands/auth.d.ts.map +1 -0
  25. package/dist/cli/commands/auth.js +105 -0
  26. package/dist/cli/commands/auth.js.map +1 -0
  27. package/dist/cli/commands/host.d.ts.map +1 -1
  28. package/dist/cli/commands/host.js +9 -0
  29. package/dist/cli/commands/host.js.map +1 -1
  30. package/dist/cli/commands/run.d.ts.map +1 -1
  31. package/dist/cli/commands/run.js +3 -0
  32. package/dist/cli/commands/run.js.map +1 -1
  33. package/dist/cli/index.d.ts.map +1 -1
  34. package/dist/cli/index.js +6 -0
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/daemon/worker-dep-proxy.d.ts +17 -0
  37. package/dist/daemon/worker-dep-proxy.d.ts.map +1 -0
  38. package/dist/daemon/worker-dep-proxy.js +92 -0
  39. package/dist/daemon/worker-dep-proxy.js.map +1 -0
  40. package/dist/daemon/worker-host.js +8 -28
  41. package/dist/daemon/worker-host.js.map +1 -1
  42. package/dist/deploy/cloudflare.d.ts +2 -0
  43. package/dist/deploy/cloudflare.d.ts.map +1 -1
  44. package/dist/deploy/cloudflare.js +135 -13
  45. package/dist/deploy/cloudflare.js.map +1 -1
  46. package/dist/editor-support/docblock-tag-catalog.d.ts.map +1 -1
  47. package/dist/editor-support/docblock-tag-catalog.js +6 -0
  48. package/dist/editor-support/docblock-tag-catalog.js.map +1 -1
  49. package/dist/loader.d.ts +3 -0
  50. package/dist/loader.d.ts.map +1 -1
  51. package/dist/loader.js +49 -0
  52. package/dist/loader.js.map +1 -1
  53. package/dist/photon-doc-extractor.d.ts +1 -0
  54. package/dist/photon-doc-extractor.d.ts.map +1 -1
  55. package/dist/photon-doc-extractor.js +13 -0
  56. package/dist/photon-doc-extractor.js.map +1 -1
  57. package/dist/resource-server.d.ts +15 -0
  58. package/dist/resource-server.d.ts.map +1 -1
  59. package/dist/resource-server.js +86 -5
  60. package/dist/resource-server.js.map +1 -1
  61. package/dist/server.d.ts.map +1 -1
  62. package/dist/server.js +168 -176
  63. package/dist/server.js.map +1 -1
  64. package/package.json +1 -1
  65. 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, readBody, setSecurityHeaders, getCorsOrigin } from './shared/security.js';
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: prefix names + add x-photon-* metadata
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: `${this.photonName}/${tool.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}/${tool.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
- // Transform incoming tools/call: strip photonName/ prefix from tool name
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
- if (slashIdx !== -1) {
365
- const targetPhoton = parsed.params.name.slice(0, slashIdx);
366
- const methodName = parsed.params.name.slice(slashIdx + 1);
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: { token: '', clientId: '', scopes: [], extra: claims },
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: tool.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: