@portel/photon 1.19.0 → 1.20.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 (92) hide show
  1. package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
  2. package/dist/auto-ui/beam/routes/api-browse.js +16 -4
  3. package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
  4. package/dist/auto-ui/beam/routes/api-config.js +4 -4
  5. package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
  6. package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -1
  7. package/dist/auto-ui/beam/routes/api-marketplace.js +14 -1
  8. package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
  9. package/dist/auto-ui/beam.d.ts.map +1 -1
  10. package/dist/auto-ui/beam.js +183 -74
  11. package/dist/auto-ui/beam.js.map +1 -1
  12. package/dist/auto-ui/bridge/index.d.ts.map +1 -1
  13. package/dist/auto-ui/bridge/index.js +17 -0
  14. package/dist/auto-ui/bridge/index.js.map +1 -1
  15. package/dist/auto-ui/streamable-http-transport.d.ts +1 -0
  16. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  17. package/dist/auto-ui/streamable-http-transport.js +64 -16
  18. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  19. package/dist/auto-ui/types.d.ts +12 -0
  20. package/dist/auto-ui/types.d.ts.map +1 -1
  21. package/dist/auto-ui/types.js.map +1 -1
  22. package/dist/beam-form.bundle.js +44 -3
  23. package/dist/beam-form.bundle.js.map +2 -2
  24. package/dist/beam.bundle.js +1404 -482
  25. package/dist/beam.bundle.js.map +4 -4
  26. package/dist/capability-negotiator.d.ts +67 -0
  27. package/dist/capability-negotiator.d.ts.map +1 -0
  28. package/dist/capability-negotiator.js +104 -0
  29. package/dist/capability-negotiator.js.map +1 -0
  30. package/dist/channel-manager.d.ts +122 -0
  31. package/dist/channel-manager.d.ts.map +1 -0
  32. package/dist/channel-manager.js +266 -0
  33. package/dist/channel-manager.js.map +1 -0
  34. package/dist/cli/commands/package.d.ts.map +1 -1
  35. package/dist/cli/commands/package.js +25 -7
  36. package/dist/cli/commands/package.js.map +1 -1
  37. package/dist/daemon/client.d.ts.map +1 -1
  38. package/dist/daemon/client.js +12 -0
  39. package/dist/daemon/client.js.map +1 -1
  40. package/dist/daemon/server.js +30 -49
  41. package/dist/daemon/server.js.map +1 -1
  42. package/dist/daemon/worker-manager.d.ts.map +1 -1
  43. package/dist/daemon/worker-manager.js +21 -7
  44. package/dist/daemon/worker-manager.js.map +1 -1
  45. package/dist/loader.d.ts +4 -1
  46. package/dist/loader.d.ts.map +1 -1
  47. package/dist/loader.js +73 -11
  48. package/dist/loader.js.map +1 -1
  49. package/dist/marketplace-manager.d.ts +6 -0
  50. package/dist/marketplace-manager.d.ts.map +1 -1
  51. package/dist/marketplace-manager.js +161 -58
  52. package/dist/marketplace-manager.js.map +1 -1
  53. package/dist/namespace-migration.d.ts +1 -0
  54. package/dist/namespace-migration.d.ts.map +1 -1
  55. package/dist/namespace-migration.js +86 -0
  56. package/dist/namespace-migration.js.map +1 -1
  57. package/dist/resource-server.d.ts +105 -0
  58. package/dist/resource-server.d.ts.map +1 -0
  59. package/dist/resource-server.js +723 -0
  60. package/dist/resource-server.js.map +1 -0
  61. package/dist/serv/auth/jwt.d.ts +2 -0
  62. package/dist/serv/auth/jwt.d.ts.map +1 -1
  63. package/dist/serv/auth/jwt.js +11 -5
  64. package/dist/serv/auth/jwt.js.map +1 -1
  65. package/dist/serv/vault/token-vault.d.ts +2 -0
  66. package/dist/serv/vault/token-vault.d.ts.map +1 -1
  67. package/dist/serv/vault/token-vault.js +6 -0
  68. package/dist/serv/vault/token-vault.js.map +1 -1
  69. package/dist/server.d.ts +20 -149
  70. package/dist/server.d.ts.map +1 -1
  71. package/dist/server.js +232 -1217
  72. package/dist/server.js.map +1 -1
  73. package/dist/shared/audit.d.ts.map +1 -1
  74. package/dist/shared/audit.js +7 -0
  75. package/dist/shared/audit.js.map +1 -1
  76. package/dist/shared/security.d.ts +10 -0
  77. package/dist/shared/security.d.ts.map +1 -1
  78. package/dist/shared/security.js +27 -0
  79. package/dist/shared/security.js.map +1 -1
  80. package/dist/task-executor.d.ts +69 -0
  81. package/dist/task-executor.d.ts.map +1 -0
  82. package/dist/task-executor.js +182 -0
  83. package/dist/task-executor.js.map +1 -0
  84. package/dist/types/photon-instance.d.ts +50 -0
  85. package/dist/types/photon-instance.d.ts.map +1 -0
  86. package/dist/types/photon-instance.js +9 -0
  87. package/dist/types/photon-instance.js.map +1 -0
  88. package/dist/types/server-types.d.ts +61 -0
  89. package/dist/types/server-types.d.ts.map +1 -0
  90. package/dist/types/server-types.js +8 -0
  91. package/dist/types/server-types.js.map +1 -0
  92. package/package.json +2 -2
@@ -0,0 +1,723 @@
1
+ /**
2
+ * ResourceServer — extracted from PhotonServer
3
+ *
4
+ * Encapsulates all MCP resource handling:
5
+ * - ListResources, ListResourceTemplates, ReadResource handlers
6
+ * - UI resource URI resolution (ui:// and photon:// schemes)
7
+ * - Icon image resolution (file → data URI)
8
+ * - Asset serving (UI HTML, prompts, static resources)
9
+ * - MCP Apps bridge script generation for Claude Desktop compatibility
10
+ *
11
+ * Dependency direction: PhotonServer → ResourceServer (never the reverse).
12
+ */
13
+ import { readFileSync } from 'node:fs';
14
+ import * as path from 'node:path';
15
+ import { readText } from './shared/io.js';
16
+ export class ResourceServer {
17
+ toolExecutor;
18
+ options;
19
+ static ICON_MIME_TYPES = {
20
+ '.png': 'image/png',
21
+ '.jpg': 'image/jpeg',
22
+ '.jpeg': 'image/jpeg',
23
+ '.gif': 'image/gif',
24
+ '.svg': 'image/svg+xml',
25
+ '.webp': 'image/webp',
26
+ '.ico': 'image/x-icon',
27
+ };
28
+ /** Cached resolved icons per tool name */
29
+ resolvedIconsCache = new Map();
30
+ constructor(toolExecutor, options) {
31
+ this.toolExecutor = toolExecutor;
32
+ this.options = options;
33
+ }
34
+ // ─── URI helpers ────────────────────────────────────────────────────
35
+ /**
36
+ * Build UI resource URI based on detected format
37
+ */
38
+ buildUIResourceUri(photonName, uiId) {
39
+ return `ui://${photonName}/${uiId}`;
40
+ }
41
+ /**
42
+ * Build tool metadata for UI based on detected format
43
+ */
44
+ buildUIToolMeta(photonName, uiId) {
45
+ const uri = this.buildUIResourceUri(photonName, uiId);
46
+ return { ui: { resourceUri: uri } };
47
+ }
48
+ /**
49
+ * Get UI mimeType based on detected format and client capabilities
50
+ */
51
+ getUIMimeType() {
52
+ return 'text/html;profile=mcp-app';
53
+ }
54
+ // ─── Icon resolution ────────────────────────────────────────────────
55
+ /**
56
+ * Resolve raw icon image paths to MCP Icon[] format (data URIs).
57
+ * Results are cached so file I/O only happens once per tool.
58
+ */
59
+ resolveIconImages(iconImages) {
60
+ const cacheKey = iconImages.map((i) => i.path).join('|');
61
+ const cached = this.resolvedIconsCache.get(cacheKey);
62
+ if (cached)
63
+ return cached;
64
+ const photonDir = path.dirname(this.options.filePath);
65
+ const icons = [];
66
+ for (const entry of iconImages) {
67
+ try {
68
+ const resolvedPath = path.resolve(photonDir, entry.path);
69
+ const ext = path.extname(resolvedPath).toLowerCase();
70
+ const mimeType = ResourceServer.ICON_MIME_TYPES[ext];
71
+ if (!mimeType)
72
+ continue;
73
+ const data = readFileSync(resolvedPath);
74
+ const dataUri = `data:${mimeType};base64,${data.toString('base64')}`;
75
+ const icon = {
76
+ src: dataUri,
77
+ mimeType,
78
+ };
79
+ if (entry.sizes)
80
+ icon.sizes = entry.sizes;
81
+ if (entry.theme)
82
+ icon.theme = entry.theme;
83
+ icons.push(icon);
84
+ }
85
+ catch {
86
+ // Skip unreadable icon files silently
87
+ }
88
+ }
89
+ this.resolvedIconsCache.set(cacheKey, icons);
90
+ return icons;
91
+ }
92
+ // ─── URI template helpers ───────────────────────────────────────────
93
+ /**
94
+ * Check if a URI is a template (contains {parameters})
95
+ */
96
+ isUriTemplate(uri) {
97
+ return /\{[^}]+\}/.test(uri);
98
+ }
99
+ /**
100
+ * Match URI pattern with actual URI
101
+ * Example: github://repos/{owner}/{repo} matches github://repos/foo/bar
102
+ */
103
+ matchUriPattern(pattern, uri) {
104
+ const regexPattern = pattern.replace(/\{[^}]+\}/g, '([^/]+)');
105
+ const regex = new RegExp(`^${regexPattern}$`);
106
+ return regex.test(uri);
107
+ }
108
+ /**
109
+ * Parse parameters from URI based on pattern
110
+ * Example: pattern="github://repos/{owner}/{repo}", uri="github://repos/foo/bar"
111
+ * Returns: { owner: "foo", repo: "bar" }
112
+ */
113
+ parseUriParams(pattern, uri) {
114
+ const params = {};
115
+ const paramNames = [];
116
+ const paramRegex = /\{([^}]+)\}/g;
117
+ let match;
118
+ while ((match = paramRegex.exec(pattern)) !== null) {
119
+ paramNames.push(match[1]);
120
+ }
121
+ const regexPattern = pattern.replace(/\{[^}]+\}/g, '([^/]+)');
122
+ const regex = new RegExp(`^${regexPattern}$`);
123
+ const values = uri.match(regex);
124
+ if (values) {
125
+ for (let i = 0; i < paramNames.length; i++) {
126
+ params[paramNames[i]] = values[i + 1];
127
+ }
128
+ }
129
+ return params;
130
+ }
131
+ // ─── MCP request handlers ──────────────────────────────────────────
132
+ handleListResources(mcp) {
133
+ if (!mcp) {
134
+ return { resources: [] };
135
+ }
136
+ const staticResources = mcp.statics.filter((s) => !this.isUriTemplate(s.uri));
137
+ const resources = staticResources.map((static_) => ({
138
+ uri: static_.uri,
139
+ name: static_.name,
140
+ description: static_.description,
141
+ mimeType: static_.mimeType || 'text/plain',
142
+ }));
143
+ if (mcp.assets) {
144
+ const photonName = mcp.name;
145
+ for (const ui of mcp.assets.ui) {
146
+ const uiUri = ui.uri || this.buildUIResourceUri(photonName, ui.id);
147
+ resources.push({
148
+ uri: uiUri,
149
+ name: `ui:${ui.id}`,
150
+ description: ui.linkedTool
151
+ ? `UI template for ${ui.linkedTool} tool`
152
+ : `UI template: ${ui.id}`,
153
+ // Always use MCP Apps mime type for UI resources — photon-core's
154
+ // getMimeTypeFromPath returns plain text/html which causes Claude
155
+ // Desktop to skip rendering the app UI
156
+ mimeType: this.getUIMimeType(),
157
+ });
158
+ }
159
+ for (const prompt of mcp.assets.prompts) {
160
+ resources.push({
161
+ uri: `photon://${photonName}/prompts/${prompt.id}`,
162
+ name: `prompt:${prompt.id}`,
163
+ description: prompt.description || `Prompt template: ${prompt.id}`,
164
+ mimeType: 'text/markdown',
165
+ });
166
+ }
167
+ for (const resource of mcp.assets.resources) {
168
+ resources.push({
169
+ uri: `photon://${photonName}/resources/${resource.id}`,
170
+ name: `resource:${resource.id}`,
171
+ description: resource.description || `Static resource: ${resource.id}`,
172
+ mimeType: resource.mimeType || 'application/octet-stream',
173
+ });
174
+ }
175
+ }
176
+ // Include sub-photon UI resources (compiled binary mode)
177
+ if (this.options.embeddedAssets) {
178
+ const allLoaded = this.toolExecutor.getLoadedPhotons();
179
+ for (const [, loaded] of allLoaded) {
180
+ if (loaded.name === mcp.name)
181
+ continue;
182
+ if (loaded.assets?.ui) {
183
+ for (const ui of loaded.assets.ui) {
184
+ const uiUri = ui.uri || `ui://${loaded.name}/${ui.id}`;
185
+ resources.push({
186
+ uri: uiUri,
187
+ name: `ui:${ui.id}`,
188
+ description: ui.linkedTool
189
+ ? `UI template for ${loaded.name}/${ui.linkedTool}`
190
+ : `UI template: ${loaded.name}/${ui.id}`,
191
+ mimeType: ui.mimeType || this.getUIMimeType(),
192
+ });
193
+ }
194
+ }
195
+ }
196
+ }
197
+ return { resources };
198
+ }
199
+ handleListResourceTemplates(mcp) {
200
+ if (!mcp) {
201
+ return { resourceTemplates: [] };
202
+ }
203
+ const templateResources = mcp.statics.filter((s) => this.isUriTemplate(s.uri));
204
+ return {
205
+ resourceTemplates: templateResources.map((static_) => ({
206
+ uriTemplate: static_.uri,
207
+ name: static_.name,
208
+ description: static_.description,
209
+ mimeType: static_.mimeType || 'text/plain',
210
+ })),
211
+ };
212
+ }
213
+ async handleReadResource(request, mcp) {
214
+ if (!mcp) {
215
+ throw new Error('MCP not loaded');
216
+ }
217
+ const { uri: rawUri } = request.params;
218
+ const uri = typeof rawUri === 'string'
219
+ ? rawUri.replace(/^ui:\/\/\/([^/]+)\/(.+)$/, 'ui://$1/$2')
220
+ : rawUri;
221
+ const uiMatch = uri.match(/^ui:\/\/([^/]+)\/(.+)$/);
222
+ if (uiMatch) {
223
+ const [, uiPhotonName, assetId] = uiMatch;
224
+ // Check main photon first
225
+ if (mcp.assets && uiPhotonName === mcp.name) {
226
+ return this.handleUIAssetRead(uri, assetId, mcp);
227
+ }
228
+ // Check sub-photons (compiled binary mode)
229
+ if (this.options.embeddedAssets) {
230
+ const allLoaded = this.toolExecutor.getLoadedPhotons();
231
+ for (const [, loaded] of allLoaded) {
232
+ if (loaded.name === uiPhotonName && loaded.assets?.ui) {
233
+ return this.handleUIAssetRead(uri, assetId, mcp, loaded);
234
+ }
235
+ }
236
+ }
237
+ // Fallback to main photon
238
+ if (mcp.assets) {
239
+ return this.handleUIAssetRead(uri, assetId, mcp);
240
+ }
241
+ }
242
+ const assetMatch = uri.match(/^photon:\/\/([^/]+)\/(ui|prompts|resources)\/(.+)$/);
243
+ if (assetMatch && mcp.assets) {
244
+ return this.handleAssetRead(uri, assetMatch, mcp);
245
+ }
246
+ return this.handleStaticRead(uri, mcp);
247
+ }
248
+ // ─── Asset reading ──────────────────────────────────────────────────
249
+ /**
250
+ * Handle SEP-1865 ui:// resource read
251
+ */
252
+ async handleUIAssetRead(uri, assetId, mcp, photon) {
253
+ const target = photon || mcp;
254
+ const photonName = target.name;
255
+ let content;
256
+ // Try embedded templates first (compiled binary mode)
257
+ if (this.options.embeddedUITemplates) {
258
+ const templates = this.options.embeddedUITemplates[photonName];
259
+ if (templates && templates[assetId]) {
260
+ content = templates[assetId];
261
+ }
262
+ }
263
+ // Fall back to disk if not embedded
264
+ if (!content) {
265
+ if (!target.assets?.ui) {
266
+ throw new Error(`UI asset not found: ${uri}`);
267
+ }
268
+ const ui = target.assets.ui.find((u) => u.id === assetId);
269
+ if (!ui || !ui.resolvedPath) {
270
+ throw new Error(`UI asset not found: ${uri}`);
271
+ }
272
+ content = await readText(ui.resolvedPath);
273
+ }
274
+ // Wrap .photon.html fragments in a full HTML document.
275
+ const isFragment = !content.trimStart().toLowerCase().startsWith('<!doctype') &&
276
+ !content.trimStart().toLowerCase().startsWith('<html');
277
+ if (isFragment) {
278
+ content = `<!doctype html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n</head>\n<body>\n${content}\n</body>\n</html>`;
279
+ }
280
+ // Inject MCP Apps bridge script for Claude Desktop compatibility
281
+ const bridgeScript = this.generateMcpAppsBridge(mcp);
282
+ content = content.replace('<head>', `<head>\n${bridgeScript}`);
283
+ return {
284
+ contents: [{ uri, mimeType: 'text/html;profile=mcp-app', text: content }],
285
+ };
286
+ }
287
+ /**
288
+ * Handle photon:// asset read (Beam format)
289
+ */
290
+ async handleAssetRead(uri, assetMatch, mcp) {
291
+ const [, _photonName, assetType, assetId] = assetMatch;
292
+ let resolvedPath;
293
+ let mimeType = 'text/plain';
294
+ if (assetType === 'ui') {
295
+ const ui = mcp.assets.ui.find((u) => u.id === assetId);
296
+ if (ui) {
297
+ resolvedPath = ui.resolvedPath;
298
+ mimeType = ui.mimeType || 'text/html;profile=mcp-app';
299
+ }
300
+ }
301
+ else if (assetType === 'prompts') {
302
+ const prompt = mcp.assets.prompts.find((p) => p.id === assetId);
303
+ if (prompt) {
304
+ resolvedPath = prompt.resolvedPath;
305
+ mimeType = 'text/markdown';
306
+ }
307
+ }
308
+ else if (assetType === 'resources') {
309
+ const resource = mcp.assets.resources.find((r) => r.id === assetId);
310
+ if (resource) {
311
+ resolvedPath = resource.resolvedPath;
312
+ mimeType = resource.mimeType || 'application/octet-stream';
313
+ }
314
+ }
315
+ if (resolvedPath) {
316
+ let content = await readText(resolvedPath);
317
+ // Inject MCP Apps bridge for UI assets
318
+ if (assetType === 'ui') {
319
+ const bridgeScript = this.generateMcpAppsBridge(mcp);
320
+ content = content.replace('<head>', `<head>\n${bridgeScript}`);
321
+ }
322
+ return {
323
+ contents: [{ uri, mimeType, text: content }],
324
+ };
325
+ }
326
+ throw new Error(`Asset not found: ${uri}`);
327
+ }
328
+ /**
329
+ * Handle static resource read (for both stdio and SSE handlers)
330
+ */
331
+ async handleStaticRead(uri, mcp) {
332
+ const static_ = mcp.statics.find((s) => s.uri === uri || this.matchUriPattern(s.uri, uri));
333
+ if (!static_) {
334
+ throw new Error(`Resource not found: ${uri}`);
335
+ }
336
+ const params = this.parseUriParams(static_.uri, uri);
337
+ const result = await this.toolExecutor.executeTool(mcp, static_.name, params);
338
+ return this.formatStaticResult(result, static_.mimeType);
339
+ }
340
+ /**
341
+ * Format static result to MCP resource response
342
+ */
343
+ formatStaticResult(result, mimeType) {
344
+ const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
345
+ return {
346
+ contents: [
347
+ {
348
+ uri: '',
349
+ mimeType: mimeType || 'text/plain',
350
+ text,
351
+ },
352
+ ],
353
+ };
354
+ }
355
+ // ─── MCP Apps bridge ────────────────────────────────────────────────
356
+ /**
357
+ * Generate minimal MCP Apps bridge script for Claude Desktop compatibility
358
+ * This handles the ui/initialize handshake and tool result delivery
359
+ */
360
+ generateMcpAppsBridge(mcp) {
361
+ const photonName = mcp?.name || 'photon-app';
362
+ const injectedPhotons = mcp?.injectedPhotons || [];
363
+ return (`<script>
364
+ (function() {
365
+ 'use strict';
366
+ var pendingCalls = {};
367
+ var callIdCounter = 0;
368
+ var toolResult = null;
369
+ var resultListeners = [];
370
+ var emitListeners = [];
371
+ var themeListeners = [];
372
+ var eventListeners = {}; // For specific event subscriptions (e.g., 'taskMove')
373
+ var photonEventListeners = {}; // Namespaced by photon name for injected photons
374
+ var currentTheme = 'dark';
375
+ var injectedPhotons = ${JSON.stringify(injectedPhotons)};
376
+
377
+ function generateCallId() {
378
+ return 'call_' + (++callIdCounter) + '_' + Math.random().toString(36).slice(2);
379
+ }
380
+
381
+ function postToHost(msg) {
382
+ window.parent.postMessage(msg, '*');
383
+ }
384
+
385
+ // Listen for messages from host
386
+ window.addEventListener('message', function(e) {
387
+ var m = e.data;
388
+ if (!m || typeof m !== 'object') return;
389
+
390
+ // Handle JSON-RPC messages
391
+ if (m.jsonrpc === '2.0') {
392
+ // Response to our request (has id, no method)
393
+ if (m.id && !m.method && pendingCalls[m.id]) {
394
+ var pending = pendingCalls[m.id];
395
+ delete pendingCalls[m.id];
396
+ if (m.error) {
397
+ pending.reject(new Error(m.error.message));
398
+ } else {
399
+ // Extract clean data from MCP result format
400
+ var result = m.result;
401
+ var cleanData = result;
402
+ if (result && result.structuredContent) {
403
+ cleanData = result.structuredContent;
404
+ } else if (result && result.content && Array.isArray(result.content)) {
405
+ var textItem = result.content.find(function(i) { return i.type === 'text'; });
406
+ if (textItem && textItem.text) {
407
+ try { cleanData = JSON.parse(textItem.text); } catch(e) { cleanData = textItem.text; }
408
+ }
409
+ }
410
+ pending.resolve(cleanData);
411
+ }
412
+ return;
413
+ }
414
+
415
+ // Tool result notification
416
+ if (m.method === 'ui/notifications/tool-result') {
417
+ var result = m.params;
418
+ // Extract data from MCP result format
419
+ if (result.structuredContent) {
420
+ toolResult = result.structuredContent;
421
+ } else if (result.content && Array.isArray(result.content)) {
422
+ var textItem = result.content.find(function(i) { return i.type === 'text'; });
423
+ if (textItem && textItem.text) {
424
+ try { toolResult = JSON.parse(textItem.text); } catch(e) { toolResult = textItem.text; }
425
+ }
426
+ } else {
427
+ toolResult = result;
428
+ }
429
+ // Set __PHOTON_DATA__ for UIs that read it at init
430
+ window.__PHOTON_DATA__ = toolResult;
431
+ // Dispatch event for UIs to re-initialize with new data
432
+ window.dispatchEvent(new CustomEvent('photon:data-ready', { detail: toolResult }));
433
+ resultListeners.forEach(function(cb) { cb(toolResult); });
434
+ }
435
+
436
+ // Host context changed (theme + embedded photon events)
437
+ if (m.method === 'ui/notifications/host-context-changed') {
438
+ // Standard theme handling
439
+ if (m.params && m.params.theme) {
440
+ currentTheme = m.params.theme;
441
+ document.documentElement.classList.remove('light', 'dark', 'light-theme');
442
+ document.documentElement.classList.add(m.params.theme);
443
+ document.documentElement.setAttribute('data-theme', m.params.theme);
444
+ // Apply theme token CSS variables (matching platform-compat applyThemeTokens)
445
+ if (m.params.styles && m.params.styles.variables) {
446
+ var root = document.documentElement;
447
+ var vars = m.params.styles.variables;
448
+ for (var key in vars) { root.style.setProperty(key, vars[key]); }
449
+ }
450
+ // Apply background/text colors to match platform-compat bridge
451
+ if (m.params.theme === 'light') {
452
+ document.documentElement.classList.add('light-theme');
453
+ document.documentElement.style.colorScheme = 'light';
454
+ document.documentElement.style.backgroundColor = '#ffffff';
455
+ if (document.body) { document.body.style.backgroundColor = '#ffffff'; document.body.style.color = '#1a1a1a'; }
456
+ } else {
457
+ document.documentElement.style.colorScheme = 'dark';
458
+ document.documentElement.style.backgroundColor = '#0d0d0d';
459
+ if (document.body) { document.body.style.backgroundColor = '#0d0d0d'; document.body.style.color = '#e6e6e6'; }
460
+ }
461
+ themeListeners.forEach(function(cb) { cb(currentTheme); });
462
+ }
463
+
464
+ // Extract embedded photon event data
465
+ // This enables real-time sync via standard MCP protocol
466
+ if (m.params && m.params._photon) {
467
+ var photonData = m.params._photon;
468
+ // Route to generic emit listeners
469
+ emitListeners.forEach(function(cb) { cb(photonData); });
470
+
471
+ var eventName = photonData.event;
472
+ var sourcePhoton = photonData.data && photonData.data._source;
473
+
474
+ // Route to photon-specific listeners if _source is specified (injected photon events)
475
+ if (sourcePhoton && photonEventListeners[sourcePhoton] && photonEventListeners[sourcePhoton][eventName]) {
476
+ photonEventListeners[sourcePhoton][eventName].forEach(function(cb) {
477
+ cb(photonData.data);
478
+ });
479
+ }
480
+
481
+ // Also route to global event listeners (main photon events, or fallback)
482
+ if (eventName && eventListeners[eventName]) {
483
+ eventListeners[eventName].forEach(function(cb) {
484
+ cb(photonData.data);
485
+ });
486
+ }
487
+ }
488
+ }
489
+ }
490
+ });
491
+
492
+ // Mark that we're in MCP Apps context (not Beam)
493
+ window.__MCP_APPS_CONTEXT__ = true;
494
+
495
+ // Expose photon bridge API
496
+ window.photon = {
497
+ get toolOutput() { return toolResult; },
498
+ onResult: function(cb) {
499
+ resultListeners.push(cb);
500
+ if (toolResult) cb(toolResult);
501
+ return function() {
502
+ var i = resultListeners.indexOf(cb);
503
+ if (i >= 0) resultListeners.splice(i, 1);
504
+ };
505
+ },
506
+ callTool: function(name, args, opts) {
507
+ var callId = generateCallId();
508
+ return new Promise(function(resolve, reject) {
509
+ pendingCalls[callId] = { resolve: resolve, reject: reject };
510
+ var a = args || {};
511
+ if (opts && opts.instance !== undefined) { a = Object.assign({}, a, { _targetInstance: opts.instance }); }
512
+ postToHost({
513
+ jsonrpc: '2.0',
514
+ id: callId,
515
+ method: 'tools/call',
516
+ params: { name: name, arguments: a }
517
+ });
518
+ setTimeout(function() {
519
+ if (pendingCalls[callId]) {
520
+ delete pendingCalls[callId];
521
+ reject(new Error('Tool call timeout'));
522
+ }
523
+ }, 30000);
524
+ });
525
+ },
526
+ invoke: function(name, args, opts) { return window.photon.callTool(name, args, opts); },
527
+ onEmit: function(cb) {
528
+ emitListeners.push(cb);
529
+ return function() {
530
+ var i = emitListeners.indexOf(cb);
531
+ if (i >= 0) emitListeners.splice(i, 1);
532
+ };
533
+ },
534
+ onThemeChange: function(cb) {
535
+ themeListeners.push(cb);
536
+ // Call immediately with current theme
537
+ cb(currentTheme);
538
+ return function() {
539
+ var i = themeListeners.indexOf(cb);
540
+ if (i >= 0) themeListeners.splice(i, 1);
541
+ };
542
+ },
543
+ get theme() { return currentTheme; },
544
+
545
+ // Generic event subscription for real-time sync
546
+ // Usage: photon.on('taskMove', function(data) { ... })
547
+ on: function(eventName, cb) {
548
+ if (!eventListeners[eventName]) eventListeners[eventName] = [];
549
+ eventListeners[eventName].push(cb);
550
+ return function() {
551
+ var i = eventListeners[eventName].indexOf(cb);
552
+ if (i >= 0) eventListeners[eventName].splice(i, 1);
553
+ };
554
+ },
555
+
556
+ // Photon-specific event subscription (for injected photon events)
557
+ // Usage: photon.onPhoton('notifications', 'alertCreated', function(data) { ... })
558
+ onPhoton: function(photonName, eventName, cb) {
559
+ if (!photonEventListeners[photonName]) photonEventListeners[photonName] = {};
560
+ if (!photonEventListeners[photonName][eventName]) photonEventListeners[photonName][eventName] = [];
561
+ photonEventListeners[photonName][eventName].push(cb);
562
+ return function() {
563
+ var i = photonEventListeners[photonName][eventName].indexOf(cb);
564
+ if (i >= 0) photonEventListeners[photonName][eventName].splice(i, 1);
565
+ };
566
+ }
567
+ };
568
+
569
+ // Create direct window object: window.{photonName}
570
+ // This provides a clean class-like API that mirrors server methods:
571
+ // Server: this.emit('taskMove', data)
572
+ // Client: kanban.onTaskMove(cb) - subscribe to events
573
+ // Client: kanban.taskMove(args) - call server method
574
+ var photonName = '${photonName}';
575
+ window[photonName] = new Proxy({}, {
576
+ get: function(target, prop) {
577
+ if (typeof prop !== 'string') return undefined;
578
+
579
+ // onEventName -> subscribe to 'eventName' event
580
+ // e.g., onTaskMove -> subscribe to 'taskMove'
581
+ if (prop.startsWith('on') && prop.length > 2) {
582
+ var eventName = prop.charAt(2).toLowerCase() + prop.slice(3);
583
+ return function(cb) {
584
+ return window.photon.on(eventName, cb);
585
+ };
586
+ }
587
+
588
+ // methodName -> call server tool
589
+ // e.g., taskMove(args) -> photon.callTool('taskMove', args)
590
+ return function(args) {
591
+ return window.photon.callTool(prop, args);
592
+ };
593
+ }
594
+ });
595
+
596
+ // Create proxies for injected photons (for event subscriptions)
597
+ // e.g., notifications.onAlertCreated(cb) subscribes to 'alertCreated' from 'notifications' photon
598
+ injectedPhotons.forEach(function(injectedName) {
599
+ window[injectedName] = new Proxy({}, {
600
+ get: function(target, prop) {
601
+ if (typeof prop !== 'string') return undefined;
602
+
603
+ // onEventName -> subscribe to photon-specific event
604
+ if (prop.startsWith('on') && prop.length > 2) {
605
+ var eventName = prop.charAt(2).toLowerCase() + prop.slice(3);
606
+ return function(cb) {
607
+ return window.photon.onPhoton(injectedName, eventName, cb);
608
+ };
609
+ }
610
+
611
+ // Method calls on injected photons are not supported from client
612
+ // (injected photon methods are only available server-side)
613
+ return undefined;
614
+ }
615
+ });
616
+ });
617
+
618
+ // Size notification helper
619
+ function sendSizeChanged() {
620
+ var body = document.body;
621
+ var root = document.documentElement;
622
+
623
+ // Calculate actual content dimensions
624
+ var width = Math.max(
625
+ body.scrollWidth,
626
+ body.offsetWidth,
627
+ root.clientWidth,
628
+ root.scrollWidth,
629
+ root.offsetWidth
630
+ );
631
+ var height = Math.max(
632
+ body.scrollHeight,
633
+ body.offsetHeight,
634
+ root.clientHeight,
635
+ root.scrollHeight,
636
+ root.offsetHeight
637
+ );
638
+
639
+ // Check for scrollable containers with overflow:hidden that hide true content size
640
+ var containers = document.querySelectorAll('.board, [style*="overflow"]');
641
+ containers.forEach(function(el) {
642
+ if (el.scrollWidth > width) width = el.scrollWidth;
643
+ if (el.scrollHeight > height) height = el.scrollHeight;
644
+ });
645
+
646
+ // For kanban-style boards, calculate from column count
647
+ var columns = document.querySelectorAll('.column');
648
+ if (columns.length > 0) {
649
+ var columnWidth = 220; // min-width + gap
650
+ var boardPadding = 48;
651
+ var neededWidth = (columns.length * columnWidth) + boardPadding;
652
+ if (neededWidth > width) width = neededWidth;
653
+ }
654
+
655
+ // Reasonable minimums, maximums, and padding
656
+ width = Math.max(width, 600) + 32;
657
+ // Force minimum height for kanban-style boards
658
+ // header(120) + column headers(50) + 3-4 cards(450) = 620
659
+ if (columns.length > 0) {
660
+ height = Math.max(height, 620);
661
+ } else {
662
+ height = Math.max(height, 400);
663
+ }
664
+
665
+ postToHost({
666
+ jsonrpc: '2.0',
667
+ method: 'ui/notifications/size-changed',
668
+ params: { width: width, height: height }
669
+ });
670
+ }
671
+
672
+ // MCP Apps handshake: send ui/initialize and wait for response
673
+ var initId = generateCallId();
674
+ pendingCalls[initId] = {
675
+ resolve: function(result) {
676
+ // Apply theme from host context (matching platform-compat bridge)
677
+ if (result.hostContext && result.hostContext.theme) {
678
+ currentTheme = result.hostContext.theme;
679
+ document.documentElement.classList.remove('light', 'dark', 'light-theme');
680
+ document.documentElement.classList.add(result.hostContext.theme);
681
+ document.documentElement.setAttribute('data-theme', result.hostContext.theme);
682
+ // Apply theme token CSS variables from host context
683
+ if (result.hostContext.styles && result.hostContext.styles.variables) {
684
+ var root = document.documentElement;
685
+ var vars = result.hostContext.styles.variables;
686
+ for (var key in vars) { root.style.setProperty(key, vars[key]); }
687
+ }
688
+ if (result.hostContext.theme === 'light') {
689
+ document.documentElement.classList.add('light-theme');
690
+ document.documentElement.style.colorScheme = 'light';
691
+ } else {
692
+ document.documentElement.style.colorScheme = 'dark';
693
+ }
694
+ }
695
+ // Complete handshake
696
+ postToHost({ jsonrpc: '2.0', method: 'ui/notifications/initialized', params: {} });
697
+
698
+ // Set up size notifications after handshake
699
+ setTimeout(sendSizeChanged, 100);
700
+ var resizeObserver = new ResizeObserver(function() {
701
+ sendSizeChanged();
702
+ });
703
+ resizeObserver.observe(document.documentElement);
704
+ resizeObserver.observe(document.body);
705
+ },
706
+ reject: function(err) { console.error('MCP Apps init failed:', err); }
707
+ };
708
+
709
+ postToHost({
710
+ jsonrpc: '2.0',
711
+ id: initId,
712
+ method: 'ui/initialize',
713
+ params: {
714
+ appInfo: { name: '${photonName}', version: '1.0.0' },
715
+ appCapabilities: {},
716
+ protocolVersion: '2026-01-26'
717
+ }
718
+ });
719
+ })();
720
+ </` + `script>`);
721
+ }
722
+ }
723
+ //# sourceMappingURL=resource-server.js.map