@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.
- package/README.md +81 -72
- package/dist/auto-ui/beam/photon-management.d.ts.map +1 -1
- package/dist/auto-ui/beam/photon-management.js +5 -0
- package/dist/auto-ui/beam/photon-management.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.d.ts +1 -2
- package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.js +140 -191
- package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-config.js +44 -1
- package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +994 -34
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/frontend/index.html +83 -60
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +53 -12
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +28 -1
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js +23 -0
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +2894 -329
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli/commands/build.d.ts +3 -0
- package/dist/cli/commands/build.d.ts.map +1 -0
- package/dist/cli/commands/build.js +339 -0
- package/dist/cli/commands/build.js.map +1 -0
- package/dist/cli/commands/package-app.d.ts.map +1 -1
- package/dist/cli/commands/package-app.js +116 -35
- package/dist/cli/commands/package-app.js.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +2 -0
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/context-store.d.ts +5 -0
- package/dist/context-store.d.ts.map +1 -1
- package/dist/context-store.js +9 -0
- package/dist/context-store.js.map +1 -1
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +81 -0
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/protocol.d.ts +3 -1
- package/dist/daemon/protocol.d.ts.map +1 -1
- package/dist/daemon/protocol.js +1 -1
- package/dist/daemon/protocol.js.map +1 -1
- package/dist/daemon/server.js +513 -18
- package/dist/daemon/server.js.map +1 -1
- package/dist/embedded-runtime.d.ts +38 -0
- package/dist/embedded-runtime.d.ts.map +1 -0
- package/dist/embedded-runtime.js +326 -0
- package/dist/embedded-runtime.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +38 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +455 -15
- package/dist/loader.js.map +1 -1
- package/dist/photon-cli-runner.d.ts +22 -0
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +244 -12
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts +6 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +22 -0
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/photons/tunnel.photon.d.ts +5 -9
- package/dist/photons/tunnel.photon.d.ts.map +1 -1
- package/dist/photons/tunnel.photon.js +36 -96
- package/dist/photons/tunnel.photon.js.map +1 -1
- package/dist/photons/tunnel.photon.ts +40 -112
- package/dist/server.d.ts +30 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +155 -10
- package/dist/server.js.map +1 -1
- package/dist/test-runner.d.ts +13 -1
- package/dist/test-runner.d.ts.map +1 -1
- package/dist/test-runner.js +529 -122
- package/dist/test-runner.js.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +10 -2
- package/dist/version.js.map +1 -1
- 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
|
-
* @
|
|
4
|
+
* @icon 🌐
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { spawn, spawnSync,
|
|
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: '
|
|
37
|
-
available:
|
|
38
|
-
|
|
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
|
|
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 = '
|
|
68
|
+
provider = 'cloudflared',
|
|
66
69
|
}: {
|
|
67
|
-
/** Provider:
|
|
68
|
-
provider?: '
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
101
|
+
message: `Starting ${provider} tunnel on port ${port}...`,
|
|
101
102
|
};
|
|
102
103
|
|
|
103
104
|
try {
|
|
104
105
|
let url: string;
|
|
105
|
-
let
|
|
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:
|
|
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
|
|
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
|
*/
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
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
|
|
476
|
-
if (this.daemonName &&
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
945
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
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
|