@portel/photon 1.7.0 → 1.8.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 +23 -24
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +117 -42
- 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 +1 -1
- package/dist/auto-ui/design-system/tokens.js.map +1 -1
- package/dist/auto-ui/frontend/index.html +1 -1
- 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 +353 -19
- 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 +22441 -4216
- 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 +628 -14
- 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 +257 -35
- 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 +53 -75
- package/dist/loader.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 +42 -6
- 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 +3 -1
- package/dist/photons/maker.photon.js.map +1 -1
- package/dist/photons/maker.photon.ts +3 -1
- package/dist/serv/index.d.ts.map +1 -1
- 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 +468 -469
- package/dist/server.js.map +1 -1
- package/dist/shared/security.d.ts.map +1 -1
- package/dist/shared/security.js +4 -8
- package/dist/shared/security.js.map +1 -1
- 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.map +1 -1
- package/package.json +10 -6
|
@@ -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
|
|
@@ -788,11 +959,12 @@ const handlers = {
|
|
|
788
959
|
}
|
|
789
960
|
// Use return value if no chunks were yielded, otherwise use chunks
|
|
790
961
|
const finalResult = chunks.length > 0 ? (chunks.length === 1 ? chunks[0] : chunks) : returnValue;
|
|
962
|
+
const genResultText = formatResultText(finalResult);
|
|
791
963
|
const genResponse = {
|
|
792
964
|
jsonrpc: '2.0',
|
|
793
965
|
id: req.id,
|
|
794
966
|
result: {
|
|
795
|
-
content: [{ type: 'text', text:
|
|
967
|
+
content: [{ type: 'text', text: genResultText }],
|
|
796
968
|
isError: false,
|
|
797
969
|
...uiMetadata,
|
|
798
970
|
},
|
|
@@ -811,11 +983,13 @@ const handlers = {
|
|
|
811
983
|
}
|
|
812
984
|
return genResponse;
|
|
813
985
|
}
|
|
986
|
+
// For void methods, provide a success acknowledgment so the UI shows feedback
|
|
987
|
+
const resultText = formatResultText(result);
|
|
814
988
|
const toolResponse = {
|
|
815
989
|
jsonrpc: '2.0',
|
|
816
990
|
id: req.id,
|
|
817
991
|
result: {
|
|
818
|
-
content: [{ type: 'text', text:
|
|
992
|
+
content: [{ type: 'text', text: resultText }],
|
|
819
993
|
isError: false,
|
|
820
994
|
...uiMetadata,
|
|
821
995
|
},
|
|
@@ -896,6 +1070,83 @@ const handlers = {
|
|
|
896
1070
|
},
|
|
897
1071
|
};
|
|
898
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
|
+
},
|
|
899
1150
|
};
|
|
900
1151
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
901
1152
|
// BEAM SYSTEM TOOLS
|
|
@@ -1459,7 +1710,7 @@ async function handleBeamStudioWrite(req, ctx, args) {
|
|
|
1459
1710
|
const versionMatch = source.match(/@version\s+(\S+)/);
|
|
1460
1711
|
const runtimeMatch = source.match(/@runtime\s+(\S+)/);
|
|
1461
1712
|
const iconMatch = source.match(/@icon\s+(\S+)/);
|
|
1462
|
-
const statefulMatch = source.match(/@stateful\
|
|
1713
|
+
const statefulMatch = source.match(/@stateful\b/);
|
|
1463
1714
|
const depsMatch = source.match(/@dependencies\s+(.+)/);
|
|
1464
1715
|
const tagsMatch = source.match(/@tags\s+(.+)/);
|
|
1465
1716
|
parseResult = {
|
|
@@ -1558,7 +1809,7 @@ async function handleBeamStudioParse(req, args) {
|
|
|
1558
1809
|
const versionMatch = source.match(/@version\s+(\S+)/);
|
|
1559
1810
|
const runtimeMatch = source.match(/@runtime\s+(\S+)/);
|
|
1560
1811
|
const iconMatch = source.match(/@icon\s+(\S+)/);
|
|
1561
|
-
const statefulMatch = source.match(/@stateful\
|
|
1812
|
+
const statefulMatch = source.match(/@stateful\b/);
|
|
1562
1813
|
const depsMatch = source.match(/@dependencies\s+(.+)/);
|
|
1563
1814
|
const tagsMatch = source.match(/@tags\s+(.+)/);
|
|
1564
1815
|
const errors = [];
|
|
@@ -1673,20 +1924,40 @@ export async function handleStreamableHTTP(req, res, options) {
|
|
|
1673
1924
|
});
|
|
1674
1925
|
// Disable Nagle's algorithm for immediate writes
|
|
1675
1926
|
res.socket?.setNoDelay(true);
|
|
1927
|
+
// Enable TCP keepalive to prevent connection drops from intermediaries
|
|
1928
|
+
res.socket?.setKeepAlive(true, 60000);
|
|
1676
1929
|
// Store SSE response for server-initiated messages
|
|
1677
1930
|
session.sseResponse = res;
|
|
1678
|
-
// Keep connection alive
|
|
1931
|
+
// Keep connection alive with SSE data events (every 15s for better reliability)
|
|
1679
1932
|
const keepAlive = setInterval(() => {
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
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 = () => {
|
|
1683
1951
|
clearInterval(keepAlive);
|
|
1684
1952
|
session.sseResponse = undefined;
|
|
1685
1953
|
// Clean up subscriptions when client disconnects
|
|
1686
1954
|
if (options.subscriptionManager) {
|
|
1687
1955
|
options.subscriptionManager.onClientDisconnect(session.id);
|
|
1688
1956
|
}
|
|
1689
|
-
}
|
|
1957
|
+
};
|
|
1958
|
+
req.on('close', cleanup);
|
|
1959
|
+
req.on('error', cleanup);
|
|
1960
|
+
res.on('error', cleanup);
|
|
1690
1961
|
return true;
|
|
1691
1962
|
}
|
|
1692
1963
|
// POST - Handle JSON-RPC requests
|
|
@@ -1791,12 +2062,33 @@ export function broadcastNotification(method, params, beamOnly = false) {
|
|
|
1791
2062
|
method,
|
|
1792
2063
|
params,
|
|
1793
2064
|
};
|
|
1794
|
-
|
|
1795
|
-
|
|
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) {
|
|
1796
2071
|
// Skip non-Beam clients if beamOnly is true
|
|
1797
2072
|
if (beamOnly && !session.isBeam)
|
|
1798
2073
|
continue;
|
|
1799
|
-
|
|
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;
|
|
1800
2092
|
}
|
|
1801
2093
|
}
|
|
1802
2094
|
}
|
|
@@ -1827,7 +2119,7 @@ export function getActiveSessionCount() {
|
|
|
1827
2119
|
*/
|
|
1828
2120
|
export function sendToSession(sessionId, method, params) {
|
|
1829
2121
|
const session = sessions.get(sessionId);
|
|
1830
|
-
if (!session?.sseResponse || session.sseResponse.writableEnded) {
|
|
2122
|
+
if (!session?.sseResponse || session.sseResponse.writableEnded || session.sseResponse.destroyed) {
|
|
1831
2123
|
return false;
|
|
1832
2124
|
}
|
|
1833
2125
|
const notification = {
|
|
@@ -1835,8 +2127,15 @@ export function sendToSession(sessionId, method, params) {
|
|
|
1835
2127
|
method,
|
|
1836
2128
|
params,
|
|
1837
2129
|
};
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
+
}
|
|
1840
2139
|
}
|
|
1841
2140
|
/**
|
|
1842
2141
|
* Request elicitation from the frontend for an external MCP.
|
|
@@ -1882,4 +2181,39 @@ export function requestExternalElicitation(mcpName, request) {
|
|
|
1882
2181
|
}, 300000);
|
|
1883
2182
|
});
|
|
1884
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
|
+
}
|
|
1885
2219
|
//# sourceMappingURL=streamable-http-transport.js.map
|