@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.
Files changed (94) hide show
  1. package/README.md +23 -24
  2. package/dist/auto-ui/beam.d.ts.map +1 -1
  3. package/dist/auto-ui/beam.js +117 -42
  4. package/dist/auto-ui/beam.js.map +1 -1
  5. package/dist/auto-ui/design-system/tokens.d.ts +1 -1
  6. package/dist/auto-ui/design-system/tokens.d.ts.map +1 -1
  7. package/dist/auto-ui/design-system/tokens.js +1 -1
  8. package/dist/auto-ui/design-system/tokens.js.map +1 -1
  9. package/dist/auto-ui/frontend/index.html +1 -1
  10. package/dist/auto-ui/rendering/components.d.ts.map +1 -1
  11. package/dist/auto-ui/rendering/components.js +568 -0
  12. package/dist/auto-ui/rendering/components.js.map +1 -1
  13. package/dist/auto-ui/rendering/field-analyzer.d.ts +56 -0
  14. package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -1
  15. package/dist/auto-ui/rendering/field-analyzer.js +177 -0
  16. package/dist/auto-ui/rendering/field-analyzer.js.map +1 -1
  17. package/dist/auto-ui/rendering/layout-selector.d.ts +14 -2
  18. package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -1
  19. package/dist/auto-ui/rendering/layout-selector.js +125 -1
  20. package/dist/auto-ui/rendering/layout-selector.js.map +1 -1
  21. package/dist/auto-ui/streamable-http-transport.d.ts +1 -1
  22. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  23. package/dist/auto-ui/streamable-http-transport.js +353 -19
  24. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  25. package/dist/auto-ui/types.d.ts +7 -1
  26. package/dist/auto-ui/types.d.ts.map +1 -1
  27. package/dist/auto-ui/types.js.map +1 -1
  28. package/dist/beam.bundle.js +22441 -4216
  29. package/dist/beam.bundle.js.map +4 -4
  30. package/dist/cli/commands/info.d.ts.map +1 -1
  31. package/dist/cli/commands/info.js +37 -0
  32. package/dist/cli/commands/info.js.map +1 -1
  33. package/dist/cli/commands/package.d.ts.map +1 -1
  34. package/dist/cli/commands/package.js +16 -0
  35. package/dist/cli/commands/package.js.map +1 -1
  36. package/dist/cli.d.ts.map +1 -1
  37. package/dist/cli.js +628 -14
  38. package/dist/cli.js.map +1 -1
  39. package/dist/context-store.d.ts +79 -0
  40. package/dist/context-store.d.ts.map +1 -0
  41. package/dist/context-store.js +210 -0
  42. package/dist/context-store.js.map +1 -0
  43. package/dist/daemon/client.d.ts +13 -4
  44. package/dist/daemon/client.d.ts.map +1 -1
  45. package/dist/daemon/client.js +138 -77
  46. package/dist/daemon/client.js.map +1 -1
  47. package/dist/daemon/manager.d.ts +0 -25
  48. package/dist/daemon/manager.d.ts.map +1 -1
  49. package/dist/daemon/manager.js +10 -38
  50. package/dist/daemon/manager.js.map +1 -1
  51. package/dist/daemon/protocol.d.ts +7 -2
  52. package/dist/daemon/protocol.d.ts.map +1 -1
  53. package/dist/daemon/protocol.js.map +1 -1
  54. package/dist/daemon/server.js +257 -35
  55. package/dist/daemon/server.js.map +1 -1
  56. package/dist/daemon/session-manager.d.ts +24 -4
  57. package/dist/daemon/session-manager.d.ts.map +1 -1
  58. package/dist/daemon/session-manager.js +62 -12
  59. package/dist/daemon/session-manager.js.map +1 -1
  60. package/dist/index.d.ts +0 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +0 -3
  63. package/dist/index.js.map +1 -1
  64. package/dist/loader.d.ts +3 -20
  65. package/dist/loader.d.ts.map +1 -1
  66. package/dist/loader.js +53 -75
  67. package/dist/loader.js.map +1 -1
  68. package/dist/photon-cli-runner.d.ts.map +1 -1
  69. package/dist/photon-cli-runner.js +258 -218
  70. package/dist/photon-cli-runner.js.map +1 -1
  71. package/dist/photon-doc-extractor.d.ts +2 -0
  72. package/dist/photon-doc-extractor.d.ts.map +1 -1
  73. package/dist/photon-doc-extractor.js +42 -6
  74. package/dist/photon-doc-extractor.js.map +1 -1
  75. package/dist/photons/maker.photon.d.ts.map +1 -1
  76. package/dist/photons/maker.photon.js +3 -1
  77. package/dist/photons/maker.photon.js.map +1 -1
  78. package/dist/photons/maker.photon.ts +3 -1
  79. package/dist/serv/index.d.ts.map +1 -1
  80. package/dist/serv/index.js.map +1 -1
  81. package/dist/server.d.ts +32 -15
  82. package/dist/server.d.ts.map +1 -1
  83. package/dist/server.js +468 -469
  84. package/dist/server.js.map +1 -1
  85. package/dist/shared/security.d.ts.map +1 -1
  86. package/dist/shared/security.js +4 -8
  87. package/dist/shared/security.js.map +1 -1
  88. package/dist/shell-completions.d.ts +21 -0
  89. package/dist/shell-completions.d.ts.map +1 -0
  90. package/dist/shell-completions.js +102 -0
  91. package/dist/shell-completions.js.map +1 -0
  92. package/dist/template-manager.d.ts.map +1 -1
  93. package/dist/template-manager.js.map +1 -1
  94. 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 { createStandaloneMCPClientFactory } from './mcp-client.js';
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, reloadDaemon, publishToChannel } from './daemon/client.js';
23
- import { isDaemonRunning, startDaemon } from './daemon/manager.js';
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
- * SEP-1865 clients advertise ui capability in experimental or root capabilities.
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(server) {
124
- const targetServer = server || this.server;
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, server) {
161
- const format = this.getUIFormat(server);
132
+ buildUIResourceUri(uiId) {
162
133
  const photonName = this.mcp?.name || 'unknown';
163
- switch (format) {
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, server) {
178
- const format = this.getUIFormat(server);
179
- const uri = this.buildUIResourceUri(uiId, server);
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(server) {
195
- const targetServer = server || this.server;
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
- // Handle tools/list
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
- // If photon is unresolved (conflict), return placeholder tools from manifest metadata
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
- if (!this.mcp) {
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
- const { name: toolName, arguments: args } = request.params;
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
- if (!this.mcp) {
460
- throw new Error('MCP not loaded');
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
- // Run in background — don't await
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
- const result = await this.loader.executeTool(this.mcp, toolName, args || {}, {
507
- inputProvider,
508
- outputHandler,
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
- // Check for config error — attempt elicitation to resolve missing env vars
541
- const errorMsg = getErrorMessage(error);
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
- // Log error with context for debugging
757
+ // STDIO-only: verbose error logging
548
758
  this.log('error', 'Tool execution failed', {
549
759
  tool: toolName,
550
- error: errorMsg,
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
- if (!this.mcp) {
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
- // Execute the template method
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 new Error(`Failed to get prompt: ${getErrorMessage(error)}`);
780
+ throw error;
599
781
  }
600
782
  });
601
- // Handle resources/list (static URIs only, no parameters)
602
783
  this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
603
- if (!this.mcp) {
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
- if (!this.mcp) {
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
- if (!this.mcp) {
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 createStandaloneMCPClientFactory(this.options.devMode);
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 (!isDaemonRunning(photonName)) {
1207
+ if (!isGlobalDaemonRunning()) {
1102
1208
  this.log('info', `Starting daemon for ${photonName}...`);
1103
- await startDaemon(photonName, this.options.filePath, true);
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
- // ignore - client may not support notifications
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
- // ignore session errors
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: `photon://${this.mcp.name}/ui/${linkedUI.id}` }
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
- await sessionServer.close();
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
- // Handle tools/list
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
- if (!this.mcp)
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
- // Create MCP-aware input provider for elicitation support (use sessionServer for SSE)
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
- if (!this.mcp)
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
- if (!this.mcp)
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
- if (!this.mcp)
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
- if (!this.mcp)
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
- if (!this.mcp)
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 legacy photon:// asset read
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
- // ignore
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
- // ignore session errors
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
  }