@portel/photon 1.19.0 → 1.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.js +16 -4
- 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 +165 -24
- package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-marketplace.js +14 -1
- package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +187 -77
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/bridge/index.d.ts.map +1 -1
- package/dist/auto-ui/bridge/index.js +17 -0
- package/dist/auto-ui/bridge/index.js.map +1 -1
- package/dist/auto-ui/bridge/renderers.d.ts.map +1 -1
- package/dist/auto-ui/bridge/renderers.js +12 -4
- package/dist/auto-ui/bridge/renderers.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +1 -0
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +179 -44
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +12 -0
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam-form.bundle.js +63 -185
- package/dist/beam-form.bundle.js.map +4 -4
- package/dist/beam.bundle.js +2115 -761
- package/dist/beam.bundle.js.map +4 -4
- package/dist/capability-negotiator.d.ts +67 -0
- package/dist/capability-negotiator.d.ts.map +1 -0
- package/dist/capability-negotiator.js +104 -0
- package/dist/capability-negotiator.js.map +1 -0
- package/dist/channel-manager.d.ts +122 -0
- package/dist/channel-manager.d.ts.map +1 -0
- package/dist/channel-manager.js +266 -0
- package/dist/channel-manager.js.map +1 -0
- package/dist/cli/commands/beam.d.ts.map +1 -1
- package/dist/cli/commands/beam.js +47 -30
- package/dist/cli/commands/beam.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +27 -2
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/daemon.d.ts.map +1 -1
- package/dist/cli/commands/daemon.js +12 -6
- package/dist/cli/commands/daemon.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +18 -6
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/package.d.ts.map +1 -1
- package/dist/cli/commands/package.js +25 -7
- package/dist/cli/commands/package.js.map +1 -1
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/serve.js +14 -2
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli-alias.d.ts.map +1 -1
- package/dist/cli-alias.js +2 -3
- package/dist/cli-alias.js.map +1 -1
- package/dist/context-store.d.ts +4 -4
- package/dist/context-store.d.ts.map +1 -1
- package/dist/context-store.js +18 -15
- package/dist/context-store.js.map +1 -1
- package/dist/context.d.ts +25 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +69 -4
- package/dist/context.js.map +1 -1
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +16 -1
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +2 -0
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +40 -8
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/server.js +89 -64
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/worker-host.js +7 -0
- package/dist/daemon/worker-host.js.map +1 -1
- package/dist/daemon/worker-manager.d.ts.map +1 -1
- package/dist/daemon/worker-manager.js +79 -17
- package/dist/daemon/worker-manager.js.map +1 -1
- package/dist/daemon/worker-protocol.d.ts +3 -0
- package/dist/daemon/worker-protocol.d.ts.map +1 -1
- package/dist/deploy/cloudflare.d.ts.map +1 -1
- package/dist/deploy/cloudflare.js +2 -4
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/loader.d.ts +11 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +129 -13
- package/dist/loader.js.map +1 -1
- package/dist/marketplace-manager.d.ts +7 -1
- package/dist/marketplace-manager.d.ts.map +1 -1
- package/dist/marketplace-manager.js +165 -61
- package/dist/marketplace-manager.js.map +1 -1
- package/dist/namespace-migration.d.ts +1 -0
- package/dist/namespace-migration.d.ts.map +1 -1
- package/dist/namespace-migration.js +86 -0
- package/dist/namespace-migration.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +40 -21
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +59 -15
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/resource-server.d.ts +105 -0
- package/dist/resource-server.d.ts.map +1 -0
- package/dist/resource-server.js +723 -0
- package/dist/resource-server.js.map +1 -0
- package/dist/serv/auth/jwt.d.ts +2 -0
- package/dist/serv/auth/jwt.d.ts.map +1 -1
- package/dist/serv/auth/jwt.js +11 -5
- package/dist/serv/auth/jwt.js.map +1 -1
- package/dist/serv/vault/token-vault.d.ts +2 -0
- package/dist/serv/vault/token-vault.d.ts.map +1 -1
- package/dist/serv/vault/token-vault.js +6 -0
- package/dist/serv/vault/token-vault.js.map +1 -1
- package/dist/server.d.ts +20 -149
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +246 -1233
- package/dist/server.js.map +1 -1
- package/dist/shared/audit.d.ts.map +1 -1
- package/dist/shared/audit.js +7 -0
- package/dist/shared/audit.js.map +1 -1
- package/dist/shared/security.d.ts +10 -0
- package/dist/shared/security.d.ts.map +1 -1
- package/dist/shared/security.js +27 -0
- package/dist/shared/security.js.map +1 -1
- package/dist/shared-utils.d.ts +4 -0
- package/dist/shared-utils.d.ts.map +1 -1
- package/dist/shared-utils.js +22 -0
- package/dist/shared-utils.js.map +1 -1
- package/dist/task-executor.d.ts +69 -0
- package/dist/task-executor.d.ts.map +1 -0
- package/dist/task-executor.js +182 -0
- package/dist/task-executor.js.map +1 -0
- package/dist/template-manager.d.ts.map +1 -1
- package/dist/template-manager.js +56 -234
- package/dist/template-manager.js.map +1 -1
- package/dist/types/photon-instance.d.ts +50 -0
- package/dist/types/photon-instance.d.ts.map +1 -0
- package/dist/types/photon-instance.js +9 -0
- package/dist/types/photon-instance.js.map +1 -0
- package/dist/types/server-types.d.ts +61 -0
- package/dist/types/server-types.d.ts.map +1 -0
- package/dist/types/server-types.js +8 -0
- package/dist/types/server-types.js.map +1 -0
- package/package.json +3 -3
|
@@ -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
|