@portel/photon 1.11.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +81 -72
  2. package/dist/auto-ui/beam/photon-management.d.ts.map +1 -1
  3. package/dist/auto-ui/beam/photon-management.js +5 -0
  4. package/dist/auto-ui/beam/photon-management.js.map +1 -1
  5. package/dist/auto-ui/beam/routes/api-browse.d.ts +1 -2
  6. package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
  7. package/dist/auto-ui/beam/routes/api-browse.js +140 -191
  8. package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
  9. package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
  10. package/dist/auto-ui/beam/routes/api-config.js +44 -1
  11. package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
  12. package/dist/auto-ui/beam.d.ts.map +1 -1
  13. package/dist/auto-ui/beam.js +994 -34
  14. package/dist/auto-ui/beam.js.map +1 -1
  15. package/dist/auto-ui/frontend/index.html +83 -60
  16. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  17. package/dist/auto-ui/streamable-http-transport.js +53 -12
  18. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  19. package/dist/auto-ui/types.d.ts +28 -1
  20. package/dist/auto-ui/types.d.ts.map +1 -1
  21. package/dist/auto-ui/types.js +23 -0
  22. package/dist/auto-ui/types.js.map +1 -1
  23. package/dist/beam.bundle.js +2894 -329
  24. package/dist/beam.bundle.js.map +4 -4
  25. package/dist/cli/commands/build.d.ts +3 -0
  26. package/dist/cli/commands/build.d.ts.map +1 -0
  27. package/dist/cli/commands/build.js +339 -0
  28. package/dist/cli/commands/build.js.map +1 -0
  29. package/dist/cli/commands/package-app.d.ts.map +1 -1
  30. package/dist/cli/commands/package-app.js +116 -35
  31. package/dist/cli/commands/package-app.js.map +1 -1
  32. package/dist/cli/commands/run.d.ts.map +1 -1
  33. package/dist/cli/commands/run.js +2 -0
  34. package/dist/cli/commands/run.js.map +1 -1
  35. package/dist/cli/index.d.ts.map +1 -1
  36. package/dist/cli/index.js +2 -0
  37. package/dist/cli/index.js.map +1 -1
  38. package/dist/context-store.d.ts +5 -0
  39. package/dist/context-store.d.ts.map +1 -1
  40. package/dist/context-store.js +9 -0
  41. package/dist/context-store.js.map +1 -1
  42. package/dist/daemon/client.d.ts.map +1 -1
  43. package/dist/daemon/client.js +81 -0
  44. package/dist/daemon/client.js.map +1 -1
  45. package/dist/daemon/protocol.d.ts +3 -1
  46. package/dist/daemon/protocol.d.ts.map +1 -1
  47. package/dist/daemon/protocol.js +1 -1
  48. package/dist/daemon/protocol.js.map +1 -1
  49. package/dist/daemon/server.js +513 -18
  50. package/dist/daemon/server.js.map +1 -1
  51. package/dist/embedded-runtime.d.ts +38 -0
  52. package/dist/embedded-runtime.d.ts.map +1 -0
  53. package/dist/embedded-runtime.js +326 -0
  54. package/dist/embedded-runtime.js.map +1 -0
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +1 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/loader.d.ts +38 -1
  60. package/dist/loader.d.ts.map +1 -1
  61. package/dist/loader.js +455 -15
  62. package/dist/loader.js.map +1 -1
  63. package/dist/photon-cli-runner.d.ts +22 -0
  64. package/dist/photon-cli-runner.d.ts.map +1 -1
  65. package/dist/photon-cli-runner.js +244 -12
  66. package/dist/photon-cli-runner.js.map +1 -1
  67. package/dist/photon-doc-extractor.d.ts +6 -0
  68. package/dist/photon-doc-extractor.d.ts.map +1 -1
  69. package/dist/photon-doc-extractor.js +22 -0
  70. package/dist/photon-doc-extractor.js.map +1 -1
  71. package/dist/photons/tunnel.photon.d.ts +5 -9
  72. package/dist/photons/tunnel.photon.d.ts.map +1 -1
  73. package/dist/photons/tunnel.photon.js +36 -96
  74. package/dist/photons/tunnel.photon.js.map +1 -1
  75. package/dist/photons/tunnel.photon.ts +40 -112
  76. package/dist/server.d.ts +30 -0
  77. package/dist/server.d.ts.map +1 -1
  78. package/dist/server.js +155 -10
  79. package/dist/server.js.map +1 -1
  80. package/dist/test-runner.d.ts +13 -1
  81. package/dist/test-runner.d.ts.map +1 -1
  82. package/dist/test-runner.js +529 -122
  83. package/dist/test-runner.js.map +1 -1
  84. package/dist/version.d.ts.map +1 -1
  85. package/dist/version.js +10 -2
  86. package/dist/version.js.map +1 -1
  87. package/package.json +23 -6
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Tunnel - Expose local servers to the internet
3
3
  * @description Multi-provider tunneling for remote access to Beam and local services
4
- * @internal
4
+ * @icon 🌐
5
5
  */
6
6
 
7
- import { spawn, spawnSync, execSync, ChildProcess } from 'child_process';
7
+ import { spawn, spawnSync, ChildProcess } from 'child_process';
8
8
 
9
9
  interface TunnelInfo {
10
10
  provider: string;
@@ -12,6 +12,7 @@ interface TunnelInfo {
12
12
  url: string;
13
13
  pid: number;
14
14
  startedAt: Date;
15
+ active: boolean;
15
16
  }
16
17
 
17
18
  // Track active tunnels
@@ -27,26 +28,27 @@ export default class Tunnel {
27
28
  name: string;
28
29
  available: boolean;
29
30
  install?: string;
30
- note?: string;
31
31
  }>;
32
32
  activeTunnels: TunnelInfo[];
33
33
  }> {
34
+ // Check which tunnels are still alive
35
+ for (const [port, tunnel] of activeTunnels) {
36
+ if (tunnel.process.killed || tunnel.process.exitCode !== null) {
37
+ tunnel.info.active = false;
38
+ activeTunnels.delete(port);
39
+ }
40
+ }
41
+
34
42
  const providers = [
35
43
  {
36
- name: 'localtunnel',
37
- available: true, // Always available via npx
38
- note: 'Ready (uses npx, no install needed)',
44
+ name: 'cloudflared',
45
+ available: this._checkCommand('cloudflared'),
46
+ install: 'brew install cloudflared',
39
47
  },
40
48
  {
41
49
  name: 'ngrok',
42
50
  available: this._checkCommand('ngrok'),
43
- install: 'brew install ngrok OR https://ngrok.com/download',
44
- },
45
- {
46
- name: 'cloudflared',
47
- available: this._checkCommand('cloudflared'),
48
- install:
49
- 'brew install cloudflared OR https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation',
51
+ install: 'brew install ngrok',
50
52
  },
51
53
  ];
52
54
 
@@ -59,92 +61,82 @@ export default class Tunnel {
59
61
  /**
60
62
  * Start a tunnel to expose Beam to the internet
61
63
  * @icon 🚀
64
+ * @format qr
62
65
  * @param provider Tunnel provider to use
63
66
  */
64
67
  async *start({
65
- provider = 'localtunnel',
68
+ provider = 'cloudflared',
66
69
  }: {
67
- /** Provider: localtunnel, ngrok, or cloudflared */
68
- provider?: 'localtunnel' | 'ngrok' | 'cloudflared';
70
+ /** Provider: cloudflared or ngrok */
71
+ provider?: 'cloudflared' | 'ngrok';
69
72
  } = {}): AsyncGenerator<
70
73
  { emit: string; value?: any; message: string },
71
74
  {
72
75
  message: string;
73
76
  url: string;
74
77
  link: string;
75
- provider: string;
76
- port: number;
77
- password?: string;
78
78
  }
79
79
  > {
80
80
  // Auto-detect Beam port from environment
81
81
  const port = parseInt(process.env.BEAM_PORT || '3117', 10);
82
82
 
83
83
  // Check if tunnel already exists for this port
84
- if (activeTunnels.has(port)) {
85
- const existing = activeTunnels.get(port)!;
86
- const publicIp = await this._getPublicIp();
87
- return {
88
- message: `Tunnel already active on port ${port}`,
89
- url: existing.info.url,
90
- link: existing.info.url,
91
- password: existing.info.provider === 'localtunnel' ? publicIp : undefined,
92
- provider: existing.info.provider,
93
- port,
94
- };
84
+ const existing = activeTunnels.get(port);
85
+ if (existing) {
86
+ // Verify the process is still alive
87
+ if (!existing.process.killed && existing.process.exitCode === null) {
88
+ return {
89
+ message: `Tunnel already running via ${existing.info.provider}`,
90
+ url: existing.info.url,
91
+ link: existing.info.url,
92
+ };
93
+ }
94
+ // Process died — clean up and start fresh
95
+ activeTunnels.delete(port);
95
96
  }
96
97
 
97
98
  yield {
98
99
  emit: 'status',
99
100
  value: { step: 'starting' },
100
- message: `Starting ${provider} tunnel for Beam (port ${port})...`,
101
+ message: `Starting ${provider} tunnel on port ${port}...`,
101
102
  };
102
103
 
103
104
  try {
104
105
  let url: string;
105
- let process: ChildProcess;
106
+ let tunnelProcess: ChildProcess;
106
107
 
107
108
  switch (provider) {
108
109
  case 'ngrok':
109
110
  if (!this._checkCommand('ngrok')) {
110
111
  throw new Error('ngrok not installed. Run: brew install ngrok');
111
112
  }
112
- ({ url, process } = await this._startNgrok(port));
113
+ ({ url, process: tunnelProcess } = await this._startNgrok(port));
113
114
  break;
114
115
 
115
116
  case 'cloudflared':
117
+ default:
116
118
  if (!this._checkCommand('cloudflared')) {
117
119
  throw new Error('cloudflared not installed. Run: brew install cloudflared');
118
120
  }
119
- ({ url, process } = await this._startCloudflared(port));
120
- break;
121
-
122
- case 'localtunnel':
123
- default:
124
- ({ url, process } = await this._startLocaltunnel(port));
121
+ ({ url, process: tunnelProcess } = await this._startCloudflared(port));
125
122
  break;
126
123
  }
127
124
 
128
- // Fetch public IP (needed for localtunnel password)
129
- const publicIp = await this._getPublicIp();
130
-
131
125
  const info: TunnelInfo = {
132
126
  provider,
133
127
  port,
134
128
  url,
135
- pid: process.pid!,
129
+ pid: tunnelProcess.pid!,
136
130
  startedAt: new Date(),
131
+ active: true,
137
132
  };
138
133
 
139
- activeTunnels.set(port, { process, info });
134
+ activeTunnels.set(port, { process: tunnelProcess, info });
140
135
 
141
136
  return {
142
- message: `Tunnel started successfully`,
137
+ message: `Tunnel started via ${provider}`,
143
138
  url,
144
139
  link: url,
145
- password: provider === 'localtunnel' ? publicIp : undefined,
146
- provider,
147
- port,
148
140
  };
149
141
  } catch (error: any) {
150
142
  throw new Error(error.message);
@@ -215,70 +207,6 @@ export default class Tunnel {
215
207
  return result.status === 0;
216
208
  }
217
209
 
218
- private async _getPublicIp(): Promise<string> {
219
- // Try multiple services in order
220
- const services = ['https://api.ipify.org', 'https://ifconfig.me/ip', 'https://icanhazip.com'];
221
-
222
- for (const service of services) {
223
- try {
224
- const result = execSync(`curl -s --max-time 3 ${service}`, {
225
- encoding: 'utf-8',
226
- timeout: 5000,
227
- });
228
- const ip = result.trim();
229
- if (ip && /^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
230
- return ip;
231
- }
232
- } catch {
233
- continue;
234
- }
235
- }
236
- return 'unknown';
237
- }
238
-
239
- private async _startLocaltunnel(port: number): Promise<{ url: string; process: ChildProcess }> {
240
- return new Promise((resolve, reject) => {
241
- const proc = spawn('npx', ['localtunnel', '--port', String(port)], {
242
- stdio: ['ignore', 'pipe', 'pipe'],
243
- });
244
-
245
- let output = '';
246
- const timeout = setTimeout(() => {
247
- reject(new Error('Timeout waiting for localtunnel URL'));
248
- }, 30000);
249
-
250
- proc.stdout?.on('data', (data) => {
251
- output += data.toString();
252
- // localtunnel outputs: "your url is: https://xxx.loca.lt"
253
- const match = output.match(/your url is: (https?:\/\/[^\s]+)/i);
254
- if (match) {
255
- clearTimeout(timeout);
256
- resolve({ url: match[1], process: proc });
257
- }
258
- });
259
-
260
- proc.stderr?.on('data', (data) => {
261
- const err = data.toString();
262
- if (err.includes('error')) {
263
- clearTimeout(timeout);
264
- reject(new Error(err));
265
- }
266
- });
267
-
268
- proc.on('error', (err) => {
269
- clearTimeout(timeout);
270
- reject(err);
271
- });
272
-
273
- proc.on('close', (code) => {
274
- if (code !== 0 && !output.includes('your url is')) {
275
- clearTimeout(timeout);
276
- reject(new Error(`localtunnel exited with code ${code}`));
277
- }
278
- });
279
- });
280
- }
281
-
282
210
  private async _startNgrok(port: number): Promise<{ url: string; process: ChildProcess }> {
283
211
  return new Promise((resolve, reject) => {
284
212
  const proc = spawn('ngrok', ['http', String(port), '--log', 'stdout'], {
package/dist/server.d.ts CHANGED
@@ -4,6 +4,8 @@
4
4
  * Wraps a .photon.ts file as an MCP server using @modelcontextprotocol/sdk
5
5
  * Supports both stdio and SSE transports
6
6
  */
7
+ import { PhotonLoader } from './loader.js';
8
+ import { PhotonClassExtended } from '@portel/photon-core';
7
9
  import type { Marketplace, PhotonMetadata } from './marketplace-manager.js';
8
10
  import { Logger, LoggerOptions } from './shared/logger.js';
9
11
  export declare class HotReloadDisabledError extends Error {
@@ -35,6 +37,22 @@ export interface PhotonServerOptions {
35
37
  unresolvedPhoton?: UnresolvedPhoton;
36
38
  /** Working directory override (base dir for state/config/cache) */
37
39
  workingDir?: string;
40
+ /** Pre-imported module (for compiled binaries — skips file I/O and compilation) */
41
+ preloadedModule?: {
42
+ default: any;
43
+ middleware?: any[];
44
+ };
45
+ /** Embedded source code (for compiled binaries — used for metadata extraction) */
46
+ embeddedSource?: string;
47
+ /** Pre-loaded @photon dependency modules (for compiled binaries) */
48
+ preloadedDependencies?: Map<string, {
49
+ module: {
50
+ default: any;
51
+ middleware?: any[];
52
+ };
53
+ source: string;
54
+ filePath: string;
55
+ }>;
38
56
  }
39
57
  export declare class PhotonServer {
40
58
  private loader;
@@ -71,6 +89,10 @@ export declare class PhotonServer {
71
89
  private rawClientCapabilities;
72
90
  private currentStatus;
73
91
  private logger;
92
+ /** Get the loaded photon (available after start()) */
93
+ getLoadedPhoton(): PhotonClassExtended | null;
94
+ /** Get the loader instance (for scheduler registration in compiled binaries) */
95
+ getLoader(): PhotonLoader;
74
96
  constructor(options: PhotonServerOptions);
75
97
  createScopedLogger(scope: string): Logger;
76
98
  getLogger(): Logger;
@@ -83,6 +105,14 @@ export declare class PhotonServer {
83
105
  * Build tool metadata for UI based on detected format
84
106
  */
85
107
  private buildUIToolMeta;
108
+ private static readonly ICON_MIME_TYPES;
109
+ /** Cached resolved icons per tool name */
110
+ private resolvedIconsCache;
111
+ /**
112
+ * Resolve raw icon image paths to MCP Icon[] format (data URIs).
113
+ * Results are cached so file I/O only happens once per tool.
114
+ */
115
+ private resolveIconImages;
86
116
  /**
87
117
  * Get UI mimeType based on detected format and client capabilities
88
118
  */
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -9,6 +9,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
9
9
  import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
10
10
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
11
11
  import * as fs from 'fs/promises';
12
+ import { readFileSync } from 'node:fs';
13
+ import * as path from 'node:path';
12
14
  import { createServer } from 'node:http';
13
15
  import { URL } from 'node:url';
14
16
  import { PhotonLoader } from './loader.js';
@@ -69,6 +71,14 @@ export class PhotonServer {
69
71
  timestamp: Date.now(),
70
72
  };
71
73
  logger;
74
+ /** Get the loaded photon (available after start()) */
75
+ getLoadedPhoton() {
76
+ return this.mcp;
77
+ }
78
+ /** Get the loader instance (for scheduler registration in compiled binaries) */
79
+ getLoader() {
80
+ return this.loader;
81
+ }
72
82
  constructor(options) {
73
83
  // Validate options (filePath validation skipped for unresolved photons)
74
84
  if (!options.unresolvedPhoton) {
@@ -145,6 +155,54 @@ export class PhotonServer {
145
155
  const uri = this.buildUIResourceUri(uiId);
146
156
  return { ui: { resourceUri: uri } };
147
157
  }
158
+ static ICON_MIME_TYPES = {
159
+ '.png': 'image/png',
160
+ '.jpg': 'image/jpeg',
161
+ '.jpeg': 'image/jpeg',
162
+ '.gif': 'image/gif',
163
+ '.svg': 'image/svg+xml',
164
+ '.webp': 'image/webp',
165
+ '.ico': 'image/x-icon',
166
+ };
167
+ /** Cached resolved icons per tool name */
168
+ resolvedIconsCache = new Map();
169
+ /**
170
+ * Resolve raw icon image paths to MCP Icon[] format (data URIs).
171
+ * Results are cached so file I/O only happens once per tool.
172
+ */
173
+ resolveIconImages(iconImages) {
174
+ const cacheKey = iconImages.map((i) => i.path).join('|');
175
+ const cached = this.resolvedIconsCache.get(cacheKey);
176
+ if (cached)
177
+ return cached;
178
+ const photonDir = path.dirname(this.options.filePath);
179
+ const icons = [];
180
+ for (const entry of iconImages) {
181
+ try {
182
+ const resolvedPath = path.resolve(photonDir, entry.path);
183
+ const ext = path.extname(resolvedPath).toLowerCase();
184
+ const mimeType = PhotonServer.ICON_MIME_TYPES[ext];
185
+ if (!mimeType)
186
+ continue;
187
+ const data = readFileSync(resolvedPath);
188
+ const dataUri = `data:${mimeType};base64,${data.toString('base64')}`;
189
+ const icon = {
190
+ src: dataUri,
191
+ mimeType,
192
+ };
193
+ if (entry.sizes)
194
+ icon.sizes = entry.sizes;
195
+ if (entry.theme)
196
+ icon.theme = entry.theme;
197
+ icons.push(icon);
198
+ }
199
+ catch {
200
+ // Skip unreadable icon files silently
201
+ }
202
+ }
203
+ this.resolvedIconsCache.set(cacheKey, icons);
204
+ return icons;
205
+ }
148
206
  /**
149
207
  * Get UI mimeType based on detected format and client capabilities
150
208
  */
@@ -438,6 +496,30 @@ export class PhotonServer {
438
496
  description,
439
497
  inputSchema: tool.inputSchema,
440
498
  };
499
+ // MCP standard annotations (2025-11-25 spec)
500
+ const schema = tool;
501
+ const annotations = {};
502
+ if (schema.title)
503
+ annotations.title = schema.title;
504
+ if (schema.readOnlyHint)
505
+ annotations.readOnlyHint = true;
506
+ if (schema.destructiveHint)
507
+ annotations.destructiveHint = true;
508
+ if (schema.idempotentHint)
509
+ annotations.idempotentHint = true;
510
+ if (schema.openWorldHint !== undefined)
511
+ annotations.openWorldHint = schema.openWorldHint;
512
+ if (Object.keys(annotations).length > 0)
513
+ toolDef.annotations = annotations;
514
+ // MCP structured output schema
515
+ if (schema.outputSchema)
516
+ toolDef.outputSchema = schema.outputSchema;
517
+ // MCP tool icons (resolve image paths to data URIs)
518
+ if (tool.iconImages && tool.iconImages.length > 0) {
519
+ const icons = this.resolveIconImages(tool.iconImages);
520
+ if (icons.length > 0)
521
+ toolDef.icons = icons;
522
+ }
441
523
  const linkedUI = this.mcp?.assets?.ui.find((u) => u.linkedTool === tool.name);
442
524
  if (linkedUI && this.clientSupportsUI(ctx.server)) {
443
525
  toolDef._meta = this.buildUIToolMeta(linkedUI.id);
@@ -464,6 +546,16 @@ export class PhotonServer {
464
546
  description: `List all available instances.`,
465
547
  inputSchema: { type: 'object', properties: {} },
466
548
  });
549
+ tools.push({
550
+ name: '_undo',
551
+ description: `Undo the last state mutation. Reverts the most recent tool call's changes.`,
552
+ inputSchema: { type: 'object', properties: {} },
553
+ });
554
+ tools.push({
555
+ name: '_redo',
556
+ description: `Redo the last undone mutation. Re-applies a previously undone change.`,
557
+ inputSchema: { type: 'object', properties: {} },
558
+ });
467
559
  }
468
560
  return { tools };
469
561
  }
@@ -472,8 +564,12 @@ export class PhotonServer {
472
564
  throw new Error('MCP not loaded');
473
565
  }
474
566
  const { name: toolName, arguments: args } = request.params;
475
- // Route _use and _instances through daemon for stateful photons
476
- if (this.daemonName && (toolName === '_use' || toolName === '_instances')) {
567
+ // Route _use, _instances, _undo, _redo through daemon for stateful photons
568
+ if (this.daemonName &&
569
+ (toolName === '_use' ||
570
+ toolName === '_instances' ||
571
+ toolName === '_undo' ||
572
+ toolName === '_redo')) {
477
573
  const { sendCommand } = await import('./daemon/client.js');
478
574
  const sendOpts = {
479
575
  photonPath: this.options.filePath,
@@ -578,19 +674,35 @@ export class PhotonServer {
578
674
  });
579
675
  const isStateful = result && typeof result === 'object' && result._stateful === true;
580
676
  const actualResult = isStateful ? result.result : result;
581
- // Build content with optional mimeType annotation
677
+ // Build content with optional annotations
582
678
  const content = {
583
679
  type: 'text',
584
680
  text: this.formatResult(actualResult),
585
681
  };
682
+ // Content annotations: audience and priority from schema, mimeType from format
683
+ const contentAnnotations = {};
684
+ const schema = tool;
685
+ if (schema?.audience)
686
+ contentAnnotations.audience = schema.audience;
687
+ if (schema?.contentPriority !== undefined)
688
+ contentAnnotations.priority = schema.contentPriority;
586
689
  if (outputFormat) {
587
690
  const { formatToMimeType } = await import('./cli-formatter.js');
588
691
  const mimeType = formatToMimeType(outputFormat);
589
- if (mimeType) {
590
- content.annotations = { mimeType };
591
- }
692
+ if (mimeType)
693
+ contentAnnotations.mimeType = mimeType;
694
+ }
695
+ if (Object.keys(contentAnnotations).length > 0) {
696
+ content.annotations = contentAnnotations;
592
697
  }
593
698
  const response = { content: [content], isError: false };
699
+ // Structured output: include structuredContent when outputSchema is declared
700
+ if (schema?.outputSchema &&
701
+ actualResult &&
702
+ typeof actualResult === 'object' &&
703
+ !Array.isArray(actualResult)) {
704
+ response.structuredContent = actualResult;
705
+ }
594
706
  // Add x-output-format for format-aware clients
595
707
  if (outputFormat) {
596
708
  response['x-output-format'] = outputFormat;
@@ -941,8 +1053,20 @@ export class PhotonServer {
941
1053
  let errorType = 'runtime_error';
942
1054
  let errorMessage = getErrorMessage(error) || String(error);
943
1055
  let suggestion = '';
944
- // Categorize common errors and provide suggestions
945
- if (errorMessage.includes('not a function') || errorMessage.includes('undefined')) {
1056
+ // Photon authors can attach userMessage and hint to errors for friendly display:
1057
+ // throw Object.assign(new Error('internal'), { userMessage: 'friendly msg', hint: 'try this' })
1058
+ const userMessage = error && typeof error === 'object' && 'userMessage' in error ? String(error.userMessage) : '';
1059
+ const userHint = error && typeof error === 'object' && 'hint' in error ? String(error.hint) : '';
1060
+ // Use userMessage as the display message if provided
1061
+ if (userMessage) {
1062
+ errorMessage = userMessage;
1063
+ }
1064
+ // Use author-provided hint over auto-generated suggestion
1065
+ if (userHint) {
1066
+ suggestion = userHint;
1067
+ }
1068
+ else if (errorMessage.includes('not a function') || errorMessage.includes('undefined')) {
1069
+ // Categorize common errors and provide suggestions
946
1070
  errorType = 'implementation_error';
947
1071
  suggestion =
948
1072
  'The tool implementation may have an issue. Check that all methods are properly defined.';
@@ -1230,8 +1354,18 @@ export class PhotonServer {
1230
1354
  }
1231
1355
  }
1232
1356
  // Load the Photon MCP file
1233
- this.log('info', `Loading ${this.options.filePath}...`);
1234
- this.mcp = await this.loader.loadFile(this.options.filePath);
1357
+ if (this.options.preloadedModule) {
1358
+ // Wire preloaded @photon dependencies before loading
1359
+ if (this.options.preloadedDependencies) {
1360
+ this.loader.preloadedDependencies = this.options.preloadedDependencies;
1361
+ }
1362
+ this.log('info', `Loading preloaded module for ${this.options.filePath}...`);
1363
+ this.mcp = await this.loader.loadFromModule(this.options.preloadedModule, this.options.filePath, this.options.embeddedSource || '');
1364
+ }
1365
+ else {
1366
+ this.log('info', `Loading ${this.options.filePath}...`);
1367
+ this.mcp = await this.loader.loadFile(this.options.filePath);
1368
+ }
1235
1369
  }
1236
1370
  // Subscribe to daemon channels for cross-process notifications
1237
1371
  await this.subscribeToChannels();
@@ -1290,6 +1424,10 @@ export class PhotonServer {
1290
1424
  if (!message || typeof message !== 'object')
1291
1425
  return;
1292
1426
  const msg = message;
1427
+ // Debug logging for cross-client event transmission
1428
+ if (process.env.PHOTON_DEBUG_EVENTS === '1') {
1429
+ console.error(`[PHOTON-SERVER] Received daemon message on ${String(msg.channel)}: event=${String(msg.event)}`);
1430
+ }
1293
1431
  // Use STANDARD notification with embedded photon data
1294
1432
  // Claude Desktop will forward this (it's a standard notification)
1295
1433
  // Our bridge extracts _photon and routes to the appropriate event handler
@@ -1306,9 +1444,16 @@ export class PhotonServer {
1306
1444
  },
1307
1445
  };
1308
1446
  try {
1447
+ if (process.env.PHOTON_DEBUG_EVENTS === '1') {
1448
+ console.error(`[PHOTON-SERVER] Sending notification to MCP clients...`);
1449
+ }
1309
1450
  await this.server.notification(payload);
1451
+ if (process.env.PHOTON_DEBUG_EVENTS === '1') {
1452
+ console.error(`[PHOTON-SERVER] Notification sent successfully`);
1453
+ }
1310
1454
  }
1311
1455
  catch (e) {
1456
+ console.error(`[PHOTON-SERVER-ERROR] Notification send failed: ${getErrorMessage(e)}`);
1312
1457
  this.log('debug', 'Notification send failed', { error: getErrorMessage(e) });
1313
1458
  }
1314
1459
  // Also send to SSE sessions — snapshot to avoid live-iterator + await issues