@portel/photon 1.6.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -160
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +218 -106
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/design-system/tokens.d.ts +1 -1
- package/dist/auto-ui/design-system/tokens.d.ts.map +1 -1
- package/dist/auto-ui/design-system/tokens.js +2 -2
- package/dist/auto-ui/design-system/tokens.js.map +1 -1
- package/dist/auto-ui/frontend/index.html +1 -1
- package/dist/auto-ui/platform-compat.d.ts.map +1 -1
- package/dist/auto-ui/platform-compat.js +12 -2
- package/dist/auto-ui/platform-compat.js.map +1 -1
- package/dist/auto-ui/playground-html.js +5 -5
- package/dist/auto-ui/rendering/components.d.ts.map +1 -1
- package/dist/auto-ui/rendering/components.js +568 -0
- package/dist/auto-ui/rendering/components.js.map +1 -1
- package/dist/auto-ui/rendering/field-analyzer.d.ts +56 -0
- package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -1
- package/dist/auto-ui/rendering/field-analyzer.js +177 -0
- package/dist/auto-ui/rendering/field-analyzer.js.map +1 -1
- package/dist/auto-ui/rendering/layout-selector.d.ts +14 -2
- package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -1
- package/dist/auto-ui/rendering/layout-selector.js +125 -1
- package/dist/auto-ui/rendering/layout-selector.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +370 -26
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +7 -1
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +21932 -3307
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +37 -0
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/package.d.ts.map +1 -1
- package/dist/cli/commands/package.js +16 -0
- package/dist/cli/commands/package.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +640 -17
- package/dist/cli.js.map +1 -1
- package/dist/context-store.d.ts +79 -0
- package/dist/context-store.d.ts.map +1 -0
- package/dist/context-store.js +210 -0
- package/dist/context-store.js.map +1 -0
- package/dist/daemon/client.d.ts +13 -4
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +138 -77
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +0 -25
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +10 -38
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/protocol.d.ts +7 -2
- package/dist/daemon/protocol.d.ts.map +1 -1
- package/dist/daemon/protocol.js.map +1 -1
- package/dist/daemon/server.js +317 -83
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/session-manager.d.ts +24 -4
- package/dist/daemon/session-manager.d.ts.map +1 -1
- package/dist/daemon/session-manager.js +62 -12
- package/dist/daemon/session-manager.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +3 -20
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +87 -77
- package/dist/loader.js.map +1 -1
- package/dist/markdown-utils.d.ts.map +1 -1
- package/dist/markdown-utils.js +2 -1
- package/dist/markdown-utils.js.map +1 -1
- package/dist/marketplace-manager.d.ts.map +1 -1
- package/dist/marketplace-manager.js +20 -3
- package/dist/marketplace-manager.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +258 -218
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts +2 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +45 -7
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/photons/maker.photon.d.ts.map +1 -1
- package/dist/photons/maker.photon.js +22 -4
- package/dist/photons/maker.photon.js.map +1 -1
- package/dist/photons/maker.photon.ts +47 -11
- package/dist/security-scanner.d.ts.map +1 -1
- package/dist/security-scanner.js +8 -2
- package/dist/security-scanner.js.map +1 -1
- package/dist/serv/index.d.ts +1 -1
- package/dist/serv/index.d.ts.map +1 -1
- package/dist/serv/index.js +6 -4
- package/dist/serv/index.js.map +1 -1
- package/dist/server.d.ts +32 -15
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +525 -483
- package/dist/server.js.map +1 -1
- package/dist/shared/security.d.ts +79 -0
- package/dist/shared/security.d.ts.map +1 -0
- package/dist/shared/security.js +251 -0
- package/dist/shared/security.js.map +1 -0
- package/dist/shell-completions.d.ts +21 -0
- package/dist/shell-completions.d.ts.map +1 -0
- package/dist/shell-completions.js +102 -0
- package/dist/shell-completions.js.map +1 -0
- package/dist/template-manager.d.ts.map +1 -1
- package/dist/template-manager.js +10 -3
- package/dist/template-manager.js.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +12 -7
|
@@ -124,6 +124,20 @@ function generateConfigurationSchema(photons) {
|
|
|
124
124
|
}
|
|
125
125
|
return schema;
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Format a tool result for MCP content text.
|
|
129
|
+
* Mirrors server.ts formatResult(): strings returned as-is, objects/arrays JSON-stringified,
|
|
130
|
+
* other primitives converted via String().
|
|
131
|
+
*/
|
|
132
|
+
function formatResultText(result) {
|
|
133
|
+
if (result === undefined || result === null)
|
|
134
|
+
return 'Done';
|
|
135
|
+
if (typeof result === 'string')
|
|
136
|
+
return result;
|
|
137
|
+
if (typeof result === 'object')
|
|
138
|
+
return JSON.stringify(result, null, 2);
|
|
139
|
+
return String(result);
|
|
140
|
+
}
|
|
127
141
|
const handlers = {
|
|
128
142
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
129
143
|
// Lifecycle
|
|
@@ -149,6 +163,7 @@ const handlers = {
|
|
|
149
163
|
},
|
|
150
164
|
capabilities: {
|
|
151
165
|
tools: { listChanged: true },
|
|
166
|
+
prompts: { listChanged: true },
|
|
152
167
|
resources: { listChanged: true },
|
|
153
168
|
},
|
|
154
169
|
// SEP-1596 inspired: configuration schema for unconfigured photons
|
|
@@ -192,14 +207,14 @@ const handlers = {
|
|
|
192
207
|
// Client notifies what resource they're viewing (for on-demand subscriptions)
|
|
193
208
|
// photonId: hash of photon path (unique across servers)
|
|
194
209
|
// itemId: whatever the photon uses to identify the item (e.g., board name)
|
|
195
|
-
//
|
|
210
|
+
// lastTimestamp: optional - for delta sync of missed events on reconnect
|
|
196
211
|
'beam/viewing': async (req, session, ctx) => {
|
|
197
212
|
const params = req.params;
|
|
198
213
|
const photonId = params?.photonId;
|
|
199
214
|
const itemId = params?.itemId;
|
|
200
|
-
const
|
|
215
|
+
const lastTimestamp = params?.lastTimestamp;
|
|
201
216
|
if (photonId && itemId && ctx.subscriptionManager) {
|
|
202
|
-
ctx.subscriptionManager.onClientViewingBoard(session.id, photonId, itemId,
|
|
217
|
+
ctx.subscriptionManager.onClientViewingBoard(session.id, photonId, itemId, lastTimestamp);
|
|
203
218
|
}
|
|
204
219
|
// Notification - no response needed
|
|
205
220
|
return { jsonrpc: '2.0' };
|
|
@@ -226,6 +241,7 @@ const handlers = {
|
|
|
226
241
|
'x-photon-description': photon.description,
|
|
227
242
|
'x-photon-icon': photon.icon,
|
|
228
243
|
'x-photon-internal': photon.internal,
|
|
244
|
+
'x-photon-stateful': photon.stateful || false,
|
|
229
245
|
'x-photon-prompt-count': photon.promptCount ?? 0,
|
|
230
246
|
'x-photon-resource-count': photon.resourceCount ?? 0,
|
|
231
247
|
...buildToolMetadataExtensions(method),
|
|
@@ -245,6 +261,33 @@ const handlers = {
|
|
|
245
261
|
});
|
|
246
262
|
}
|
|
247
263
|
}
|
|
264
|
+
// Add runtime-injected instance tools for stateful photons
|
|
265
|
+
for (const photon of ctx.photons) {
|
|
266
|
+
if (!photon.configured || !photon.stateful)
|
|
267
|
+
continue;
|
|
268
|
+
tools.push({
|
|
269
|
+
name: `${photon.name}/_use`,
|
|
270
|
+
description: `Switch to a named instance of ${photon.name}. Omit name to select interactively.`,
|
|
271
|
+
inputSchema: {
|
|
272
|
+
type: 'object',
|
|
273
|
+
properties: {
|
|
274
|
+
name: {
|
|
275
|
+
type: 'string',
|
|
276
|
+
description: 'Instance name (empty for default). Omit to select interactively.',
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
'x-photon-id': photon.id,
|
|
281
|
+
'x-photon-internal': true,
|
|
282
|
+
});
|
|
283
|
+
tools.push({
|
|
284
|
+
name: `${photon.name}/_instances`,
|
|
285
|
+
description: `List all available instances of ${photon.name}.`,
|
|
286
|
+
inputSchema: { type: 'object', properties: {} },
|
|
287
|
+
'x-photon-id': photon.id,
|
|
288
|
+
'x-photon-internal': true,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
248
291
|
// Add external MCP tools (from mcpServers in config.json)
|
|
249
292
|
if (ctx.externalMCPs) {
|
|
250
293
|
for (const mcp of ctx.externalMCPs) {
|
|
@@ -547,7 +590,7 @@ const handlers = {
|
|
|
547
590
|
jsonrpc: '2.0',
|
|
548
591
|
id: req.id,
|
|
549
592
|
result: {
|
|
550
|
-
content: [{ type: 'text', text:
|
|
593
|
+
content: [{ type: 'text', text: formatResultText(result) }],
|
|
551
594
|
isError: false,
|
|
552
595
|
},
|
|
553
596
|
};
|
|
@@ -576,6 +619,134 @@ const handlers = {
|
|
|
576
619
|
if (methodInfo?.outputFormat) {
|
|
577
620
|
uiMetadata['x-output-format'] = methodInfo.outputFormat;
|
|
578
621
|
}
|
|
622
|
+
// Stateful photons: route through daemon for shared instance across all clients
|
|
623
|
+
if (photonInfo?.stateful && photonInfo.path) {
|
|
624
|
+
try {
|
|
625
|
+
const { sendCommand, pingDaemon } = await import('../daemon/client.js');
|
|
626
|
+
const { isGlobalDaemonRunning, startGlobalDaemon } = await import('../daemon/manager.js');
|
|
627
|
+
// Ensure daemon is running
|
|
628
|
+
if (!isGlobalDaemonRunning()) {
|
|
629
|
+
await startGlobalDaemon(true);
|
|
630
|
+
// Wait for daemon readiness
|
|
631
|
+
for (let i = 0; i < 10; i++) {
|
|
632
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
633
|
+
if (await pingDaemon(photonName))
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// Each browser tab gets its own daemon session via the MCP session ID.
|
|
638
|
+
// Instance state is tracked per-session on the daemon — no global persistence.
|
|
639
|
+
const beamSessionId = `beam-${session.id}`;
|
|
640
|
+
const sendOpts = {
|
|
641
|
+
photonPath: photonInfo.path,
|
|
642
|
+
sessionId: beamSessionId,
|
|
643
|
+
instanceName: session.instanceName,
|
|
644
|
+
};
|
|
645
|
+
// Elicitation-based instance selection when _use called without name
|
|
646
|
+
if (methodName === '_use' && (!args || !('name' in args))) {
|
|
647
|
+
const instancesResult = (await sendCommand(photonName, '_instances', {}, sendOpts));
|
|
648
|
+
const instances = instancesResult?.instances || ['default'];
|
|
649
|
+
// Build select options for elicitation modal
|
|
650
|
+
const selectOptions = instances.map((inst) => ({
|
|
651
|
+
value: inst,
|
|
652
|
+
label: inst === 'default' ? '(default)' : inst,
|
|
653
|
+
selected: inst === (instancesResult?.current || 'default'),
|
|
654
|
+
}));
|
|
655
|
+
selectOptions.push({ value: '__create_new__', label: 'Create new...', selected: false });
|
|
656
|
+
const elicitResult = await requestBeamElicitation({
|
|
657
|
+
ask: 'select',
|
|
658
|
+
message: 'Select an instance',
|
|
659
|
+
options: selectOptions,
|
|
660
|
+
});
|
|
661
|
+
if (elicitResult.action !== 'accept' || !elicitResult.content) {
|
|
662
|
+
return {
|
|
663
|
+
jsonrpc: '2.0',
|
|
664
|
+
id: req.id,
|
|
665
|
+
result: {
|
|
666
|
+
content: [{ type: 'text', text: 'Cancelled' }],
|
|
667
|
+
isError: false,
|
|
668
|
+
},
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
let selectedName = elicitResult.content;
|
|
672
|
+
// Handle "Create new..." selection
|
|
673
|
+
if (selectedName === '__create_new__') {
|
|
674
|
+
const nameResult = await requestBeamElicitation({
|
|
675
|
+
ask: 'text',
|
|
676
|
+
message: 'Enter a name for the new instance',
|
|
677
|
+
placeholder: 'e.g. groceries, work, personal',
|
|
678
|
+
});
|
|
679
|
+
if (nameResult.action !== 'accept' || !nameResult.content) {
|
|
680
|
+
return {
|
|
681
|
+
jsonrpc: '2.0',
|
|
682
|
+
id: req.id,
|
|
683
|
+
result: {
|
|
684
|
+
content: [{ type: 'text', text: 'Cancelled' }],
|
|
685
|
+
isError: false,
|
|
686
|
+
},
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
selectedName = nameResult.content;
|
|
690
|
+
}
|
|
691
|
+
const useResult = await sendCommand(photonName, '_use', { name: selectedName }, sendOpts);
|
|
692
|
+
session.instanceName = selectedName;
|
|
693
|
+
// Notify UI to refresh after instance switch
|
|
694
|
+
broadcastToBeam('photon/state-changed', {
|
|
695
|
+
photon: photonName,
|
|
696
|
+
method: '_use',
|
|
697
|
+
data: { instance: selectedName },
|
|
698
|
+
});
|
|
699
|
+
return {
|
|
700
|
+
jsonrpc: '2.0',
|
|
701
|
+
id: req.id,
|
|
702
|
+
result: {
|
|
703
|
+
content: [{ type: 'text', text: formatResultText(useResult) }],
|
|
704
|
+
isError: false,
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
// For direct _use with name, also broadcast state-changed
|
|
709
|
+
if (methodName === '_use') {
|
|
710
|
+
const result = await sendCommand(photonName, methodName, (args || {}), sendOpts);
|
|
711
|
+
session.instanceName = String(args?.name || '');
|
|
712
|
+
broadcastToBeam('photon/state-changed', {
|
|
713
|
+
photon: photonName,
|
|
714
|
+
method: '_use',
|
|
715
|
+
data: { instance: args?.name || 'default' },
|
|
716
|
+
});
|
|
717
|
+
return {
|
|
718
|
+
jsonrpc: '2.0',
|
|
719
|
+
id: req.id,
|
|
720
|
+
result: {
|
|
721
|
+
content: [{ type: 'text', text: formatResultText(result) }],
|
|
722
|
+
isError: false,
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const result = await sendCommand(photonName, methodName, (args || {}), sendOpts);
|
|
727
|
+
const resultText = formatResultText(result);
|
|
728
|
+
return {
|
|
729
|
+
jsonrpc: '2.0',
|
|
730
|
+
id: req.id,
|
|
731
|
+
result: {
|
|
732
|
+
content: [{ type: 'text', text: resultText }],
|
|
733
|
+
isError: false,
|
|
734
|
+
...uiMetadata,
|
|
735
|
+
},
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
catch (error) {
|
|
739
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
740
|
+
return {
|
|
741
|
+
jsonrpc: '2.0',
|
|
742
|
+
id: req.id,
|
|
743
|
+
result: {
|
|
744
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
745
|
+
isError: true,
|
|
746
|
+
},
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
}
|
|
579
750
|
const mcp = ctx.photonMCPs.get(photonName);
|
|
580
751
|
if (!mcp?.instance) {
|
|
581
752
|
// Check if it's a disconnected external MCP
|
|
@@ -787,14 +958,13 @@ const handlers = {
|
|
|
787
958
|
}
|
|
788
959
|
}
|
|
789
960
|
// Use return value if no chunks were yielded, otherwise use chunks
|
|
790
|
-
const finalResult = chunks.length > 0
|
|
791
|
-
|
|
792
|
-
: returnValue;
|
|
961
|
+
const finalResult = chunks.length > 0 ? (chunks.length === 1 ? chunks[0] : chunks) : returnValue;
|
|
962
|
+
const genResultText = formatResultText(finalResult);
|
|
793
963
|
const genResponse = {
|
|
794
964
|
jsonrpc: '2.0',
|
|
795
965
|
id: req.id,
|
|
796
966
|
result: {
|
|
797
|
-
content: [{ type: 'text', text:
|
|
967
|
+
content: [{ type: 'text', text: genResultText }],
|
|
798
968
|
isError: false,
|
|
799
969
|
...uiMetadata,
|
|
800
970
|
},
|
|
@@ -813,11 +983,13 @@ const handlers = {
|
|
|
813
983
|
}
|
|
814
984
|
return genResponse;
|
|
815
985
|
}
|
|
986
|
+
// For void methods, provide a success acknowledgment so the UI shows feedback
|
|
987
|
+
const resultText = formatResultText(result);
|
|
816
988
|
const toolResponse = {
|
|
817
989
|
jsonrpc: '2.0',
|
|
818
990
|
id: req.id,
|
|
819
991
|
result: {
|
|
820
|
-
content: [{ type: 'text', text:
|
|
992
|
+
content: [{ type: 'text', text: resultText }],
|
|
821
993
|
isError: false,
|
|
822
994
|
...uiMetadata,
|
|
823
995
|
},
|
|
@@ -898,6 +1070,83 @@ const handlers = {
|
|
|
898
1070
|
},
|
|
899
1071
|
};
|
|
900
1072
|
},
|
|
1073
|
+
'prompts/list': async (req, _session, ctx) => {
|
|
1074
|
+
const prompts = [];
|
|
1075
|
+
for (const photon of ctx.photons) {
|
|
1076
|
+
if (!photon.configured)
|
|
1077
|
+
continue;
|
|
1078
|
+
const mcp = ctx.photonMCPs.get(photon.name);
|
|
1079
|
+
if (!mcp?.templates)
|
|
1080
|
+
continue;
|
|
1081
|
+
for (const template of mcp.templates) {
|
|
1082
|
+
prompts.push({
|
|
1083
|
+
name: `${photon.name}/${template.name}`,
|
|
1084
|
+
description: template.description,
|
|
1085
|
+
arguments: Object.entries(template.inputSchema?.properties || {}).map(([name, schema]) => ({
|
|
1086
|
+
name,
|
|
1087
|
+
description: (typeof schema === 'object' && schema && 'description' in schema
|
|
1088
|
+
? schema.description
|
|
1089
|
+
: '') || '',
|
|
1090
|
+
required: template.inputSchema?.required?.includes(name) || false,
|
|
1091
|
+
})),
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return { jsonrpc: '2.0', id: req.id, result: { prompts } };
|
|
1096
|
+
},
|
|
1097
|
+
'prompts/get': async (req, _session, ctx) => {
|
|
1098
|
+
const { name } = req.params;
|
|
1099
|
+
const args = req.params.arguments || {};
|
|
1100
|
+
const slashIndex = name.indexOf('/');
|
|
1101
|
+
if (slashIndex === -1) {
|
|
1102
|
+
return {
|
|
1103
|
+
jsonrpc: '2.0',
|
|
1104
|
+
id: req.id,
|
|
1105
|
+
error: { code: -32602, message: `Invalid prompt name: ${name}` },
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
const photonName = name.slice(0, slashIndex);
|
|
1109
|
+
const promptName = name.slice(slashIndex + 1);
|
|
1110
|
+
const mcp = ctx.photonMCPs.get(photonName);
|
|
1111
|
+
if (!mcp) {
|
|
1112
|
+
return {
|
|
1113
|
+
jsonrpc: '2.0',
|
|
1114
|
+
id: req.id,
|
|
1115
|
+
error: { code: -32602, message: `Photon not found: ${photonName}` },
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
const template = mcp.templates?.find((t) => t.name === promptName);
|
|
1119
|
+
if (!template) {
|
|
1120
|
+
return {
|
|
1121
|
+
jsonrpc: '2.0',
|
|
1122
|
+
id: req.id,
|
|
1123
|
+
error: { code: -32602, message: `Prompt not found: ${promptName}` },
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
try {
|
|
1127
|
+
const result = await ctx.loader.executeTool(mcp, promptName, args);
|
|
1128
|
+
// Format as prompt response
|
|
1129
|
+
if (result && typeof result === 'object' && 'messages' in result) {
|
|
1130
|
+
return { jsonrpc: '2.0', id: req.id, result: { messages: result.messages } };
|
|
1131
|
+
}
|
|
1132
|
+
const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
1133
|
+
return {
|
|
1134
|
+
jsonrpc: '2.0',
|
|
1135
|
+
id: req.id,
|
|
1136
|
+
result: {
|
|
1137
|
+
messages: [{ role: 'user', content: { type: 'text', text } }],
|
|
1138
|
+
},
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
catch (error) {
|
|
1142
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1143
|
+
return {
|
|
1144
|
+
jsonrpc: '2.0',
|
|
1145
|
+
id: req.id,
|
|
1146
|
+
error: { code: -32603, message: `Prompt execution failed: ${message}` },
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
},
|
|
901
1150
|
};
|
|
902
1151
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
903
1152
|
// BEAM SYSTEM TOOLS
|
|
@@ -1461,7 +1710,7 @@ async function handleBeamStudioWrite(req, ctx, args) {
|
|
|
1461
1710
|
const versionMatch = source.match(/@version\s+(\S+)/);
|
|
1462
1711
|
const runtimeMatch = source.match(/@runtime\s+(\S+)/);
|
|
1463
1712
|
const iconMatch = source.match(/@icon\s+(\S+)/);
|
|
1464
|
-
const statefulMatch = source.match(/@stateful\
|
|
1713
|
+
const statefulMatch = source.match(/@stateful\b/);
|
|
1465
1714
|
const depsMatch = source.match(/@dependencies\s+(.+)/);
|
|
1466
1715
|
const tagsMatch = source.match(/@tags\s+(.+)/);
|
|
1467
1716
|
parseResult = {
|
|
@@ -1471,8 +1720,14 @@ async function handleBeamStudioWrite(req, ctx, args) {
|
|
|
1471
1720
|
version: versionMatch?.[1],
|
|
1472
1721
|
runtime: runtimeMatch?.[1],
|
|
1473
1722
|
stateful: !!statefulMatch,
|
|
1474
|
-
dependencies: depsMatch?.[1]
|
|
1475
|
-
|
|
1723
|
+
dependencies: depsMatch?.[1]
|
|
1724
|
+
?.split(',')
|
|
1725
|
+
.map((d) => d.trim())
|
|
1726
|
+
.filter(Boolean),
|
|
1727
|
+
tags: tagsMatch?.[1]
|
|
1728
|
+
?.split(',')
|
|
1729
|
+
.map((t) => t.trim())
|
|
1730
|
+
.filter(Boolean),
|
|
1476
1731
|
methods: schemas
|
|
1477
1732
|
.filter((s) => !['onInitialize', 'onShutdown', 'constructor'].includes(s.name))
|
|
1478
1733
|
.map((s) => ({
|
|
@@ -1554,7 +1809,7 @@ async function handleBeamStudioParse(req, args) {
|
|
|
1554
1809
|
const versionMatch = source.match(/@version\s+(\S+)/);
|
|
1555
1810
|
const runtimeMatch = source.match(/@runtime\s+(\S+)/);
|
|
1556
1811
|
const iconMatch = source.match(/@icon\s+(\S+)/);
|
|
1557
|
-
const statefulMatch = source.match(/@stateful\
|
|
1812
|
+
const statefulMatch = source.match(/@stateful\b/);
|
|
1558
1813
|
const depsMatch = source.match(/@dependencies\s+(.+)/);
|
|
1559
1814
|
const tagsMatch = source.match(/@tags\s+(.+)/);
|
|
1560
1815
|
const errors = [];
|
|
@@ -1570,8 +1825,14 @@ async function handleBeamStudioParse(req, args) {
|
|
|
1570
1825
|
version: versionMatch?.[1],
|
|
1571
1826
|
runtime: runtimeMatch?.[1],
|
|
1572
1827
|
stateful: !!statefulMatch,
|
|
1573
|
-
dependencies: depsMatch?.[1]
|
|
1574
|
-
|
|
1828
|
+
dependencies: depsMatch?.[1]
|
|
1829
|
+
?.split(',')
|
|
1830
|
+
.map((d) => d.trim())
|
|
1831
|
+
.filter(Boolean),
|
|
1832
|
+
tags: tagsMatch?.[1]
|
|
1833
|
+
?.split(',')
|
|
1834
|
+
.map((t) => t.trim())
|
|
1835
|
+
.filter(Boolean),
|
|
1575
1836
|
methods: schemas
|
|
1576
1837
|
.filter((s) => !['onInitialize', 'onShutdown', 'constructor'].includes(s.name))
|
|
1577
1838
|
.map((s) => ({
|
|
@@ -1663,20 +1924,40 @@ export async function handleStreamableHTTP(req, res, options) {
|
|
|
1663
1924
|
});
|
|
1664
1925
|
// Disable Nagle's algorithm for immediate writes
|
|
1665
1926
|
res.socket?.setNoDelay(true);
|
|
1927
|
+
// Enable TCP keepalive to prevent connection drops from intermediaries
|
|
1928
|
+
res.socket?.setKeepAlive(true, 60000);
|
|
1666
1929
|
// Store SSE response for server-initiated messages
|
|
1667
1930
|
session.sseResponse = res;
|
|
1668
|
-
// Keep connection alive
|
|
1931
|
+
// Keep connection alive with SSE data events (every 15s for better reliability)
|
|
1669
1932
|
const keepAlive = setInterval(() => {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1933
|
+
// Check if response is still writable before sending keepalive
|
|
1934
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
1935
|
+
try {
|
|
1936
|
+
// Send as data event so client onmessage handler fires and updates lastMessageTime
|
|
1937
|
+
res.write('data: {"type":"keepalive"}\n\n');
|
|
1938
|
+
}
|
|
1939
|
+
catch (err) {
|
|
1940
|
+
// If write fails, connection is dead - clean up
|
|
1941
|
+
clearInterval(keepAlive);
|
|
1942
|
+
session.sseResponse = undefined;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
else {
|
|
1946
|
+
clearInterval(keepAlive);
|
|
1947
|
+
}
|
|
1948
|
+
}, 15000); // Reduced from 30s to 15s for better responsiveness
|
|
1949
|
+
// Handle client disconnect
|
|
1950
|
+
const cleanup = () => {
|
|
1673
1951
|
clearInterval(keepAlive);
|
|
1674
1952
|
session.sseResponse = undefined;
|
|
1675
1953
|
// Clean up subscriptions when client disconnects
|
|
1676
1954
|
if (options.subscriptionManager) {
|
|
1677
1955
|
options.subscriptionManager.onClientDisconnect(session.id);
|
|
1678
1956
|
}
|
|
1679
|
-
}
|
|
1957
|
+
};
|
|
1958
|
+
req.on('close', cleanup);
|
|
1959
|
+
req.on('error', cleanup);
|
|
1960
|
+
res.on('error', cleanup);
|
|
1680
1961
|
return true;
|
|
1681
1962
|
}
|
|
1682
1963
|
// POST - Handle JSON-RPC requests
|
|
@@ -1781,12 +2062,33 @@ export function broadcastNotification(method, params, beamOnly = false) {
|
|
|
1781
2062
|
method,
|
|
1782
2063
|
params,
|
|
1783
2064
|
};
|
|
1784
|
-
|
|
1785
|
-
|
|
2065
|
+
const data = `data: ${JSON.stringify(notification)}\n\n`;
|
|
2066
|
+
const deadSessions = [];
|
|
2067
|
+
for (const [sessionId, session] of sessions) {
|
|
2068
|
+
if (session.sseResponse &&
|
|
2069
|
+
!session.sseResponse.writableEnded &&
|
|
2070
|
+
!session.sseResponse.destroyed) {
|
|
1786
2071
|
// Skip non-Beam clients if beamOnly is true
|
|
1787
2072
|
if (beamOnly && !session.isBeam)
|
|
1788
2073
|
continue;
|
|
1789
|
-
|
|
2074
|
+
try {
|
|
2075
|
+
session.sseResponse.write(data);
|
|
2076
|
+
}
|
|
2077
|
+
catch (err) {
|
|
2078
|
+
// Mark session for cleanup if write fails
|
|
2079
|
+
deadSessions.push(sessionId);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
else if (session.sseResponse) {
|
|
2083
|
+
// Response is ended/destroyed - mark for cleanup
|
|
2084
|
+
deadSessions.push(sessionId);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
// Clean up dead sessions
|
|
2088
|
+
for (const sessionId of deadSessions) {
|
|
2089
|
+
const session = sessions.get(sessionId);
|
|
2090
|
+
if (session) {
|
|
2091
|
+
session.sseResponse = undefined;
|
|
1790
2092
|
}
|
|
1791
2093
|
}
|
|
1792
2094
|
}
|
|
@@ -1817,7 +2119,7 @@ export function getActiveSessionCount() {
|
|
|
1817
2119
|
*/
|
|
1818
2120
|
export function sendToSession(sessionId, method, params) {
|
|
1819
2121
|
const session = sessions.get(sessionId);
|
|
1820
|
-
if (!session?.sseResponse || session.sseResponse.writableEnded) {
|
|
2122
|
+
if (!session?.sseResponse || session.sseResponse.writableEnded || session.sseResponse.destroyed) {
|
|
1821
2123
|
return false;
|
|
1822
2124
|
}
|
|
1823
2125
|
const notification = {
|
|
@@ -1825,8 +2127,15 @@ export function sendToSession(sessionId, method, params) {
|
|
|
1825
2127
|
method,
|
|
1826
2128
|
params,
|
|
1827
2129
|
};
|
|
1828
|
-
|
|
1829
|
-
|
|
2130
|
+
try {
|
|
2131
|
+
session.sseResponse.write(`data: ${JSON.stringify(notification)}\n\n`);
|
|
2132
|
+
return true;
|
|
2133
|
+
}
|
|
2134
|
+
catch (err) {
|
|
2135
|
+
// Write failed - connection is dead
|
|
2136
|
+
session.sseResponse = undefined;
|
|
2137
|
+
return false;
|
|
2138
|
+
}
|
|
1830
2139
|
}
|
|
1831
2140
|
/**
|
|
1832
2141
|
* Request elicitation from the frontend for an external MCP.
|
|
@@ -1872,4 +2181,39 @@ export function requestExternalElicitation(mcpName, request) {
|
|
|
1872
2181
|
}, 300000);
|
|
1873
2182
|
});
|
|
1874
2183
|
}
|
|
2184
|
+
/**
|
|
2185
|
+
* Request elicitation from Beam using Photon-native ask types (select, text, etc.)
|
|
2186
|
+
* Unlike requestExternalElicitation which uses MCP form/url mode, this sends
|
|
2187
|
+
* the ask type directly so the elicitation modal renders the appropriate UI.
|
|
2188
|
+
*/
|
|
2189
|
+
function requestBeamElicitation(data) {
|
|
2190
|
+
const elicitationId = randomUUID();
|
|
2191
|
+
return new Promise((resolve) => {
|
|
2192
|
+
pendingElicitations.set(elicitationId, {
|
|
2193
|
+
resolve: (value) => {
|
|
2194
|
+
resolve({ action: 'accept', content: value });
|
|
2195
|
+
},
|
|
2196
|
+
reject: (error) => {
|
|
2197
|
+
if (error.message.includes('cancelled')) {
|
|
2198
|
+
resolve({ action: 'cancel' });
|
|
2199
|
+
}
|
|
2200
|
+
else {
|
|
2201
|
+
resolve({ action: 'decline' });
|
|
2202
|
+
}
|
|
2203
|
+
},
|
|
2204
|
+
sessionId: '',
|
|
2205
|
+
});
|
|
2206
|
+
// Broadcast with Photon-native ask format (not MCP form mode)
|
|
2207
|
+
broadcastToBeam('beam/elicitation', {
|
|
2208
|
+
elicitationId,
|
|
2209
|
+
...data,
|
|
2210
|
+
});
|
|
2211
|
+
setTimeout(() => {
|
|
2212
|
+
if (pendingElicitations.has(elicitationId)) {
|
|
2213
|
+
pendingElicitations.delete(elicitationId);
|
|
2214
|
+
resolve({ action: 'cancel' });
|
|
2215
|
+
}
|
|
2216
|
+
}, 300000);
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
1875
2219
|
//# sourceMappingURL=streamable-http-transport.js.map
|