@portel/photon 1.5.1 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +361 -339
  2. package/dist/auto-ui/beam.d.ts +5 -0
  3. package/dist/auto-ui/beam.d.ts.map +1 -1
  4. package/dist/auto-ui/beam.js +727 -51
  5. package/dist/auto-ui/beam.js.map +1 -1
  6. package/dist/auto-ui/bridge/index.d.ts +37 -0
  7. package/dist/auto-ui/bridge/index.d.ts.map +1 -0
  8. package/dist/auto-ui/bridge/index.js +555 -0
  9. package/dist/auto-ui/bridge/index.js.map +1 -0
  10. package/dist/auto-ui/bridge/openai-shim.d.ts +20 -0
  11. package/dist/auto-ui/bridge/openai-shim.d.ts.map +1 -0
  12. package/dist/auto-ui/bridge/openai-shim.js +231 -0
  13. package/dist/auto-ui/bridge/openai-shim.js.map +1 -0
  14. package/dist/auto-ui/bridge/photon-app.d.ts +162 -0
  15. package/dist/auto-ui/bridge/photon-app.d.ts.map +1 -0
  16. package/dist/auto-ui/bridge/photon-app.js +460 -0
  17. package/dist/auto-ui/bridge/photon-app.js.map +1 -0
  18. package/dist/auto-ui/bridge/types.d.ts +128 -0
  19. package/dist/auto-ui/bridge/types.d.ts.map +1 -0
  20. package/dist/auto-ui/bridge/types.js +7 -0
  21. package/dist/auto-ui/bridge/types.js.map +1 -0
  22. package/dist/auto-ui/index.d.ts +3 -1
  23. package/dist/auto-ui/index.d.ts.map +1 -1
  24. package/dist/auto-ui/index.js +5 -2
  25. package/dist/auto-ui/index.js.map +1 -1
  26. package/dist/auto-ui/platform-compat.d.ts.map +1 -1
  27. package/dist/auto-ui/platform-compat.js +60 -6
  28. package/dist/auto-ui/platform-compat.js.map +1 -1
  29. package/dist/auto-ui/streamable-http-transport.d.ts +25 -1
  30. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  31. package/dist/auto-ui/streamable-http-transport.js +581 -20
  32. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  33. package/dist/auto-ui/types.d.ts +74 -0
  34. package/dist/auto-ui/types.d.ts.map +1 -1
  35. package/dist/auto-ui/types.js +21 -0
  36. package/dist/auto-ui/types.js.map +1 -1
  37. package/dist/beam.bundle.js +51377 -1778
  38. package/dist/beam.bundle.js.map +4 -4
  39. package/dist/cli.js +12 -2
  40. package/dist/cli.js.map +1 -1
  41. package/dist/daemon/client.d.ts +5 -3
  42. package/dist/daemon/client.d.ts.map +1 -1
  43. package/dist/daemon/client.js +30 -4
  44. package/dist/daemon/client.js.map +1 -1
  45. package/dist/daemon/manager.d.ts +5 -0
  46. package/dist/daemon/manager.d.ts.map +1 -1
  47. package/dist/daemon/manager.js +20 -0
  48. package/dist/daemon/manager.js.map +1 -1
  49. package/dist/loader.d.ts +23 -0
  50. package/dist/loader.d.ts.map +1 -1
  51. package/dist/loader.js +77 -12
  52. package/dist/loader.js.map +1 -1
  53. package/dist/photon-cli-runner.d.ts.map +1 -1
  54. package/dist/photon-cli-runner.js +2 -0
  55. package/dist/photon-cli-runner.js.map +1 -1
  56. package/dist/photon-doc-extractor.d.ts +1 -0
  57. package/dist/photon-doc-extractor.d.ts.map +1 -1
  58. package/dist/photon-doc-extractor.js +25 -6
  59. package/dist/photon-doc-extractor.js.map +1 -1
  60. package/dist/server.d.ts +12 -1
  61. package/dist/server.d.ts.map +1 -1
  62. package/dist/server.js +386 -13
  63. package/dist/server.js.map +1 -1
  64. package/dist/template-manager.js +2 -2
  65. package/dist/version.d.ts +8 -0
  66. package/dist/version.d.ts.map +1 -1
  67. package/dist/version.js +16 -0
  68. package/dist/version.js.map +1 -1
  69. package/package.json +18 -8
@@ -17,7 +17,7 @@
17
17
  * @see https://modelcontextprotocol.io/specification/2025-03-26/basic/transports
18
18
  */
19
19
  import { randomUUID } from 'crypto';
20
- import { readdir, stat } from 'fs/promises';
20
+ import { readdir, stat, readFile, writeFile } from 'fs/promises';
21
21
  import { join, dirname } from 'path';
22
22
  import { homedir } from 'os';
23
23
  import { PHOTON_VERSION } from '../version.js';
@@ -92,21 +92,19 @@ function configParamToJsonSchema(param) {
92
92
  return schema;
93
93
  }
94
94
  /**
95
- * Generate configurationSchema for all unconfigured photons
95
+ * Generate configurationSchema for all photons with constructor params
96
96
  * Uses JSON Schema format for rich UI generation
97
+ * Includes both unconfigured and configured photons (for reconfiguration)
97
98
  */
98
99
  function generateConfigurationSchema(photons) {
99
100
  const schema = {};
100
101
  for (const photon of photons) {
101
- // Only include unconfigured photons with params
102
- if (photon.configured)
103
- continue;
104
- const unconfigured = photon;
105
- if (!unconfigured.requiredParams || unconfigured.requiredParams.length === 0)
102
+ const params = photon.requiredParams;
103
+ if (!params || params.length === 0)
106
104
  continue;
107
105
  const properties = {};
108
106
  const required = [];
109
- for (const param of unconfigured.requiredParams) {
107
+ for (const param of params) {
110
108
  properties[param.name] = configParamToJsonSchema(param);
111
109
  // Mark as required if not optional and no default
112
110
  if (!param.isOptional && !param.hasDefault) {
@@ -117,8 +115,11 @@ function generateConfigurationSchema(photons) {
117
115
  type: 'object',
118
116
  properties,
119
117
  required: required.length > 0 ? required : undefined,
120
- 'x-error-message': unconfigured.errorMessage,
121
- 'x-internal': unconfigured.internal,
118
+ 'x-error-message': !photon.configured
119
+ ? photon.errorMessage
120
+ : undefined,
121
+ 'x-internal': photon.internal,
122
+ 'x-configured': photon.configured || undefined,
122
123
  };
123
124
  }
124
125
  return schema;
@@ -244,6 +245,41 @@ const handlers = {
244
245
  });
245
246
  }
246
247
  }
248
+ // Add external MCP tools (from mcpServers in config.json)
249
+ if (ctx.externalMCPs) {
250
+ for (const mcp of ctx.externalMCPs) {
251
+ if (!mcp.connected || !mcp.methods)
252
+ continue;
253
+ for (const method of mcp.methods) {
254
+ tools.push({
255
+ name: `${mcp.name}/${method.name}`,
256
+ description: method.description || `Execute ${method.name}`,
257
+ inputSchema: method.params || { type: 'object', properties: {} },
258
+ 'x-external-mcp': true, // Marker for frontend to identify external MCPs
259
+ 'x-external-mcp-id': mcp.id,
260
+ 'x-photon-icon': mcp.icon || '🔌',
261
+ 'x-photon-description': mcp.description,
262
+ 'x-photon-prompt-count': mcp.promptCount ?? 0,
263
+ 'x-photon-resource-count': mcp.resourceCount ?? 0,
264
+ 'x-has-mcp-app': mcp.hasApp ?? false, // MCP Apps Extension detected
265
+ 'x-mcp-app-uri': mcp.appResourceUri, // MCP App resource URI (default/first)
266
+ 'x-mcp-app-uris': mcp.appResourceUris || [], // All MCP App resource URIs
267
+ ...buildToolMetadataExtensions(method),
268
+ // MCP Apps standard: _meta.ui for linked UI resources and visibility
269
+ ...(method.linkedUi || method.visibility
270
+ ? {
271
+ _meta: {
272
+ ui: {
273
+ ...(method.linkedUi ? { resourceUri: method.linkedUi } : {}),
274
+ ...(method.visibility ? { visibility: method.visibility } : {}),
275
+ },
276
+ },
277
+ }
278
+ : {}),
279
+ });
280
+ }
281
+ }
282
+ }
247
283
  // Add beam system tools (internal — hidden from sidebar)
248
284
  tools.push({
249
285
  name: 'beam/configure',
@@ -350,6 +386,70 @@ const handlers = {
350
386
  required: ['photon', 'metadata'],
351
387
  },
352
388
  });
389
+ tools.push({
390
+ name: 'beam/reconnect-mcp',
391
+ 'x-photon-internal': true,
392
+ description: 'Reconnect a disconnected external MCP server',
393
+ inputSchema: {
394
+ type: 'object',
395
+ properties: {
396
+ name: {
397
+ type: 'string',
398
+ description: 'Name of the external MCP to reconnect',
399
+ },
400
+ },
401
+ required: ['name'],
402
+ },
403
+ });
404
+ tools.push({
405
+ name: 'beam/studio-read',
406
+ 'x-photon-internal': true,
407
+ description: 'Read a photon source file for editing in Studio',
408
+ inputSchema: {
409
+ type: 'object',
410
+ properties: {
411
+ name: {
412
+ type: 'string',
413
+ description: 'Name of the photon to read',
414
+ },
415
+ },
416
+ required: ['name'],
417
+ },
418
+ });
419
+ tools.push({
420
+ name: 'beam/studio-write',
421
+ 'x-photon-internal': true,
422
+ description: 'Write photon source and trigger hot-reload',
423
+ inputSchema: {
424
+ type: 'object',
425
+ properties: {
426
+ name: {
427
+ type: 'string',
428
+ description: 'Name of the photon to write',
429
+ },
430
+ source: {
431
+ type: 'string',
432
+ description: 'The new source code',
433
+ },
434
+ },
435
+ required: ['name', 'source'],
436
+ },
437
+ });
438
+ tools.push({
439
+ name: 'beam/studio-parse',
440
+ 'x-photon-internal': true,
441
+ description: 'Parse photon source and return extracted schema',
442
+ inputSchema: {
443
+ type: 'object',
444
+ properties: {
445
+ source: {
446
+ type: 'string',
447
+ description: 'Source code to parse',
448
+ },
449
+ },
450
+ required: ['source'],
451
+ },
452
+ });
353
453
  // Filter out app-only tools for external (non-Beam) MCP clients
354
454
  const visibleTools = session.isBeam
355
455
  ? tools
@@ -380,10 +480,22 @@ const handlers = {
380
480
  if (name === 'beam/update-metadata') {
381
481
  return handleBeamUpdateMetadata(req, ctx, args || {});
382
482
  }
483
+ if (name === 'beam/reconnect-mcp') {
484
+ return handleBeamReconnectMCP(req, ctx, args || {});
485
+ }
383
486
  if (name === 'beam/photon-help') {
384
487
  return handleBeamPhotonHelp(req, ctx, args || {});
385
488
  }
386
- // Parse tool name: photon-name/method-name
489
+ if (name === 'beam/studio-read') {
490
+ return handleBeamStudioRead(req, ctx, args || {});
491
+ }
492
+ if (name === 'beam/studio-write') {
493
+ return handleBeamStudioWrite(req, ctx, args || {});
494
+ }
495
+ if (name === 'beam/studio-parse') {
496
+ return handleBeamStudioParse(req, args || {});
497
+ }
498
+ // Parse tool name: server-name/method-name
387
499
  const slashIndex = name.indexOf('/');
388
500
  if (slashIndex === -1) {
389
501
  return {
@@ -395,8 +507,65 @@ const handlers = {
395
507
  },
396
508
  };
397
509
  }
398
- const photonName = name.slice(0, slashIndex);
510
+ const serverName = name.slice(0, slashIndex);
399
511
  const methodName = name.slice(slashIndex + 1);
512
+ // Check if this is an external MCP tool call
513
+ // Prefer SDK client for full CallToolResult support (structuredContent)
514
+ if (ctx.externalMCPSDKClients?.has(serverName)) {
515
+ const sdkClient = ctx.externalMCPSDKClients.get(serverName);
516
+ try {
517
+ // SDK client.callTool returns full CallToolResult with structuredContent
518
+ const result = await sdkClient.callTool({ name: methodName, arguments: args || {} });
519
+ return {
520
+ jsonrpc: '2.0',
521
+ id: req.id,
522
+ result: {
523
+ content: result.content,
524
+ structuredContent: result.structuredContent,
525
+ isError: result.isError ?? false,
526
+ },
527
+ };
528
+ }
529
+ catch (error) {
530
+ const message = error instanceof Error ? error.message : String(error);
531
+ return {
532
+ jsonrpc: '2.0',
533
+ id: req.id,
534
+ result: {
535
+ content: [{ type: 'text', text: `Error: ${message}` }],
536
+ isError: true,
537
+ },
538
+ };
539
+ }
540
+ }
541
+ // Fallback to wrapper client (no structuredContent support)
542
+ if (ctx.externalMCPClients?.has(serverName)) {
543
+ const client = ctx.externalMCPClients.get(serverName);
544
+ try {
545
+ const result = await client.call(methodName, args || {});
546
+ return {
547
+ jsonrpc: '2.0',
548
+ id: req.id,
549
+ result: {
550
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
551
+ isError: false,
552
+ },
553
+ };
554
+ }
555
+ catch (error) {
556
+ const message = error instanceof Error ? error.message : String(error);
557
+ return {
558
+ jsonrpc: '2.0',
559
+ id: req.id,
560
+ result: {
561
+ content: [{ type: 'text', text: `Error: ${message}` }],
562
+ isError: true,
563
+ },
564
+ };
565
+ }
566
+ }
567
+ // Handle as photon tool call
568
+ const photonName = serverName;
400
569
  // Find photon info for UI metadata
401
570
  const photonInfo = ctx.photons.find((p) => p.name === photonName);
402
571
  const methodInfo = photonInfo?.configured
@@ -409,6 +578,23 @@ const handlers = {
409
578
  }
410
579
  const mcp = ctx.photonMCPs.get(photonName);
411
580
  if (!mcp?.instance) {
581
+ // Check if it's a disconnected external MCP
582
+ const externalMCP = ctx.externalMCPs?.find((m) => m.name === photonName);
583
+ if (externalMCP) {
584
+ return {
585
+ jsonrpc: '2.0',
586
+ id: req.id,
587
+ result: {
588
+ content: [
589
+ {
590
+ type: 'text',
591
+ text: `External MCP "${photonName}" is not connected${externalMCP.errorMessage ? `: ${externalMCP.errorMessage}` : ''}`,
592
+ },
593
+ ],
594
+ isError: true,
595
+ },
596
+ };
597
+ }
412
598
  return {
413
599
  jsonrpc: '2.0',
414
600
  id: req.id,
@@ -573,23 +759,37 @@ const handlers = {
573
759
  // Handle async generators (when not using loader)
574
760
  if (result && typeof result[Symbol.asyncIterator] === 'function') {
575
761
  const chunks = [];
576
- for await (const chunk of result) {
577
- if (chunk.emit === 'result') {
578
- chunks.push(chunk.data);
762
+ let returnValue = undefined;
763
+ // Manually iterate to capture both yielded values AND the return value
764
+ // Note: for-await-of doesn't capture return values, only yielded values
765
+ const iterator = result[Symbol.asyncIterator]();
766
+ while (true) {
767
+ const { value, done } = await iterator.next();
768
+ if (done) {
769
+ // Generator returned - capture the return value
770
+ returnValue = value;
771
+ break;
772
+ }
773
+ // Process yielded values
774
+ if (value?.emit === 'result') {
775
+ chunks.push(value.data);
579
776
  }
580
- else if (chunk.emit === 'board-update' && ctx.broadcast) {
777
+ else if (value?.emit === 'board-update' && ctx.broadcast) {
581
778
  // Forward board-update from generator
582
779
  ctx.broadcast({
583
780
  type: 'board-update',
584
781
  photon: photonName,
585
- board: chunk.board,
782
+ board: value.board,
586
783
  });
587
784
  }
588
- else if (chunk.emit !== 'progress') {
589
- chunks.push(chunk);
785
+ else if (value?.emit !== 'progress') {
786
+ chunks.push(value);
590
787
  }
591
788
  }
592
- const finalResult = chunks.length === 1 ? chunks[0] : chunks;
789
+ // Use return value if no chunks were yielded, otherwise use chunks
790
+ const finalResult = chunks.length > 0
791
+ ? (chunks.length === 1 ? chunks[0] : chunks)
792
+ : returnValue;
593
793
  const genResponse = {
594
794
  jsonrpc: '2.0',
595
795
  id: req.id,
@@ -1106,6 +1306,319 @@ async function handleBeamPhotonHelp(req, ctx, args) {
1106
1306
  };
1107
1307
  }
1108
1308
  }
1309
+ /**
1310
+ * Handle beam/reconnect-mcp tool - reconnect a disconnected external MCP
1311
+ */
1312
+ async function handleBeamReconnectMCP(req, ctx, args) {
1313
+ const { name: mcpName } = args;
1314
+ if (!mcpName) {
1315
+ return {
1316
+ jsonrpc: '2.0',
1317
+ id: req.id,
1318
+ result: {
1319
+ content: [{ type: 'text', text: 'Error: MCP name is required' }],
1320
+ isError: true,
1321
+ },
1322
+ };
1323
+ }
1324
+ if (!ctx.reconnectExternalMCP) {
1325
+ return {
1326
+ jsonrpc: '2.0',
1327
+ id: req.id,
1328
+ result: {
1329
+ content: [{ type: 'text', text: 'Error: Reconnection not supported in this context' }],
1330
+ isError: true,
1331
+ },
1332
+ };
1333
+ }
1334
+ try {
1335
+ const result = await ctx.reconnectExternalMCP(mcpName);
1336
+ if (result.success) {
1337
+ return {
1338
+ jsonrpc: '2.0',
1339
+ id: req.id,
1340
+ result: {
1341
+ content: [
1342
+ {
1343
+ type: 'text',
1344
+ text: `Successfully reconnected to external MCP "${mcpName}". Tools list will be updated.`,
1345
+ },
1346
+ ],
1347
+ isError: false,
1348
+ },
1349
+ };
1350
+ }
1351
+ else {
1352
+ return {
1353
+ jsonrpc: '2.0',
1354
+ id: req.id,
1355
+ result: {
1356
+ content: [{ type: 'text', text: `Failed to reconnect to "${mcpName}": ${result.error}` }],
1357
+ isError: true,
1358
+ },
1359
+ };
1360
+ }
1361
+ }
1362
+ catch (error) {
1363
+ const message = error instanceof Error ? error.message : String(error);
1364
+ return {
1365
+ jsonrpc: '2.0',
1366
+ id: req.id,
1367
+ result: {
1368
+ content: [{ type: 'text', text: `Error reconnecting to "${mcpName}": ${message}` }],
1369
+ isError: true,
1370
+ },
1371
+ };
1372
+ }
1373
+ }
1374
+ /**
1375
+ * Handle beam/studio-read — read a photon source file for editing
1376
+ */
1377
+ async function handleBeamStudioRead(req, ctx, args) {
1378
+ const { name: photonName } = args;
1379
+ if (!photonName) {
1380
+ return {
1381
+ jsonrpc: '2.0',
1382
+ id: req.id,
1383
+ result: {
1384
+ content: [{ type: 'text', text: 'Error: photon name is required' }],
1385
+ isError: true,
1386
+ },
1387
+ };
1388
+ }
1389
+ // Find the photon by name to get its file path
1390
+ const photon = ctx.photons.find((p) => p.name === photonName);
1391
+ if (!photon || !photon.path) {
1392
+ return {
1393
+ jsonrpc: '2.0',
1394
+ id: req.id,
1395
+ result: {
1396
+ content: [{ type: 'text', text: `Error: photon "${photonName}" not found or has no path` }],
1397
+ isError: true,
1398
+ },
1399
+ };
1400
+ }
1401
+ try {
1402
+ const source = await readFile(photon.path, 'utf-8');
1403
+ return {
1404
+ jsonrpc: '2.0',
1405
+ id: req.id,
1406
+ result: {
1407
+ content: [{ type: 'text', text: JSON.stringify({ source, path: photon.path }) }],
1408
+ isError: false,
1409
+ },
1410
+ };
1411
+ }
1412
+ catch (error) {
1413
+ const message = error instanceof Error ? error.message : String(error);
1414
+ return {
1415
+ jsonrpc: '2.0',
1416
+ id: req.id,
1417
+ result: {
1418
+ content: [{ type: 'text', text: `Error reading source: ${message}` }],
1419
+ isError: true,
1420
+ },
1421
+ };
1422
+ }
1423
+ }
1424
+ /**
1425
+ * Handle beam/studio-write — write photon source and trigger hot-reload
1426
+ */
1427
+ async function handleBeamStudioWrite(req, ctx, args) {
1428
+ const { name: photonName, source } = args;
1429
+ if (!photonName || typeof source !== 'string') {
1430
+ return {
1431
+ jsonrpc: '2.0',
1432
+ id: req.id,
1433
+ result: {
1434
+ content: [{ type: 'text', text: 'Error: photon name and source are required' }],
1435
+ isError: true,
1436
+ },
1437
+ };
1438
+ }
1439
+ const photon = ctx.photons.find((p) => p.name === photonName);
1440
+ if (!photon || !photon.path) {
1441
+ return {
1442
+ jsonrpc: '2.0',
1443
+ id: req.id,
1444
+ result: {
1445
+ content: [{ type: 'text', text: `Error: photon "${photonName}" not found or has no path` }],
1446
+ isError: true,
1447
+ },
1448
+ };
1449
+ }
1450
+ try {
1451
+ // Write source to disk
1452
+ await writeFile(photon.path, source, 'utf-8');
1453
+ // Parse the new source for preview
1454
+ let parseResult = null;
1455
+ try {
1456
+ const { SchemaExtractor } = await import('@portel/photon-core');
1457
+ const extractor = new SchemaExtractor();
1458
+ const { tools: schemas } = extractor.extractAllFromSource(source);
1459
+ const classMatch = source.match(/export\s+default\s+class\s+(\w+)/);
1460
+ const descMatch = source.match(/\/\*\*\s*\n\s*\*\s*(.+)/);
1461
+ const versionMatch = source.match(/@version\s+(\S+)/);
1462
+ const runtimeMatch = source.match(/@runtime\s+(\S+)/);
1463
+ const iconMatch = source.match(/@icon\s+(\S+)/);
1464
+ const statefulMatch = source.match(/@stateful\s+true/);
1465
+ const depsMatch = source.match(/@dependencies\s+(.+)/);
1466
+ const tagsMatch = source.match(/@tags\s+(.+)/);
1467
+ parseResult = {
1468
+ className: classMatch?.[1] || 'Unknown',
1469
+ description: descMatch?.[1]?.replace(/\s*\*\/$/, '').trim(),
1470
+ icon: iconMatch?.[1],
1471
+ version: versionMatch?.[1],
1472
+ runtime: runtimeMatch?.[1],
1473
+ stateful: !!statefulMatch,
1474
+ dependencies: depsMatch?.[1]?.split(',').map((d) => d.trim()).filter(Boolean),
1475
+ tags: tagsMatch?.[1]?.split(',').map((t) => t.trim()).filter(Boolean),
1476
+ methods: schemas
1477
+ .filter((s) => !['onInitialize', 'onShutdown', 'constructor'].includes(s.name))
1478
+ .map((s) => ({
1479
+ name: s.name,
1480
+ description: s.description,
1481
+ icon: s.icon,
1482
+ params: s.inputSchema,
1483
+ autorun: s.autorun,
1484
+ outputFormat: s.outputFormat,
1485
+ buttonLabel: s.buttonLabel,
1486
+ webhook: s.webhook,
1487
+ scheduled: s.scheduled || s.cron,
1488
+ locked: s.locked,
1489
+ })),
1490
+ };
1491
+ }
1492
+ catch {
1493
+ // Parse is best-effort — don't fail the write
1494
+ }
1495
+ // Trigger hot-reload if available
1496
+ if (ctx.reloadPhoton) {
1497
+ try {
1498
+ const reloadResult = await ctx.reloadPhoton(photonName);
1499
+ if (reloadResult.success) {
1500
+ broadcastToBeam('beam/hot-reload', { photon: reloadResult.photon });
1501
+ }
1502
+ }
1503
+ catch {
1504
+ // Reload failure doesn't fail the write
1505
+ }
1506
+ }
1507
+ return {
1508
+ jsonrpc: '2.0',
1509
+ id: req.id,
1510
+ result: {
1511
+ content: [
1512
+ {
1513
+ type: 'text',
1514
+ text: JSON.stringify({ success: true, parseResult }),
1515
+ },
1516
+ ],
1517
+ isError: false,
1518
+ },
1519
+ };
1520
+ }
1521
+ catch (error) {
1522
+ const message = error instanceof Error ? error.message : String(error);
1523
+ return {
1524
+ jsonrpc: '2.0',
1525
+ id: req.id,
1526
+ result: {
1527
+ content: [{ type: 'text', text: JSON.stringify({ success: false, error: message }) }],
1528
+ isError: true,
1529
+ },
1530
+ };
1531
+ }
1532
+ }
1533
+ /**
1534
+ * Handle beam/studio-parse — parse photon source and return schema
1535
+ */
1536
+ async function handleBeamStudioParse(req, args) {
1537
+ const { source } = args;
1538
+ if (typeof source !== 'string') {
1539
+ return {
1540
+ jsonrpc: '2.0',
1541
+ id: req.id,
1542
+ result: {
1543
+ content: [{ type: 'text', text: 'Error: source is required' }],
1544
+ isError: true,
1545
+ },
1546
+ };
1547
+ }
1548
+ try {
1549
+ const { SchemaExtractor } = await import('@portel/photon-core');
1550
+ const extractor = new SchemaExtractor();
1551
+ const { tools: schemas } = extractor.extractAllFromSource(source);
1552
+ const classMatch = source.match(/export\s+default\s+class\s+(\w+)/);
1553
+ const descMatch = source.match(/\/\*\*\s*\n\s*\*\s*(.+)/);
1554
+ const versionMatch = source.match(/@version\s+(\S+)/);
1555
+ const runtimeMatch = source.match(/@runtime\s+(\S+)/);
1556
+ const iconMatch = source.match(/@icon\s+(\S+)/);
1557
+ const statefulMatch = source.match(/@stateful\s+true/);
1558
+ const depsMatch = source.match(/@dependencies\s+(.+)/);
1559
+ const tagsMatch = source.match(/@tags\s+(.+)/);
1560
+ const errors = [];
1561
+ const warnings = [];
1562
+ if (!classMatch)
1563
+ errors.push('No default export class found');
1564
+ if (!descMatch)
1565
+ warnings.push('Missing class description (first line in JSDoc)');
1566
+ const result = {
1567
+ className: classMatch?.[1] || 'Unknown',
1568
+ description: descMatch?.[1]?.replace(/\s*\*\/$/, '').trim(),
1569
+ icon: iconMatch?.[1],
1570
+ version: versionMatch?.[1],
1571
+ runtime: runtimeMatch?.[1],
1572
+ stateful: !!statefulMatch,
1573
+ dependencies: depsMatch?.[1]?.split(',').map((d) => d.trim()).filter(Boolean),
1574
+ tags: tagsMatch?.[1]?.split(',').map((t) => t.trim()).filter(Boolean),
1575
+ methods: schemas
1576
+ .filter((s) => !['onInitialize', 'onShutdown', 'constructor'].includes(s.name))
1577
+ .map((s) => ({
1578
+ name: s.name,
1579
+ description: s.description,
1580
+ icon: s.icon,
1581
+ params: s.inputSchema,
1582
+ autorun: s.autorun,
1583
+ outputFormat: s.outputFormat,
1584
+ buttonLabel: s.buttonLabel,
1585
+ webhook: s.webhook,
1586
+ scheduled: s.scheduled || s.cron,
1587
+ locked: s.locked,
1588
+ })),
1589
+ errors: errors.length > 0 ? errors : undefined,
1590
+ warnings: warnings.length > 0 ? warnings : undefined,
1591
+ };
1592
+ return {
1593
+ jsonrpc: '2.0',
1594
+ id: req.id,
1595
+ result: {
1596
+ content: [{ type: 'text', text: JSON.stringify(result) }],
1597
+ isError: false,
1598
+ },
1599
+ };
1600
+ }
1601
+ catch (error) {
1602
+ const message = error instanceof Error ? error.message : String(error);
1603
+ return {
1604
+ jsonrpc: '2.0',
1605
+ id: req.id,
1606
+ result: {
1607
+ content: [
1608
+ {
1609
+ type: 'text',
1610
+ text: JSON.stringify({
1611
+ className: 'Unknown',
1612
+ methods: [],
1613
+ errors: [`Parse error: ${message}`],
1614
+ }),
1615
+ },
1616
+ ],
1617
+ isError: false,
1618
+ },
1619
+ };
1620
+ }
1621
+ }
1109
1622
  /**
1110
1623
  * Handle MCP Streamable HTTP requests
1111
1624
  */
@@ -1188,6 +1701,10 @@ export async function handleStreamableHTTP(req, res, options) {
1188
1701
  const context = {
1189
1702
  photons: options.photons,
1190
1703
  photonMCPs: options.photonMCPs,
1704
+ externalMCPs: options.externalMCPs,
1705
+ externalMCPClients: options.externalMCPClients,
1706
+ externalMCPSDKClients: options.externalMCPSDKClients,
1707
+ reconnectExternalMCP: options.reconnectExternalMCP,
1191
1708
  loadUIAsset: options.loadUIAsset,
1192
1709
  configurePhoton: options.configurePhoton,
1193
1710
  reloadPhoton: options.reloadPhoton,
@@ -1311,4 +1828,48 @@ export function sendToSession(sessionId, method, params) {
1311
1828
  session.sseResponse.write(`data: ${JSON.stringify(notification)}\n\n`);
1312
1829
  return true;
1313
1830
  }
1831
+ /**
1832
+ * Request elicitation from the frontend for an external MCP.
1833
+ * This is used when external MCP servers send elicitation/create requests.
1834
+ *
1835
+ * @param mcpName - Name of the external MCP requesting elicitation
1836
+ * @param request - The elicitation request params from the MCP server
1837
+ * @returns Promise resolving to the user's response
1838
+ */
1839
+ export function requestExternalElicitation(mcpName, request) {
1840
+ const elicitationId = randomUUID();
1841
+ return new Promise((resolve, reject) => {
1842
+ // Store pending elicitation
1843
+ pendingElicitations.set(elicitationId, {
1844
+ resolve: (value) => {
1845
+ resolve({ action: 'accept', content: value });
1846
+ },
1847
+ reject: (error) => {
1848
+ if (error.message.includes('cancelled')) {
1849
+ resolve({ action: 'cancel' });
1850
+ }
1851
+ else {
1852
+ resolve({ action: 'decline' });
1853
+ }
1854
+ },
1855
+ sessionId: '', // External MCP elicitations aren't tied to a specific session
1856
+ });
1857
+ // Broadcast elicitation request to all Beam clients
1858
+ broadcastToBeam('beam/elicitation', {
1859
+ elicitationId,
1860
+ mcpName,
1861
+ message: request.message,
1862
+ mode: request.mode,
1863
+ schema: request.requestedSchema,
1864
+ url: request.url,
1865
+ });
1866
+ // Timeout after 5 minutes
1867
+ setTimeout(() => {
1868
+ if (pendingElicitations.has(elicitationId)) {
1869
+ pendingElicitations.delete(elicitationId);
1870
+ resolve({ action: 'cancel' });
1871
+ }
1872
+ }, 300000);
1873
+ });
1874
+ }
1314
1875
  //# sourceMappingURL=streamable-http-transport.js.map