@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.
- package/README.md +361 -339
- package/dist/auto-ui/beam.d.ts +5 -0
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +727 -51
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/bridge/index.d.ts +37 -0
- package/dist/auto-ui/bridge/index.d.ts.map +1 -0
- package/dist/auto-ui/bridge/index.js +555 -0
- package/dist/auto-ui/bridge/index.js.map +1 -0
- package/dist/auto-ui/bridge/openai-shim.d.ts +20 -0
- package/dist/auto-ui/bridge/openai-shim.d.ts.map +1 -0
- package/dist/auto-ui/bridge/openai-shim.js +231 -0
- package/dist/auto-ui/bridge/openai-shim.js.map +1 -0
- package/dist/auto-ui/bridge/photon-app.d.ts +162 -0
- package/dist/auto-ui/bridge/photon-app.d.ts.map +1 -0
- package/dist/auto-ui/bridge/photon-app.js +460 -0
- package/dist/auto-ui/bridge/photon-app.js.map +1 -0
- package/dist/auto-ui/bridge/types.d.ts +128 -0
- package/dist/auto-ui/bridge/types.d.ts.map +1 -0
- package/dist/auto-ui/bridge/types.js +7 -0
- package/dist/auto-ui/bridge/types.js.map +1 -0
- package/dist/auto-ui/index.d.ts +3 -1
- package/dist/auto-ui/index.d.ts.map +1 -1
- package/dist/auto-ui/index.js +5 -2
- package/dist/auto-ui/index.js.map +1 -1
- package/dist/auto-ui/platform-compat.d.ts.map +1 -1
- package/dist/auto-ui/platform-compat.js +60 -6
- package/dist/auto-ui/platform-compat.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +25 -1
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +581 -20
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +74 -0
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js +21 -0
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +51377 -1778
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli.js +12 -2
- package/dist/cli.js.map +1 -1
- package/dist/daemon/client.d.ts +5 -3
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +30 -4
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +5 -0
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +20 -0
- package/dist/daemon/manager.js.map +1 -1
- package/dist/loader.d.ts +23 -0
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +77 -12
- package/dist/loader.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +2 -0
- 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 +25 -6
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/server.d.ts +12 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +386 -13
- package/dist/server.js.map +1 -1
- package/dist/template-manager.js +2 -2
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +16 -0
- package/dist/version.js.map +1 -1
- 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
|
|
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
|
-
|
|
102
|
-
if (
|
|
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
|
|
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':
|
|
121
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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 (
|
|
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:
|
|
782
|
+
board: value.board,
|
|
586
783
|
});
|
|
587
784
|
}
|
|
588
|
-
else if (
|
|
589
|
-
chunks.push(
|
|
785
|
+
else if (value?.emit !== 'progress') {
|
|
786
|
+
chunks.push(value);
|
|
590
787
|
}
|
|
591
788
|
}
|
|
592
|
-
|
|
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
|