@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
package/dist/server.js
CHANGED
|
@@ -13,16 +13,16 @@ import { createServer } from 'node:http';
|
|
|
13
13
|
import { URL } from 'node:url';
|
|
14
14
|
import { PhotonLoader } from './loader.js';
|
|
15
15
|
import { generateExecutionId, } from '@portel/photon-core';
|
|
16
|
-
import {
|
|
16
|
+
import { createSDKMCPClientFactory } from '@portel/photon-core';
|
|
17
17
|
import { PHOTON_VERSION } from './version.js';
|
|
18
18
|
import { createLogger } from './shared/logger.js';
|
|
19
19
|
import { getErrorMessage } from './shared/error-handler.js';
|
|
20
20
|
import { validateOrThrow, assertString, notEmpty, inRange, oneOf, hasExtension, } from './shared/validation.js';
|
|
21
21
|
import { generatePlaygroundHTML } from './auto-ui/playground-html.js';
|
|
22
|
-
import { subscribeChannel, pingDaemon,
|
|
23
|
-
import {
|
|
22
|
+
import { subscribeChannel, pingDaemon, publishToChannel } from './daemon/client.js';
|
|
23
|
+
import { isGlobalDaemonRunning, startGlobalDaemon } from './daemon/manager.js';
|
|
24
24
|
import { PhotonDocExtractor } from './photon-doc-extractor.js';
|
|
25
|
-
import { isLocalRequest, readBody, setSecurityHeaders, isPathWithin, validateAssetPath } from './shared/security.js';
|
|
25
|
+
import { isLocalRequest, readBody, setSecurityHeaders, isPathWithin, validateAssetPath, } from './shared/security.js';
|
|
26
26
|
export class HotReloadDisabledError extends Error {
|
|
27
27
|
constructor(message) {
|
|
28
28
|
super(message);
|
|
@@ -43,6 +43,12 @@ export class PhotonServer {
|
|
|
43
43
|
statusClients = new Set();
|
|
44
44
|
channelUnsubscribers = [];
|
|
45
45
|
daemonName = null;
|
|
46
|
+
/** Tracked instance name for daemon drift recovery (STDIO path) */
|
|
47
|
+
daemonInstanceName;
|
|
48
|
+
/** Tracked instance names per SSE session for daemon drift recovery */
|
|
49
|
+
sseInstanceNames = new Map();
|
|
50
|
+
/** Whether client capabilities have been logged (one-time on first tools/list) */
|
|
51
|
+
clientCapabilitiesLogged = false;
|
|
46
52
|
currentStatus = {
|
|
47
53
|
type: 'info',
|
|
48
54
|
message: 'Ready',
|
|
@@ -114,95 +120,31 @@ export class PhotonServer {
|
|
|
114
120
|
/**
|
|
115
121
|
* Detect UI format based on client capabilities
|
|
116
122
|
*
|
|
117
|
-
*
|
|
118
|
-
* Legacy Photon clients may not have explicit UI capability but support photon:// URIs.
|
|
123
|
+
* All clients use the MCP Apps standard (SEP-1865) ui:// format.
|
|
119
124
|
* Text-only clients have no UI support.
|
|
120
|
-
*
|
|
121
|
-
* @param server - Optional server instance (for SSE sessions), defaults to main server
|
|
122
125
|
*/
|
|
123
|
-
getUIFormat(
|
|
124
|
-
|
|
125
|
-
const capabilities = targetServer.getClientCapabilities();
|
|
126
|
-
if (!capabilities) {
|
|
127
|
-
// Before initialization or no capabilities - assume legacy Photon
|
|
128
|
-
return 'photon';
|
|
129
|
-
}
|
|
130
|
-
// Check for MCP Apps extension (io.modelcontextprotocol/ui)
|
|
131
|
-
// Claude Desktop and other MCP Apps clients use this format
|
|
132
|
-
const extensions = capabilities.extensions;
|
|
133
|
-
if (extensions?.['io.modelcontextprotocol/ui']) {
|
|
134
|
-
return 'sep-1865';
|
|
135
|
-
}
|
|
136
|
-
// Check for SEP-1865 UI capability
|
|
137
|
-
// SEP-1865 clients advertise: { experimental: { ui: {} } } or { ui: {} }
|
|
138
|
-
const experimental = capabilities.experimental;
|
|
139
|
-
if (experimental?.ui || capabilities.ui) {
|
|
140
|
-
return 'sep-1865';
|
|
141
|
-
}
|
|
142
|
-
// Check client info for known SEP-1865 compatible clients
|
|
143
|
-
const clientInfo = targetServer._clientVersion;
|
|
144
|
-
if (clientInfo?.name) {
|
|
145
|
-
const name = clientInfo.name.toLowerCase();
|
|
146
|
-
// Known SEP-1865 compatible clients
|
|
147
|
-
if (name.includes('claude') || name.includes('chatgpt') || name.includes('openai')) {
|
|
148
|
-
return 'sep-1865';
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
// Default to Photon format for backward compatibility
|
|
152
|
-
return 'photon';
|
|
126
|
+
getUIFormat() {
|
|
127
|
+
return 'sep-1865';
|
|
153
128
|
}
|
|
154
129
|
/**
|
|
155
130
|
* Build UI resource URI based on detected format
|
|
156
|
-
*
|
|
157
|
-
* @param uiId - UI template identifier
|
|
158
|
-
* @param server - Optional server instance (for SSE sessions)
|
|
159
131
|
*/
|
|
160
|
-
buildUIResourceUri(uiId
|
|
161
|
-
const format = this.getUIFormat(server);
|
|
132
|
+
buildUIResourceUri(uiId) {
|
|
162
133
|
const photonName = this.mcp?.name || 'unknown';
|
|
163
|
-
|
|
164
|
-
case 'sep-1865':
|
|
165
|
-
return `ui://${photonName}/${uiId}`;
|
|
166
|
-
case 'photon':
|
|
167
|
-
default:
|
|
168
|
-
return `photon://${photonName}/ui/${uiId}`;
|
|
169
|
-
}
|
|
134
|
+
return `ui://${photonName}/${uiId}`;
|
|
170
135
|
}
|
|
171
136
|
/**
|
|
172
137
|
* Build tool metadata for UI based on detected format
|
|
173
|
-
*
|
|
174
|
-
* @param uiId - UI template identifier
|
|
175
|
-
* @param server - Optional server instance (for SSE sessions)
|
|
176
138
|
*/
|
|
177
|
-
buildUIToolMeta(uiId
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
switch (format) {
|
|
181
|
-
case 'sep-1865':
|
|
182
|
-
// Official MCP Apps spec: _meta.ui.resourceUri
|
|
183
|
-
return { ui: { resourceUri: uri } };
|
|
184
|
-
case 'photon':
|
|
185
|
-
default:
|
|
186
|
-
return { outputTemplate: uri };
|
|
187
|
-
}
|
|
139
|
+
buildUIToolMeta(uiId) {
|
|
140
|
+
const uri = this.buildUIResourceUri(uiId);
|
|
141
|
+
return { ui: { resourceUri: uri } };
|
|
188
142
|
}
|
|
189
143
|
/**
|
|
190
144
|
* Get UI mimeType based on detected format and client capabilities
|
|
191
|
-
*
|
|
192
|
-
* @param server - Optional server instance (for SSE sessions)
|
|
193
145
|
*/
|
|
194
|
-
getUIMimeType(
|
|
195
|
-
|
|
196
|
-
const capabilities = targetServer.getClientCapabilities();
|
|
197
|
-
// Check for MCP Apps extension with declared mimeTypes
|
|
198
|
-
// Claude Desktop uses: { extensions: { "io.modelcontextprotocol/ui": { mimeTypes: ["text/html;profile=mcp-app"] } } }
|
|
199
|
-
const extensions = capabilities?.extensions;
|
|
200
|
-
const mcpUI = extensions?.['io.modelcontextprotocol/ui'];
|
|
201
|
-
if (mcpUI?.mimeTypes?.[0]) {
|
|
202
|
-
return mcpUI.mimeTypes[0];
|
|
203
|
-
}
|
|
204
|
-
const format = this.getUIFormat(server);
|
|
205
|
-
return format === 'sep-1865' ? 'text/html;profile=mcp-app' : 'text/html';
|
|
146
|
+
getUIMimeType() {
|
|
147
|
+
return 'text/html;profile=mcp-app';
|
|
206
148
|
}
|
|
207
149
|
/**
|
|
208
150
|
* Check if client supports elicitation
|
|
@@ -219,6 +161,51 @@ export class PhotonServer {
|
|
|
219
161
|
// Check for elicitation capability (MCP 2025-06 spec)
|
|
220
162
|
return !!capabilities.elicitation;
|
|
221
163
|
}
|
|
164
|
+
// Known clients that support MCP Apps UI (fallback when capability isn't announced)
|
|
165
|
+
static UI_CAPABLE_CLIENTS = new Set(['chatgpt', 'mcpjam', 'mcp-inspector']);
|
|
166
|
+
/**
|
|
167
|
+
* Check if client supports MCP Apps UI (structuredContent + _meta.ui)
|
|
168
|
+
*
|
|
169
|
+
* Detection order:
|
|
170
|
+
* 1. capabilities.experimental["io.modelcontextprotocol/ui"] — official MCP Apps negotiation
|
|
171
|
+
* 2. clientInfo.name — fallback for known UI-capable clients
|
|
172
|
+
*
|
|
173
|
+
* Basic/unknown clients get text-only responses (no structuredContent, no _meta.ui).
|
|
174
|
+
*/
|
|
175
|
+
clientSupportsUI(server) {
|
|
176
|
+
const targetServer = server || this.server;
|
|
177
|
+
// 1. Check capabilities (official MCP Apps negotiation)
|
|
178
|
+
const capabilities = targetServer.getClientCapabilities();
|
|
179
|
+
if (capabilities?.experimental?.['io.modelcontextprotocol/ui']) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
// 2. Check clientInfo.name (fallback for known UI-capable clients)
|
|
183
|
+
const clientInfo = targetServer.getClientVersion();
|
|
184
|
+
if (clientInfo?.name) {
|
|
185
|
+
if (clientInfo.name === 'beam')
|
|
186
|
+
return true;
|
|
187
|
+
if (PhotonServer.UI_CAPABLE_CLIENTS.has(clientInfo.name))
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Log client identity and capabilities for debugging tier detection
|
|
194
|
+
*/
|
|
195
|
+
logClientCapabilities(server) {
|
|
196
|
+
const clientInfo = server.getClientVersion();
|
|
197
|
+
const capabilities = server.getClientCapabilities();
|
|
198
|
+
const supportsUI = this.clientSupportsUI(server);
|
|
199
|
+
const supportsElicitation = this.clientSupportsElicitation(server);
|
|
200
|
+
this.log('debug', 'Client connected', {
|
|
201
|
+
name: clientInfo?.name ?? 'unknown',
|
|
202
|
+
version: clientInfo?.version ?? 'unknown',
|
|
203
|
+
tier: supportsUI ? 'mcp-apps' : 'basic',
|
|
204
|
+
supportsUI,
|
|
205
|
+
supportsElicitation,
|
|
206
|
+
capabilities: JSON.stringify(capabilities),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
222
209
|
/**
|
|
223
210
|
* Create an MCP-aware input provider for generator ask yields
|
|
224
211
|
*
|
|
@@ -420,68 +407,320 @@ export class PhotonServer {
|
|
|
420
407
|
return '';
|
|
421
408
|
}
|
|
422
409
|
}
|
|
410
|
+
// ─── Shared handler implementations ─────────────────────────────────
|
|
411
|
+
// These methods contain the core logic shared between STDIO and SSE transports.
|
|
412
|
+
// Both setupHandlers() and setupSessionHandlers() delegate to these.
|
|
413
|
+
handleListTools(ctx) {
|
|
414
|
+
if (!this.mcp) {
|
|
415
|
+
return { tools: [] };
|
|
416
|
+
}
|
|
417
|
+
const tools = this.mcp.tools.map((tool) => {
|
|
418
|
+
const toolDef = {
|
|
419
|
+
name: tool.name,
|
|
420
|
+
description: tool.description,
|
|
421
|
+
inputSchema: tool.inputSchema,
|
|
422
|
+
};
|
|
423
|
+
const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name);
|
|
424
|
+
if (linkedUI && this.clientSupportsUI(ctx.server)) {
|
|
425
|
+
toolDef._meta = this.buildUIToolMeta(linkedUI.id);
|
|
426
|
+
}
|
|
427
|
+
return toolDef;
|
|
428
|
+
});
|
|
429
|
+
// Add runtime-injected instance tools for stateful photons
|
|
430
|
+
if (this.daemonName) {
|
|
431
|
+
tools.push({
|
|
432
|
+
name: '_use',
|
|
433
|
+
description: `Switch to a named instance. Pass empty name for default. Omit name to select interactively.`,
|
|
434
|
+
inputSchema: {
|
|
435
|
+
type: 'object',
|
|
436
|
+
properties: {
|
|
437
|
+
name: {
|
|
438
|
+
type: 'string',
|
|
439
|
+
description: 'Instance name. Pass empty string "" for default. Omit entirely to select interactively.',
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
tools.push({
|
|
445
|
+
name: '_instances',
|
|
446
|
+
description: `List all available instances.`,
|
|
447
|
+
inputSchema: { type: 'object', properties: {} },
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return { tools };
|
|
451
|
+
}
|
|
452
|
+
async handleCallTool(ctx, request) {
|
|
453
|
+
if (!this.mcp) {
|
|
454
|
+
throw new Error('MCP not loaded');
|
|
455
|
+
}
|
|
456
|
+
const { name: toolName, arguments: args } = request.params;
|
|
457
|
+
// Route _use and _instances through daemon for stateful photons
|
|
458
|
+
if (this.daemonName && (toolName === '_use' || toolName === '_instances')) {
|
|
459
|
+
const { sendCommand } = await import('./daemon/client.js');
|
|
460
|
+
const sendOpts = {
|
|
461
|
+
photonPath: this.options.filePath,
|
|
462
|
+
sessionId: ctx.sessionId,
|
|
463
|
+
instanceName: ctx.getInstanceName(),
|
|
464
|
+
};
|
|
465
|
+
// Elicitation-based instance selection when _use called without name
|
|
466
|
+
if (toolName === '_use' &&
|
|
467
|
+
(!args || !('name' in args)) &&
|
|
468
|
+
this.clientSupportsElicitation(ctx.server)) {
|
|
469
|
+
const instancesResult = (await sendCommand(this.daemonName, '_instances', {}, sendOpts));
|
|
470
|
+
const instances = instancesResult?.instances || ['default'];
|
|
471
|
+
const options = instances.map((inst) => ({
|
|
472
|
+
const: inst,
|
|
473
|
+
title: inst === 'default' ? '(default)' : inst,
|
|
474
|
+
}));
|
|
475
|
+
options.push({ const: '__create_new__', title: 'Create new...' });
|
|
476
|
+
const result = await ctx.server.elicitInput({
|
|
477
|
+
message: 'Select an instance',
|
|
478
|
+
requestedSchema: {
|
|
479
|
+
type: 'object',
|
|
480
|
+
properties: {
|
|
481
|
+
instance: {
|
|
482
|
+
type: 'string',
|
|
483
|
+
title: 'Instance',
|
|
484
|
+
oneOf: options,
|
|
485
|
+
default: instancesResult?.current || 'default',
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
required: ['instance'],
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
if (result.action !== 'accept' || !result.content) {
|
|
492
|
+
return { content: [{ type: 'text', text: 'Cancelled' }] };
|
|
493
|
+
}
|
|
494
|
+
let selectedName = result.content.instance;
|
|
495
|
+
// Handle "Create new..." selection
|
|
496
|
+
if (selectedName === '__create_new__') {
|
|
497
|
+
const nameResult = await ctx.server.elicitInput({
|
|
498
|
+
message: 'Enter a name for the new instance',
|
|
499
|
+
requestedSchema: {
|
|
500
|
+
type: 'object',
|
|
501
|
+
properties: {
|
|
502
|
+
name: { type: 'string', title: 'Instance name' },
|
|
503
|
+
},
|
|
504
|
+
required: ['name'],
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
if (nameResult.action !== 'accept' || !nameResult.content) {
|
|
508
|
+
return { content: [{ type: 'text', text: 'Cancelled' }] };
|
|
509
|
+
}
|
|
510
|
+
selectedName = nameResult.content.name;
|
|
511
|
+
}
|
|
512
|
+
const useResult = await sendCommand(this.daemonName, '_use', { name: selectedName }, sendOpts);
|
|
513
|
+
ctx.setInstanceName(selectedName);
|
|
514
|
+
return {
|
|
515
|
+
content: [{ type: 'text', text: JSON.stringify(useResult, null, 2) }],
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
const result = await sendCommand(this.daemonName, toolName, (args || {}), sendOpts);
|
|
519
|
+
// Track instance name after successful _use
|
|
520
|
+
if (toolName === '_use') {
|
|
521
|
+
ctx.setInstanceName(String(args?.name || ''));
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
// Create MCP-aware input provider for elicitation support
|
|
528
|
+
const inputProvider = this.createMCPInputProvider(ctx.server);
|
|
529
|
+
// Handler for channel events - forward to daemon for cross-process pub/sub
|
|
530
|
+
const outputHandler = (emit) => {
|
|
531
|
+
if (this.daemonName && emit?.channel) {
|
|
532
|
+
publishToChannel(this.daemonName, emit.channel, emit).catch(() => {
|
|
533
|
+
// Ignore publish errors - daemon may not be running
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
const tool = this.mcp.tools.find((t) => t.name === toolName);
|
|
538
|
+
const outputFormat = tool?.outputFormat;
|
|
539
|
+
const result = await this.loader.executeTool(this.mcp, toolName, args || {}, {
|
|
540
|
+
inputProvider,
|
|
541
|
+
outputHandler,
|
|
542
|
+
});
|
|
543
|
+
const isStateful = result && typeof result === 'object' && result._stateful === true;
|
|
544
|
+
const actualResult = isStateful ? result.result : result;
|
|
545
|
+
// Build content with optional mimeType annotation
|
|
546
|
+
const content = {
|
|
547
|
+
type: 'text',
|
|
548
|
+
text: this.formatResult(actualResult),
|
|
549
|
+
};
|
|
550
|
+
if (outputFormat) {
|
|
551
|
+
const { formatToMimeType } = await import('./cli-formatter.js');
|
|
552
|
+
const mimeType = formatToMimeType(outputFormat);
|
|
553
|
+
if (mimeType) {
|
|
554
|
+
content.annotations = { mimeType };
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const response = { content: [content], isError: false };
|
|
558
|
+
// Add x-output-format for format-aware clients
|
|
559
|
+
if (outputFormat) {
|
|
560
|
+
response['x-output-format'] = outputFormat;
|
|
561
|
+
}
|
|
562
|
+
// Add stateful workflow metadata (machine-readable _meta)
|
|
563
|
+
if (isStateful && result.runId) {
|
|
564
|
+
response._meta = { ...response._meta, runId: result.runId, status: result.status };
|
|
565
|
+
}
|
|
566
|
+
// Enrich response with structuredContent + _meta for tools with linked UIs
|
|
567
|
+
const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === toolName);
|
|
568
|
+
if (linkedUI && this.clientSupportsUI(ctx.server)) {
|
|
569
|
+
if (actualResult !== undefined && actualResult !== null) {
|
|
570
|
+
response.structuredContent =
|
|
571
|
+
typeof actualResult === 'string' ? { text: actualResult } : actualResult;
|
|
572
|
+
}
|
|
573
|
+
const uiMeta = this.buildUIToolMeta(linkedUI.id);
|
|
574
|
+
response._meta = { ...response._meta, ...uiMeta };
|
|
575
|
+
}
|
|
576
|
+
return response;
|
|
577
|
+
}
|
|
578
|
+
handleListPrompts() {
|
|
579
|
+
if (!this.mcp) {
|
|
580
|
+
return { prompts: [] };
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
prompts: this.mcp.templates.map((template) => ({
|
|
584
|
+
name: template.name,
|
|
585
|
+
description: template.description,
|
|
586
|
+
arguments: Object.entries(template.inputSchema.properties || {}).map(([name, schema]) => ({
|
|
587
|
+
name,
|
|
588
|
+
description: (typeof schema === 'object' && schema && 'description' in schema
|
|
589
|
+
? schema.description
|
|
590
|
+
: '') || '',
|
|
591
|
+
required: template.inputSchema.required?.includes(name) || false,
|
|
592
|
+
})),
|
|
593
|
+
})),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
async handleGetPrompt(request) {
|
|
597
|
+
if (!this.mcp) {
|
|
598
|
+
throw new Error('MCP not loaded');
|
|
599
|
+
}
|
|
600
|
+
const { name: promptName, arguments: args } = request.params;
|
|
601
|
+
const template = this.mcp.templates.find((t) => t.name === promptName);
|
|
602
|
+
if (!template) {
|
|
603
|
+
throw new Error(`Prompt not found: ${promptName}`);
|
|
604
|
+
}
|
|
605
|
+
const result = await this.loader.executeTool(this.mcp, promptName, args || {});
|
|
606
|
+
return this.formatTemplateResult(result);
|
|
607
|
+
}
|
|
608
|
+
handleListResources(ctx) {
|
|
609
|
+
if (!this.mcp) {
|
|
610
|
+
return { resources: [] };
|
|
611
|
+
}
|
|
612
|
+
const staticResources = this.mcp.statics.filter((s) => !this.isUriTemplate(s.uri));
|
|
613
|
+
const resources = staticResources.map((static_) => ({
|
|
614
|
+
uri: static_.uri,
|
|
615
|
+
name: static_.name,
|
|
616
|
+
description: static_.description,
|
|
617
|
+
mimeType: static_.mimeType || 'text/plain',
|
|
618
|
+
}));
|
|
619
|
+
if (this.mcp.assets) {
|
|
620
|
+
const photonName = this.mcp.name;
|
|
621
|
+
for (const ui of this.mcp.assets.ui) {
|
|
622
|
+
const uiUri = ui.uri || this.buildUIResourceUri(ui.id);
|
|
623
|
+
resources.push({
|
|
624
|
+
uri: uiUri,
|
|
625
|
+
name: `ui:${ui.id}`,
|
|
626
|
+
description: ui.linkedTool
|
|
627
|
+
? `UI template for ${ui.linkedTool} tool`
|
|
628
|
+
: `UI template: ${ui.id}`,
|
|
629
|
+
mimeType: ui.mimeType || this.getUIMimeType(),
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
for (const prompt of this.mcp.assets.prompts) {
|
|
633
|
+
resources.push({
|
|
634
|
+
uri: `photon://${photonName}/prompts/${prompt.id}`,
|
|
635
|
+
name: `prompt:${prompt.id}`,
|
|
636
|
+
description: prompt.description || `Prompt template: ${prompt.id}`,
|
|
637
|
+
mimeType: 'text/markdown',
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
for (const resource of this.mcp.assets.resources) {
|
|
641
|
+
resources.push({
|
|
642
|
+
uri: `photon://${photonName}/resources/${resource.id}`,
|
|
643
|
+
name: `resource:${resource.id}`,
|
|
644
|
+
description: resource.description || `Static resource: ${resource.id}`,
|
|
645
|
+
mimeType: resource.mimeType || 'application/octet-stream',
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return { resources };
|
|
650
|
+
}
|
|
651
|
+
handleListResourceTemplates() {
|
|
652
|
+
if (!this.mcp) {
|
|
653
|
+
return { resourceTemplates: [] };
|
|
654
|
+
}
|
|
655
|
+
const templateResources = this.mcp.statics.filter((s) => this.isUriTemplate(s.uri));
|
|
656
|
+
return {
|
|
657
|
+
resourceTemplates: templateResources.map((static_) => ({
|
|
658
|
+
uriTemplate: static_.uri,
|
|
659
|
+
name: static_.name,
|
|
660
|
+
description: static_.description,
|
|
661
|
+
mimeType: static_.mimeType || 'text/plain',
|
|
662
|
+
})),
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
async handleReadResource(request) {
|
|
666
|
+
if (!this.mcp) {
|
|
667
|
+
throw new Error('MCP not loaded');
|
|
668
|
+
}
|
|
669
|
+
const { uri } = request.params;
|
|
670
|
+
const uiMatch = uri.match(/^ui:\/\/([^/]+)\/(.+)$/);
|
|
671
|
+
if (uiMatch && this.mcp.assets) {
|
|
672
|
+
const [, _photonName, assetId] = uiMatch;
|
|
673
|
+
return this.handleUIAssetRead(uri, assetId);
|
|
674
|
+
}
|
|
675
|
+
const assetMatch = uri.match(/^photon:\/\/([^/]+)\/(ui|prompts|resources)\/(.+)$/);
|
|
676
|
+
if (assetMatch && this.mcp.assets) {
|
|
677
|
+
return this.handleAssetRead(uri, assetMatch);
|
|
678
|
+
}
|
|
679
|
+
return this.handleStaticRead(uri);
|
|
680
|
+
}
|
|
681
|
+
// ─── Transport-specific setup ─────────────────────────────────────
|
|
423
682
|
/**
|
|
424
|
-
* Set up MCP protocol handlers
|
|
683
|
+
* Set up MCP protocol handlers (STDIO transport)
|
|
425
684
|
*/
|
|
426
685
|
setupHandlers() {
|
|
427
|
-
|
|
686
|
+
const ctx = {
|
|
687
|
+
server: this.server,
|
|
688
|
+
getInstanceName: () => this.daemonInstanceName,
|
|
689
|
+
setInstanceName: (name) => {
|
|
690
|
+
this.daemonInstanceName = name;
|
|
691
|
+
},
|
|
692
|
+
sessionId: `stdio-${this.daemonName}`,
|
|
693
|
+
};
|
|
428
694
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
429
|
-
|
|
695
|
+
if (!this.clientCapabilitiesLogged) {
|
|
696
|
+
this.clientCapabilitiesLogged = true;
|
|
697
|
+
this.logClientCapabilities(this.server);
|
|
698
|
+
}
|
|
699
|
+
// STDIO-only: deferred conflict resolution
|
|
430
700
|
if (!this.mcp && this.options.unresolvedPhoton) {
|
|
431
701
|
return { tools: this.buildPlaceholderTools() };
|
|
432
702
|
}
|
|
433
|
-
|
|
434
|
-
return { tools: [] };
|
|
435
|
-
}
|
|
436
|
-
return {
|
|
437
|
-
tools: this.mcp.tools.map((tool) => {
|
|
438
|
-
const toolDef = {
|
|
439
|
-
name: tool.name,
|
|
440
|
-
description: tool.description,
|
|
441
|
-
inputSchema: tool.inputSchema,
|
|
442
|
-
};
|
|
443
|
-
// Add _meta with UI template reference (format depends on client capabilities)
|
|
444
|
-
const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name);
|
|
445
|
-
if (linkedUI) {
|
|
446
|
-
toolDef._meta = this.buildUIToolMeta(linkedUI.id);
|
|
447
|
-
}
|
|
448
|
-
return toolDef;
|
|
449
|
-
}),
|
|
450
|
-
};
|
|
703
|
+
return this.handleListTools(ctx);
|
|
451
704
|
});
|
|
452
|
-
// Handle tools/call
|
|
453
705
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
454
|
-
|
|
455
|
-
// Deferred conflict resolution: resolve photon on first tool call
|
|
706
|
+
// STDIO-only: deferred conflict resolution
|
|
456
707
|
if (!this.mcp && this.options.unresolvedPhoton) {
|
|
457
708
|
await this.resolveUnresolvedPhoton();
|
|
458
709
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
try {
|
|
463
|
-
// Create MCP-aware input provider for elicitation support
|
|
464
|
-
const inputProvider = this.createMCPInputProvider();
|
|
465
|
-
// Handler for channel events - forward to daemon for cross-process pub/sub
|
|
466
|
-
const outputHandler = (emit) => {
|
|
467
|
-
if (this.daemonName && emit?.channel) {
|
|
468
|
-
publishToChannel(this.daemonName, emit.channel, emit).catch(() => {
|
|
469
|
-
// Ignore publish errors - daemon may not be running
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
// Find the tool to get its metadata
|
|
710
|
+
// STDIO-only: @async fire-and-forget execution
|
|
711
|
+
if (this.mcp) {
|
|
712
|
+
const { name: toolName, arguments: args } = request.params;
|
|
474
713
|
const tool = this.mcp.tools.find((t) => t.name === toolName);
|
|
475
|
-
const outputFormat = tool?.outputFormat;
|
|
476
|
-
// Check for @async tag — fire-and-forget execution
|
|
477
714
|
if (tool?.isAsync) {
|
|
478
715
|
const executionId = generateExecutionId();
|
|
479
|
-
|
|
716
|
+
const inputProvider = this.createMCPInputProvider();
|
|
717
|
+
const outputHandler = (emit) => {
|
|
718
|
+
if (this.daemonName && emit?.channel) {
|
|
719
|
+
publishToChannel(this.daemonName, emit.channel, emit).catch(() => { });
|
|
720
|
+
}
|
|
721
|
+
};
|
|
480
722
|
this.loader
|
|
481
|
-
.executeTool(this.mcp, toolName, args || {}, {
|
|
482
|
-
inputProvider,
|
|
483
|
-
outputHandler,
|
|
484
|
-
})
|
|
723
|
+
.executeTool(this.mcp, toolName, args || {}, { inputProvider, outputHandler })
|
|
485
724
|
.catch((error) => {
|
|
486
725
|
this.log('error', `Async tool ${toolName} failed`, {
|
|
487
726
|
executionId,
|
|
@@ -503,186 +742,52 @@ export class PhotonServer {
|
|
|
503
742
|
],
|
|
504
743
|
};
|
|
505
744
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
});
|
|
510
|
-
// Check if this was a stateful workflow execution
|
|
511
|
-
const isStateful = result && typeof result === 'object' && result._stateful === true;
|
|
512
|
-
const actualResult = isStateful ? result.result : result;
|
|
513
|
-
// Build content with optional mimeType annotation
|
|
514
|
-
const content = {
|
|
515
|
-
type: 'text',
|
|
516
|
-
text: this.formatResult(actualResult),
|
|
517
|
-
};
|
|
518
|
-
// Add mimeType annotation if outputFormat is a content type
|
|
519
|
-
if (outputFormat) {
|
|
520
|
-
const { formatToMimeType } = await import('./cli-formatter.js');
|
|
521
|
-
const mimeType = formatToMimeType(outputFormat);
|
|
522
|
-
if (mimeType) {
|
|
523
|
-
content.annotations = { mimeType };
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
// For stateful workflows, add run ID as a separate content block
|
|
527
|
-
// This allows the AI to inform the user about the workflow run
|
|
528
|
-
if (isStateful && result.runId) {
|
|
529
|
-
const workflowInfo = {
|
|
530
|
-
type: 'text',
|
|
531
|
-
text: `\n\n---\n📋 **Workflow Run**: ${result.runId}\n` +
|
|
532
|
-
`Status: ${result.status}${result.resumed ? ' (resumed)' : ''}\n` +
|
|
533
|
-
`This is a stateful workflow. To resume if interrupted, use run ID: ${result.runId}`,
|
|
534
|
-
};
|
|
535
|
-
return { content: [content, workflowInfo] };
|
|
536
|
-
}
|
|
537
|
-
return { content: [content] };
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
return await this.handleCallTool(ctx, request);
|
|
538
748
|
}
|
|
539
749
|
catch (error) {
|
|
540
|
-
//
|
|
541
|
-
const
|
|
750
|
+
// STDIO-only: config elicitation retry
|
|
751
|
+
const { name: toolName, arguments: args } = request.params;
|
|
542
752
|
if (this.mcp?.instance?._photonConfigError && this.clientSupportsElicitation()) {
|
|
543
753
|
const retryResult = await this.attemptConfigElicitation(toolName, args || {});
|
|
544
754
|
if (retryResult)
|
|
545
755
|
return retryResult;
|
|
546
756
|
}
|
|
547
|
-
//
|
|
757
|
+
// STDIO-only: verbose error logging
|
|
548
758
|
this.log('error', 'Tool execution failed', {
|
|
549
759
|
tool: toolName,
|
|
550
|
-
error:
|
|
760
|
+
error: getErrorMessage(error),
|
|
551
761
|
args: this.options.devMode ? args : undefined,
|
|
552
762
|
});
|
|
553
|
-
// Format error for AI consumption
|
|
554
763
|
return this.formatError(error, toolName, args);
|
|
555
764
|
}
|
|
556
765
|
});
|
|
557
|
-
// Handle prompts/list
|
|
558
766
|
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
559
|
-
|
|
560
|
-
return { prompts: [] };
|
|
561
|
-
}
|
|
562
|
-
return {
|
|
563
|
-
prompts: this.mcp.templates.map((template) => ({
|
|
564
|
-
name: template.name,
|
|
565
|
-
description: template.description,
|
|
566
|
-
arguments: Object.entries(template.inputSchema.properties || {}).map(([name, schema]) => ({
|
|
567
|
-
name,
|
|
568
|
-
description: (typeof schema === 'object' && schema && 'description' in schema
|
|
569
|
-
? schema.description
|
|
570
|
-
: '') || '',
|
|
571
|
-
required: template.inputSchema.required?.includes(name) || false,
|
|
572
|
-
})),
|
|
573
|
-
})),
|
|
574
|
-
};
|
|
767
|
+
return this.handleListPrompts();
|
|
575
768
|
});
|
|
576
|
-
// Handle prompts/get
|
|
577
769
|
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
578
|
-
if (!this.mcp) {
|
|
579
|
-
throw new Error('MCP not loaded');
|
|
580
|
-
}
|
|
581
|
-
const { name: promptName, arguments: args } = request.params;
|
|
582
|
-
// Find the template
|
|
583
|
-
const template = this.mcp.templates.find((t) => t.name === promptName);
|
|
584
|
-
if (!template) {
|
|
585
|
-
throw new Error(`Prompt not found: ${promptName}`);
|
|
586
|
-
}
|
|
587
770
|
try {
|
|
588
|
-
|
|
589
|
-
const result = await this.loader.executeTool(this.mcp, promptName, args || {});
|
|
590
|
-
// Handle Template/TemplateResponse return types
|
|
591
|
-
return this.formatTemplateResult(result);
|
|
771
|
+
return await this.handleGetPrompt(request);
|
|
592
772
|
}
|
|
593
773
|
catch (error) {
|
|
774
|
+
// STDIO-only: verbose error logging for prompts
|
|
775
|
+
const { name: promptName } = request.params;
|
|
594
776
|
this.log('error', 'Prompt execution failed', {
|
|
595
777
|
prompt: promptName,
|
|
596
778
|
error: getErrorMessage(error),
|
|
597
779
|
});
|
|
598
|
-
throw
|
|
780
|
+
throw error;
|
|
599
781
|
}
|
|
600
782
|
});
|
|
601
|
-
// Handle resources/list (static URIs only, no parameters)
|
|
602
783
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
603
|
-
|
|
604
|
-
return { resources: [] };
|
|
605
|
-
}
|
|
606
|
-
// Only return resources with static URIs (no {parameters})
|
|
607
|
-
const staticResources = this.mcp.statics.filter((s) => !this.isUriTemplate(s.uri));
|
|
608
|
-
const resources = staticResources.map((static_) => ({
|
|
609
|
-
uri: static_.uri,
|
|
610
|
-
name: static_.name,
|
|
611
|
-
description: static_.description,
|
|
612
|
-
mimeType: static_.mimeType || 'text/plain',
|
|
613
|
-
}));
|
|
614
|
-
// Add assets from asset folder (UI, prompts, resources)
|
|
615
|
-
if (this.mcp.assets) {
|
|
616
|
-
const photonName = this.mcp.name;
|
|
617
|
-
// Add UI assets (format depends on client capabilities)
|
|
618
|
-
for (const ui of this.mcp.assets.ui) {
|
|
619
|
-
// Use pre-generated URI from loader, or build one
|
|
620
|
-
const uiUri = ui.uri || this.buildUIResourceUri(ui.id);
|
|
621
|
-
resources.push({
|
|
622
|
-
uri: uiUri,
|
|
623
|
-
name: `ui:${ui.id}`,
|
|
624
|
-
description: ui.linkedTool
|
|
625
|
-
? `UI template for ${ui.linkedTool} tool`
|
|
626
|
-
: `UI template: ${ui.id}`,
|
|
627
|
-
mimeType: ui.mimeType || this.getUIMimeType(),
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
// Add prompt assets
|
|
631
|
-
for (const prompt of this.mcp.assets.prompts) {
|
|
632
|
-
resources.push({
|
|
633
|
-
uri: `photon://${photonName}/prompts/${prompt.id}`,
|
|
634
|
-
name: `prompt:${prompt.id}`,
|
|
635
|
-
description: prompt.description || `Prompt template: ${prompt.id}`,
|
|
636
|
-
mimeType: 'text/markdown',
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
// Add resource assets
|
|
640
|
-
for (const resource of this.mcp.assets.resources) {
|
|
641
|
-
resources.push({
|
|
642
|
-
uri: `photon://${photonName}/resources/${resource.id}`,
|
|
643
|
-
name: `resource:${resource.id}`,
|
|
644
|
-
description: resource.description || `Static resource: ${resource.id}`,
|
|
645
|
-
mimeType: resource.mimeType || 'application/octet-stream',
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
return { resources };
|
|
784
|
+
return this.handleListResources(ctx);
|
|
650
785
|
});
|
|
651
|
-
// Handle resources/templates/list (parameterized URIs)
|
|
652
786
|
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
653
|
-
|
|
654
|
-
return { resourceTemplates: [] };
|
|
655
|
-
}
|
|
656
|
-
// Only return resources with URI templates (has {parameters})
|
|
657
|
-
const templateResources = this.mcp.statics.filter((s) => this.isUriTemplate(s.uri));
|
|
658
|
-
return {
|
|
659
|
-
resourceTemplates: templateResources.map((static_) => ({
|
|
660
|
-
uriTemplate: static_.uri,
|
|
661
|
-
name: static_.name,
|
|
662
|
-
description: static_.description,
|
|
663
|
-
mimeType: static_.mimeType || 'text/plain',
|
|
664
|
-
})),
|
|
665
|
-
};
|
|
787
|
+
return this.handleListResourceTemplates();
|
|
666
788
|
});
|
|
667
|
-
// Handle resources/read
|
|
668
789
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
669
|
-
|
|
670
|
-
throw new Error('MCP not loaded');
|
|
671
|
-
}
|
|
672
|
-
const { uri } = request.params;
|
|
673
|
-
// Check for SEP-1865 ui:// URI format
|
|
674
|
-
const uiMatch = uri.match(/^ui:\/\/([^/]+)\/(.+)$/);
|
|
675
|
-
if (uiMatch && this.mcp.assets) {
|
|
676
|
-
const [, _photonName, assetId] = uiMatch;
|
|
677
|
-
return this.handleUIAssetRead(uri, assetId);
|
|
678
|
-
}
|
|
679
|
-
// Check for legacy photon:// asset URI format
|
|
680
|
-
const assetMatch = uri.match(/^photon:\/\/([^/]+)\/(ui|prompts|resources)\/(.+)$/);
|
|
681
|
-
if (assetMatch && this.mcp.assets) {
|
|
682
|
-
return this.handleAssetRead(uri, assetMatch);
|
|
683
|
-
}
|
|
684
|
-
// Handle static resources
|
|
685
|
-
return this.handleStaticRead(uri);
|
|
790
|
+
return this.handleReadResource(request);
|
|
686
791
|
});
|
|
687
792
|
}
|
|
688
793
|
/**
|
|
@@ -905,7 +1010,6 @@ export class PhotonServer {
|
|
|
905
1010
|
}
|
|
906
1011
|
else if (this.clientSupportsElicitation()) {
|
|
907
1012
|
// Present choices via elicitation
|
|
908
|
-
const options = {};
|
|
909
1013
|
const sourceLabels = [];
|
|
910
1014
|
for (const source of unresolved.sources) {
|
|
911
1015
|
const version = source.metadata?.version || 'unknown';
|
|
@@ -1048,7 +1152,9 @@ export class PhotonServer {
|
|
|
1048
1152
|
const inputProvider = this.createMCPInputProvider();
|
|
1049
1153
|
const outputHandler = (emit) => {
|
|
1050
1154
|
if (this.daemonName && emit?.channel) {
|
|
1051
|
-
publishToChannel(this.daemonName, emit.channel, emit).catch(() => {
|
|
1155
|
+
publishToChannel(this.daemonName, emit.channel, emit).catch((e) => {
|
|
1156
|
+
this.log('debug', 'Publish to channel failed', { error: getErrorMessage(e) });
|
|
1157
|
+
});
|
|
1052
1158
|
}
|
|
1053
1159
|
};
|
|
1054
1160
|
const retryResult = await this.loader.executeTool(this.mcp, toolName, args, {
|
|
@@ -1079,7 +1185,7 @@ export class PhotonServer {
|
|
|
1079
1185
|
// Initialize MCP client factory for enabling this.mcp() in Photons
|
|
1080
1186
|
// This allows Photons to call external MCPs via protocol
|
|
1081
1187
|
try {
|
|
1082
|
-
this.mcpClientFactory = await
|
|
1188
|
+
this.mcpClientFactory = await createSDKMCPClientFactory(this.options.devMode);
|
|
1083
1189
|
const servers = await this.mcpClientFactory.listServers();
|
|
1084
1190
|
if (servers.length > 0) {
|
|
1085
1191
|
this.log('info', `MCP access enabled: ${servers.join(', ')}`);
|
|
@@ -1098,9 +1204,9 @@ export class PhotonServer {
|
|
|
1098
1204
|
const photonName = metadata.name;
|
|
1099
1205
|
this.daemonName = photonName; // Store for subscription
|
|
1100
1206
|
this.log('info', `Stateful photon detected: ${photonName}`);
|
|
1101
|
-
if (!
|
|
1207
|
+
if (!isGlobalDaemonRunning()) {
|
|
1102
1208
|
this.log('info', `Starting daemon for ${photonName}...`);
|
|
1103
|
-
await
|
|
1209
|
+
await startGlobalDaemon(true);
|
|
1104
1210
|
// Wait for daemon to be ready
|
|
1105
1211
|
for (let i = 0; i < 10; i++) {
|
|
1106
1212
|
await new Promise((r) => setTimeout(r, 500));
|
|
@@ -1193,16 +1299,16 @@ export class PhotonServer {
|
|
|
1193
1299
|
try {
|
|
1194
1300
|
await this.server.notification(payload);
|
|
1195
1301
|
}
|
|
1196
|
-
catch {
|
|
1197
|
-
|
|
1302
|
+
catch (e) {
|
|
1303
|
+
this.log('debug', 'Notification send failed', { error: getErrorMessage(e) });
|
|
1198
1304
|
}
|
|
1199
1305
|
// Also send to SSE sessions
|
|
1200
1306
|
for (const session of this.sseSessions.values()) {
|
|
1201
1307
|
try {
|
|
1202
1308
|
await session.server.notification(payload);
|
|
1203
1309
|
}
|
|
1204
|
-
catch {
|
|
1205
|
-
|
|
1310
|
+
catch (e) {
|
|
1311
|
+
this.log('debug', 'Session notification failed', { error: getErrorMessage(e) });
|
|
1206
1312
|
}
|
|
1207
1313
|
}
|
|
1208
1314
|
}
|
|
@@ -1310,7 +1416,7 @@ export class PhotonServer {
|
|
|
1310
1416
|
description: tool.description,
|
|
1311
1417
|
inputSchema: tool.inputSchema,
|
|
1312
1418
|
ui: linkedUI
|
|
1313
|
-
? { id: linkedUI.id, uri: `
|
|
1419
|
+
? { id: linkedUI.id, uri: `ui://${this.mcp.name}/${linkedUI.id}` }
|
|
1314
1420
|
: null,
|
|
1315
1421
|
};
|
|
1316
1422
|
}) || [];
|
|
@@ -1340,6 +1446,11 @@ export class PhotonServer {
|
|
|
1340
1446
|
res.end(JSON.stringify({ success: false, error: 'Forbidden: non-local request' }));
|
|
1341
1447
|
return;
|
|
1342
1448
|
}
|
|
1449
|
+
if (!this.mcp) {
|
|
1450
|
+
res.writeHead(503);
|
|
1451
|
+
res.end(JSON.stringify({ success: false, error: 'Photon not loaded' }));
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1343
1454
|
try {
|
|
1344
1455
|
const body = await readBody(req);
|
|
1345
1456
|
const { tool, args } = JSON.parse(body);
|
|
@@ -1364,6 +1475,11 @@ export class PhotonServer {
|
|
|
1364
1475
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
1365
1476
|
res.setHeader('Cache-Control', 'no-cache');
|
|
1366
1477
|
res.setHeader('Connection', 'keep-alive');
|
|
1478
|
+
if (!this.mcp) {
|
|
1479
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
1480
|
+
res.end(JSON.stringify({ success: false, error: 'Photon not loaded' }));
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1367
1483
|
let body = '';
|
|
1368
1484
|
req.on('data', (chunk) => (body += chunk));
|
|
1369
1485
|
req.on('end', async () => {
|
|
@@ -1551,10 +1667,21 @@ export class PhotonServer {
|
|
|
1551
1667
|
const sessionId = transport.sessionId;
|
|
1552
1668
|
// Store session
|
|
1553
1669
|
this.sseSessions.set(sessionId, { server: sessionServer, transport });
|
|
1554
|
-
// Clean up on close
|
|
1670
|
+
// Clean up on close (guard against recursive close:
|
|
1671
|
+
// onclose → sessionServer.close() → transport.close() → onclose)
|
|
1672
|
+
let closing = false;
|
|
1555
1673
|
transport.onclose = async () => {
|
|
1674
|
+
if (closing)
|
|
1675
|
+
return;
|
|
1676
|
+
closing = true;
|
|
1556
1677
|
this.sseSessions.delete(sessionId);
|
|
1557
|
-
|
|
1678
|
+
this.log('info', 'SSE client disconnected', { sessionId });
|
|
1679
|
+
try {
|
|
1680
|
+
await sessionServer.close();
|
|
1681
|
+
}
|
|
1682
|
+
catch {
|
|
1683
|
+
// Ignore errors during cleanup (transport already closed)
|
|
1684
|
+
}
|
|
1558
1685
|
};
|
|
1559
1686
|
transport.onerror = (error) => {
|
|
1560
1687
|
this.log('warn', 'SSE transport error', {
|
|
@@ -1611,184 +1738,42 @@ export class PhotonServer {
|
|
|
1611
1738
|
* This duplicates handlers from the main server to each session
|
|
1612
1739
|
*/
|
|
1613
1740
|
setupSessionHandlers(sessionServer) {
|
|
1614
|
-
|
|
1741
|
+
this.logClientCapabilities(sessionServer);
|
|
1742
|
+
const sseSessionKey = `sse-${this.daemonName}`;
|
|
1743
|
+
const ctx = {
|
|
1744
|
+
server: sessionServer,
|
|
1745
|
+
getInstanceName: () => this.sseInstanceNames.get(sseSessionKey),
|
|
1746
|
+
setInstanceName: (name) => {
|
|
1747
|
+
this.sseInstanceNames.set(sseSessionKey, name);
|
|
1748
|
+
},
|
|
1749
|
+
sessionId: sseSessionKey,
|
|
1750
|
+
};
|
|
1615
1751
|
sessionServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1616
|
-
|
|
1617
|
-
return { tools: [] };
|
|
1618
|
-
return {
|
|
1619
|
-
tools: this.mcp.tools.map((tool) => {
|
|
1620
|
-
const toolDef = {
|
|
1621
|
-
name: tool.name,
|
|
1622
|
-
description: tool.description,
|
|
1623
|
-
inputSchema: tool.inputSchema,
|
|
1624
|
-
};
|
|
1625
|
-
// Add _meta with UI template reference (format depends on client capabilities)
|
|
1626
|
-
const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name);
|
|
1627
|
-
if (linkedUI) {
|
|
1628
|
-
toolDef._meta = this.buildUIToolMeta(linkedUI.id, sessionServer);
|
|
1629
|
-
}
|
|
1630
|
-
return toolDef;
|
|
1631
|
-
}),
|
|
1632
|
-
};
|
|
1752
|
+
return this.handleListTools(ctx);
|
|
1633
1753
|
});
|
|
1634
|
-
// Handle tools/call
|
|
1635
1754
|
sessionServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1636
|
-
if (!this.mcp)
|
|
1637
|
-
throw new Error('MCP not loaded');
|
|
1638
|
-
const { name: toolName, arguments: args } = request.params;
|
|
1639
1755
|
try {
|
|
1640
|
-
|
|
1641
|
-
const inputProvider = this.createMCPInputProvider(sessionServer);
|
|
1642
|
-
// Handler for channel events - forward to daemon for cross-process pub/sub
|
|
1643
|
-
const outputHandler = (emit) => {
|
|
1644
|
-
if (this.daemonName && emit?.channel) {
|
|
1645
|
-
publishToChannel(this.daemonName, emit.channel, emit).catch(() => {
|
|
1646
|
-
// Ignore publish errors - daemon may not be running
|
|
1647
|
-
});
|
|
1648
|
-
}
|
|
1649
|
-
};
|
|
1650
|
-
const result = await this.loader.executeTool(this.mcp, toolName, args || {}, {
|
|
1651
|
-
inputProvider,
|
|
1652
|
-
outputHandler,
|
|
1653
|
-
});
|
|
1654
|
-
const tool = this.mcp.tools.find((t) => t.name === toolName);
|
|
1655
|
-
const outputFormat = tool?.outputFormat;
|
|
1656
|
-
const isStateful = result && typeof result === 'object' && result._stateful === true;
|
|
1657
|
-
const actualResult = isStateful ? result.result : result;
|
|
1658
|
-
const content = {
|
|
1659
|
-
type: 'text',
|
|
1660
|
-
text: this.formatResult(actualResult),
|
|
1661
|
-
};
|
|
1662
|
-
if (outputFormat) {
|
|
1663
|
-
const { formatToMimeType } = await import('./cli-formatter.js');
|
|
1664
|
-
const mimeType = formatToMimeType(outputFormat);
|
|
1665
|
-
if (mimeType) {
|
|
1666
|
-
content.annotations = { mimeType };
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
const response = { content: [content] };
|
|
1670
|
-
if (isStateful) {
|
|
1671
|
-
response._meta = { runId: result.runId, status: result.status };
|
|
1672
|
-
}
|
|
1673
|
-
return response;
|
|
1756
|
+
return await this.handleCallTool(ctx, request);
|
|
1674
1757
|
}
|
|
1675
1758
|
catch (error) {
|
|
1759
|
+
const { name: toolName, arguments: args } = request.params;
|
|
1676
1760
|
return this.formatError(error, toolName, args);
|
|
1677
1761
|
}
|
|
1678
1762
|
});
|
|
1679
|
-
// Handle prompts/list
|
|
1680
1763
|
sessionServer.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
1681
|
-
|
|
1682
|
-
return { prompts: [] };
|
|
1683
|
-
return {
|
|
1684
|
-
prompts: this.mcp.templates.map((template) => ({
|
|
1685
|
-
name: template.name,
|
|
1686
|
-
description: template.description,
|
|
1687
|
-
arguments: template.inputSchema?.properties
|
|
1688
|
-
? Object.entries(template.inputSchema.properties).map(([name, schema]) => ({
|
|
1689
|
-
name,
|
|
1690
|
-
description: schema.description || '',
|
|
1691
|
-
required: template.inputSchema?.required?.includes(name) || false,
|
|
1692
|
-
}))
|
|
1693
|
-
: [],
|
|
1694
|
-
})),
|
|
1695
|
-
};
|
|
1764
|
+
return this.handleListPrompts();
|
|
1696
1765
|
});
|
|
1697
|
-
// Handle prompts/get
|
|
1698
1766
|
sessionServer.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1699
|
-
|
|
1700
|
-
throw new Error('MCP not loaded');
|
|
1701
|
-
const { name: promptName, arguments: args } = request.params;
|
|
1702
|
-
try {
|
|
1703
|
-
const result = await this.loader.executeTool(this.mcp, promptName, args || {});
|
|
1704
|
-
return this.formatTemplateResult(result);
|
|
1705
|
-
}
|
|
1706
|
-
catch (error) {
|
|
1707
|
-
throw new Error(`Failed to get prompt: ${getErrorMessage(error)}`);
|
|
1708
|
-
}
|
|
1767
|
+
return this.handleGetPrompt(request);
|
|
1709
1768
|
});
|
|
1710
|
-
// Handle resources/list
|
|
1711
1769
|
sessionServer.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
1712
|
-
|
|
1713
|
-
return { resources: [] };
|
|
1714
|
-
const resources = [];
|
|
1715
|
-
// Add static resources
|
|
1716
|
-
for (const static_ of this.mcp.statics) {
|
|
1717
|
-
if (!this.isUriTemplate(static_.uri)) {
|
|
1718
|
-
resources.push({
|
|
1719
|
-
uri: static_.uri,
|
|
1720
|
-
name: static_.name,
|
|
1721
|
-
description: static_.description,
|
|
1722
|
-
mimeType: static_.mimeType,
|
|
1723
|
-
});
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
// Add asset resources (UI format depends on client capabilities)
|
|
1727
|
-
if (this.mcp.assets) {
|
|
1728
|
-
for (const ui of this.mcp.assets.ui) {
|
|
1729
|
-
// Use pre-generated URI from loader, or build one
|
|
1730
|
-
const uiUri = ui.uri || this.buildUIResourceUri(ui.id, sessionServer);
|
|
1731
|
-
resources.push({
|
|
1732
|
-
uri: uiUri,
|
|
1733
|
-
name: `ui:${ui.id}`,
|
|
1734
|
-
description: ui.linkedTool
|
|
1735
|
-
? `UI template for ${ui.linkedTool} tool`
|
|
1736
|
-
: `UI template: ${ui.id}`,
|
|
1737
|
-
mimeType: ui.mimeType || this.getUIMimeType(sessionServer),
|
|
1738
|
-
});
|
|
1739
|
-
}
|
|
1740
|
-
for (const prompt of this.mcp.assets.prompts) {
|
|
1741
|
-
resources.push({
|
|
1742
|
-
uri: `photon://${this.mcp.name}/prompts/${prompt.id}`,
|
|
1743
|
-
name: `prompt:${prompt.id}`,
|
|
1744
|
-
description: prompt.description || `Prompt template: ${prompt.id}`,
|
|
1745
|
-
mimeType: 'text/markdown',
|
|
1746
|
-
});
|
|
1747
|
-
}
|
|
1748
|
-
for (const resource of this.mcp.assets.resources) {
|
|
1749
|
-
resources.push({
|
|
1750
|
-
uri: `photon://${this.mcp.name}/resources/${resource.id}`,
|
|
1751
|
-
name: `resource:${resource.id}`,
|
|
1752
|
-
description: `Static resource: ${resource.id}`,
|
|
1753
|
-
mimeType: resource.mimeType || 'application/json',
|
|
1754
|
-
});
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
return { resources };
|
|
1770
|
+
return this.handleListResources(ctx);
|
|
1758
1771
|
});
|
|
1759
|
-
// Handle resources/templates/list
|
|
1760
1772
|
sessionServer.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
1761
|
-
|
|
1762
|
-
return { resourceTemplates: [] };
|
|
1763
|
-
return {
|
|
1764
|
-
resourceTemplates: this.mcp.statics
|
|
1765
|
-
.filter((static_) => this.isUriTemplate(static_.uri))
|
|
1766
|
-
.map((static_) => ({
|
|
1767
|
-
uriTemplate: static_.uri,
|
|
1768
|
-
name: static_.name,
|
|
1769
|
-
description: static_.description,
|
|
1770
|
-
mimeType: static_.mimeType,
|
|
1771
|
-
})),
|
|
1772
|
-
};
|
|
1773
|
+
return this.handleListResourceTemplates();
|
|
1773
1774
|
});
|
|
1774
|
-
// Handle resources/read
|
|
1775
1775
|
sessionServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
1776
|
-
|
|
1777
|
-
throw new Error('MCP not loaded');
|
|
1778
|
-
const { uri } = request.params;
|
|
1779
|
-
// Check for SEP-1865 ui:// URI format
|
|
1780
|
-
const uiMatch = uri.match(/^ui:\/\/([^/]+)\/(.+)$/);
|
|
1781
|
-
if (uiMatch && this.mcp.assets) {
|
|
1782
|
-
const [, _photonName, assetId] = uiMatch;
|
|
1783
|
-
return this.handleUIAssetRead(uri, assetId);
|
|
1784
|
-
}
|
|
1785
|
-
// Check for legacy photon:// asset URI format
|
|
1786
|
-
const assetMatch = uri.match(/^photon:\/\/([^/]+)\/(ui|prompts|resources)\/(.+)$/);
|
|
1787
|
-
if (assetMatch && this.mcp.assets) {
|
|
1788
|
-
return this.handleAssetRead(uri, assetMatch);
|
|
1789
|
-
}
|
|
1790
|
-
// Handle static resources
|
|
1791
|
-
return this.handleStaticRead(uri);
|
|
1776
|
+
return this.handleReadResource(request);
|
|
1792
1777
|
});
|
|
1793
1778
|
}
|
|
1794
1779
|
/**
|
|
@@ -1895,9 +1880,26 @@ export class PhotonServer {
|
|
|
1895
1880
|
// Standard theme handling
|
|
1896
1881
|
if (m.params && m.params.theme) {
|
|
1897
1882
|
currentTheme = m.params.theme;
|
|
1898
|
-
document.documentElement.classList.remove('light', 'dark');
|
|
1883
|
+
document.documentElement.classList.remove('light', 'dark', 'light-theme');
|
|
1899
1884
|
document.documentElement.classList.add(m.params.theme);
|
|
1900
1885
|
document.documentElement.setAttribute('data-theme', m.params.theme);
|
|
1886
|
+
// Apply theme token CSS variables (matching platform-compat applyThemeTokens)
|
|
1887
|
+
if (m.params.styles && m.params.styles.variables) {
|
|
1888
|
+
var root = document.documentElement;
|
|
1889
|
+
var vars = m.params.styles.variables;
|
|
1890
|
+
for (var key in vars) { root.style.setProperty(key, vars[key]); }
|
|
1891
|
+
}
|
|
1892
|
+
// Apply background/text colors to match platform-compat bridge
|
|
1893
|
+
if (m.params.theme === 'light') {
|
|
1894
|
+
document.documentElement.classList.add('light-theme');
|
|
1895
|
+
document.documentElement.style.colorScheme = 'light';
|
|
1896
|
+
document.documentElement.style.backgroundColor = '#ffffff';
|
|
1897
|
+
if (document.body) { document.body.style.backgroundColor = '#ffffff'; document.body.style.color = '#1a1a1a'; }
|
|
1898
|
+
} else {
|
|
1899
|
+
document.documentElement.style.colorScheme = 'dark';
|
|
1900
|
+
document.documentElement.style.backgroundColor = '#0d0d0d';
|
|
1901
|
+
if (document.body) { document.body.style.backgroundColor = '#0d0d0d'; document.body.style.color = '#e6e6e6'; }
|
|
1902
|
+
}
|
|
1901
1903
|
themeListeners.forEach(function(cb) { cb(currentTheme); });
|
|
1902
1904
|
}
|
|
1903
1905
|
|
|
@@ -2111,10 +2113,24 @@ export class PhotonServer {
|
|
|
2111
2113
|
var initId = generateCallId();
|
|
2112
2114
|
pendingCalls[initId] = {
|
|
2113
2115
|
resolve: function(result) {
|
|
2114
|
-
// Apply theme from host context
|
|
2116
|
+
// Apply theme from host context (matching platform-compat bridge)
|
|
2115
2117
|
if (result.hostContext && result.hostContext.theme) {
|
|
2118
|
+
currentTheme = result.hostContext.theme;
|
|
2119
|
+
document.documentElement.classList.remove('light', 'dark', 'light-theme');
|
|
2116
2120
|
document.documentElement.classList.add(result.hostContext.theme);
|
|
2117
2121
|
document.documentElement.setAttribute('data-theme', result.hostContext.theme);
|
|
2122
|
+
// Apply theme token CSS variables from host context
|
|
2123
|
+
if (result.hostContext.styles && result.hostContext.styles.variables) {
|
|
2124
|
+
var root = document.documentElement;
|
|
2125
|
+
var vars = result.hostContext.styles.variables;
|
|
2126
|
+
for (var key in vars) { root.style.setProperty(key, vars[key]); }
|
|
2127
|
+
}
|
|
2128
|
+
if (result.hostContext.theme === 'light') {
|
|
2129
|
+
document.documentElement.classList.add('light-theme');
|
|
2130
|
+
document.documentElement.style.colorScheme = 'light';
|
|
2131
|
+
} else {
|
|
2132
|
+
document.documentElement.style.colorScheme = 'dark';
|
|
2133
|
+
}
|
|
2118
2134
|
}
|
|
2119
2135
|
// Complete handshake
|
|
2120
2136
|
postToHost({ jsonrpc: '2.0', method: 'ui/notifications/initialized', params: {} });
|
|
@@ -2144,7 +2160,7 @@ export class PhotonServer {
|
|
|
2144
2160
|
</script>`;
|
|
2145
2161
|
}
|
|
2146
2162
|
/**
|
|
2147
|
-
* Handle
|
|
2163
|
+
* Handle photon:// asset read (Beam format)
|
|
2148
2164
|
*/
|
|
2149
2165
|
async handleAssetRead(uri, assetMatch) {
|
|
2150
2166
|
const [, _photonName, assetType, assetId] = assetMatch;
|
|
@@ -2285,15 +2301,15 @@ export class PhotonServer {
|
|
|
2285
2301
|
try {
|
|
2286
2302
|
await this.server.notification(payload);
|
|
2287
2303
|
}
|
|
2288
|
-
catch {
|
|
2289
|
-
|
|
2304
|
+
catch (e) {
|
|
2305
|
+
this.log('debug', 'Notification send failed', { error: getErrorMessage(e) });
|
|
2290
2306
|
}
|
|
2291
2307
|
for (const session of this.sseSessions.values()) {
|
|
2292
2308
|
try {
|
|
2293
2309
|
await session.server.notification(payload);
|
|
2294
2310
|
}
|
|
2295
|
-
catch {
|
|
2296
|
-
|
|
2311
|
+
catch (e) {
|
|
2312
|
+
this.log('debug', 'Session notification failed', { error: getErrorMessage(e) });
|
|
2297
2313
|
}
|
|
2298
2314
|
}
|
|
2299
2315
|
}
|
|
@@ -2341,23 +2357,6 @@ export class PhotonServer {
|
|
|
2341
2357
|
this.lastReloadError = undefined;
|
|
2342
2358
|
// Send list_changed notifications to inform client of updates
|
|
2343
2359
|
await this.notifyListsChanged();
|
|
2344
|
-
// If daemon is running for this photon, reload it too
|
|
2345
|
-
if (this.daemonName && isDaemonRunning(this.daemonName)) {
|
|
2346
|
-
try {
|
|
2347
|
-
const result = await reloadDaemon(this.daemonName, this.options.filePath);
|
|
2348
|
-
if (result.success) {
|
|
2349
|
-
this.log('info', 'Daemon reloaded', { sessionsUpdated: result.sessionsUpdated });
|
|
2350
|
-
}
|
|
2351
|
-
else {
|
|
2352
|
-
this.log('warn', 'Daemon reload failed', { error: result.error });
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
catch (err) {
|
|
2356
|
-
this.log('warn', 'Daemon reload failed, may need manual restart', {
|
|
2357
|
-
error: getErrorMessage(err),
|
|
2358
|
-
});
|
|
2359
|
-
}
|
|
2360
|
-
}
|
|
2361
2360
|
await this.broadcastReloadStatus('info', 'Hot reload complete');
|
|
2362
2361
|
this.log('info', 'Reload complete');
|
|
2363
2362
|
}
|