@portel/photon 1.28.2 → 1.29.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 +1 -0
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +77 -43
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +7 -0
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +228 -29
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +9 -1
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +32 -2
- package/dist/beam.bundle.js.map +2 -2
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +123 -15
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +45 -11
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/server.js +41 -0
- package/dist/daemon/server.js.map +1 -1
- package/dist/deploy/cloudflare.d.ts.map +1 -1
- package/dist/deploy/cloudflare.js +82 -2
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/editor-support/docblock-tag-catalog.d.ts.map +1 -1
- package/dist/editor-support/docblock-tag-catalog.js +32 -2
- package/dist/editor-support/docblock-tag-catalog.js.map +1 -1
- package/dist/format/registry.d.ts +83 -0
- package/dist/format/registry.d.ts.map +1 -0
- package/dist/format/registry.js +139 -0
- package/dist/format/registry.js.map +1 -0
- package/dist/format/seed.d.ts +18 -0
- package/dist/format/seed.d.ts.map +1 -0
- package/dist/format/seed.js +246 -0
- package/dist/format/seed.js.map +1 -0
- package/dist/loader.d.ts +18 -0
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +130 -22
- package/dist/loader.js.map +1 -1
- package/dist/photons/maker.photon.d.ts +2 -2
- package/dist/photons/maker.photon.d.ts.map +1 -1
- package/dist/photons/maker.photon.js +5 -6
- package/dist/photons/maker.photon.js.map +1 -1
- package/dist/photons/maker.photon.ts +5 -6
- package/dist/resource-server.d.ts +52 -12
- package/dist/resource-server.d.ts.map +1 -1
- package/dist/resource-server.js +205 -50
- package/dist/resource-server.js.map +1 -1
- package/dist/server.d.ts +75 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +515 -53
- package/dist/server.js.map +1 -1
- package/dist/shared/asset-encoding.d.ts +30 -0
- package/dist/shared/asset-encoding.d.ts.map +1 -0
- package/dist/shared/asset-encoding.js +0 -0
- package/dist/shared/asset-encoding.js.map +1 -0
- package/dist/shared/cross-origin-headers.d.ts +47 -0
- package/dist/shared/cross-origin-headers.d.ts.map +1 -0
- package/dist/shared/cross-origin-headers.js +61 -0
- package/dist/shared/cross-origin-headers.js.map +1 -0
- package/dist/shared/expose-route-extractor.d.ts +36 -0
- package/dist/shared/expose-route-extractor.d.ts.map +1 -0
- package/dist/shared/expose-route-extractor.js +64 -0
- package/dist/shared/expose-route-extractor.js.map +1 -0
- package/dist/shared/extract-claims.d.ts +33 -0
- package/dist/shared/extract-claims.d.ts.map +1 -0
- package/dist/shared/extract-claims.js +60 -0
- package/dist/shared/extract-claims.js.map +1 -0
- package/dist/shared/http-route-extractor.d.ts +6 -0
- package/dist/shared/http-route-extractor.d.ts.map +1 -1
- package/dist/shared/http-route-extractor.js +29 -5
- package/dist/shared/http-route-extractor.js.map +1 -1
- package/dist/shared/instance-binding.d.ts +53 -0
- package/dist/shared/instance-binding.d.ts.map +1 -0
- package/dist/shared/instance-binding.js +85 -0
- package/dist/shared/instance-binding.js.map +1 -0
- package/dist/types/server-types.d.ts +1 -0
- package/dist/types/server-types.d.ts.map +1 -1
- package/package.json +6 -3
- package/templates/cloudflare/worker.ts.template +90 -3
- package/templates/cloudflare/wrangler.toml.template +1 -1
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { randomUUID } from 'crypto';
|
|
20
20
|
import { readdir, stat, readFile, writeFile } from 'fs/promises';
|
|
21
|
+
import { readText } from '../shared/io.js';
|
|
21
22
|
import { join, dirname, extname, resolve, normalize } from 'path';
|
|
22
23
|
import { homedir } from 'os';
|
|
23
24
|
import { PHOTON_VERSION } from '../version.js';
|
|
@@ -34,6 +35,7 @@ import { buildToolMetadataExtensions } from './types.js';
|
|
|
34
35
|
import { generateServerCard } from '../server-card.js';
|
|
35
36
|
import { audit } from '../shared/audit.js';
|
|
36
37
|
import { writePhotonEditorDeclaration } from '../photon-editor-declarations.js';
|
|
38
|
+
import { isUriTemplate, matchUriTemplate, parseUriTemplateParams, SubscriptionRegistry, } from '../resource-server.js';
|
|
37
39
|
import { createTask, getTask, updateTask, listTasks, registerController, unregisterController, getController, taskEvents, } from '../tasks/store.js';
|
|
38
40
|
import { toWireFormat, relatedTaskMeta, TERMINAL_STATES } from '../tasks/types.js';
|
|
39
41
|
import { runTaskExecution, resolveTaskInput, waitForTerminalOrInput } from '../tasks/executor.js';
|
|
@@ -76,6 +78,40 @@ const sessions = new Map();
|
|
|
76
78
|
const pendingElicitations = new Map();
|
|
77
79
|
const pendingServerRequests = new Map();
|
|
78
80
|
let nextServerRequestId = 1;
|
|
81
|
+
/**
|
|
82
|
+
* Registry of `resources/subscribe` state for the streamable-HTTP transport.
|
|
83
|
+
* Sinks are keyed by sessionId so per-session disconnect can purge in O(1).
|
|
84
|
+
*
|
|
85
|
+
* `attachLoaderForResourceUpdates(loader)` (exported) bridges photon-author
|
|
86
|
+
* `this.notifyResourceUpdated(uri)` calls into `streamableSubscriptions.notify(uri)`
|
|
87
|
+
* so subscribers get `notifications/resources/updated` over the SSE stream.
|
|
88
|
+
*/
|
|
89
|
+
const streamableSubscriptions = new SubscriptionRegistry();
|
|
90
|
+
const sessionSubscriptionSinks = new Map();
|
|
91
|
+
function getOrCreateSessionSink(sessionId) {
|
|
92
|
+
const existing = sessionSubscriptionSinks.get(sessionId);
|
|
93
|
+
if (existing)
|
|
94
|
+
return existing;
|
|
95
|
+
const sink = (uri) => {
|
|
96
|
+
sendToSession(sessionId, 'notifications/resources/updated', { uri });
|
|
97
|
+
};
|
|
98
|
+
sessionSubscriptionSinks.set(sessionId, sink);
|
|
99
|
+
return sink;
|
|
100
|
+
}
|
|
101
|
+
function disconnectSessionSubscriptions(sessionId) {
|
|
102
|
+
const sink = sessionSubscriptionSinks.get(sessionId);
|
|
103
|
+
if (!sink)
|
|
104
|
+
return;
|
|
105
|
+
streamableSubscriptions.disconnect(sink);
|
|
106
|
+
sessionSubscriptionSinks.delete(sessionId);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Wire a PhotonLoader's `notifyResourceUpdated` channel into the streamable-HTTP
|
|
110
|
+
* subscription registry. Call once at server start.
|
|
111
|
+
*/
|
|
112
|
+
export function attachLoaderForResourceUpdates(loader) {
|
|
113
|
+
loader.setResourceUpdateNotifier((uri) => streamableSubscriptions.notify(uri));
|
|
114
|
+
}
|
|
79
115
|
/**
|
|
80
116
|
* Send a JSON-RPC request to a Beam session over its SSE stream and
|
|
81
117
|
* await the reply. Backs photon-facing providers (sampling, future
|
|
@@ -339,6 +375,7 @@ function startSessionCleanup() {
|
|
|
339
375
|
for (const [id, session] of sessions) {
|
|
340
376
|
if (now - session.lastActivity.getTime() > SESSION_TIMEOUT_MS) {
|
|
341
377
|
sessions.delete(id);
|
|
378
|
+
disconnectSessionSubscriptions(id);
|
|
342
379
|
}
|
|
343
380
|
}
|
|
344
381
|
}, 60 * 1000);
|
|
@@ -556,7 +593,7 @@ const handlers = {
|
|
|
556
593
|
capabilities: {
|
|
557
594
|
tools: { listChanged: true },
|
|
558
595
|
prompts: { listChanged: true },
|
|
559
|
-
resources: { listChanged: true },
|
|
596
|
+
resources: { listChanged: true, subscribe: true },
|
|
560
597
|
tasks: {
|
|
561
598
|
list: {},
|
|
562
599
|
cancel: {},
|
|
@@ -2049,23 +2086,72 @@ const handlers = {
|
|
|
2049
2086
|
}
|
|
2050
2087
|
},
|
|
2051
2088
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2052
|
-
// Resources
|
|
2089
|
+
// Resources
|
|
2090
|
+
//
|
|
2091
|
+
// Mirrors what STDIO surfaces via ResourceServer (src/resource-server.ts):
|
|
2092
|
+
// - ui://<photon>/<id> UI Apps templates (assets.ui)
|
|
2093
|
+
// - approval://<photon>/<id> pending approval requests
|
|
2094
|
+
// - photon://<photon>/prompts/<id> class-level @prompt static-file
|
|
2095
|
+
// - photon://<photon>/resources/<id> class-level @resource static-file
|
|
2096
|
+
// - <author-defined-uri> method-level @resource / @Static
|
|
2097
|
+
// (non-templated entries from mcp.statics)
|
|
2098
|
+
//
|
|
2099
|
+
// Templated method-level URIs (`person://{slug}`) belong on
|
|
2100
|
+
// resources/templates/list and resolve via resources/read.
|
|
2053
2101
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2054
2102
|
'resources/list': async (req, session, ctx) => {
|
|
2055
2103
|
const resources = [];
|
|
2056
2104
|
for (const photon of ctx.photons) {
|
|
2057
|
-
if (!photon.configured
|
|
2105
|
+
if (!photon.configured)
|
|
2058
2106
|
continue;
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
uri
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
:
|
|
2068
|
-
|
|
2107
|
+
const mcp = ctx.photonMCPs.get(photon.name);
|
|
2108
|
+
if (photon.assets?.ui) {
|
|
2109
|
+
for (const uiAsset of photon.assets.ui) {
|
|
2110
|
+
const uri = uiAsset.uri || `ui://${photon.name}/${uiAsset.id}`;
|
|
2111
|
+
resources.push({
|
|
2112
|
+
uri,
|
|
2113
|
+
name: uiAsset.id,
|
|
2114
|
+
mimeType: uiAsset.mimeType || 'text/html;profile=mcp-app',
|
|
2115
|
+
description: uiAsset.linkedTool
|
|
2116
|
+
? `UI template for ${photon.name}/${uiAsset.linkedTool}`
|
|
2117
|
+
: `UI template: ${uiAsset.id}`,
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
// Class-level @prompt <id> <path> static-file prompts
|
|
2122
|
+
if (mcp?.assets?.prompts) {
|
|
2123
|
+
for (const prompt of mcp.assets.prompts) {
|
|
2124
|
+
resources.push({
|
|
2125
|
+
uri: `photon://${photon.name}/prompts/${prompt.id}`,
|
|
2126
|
+
name: `prompt:${prompt.id}`,
|
|
2127
|
+
mimeType: 'text/markdown',
|
|
2128
|
+
description: `Prompt template: ${prompt.id}`,
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
// Class-level @resource <id> <path> static-file resources
|
|
2133
|
+
if (mcp?.assets?.resources) {
|
|
2134
|
+
for (const resource of mcp.assets.resources) {
|
|
2135
|
+
resources.push({
|
|
2136
|
+
uri: `photon://${photon.name}/resources/${resource.id}`,
|
|
2137
|
+
name: `resource:${resource.id}`,
|
|
2138
|
+
mimeType: resource.mimeType || 'application/octet-stream',
|
|
2139
|
+
description: `Static resource: ${resource.id}`,
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
// Method-level @resource / @Static — non-templated URIs only.
|
|
2144
|
+
if (mcp?.statics) {
|
|
2145
|
+
for (const stat of mcp.statics) {
|
|
2146
|
+
if (isUriTemplate(stat.uri))
|
|
2147
|
+
continue;
|
|
2148
|
+
resources.push({
|
|
2149
|
+
uri: stat.uri,
|
|
2150
|
+
name: stat.name,
|
|
2151
|
+
mimeType: stat.mimeType || 'text/plain',
|
|
2152
|
+
description: stat.description || `Resource: ${stat.name}`,
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2069
2155
|
}
|
|
2070
2156
|
}
|
|
2071
2157
|
// Add pending approval resources (approval:// scheme)
|
|
@@ -2081,9 +2167,30 @@ const handlers = {
|
|
|
2081
2167
|
}
|
|
2082
2168
|
return { jsonrpc: '2.0', id: req.id, result: { resources } };
|
|
2083
2169
|
},
|
|
2170
|
+
'resources/templates/list': async (req, _session, ctx) => {
|
|
2171
|
+
const resourceTemplates = [];
|
|
2172
|
+
for (const photon of ctx.photons) {
|
|
2173
|
+
if (!photon.configured)
|
|
2174
|
+
continue;
|
|
2175
|
+
const mcp = ctx.photonMCPs.get(photon.name);
|
|
2176
|
+
if (!mcp?.statics)
|
|
2177
|
+
continue;
|
|
2178
|
+
for (const stat of mcp.statics) {
|
|
2179
|
+
if (!isUriTemplate(stat.uri))
|
|
2180
|
+
continue;
|
|
2181
|
+
resourceTemplates.push({
|
|
2182
|
+
uriTemplate: stat.uri,
|
|
2183
|
+
name: stat.name,
|
|
2184
|
+
mimeType: stat.mimeType || 'text/plain',
|
|
2185
|
+
description: stat.description || `Resource template: ${stat.name}`,
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
return { jsonrpc: '2.0', id: req.id, result: { resourceTemplates } };
|
|
2190
|
+
},
|
|
2084
2191
|
'resources/read': async (req, session, ctx) => {
|
|
2085
2192
|
const { uri } = req.params;
|
|
2086
|
-
//
|
|
2193
|
+
// approval://<photon>/<id>
|
|
2087
2194
|
const approvalMatch = uri.match(/^approval:\/\/([^/]+)\/(.+)$/);
|
|
2088
2195
|
if (approvalMatch) {
|
|
2089
2196
|
const [, photonName, approvalId] = approvalMatch;
|
|
@@ -2106,36 +2213,128 @@ const handlers = {
|
|
|
2106
2213
|
},
|
|
2107
2214
|
};
|
|
2108
2215
|
}
|
|
2109
|
-
//
|
|
2110
|
-
const
|
|
2111
|
-
if (
|
|
2216
|
+
// ui://<photon>/<id>
|
|
2217
|
+
const uiMatch = uri.match(/^ui:\/\/([^/]+)\/(.+)$/);
|
|
2218
|
+
if (uiMatch) {
|
|
2219
|
+
const [, photonName, uiId] = uiMatch;
|
|
2220
|
+
const result = await ctx.loadUIAsset(photonName, uiId);
|
|
2221
|
+
if (!result) {
|
|
2222
|
+
return {
|
|
2223
|
+
jsonrpc: '2.0',
|
|
2224
|
+
id: req.id,
|
|
2225
|
+
error: { code: -32602, message: `Resource not found: ${uri}` },
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
const mimeType = result.isPhotonTemplate
|
|
2229
|
+
? 'text/html;profile=mcp-app;photon-template=true'
|
|
2230
|
+
: 'text/html;profile=mcp-app';
|
|
2112
2231
|
return {
|
|
2113
2232
|
jsonrpc: '2.0',
|
|
2114
2233
|
id: req.id,
|
|
2115
|
-
|
|
2234
|
+
result: { contents: [{ uri, mimeType, text: result.content }] },
|
|
2116
2235
|
};
|
|
2117
2236
|
}
|
|
2118
|
-
|
|
2119
|
-
const
|
|
2120
|
-
if (
|
|
2237
|
+
// photon://<photon>/(prompts|resources)/<id> — class-level static-file assets
|
|
2238
|
+
const assetMatch = uri.match(/^photon:\/\/([^/]+)\/(prompts|resources)\/(.+)$/);
|
|
2239
|
+
if (assetMatch) {
|
|
2240
|
+
const [, photonName, kind, assetId] = assetMatch;
|
|
2241
|
+
const mcp = ctx.photonMCPs.get(photonName);
|
|
2242
|
+
if (!mcp?.assets) {
|
|
2243
|
+
return {
|
|
2244
|
+
jsonrpc: '2.0',
|
|
2245
|
+
id: req.id,
|
|
2246
|
+
error: { code: -32602, message: `Photon assets not found: ${uri}` },
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
const list = kind === 'prompts' ? mcp.assets.prompts : mcp.assets.resources;
|
|
2250
|
+
const asset = list?.find((a) => a.id === assetId);
|
|
2251
|
+
const resolvedPath = asset?.resolvedPath;
|
|
2252
|
+
if (!resolvedPath) {
|
|
2253
|
+
return {
|
|
2254
|
+
jsonrpc: '2.0',
|
|
2255
|
+
id: req.id,
|
|
2256
|
+
error: { code: -32602, message: `Asset not found: ${uri}` },
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
const text = await readText(resolvedPath);
|
|
2260
|
+
const mimeType = kind === 'prompts'
|
|
2261
|
+
? 'text/markdown'
|
|
2262
|
+
: asset?.mimeType || 'application/octet-stream';
|
|
2121
2263
|
return {
|
|
2122
2264
|
jsonrpc: '2.0',
|
|
2123
2265
|
id: req.id,
|
|
2124
|
-
|
|
2266
|
+
result: { contents: [{ uri, mimeType, text }] },
|
|
2125
2267
|
};
|
|
2126
2268
|
}
|
|
2127
|
-
//
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2269
|
+
// Method-level @resource / @Static — match against any photon's statics.
|
|
2270
|
+
// The URI may be exact or match a registered template; on match, dispatch
|
|
2271
|
+
// through the loader so middleware (auth, logging, audit) applies.
|
|
2272
|
+
for (const photon of ctx.photons) {
|
|
2273
|
+
const mcp = ctx.photonMCPs.get(photon.name);
|
|
2274
|
+
if (!mcp?.statics)
|
|
2275
|
+
continue;
|
|
2276
|
+
for (const stat of mcp.statics) {
|
|
2277
|
+
const matched = stat.uri === uri || matchUriTemplate(stat.uri, uri);
|
|
2278
|
+
if (!matched)
|
|
2279
|
+
continue;
|
|
2280
|
+
const params = isUriTemplate(stat.uri) ? parseUriTemplateParams(stat.uri, uri) : {};
|
|
2281
|
+
if (!ctx.loader) {
|
|
2282
|
+
return {
|
|
2283
|
+
jsonrpc: '2.0',
|
|
2284
|
+
id: req.id,
|
|
2285
|
+
error: {
|
|
2286
|
+
code: -32603,
|
|
2287
|
+
message: 'Loader not available for static resource resolution',
|
|
2288
|
+
},
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
const result = await ctx.loader.executeTool(mcp, stat.name, params);
|
|
2292
|
+
const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
2293
|
+
return {
|
|
2294
|
+
jsonrpc: '2.0',
|
|
2295
|
+
id: req.id,
|
|
2296
|
+
result: {
|
|
2297
|
+
contents: [{ uri, mimeType: stat.mimeType || 'text/plain', text }],
|
|
2298
|
+
},
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2131
2302
|
return {
|
|
2132
2303
|
jsonrpc: '2.0',
|
|
2133
2304
|
id: req.id,
|
|
2134
|
-
|
|
2135
|
-
contents: [{ uri, mimeType, text: result.content }],
|
|
2136
|
-
},
|
|
2305
|
+
error: { code: -32602, message: `Resource not found: ${uri}` },
|
|
2137
2306
|
};
|
|
2138
2307
|
},
|
|
2308
|
+
// resources/subscribe and resources/unsubscribe — exact-URI keyed.
|
|
2309
|
+
// The session's sink is keyed off session.id so disconnect logic in the
|
|
2310
|
+
// session-cleanup path can purge subscriptions in O(1).
|
|
2311
|
+
'resources/subscribe': async (req, session) => {
|
|
2312
|
+
const { uri } = req.params;
|
|
2313
|
+
if (typeof uri !== 'string' || !uri) {
|
|
2314
|
+
return {
|
|
2315
|
+
jsonrpc: '2.0',
|
|
2316
|
+
id: req.id,
|
|
2317
|
+
error: { code: -32602, message: 'resources/subscribe requires `uri`' },
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
const sink = getOrCreateSessionSink(session.id);
|
|
2321
|
+
streamableSubscriptions.subscribe(sink, uri);
|
|
2322
|
+
return { jsonrpc: '2.0', id: req.id, result: {} };
|
|
2323
|
+
},
|
|
2324
|
+
'resources/unsubscribe': async (req, session) => {
|
|
2325
|
+
const { uri } = req.params;
|
|
2326
|
+
if (typeof uri !== 'string' || !uri) {
|
|
2327
|
+
return {
|
|
2328
|
+
jsonrpc: '2.0',
|
|
2329
|
+
id: req.id,
|
|
2330
|
+
error: { code: -32602, message: 'resources/unsubscribe requires `uri`' },
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
const sink = sessionSubscriptionSinks.get(session.id);
|
|
2334
|
+
if (sink)
|
|
2335
|
+
streamableSubscriptions.unsubscribe(sink, uri);
|
|
2336
|
+
return { jsonrpc: '2.0', id: req.id, result: {} };
|
|
2337
|
+
},
|
|
2139
2338
|
'prompts/list': async (req, _session, ctx) => {
|
|
2140
2339
|
const prompts = [];
|
|
2141
2340
|
for (const photon of ctx.photons) {
|