@ggui-ai/mcp-server 0.9.0 → 0.10.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/dist/api-renders-routes.d.ts +21 -0
- package/dist/api-renders-routes.d.ts.map +1 -1
- package/dist/api-renders-routes.js +22 -5
- package/dist/build-mcp.d.ts +38 -7
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +23 -2
- package/dist/code-module-variant.d.ts +150 -0
- package/dist/code-module-variant.d.ts.map +1 -0
- package/dist/code-module-variant.js +243 -0
- package/dist/code-routes.d.ts +12 -2
- package/dist/code-routes.d.ts.map +1 -1
- package/dist/code-routes.js +12 -2
- package/dist/control-service.d.ts +29 -3
- package/dist/control-service.d.ts.map +1 -1
- package/dist/control-service.js +26 -2
- package/dist/health-routes.d.ts +19 -3
- package/dist/health-routes.d.ts.map +1 -1
- package/dist/health-routes.js +26 -18
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/mcp-apps-outbound.d.ts +40 -8
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +127 -30
- package/dist/mcp-endpoint-routes.d.ts +6 -0
- package/dist/mcp-endpoint-routes.d.ts.map +1 -1
- package/dist/mcp-endpoint-routes.js +67 -1
- package/dist/oauth-as-routes.d.ts +11 -0
- package/dist/oauth-as-routes.d.ts.map +1 -1
- package/dist/oauth-as-routes.js +9 -1
- package/dist/runtime-bundle-hash.d.ts +12 -0
- package/dist/runtime-bundle-hash.d.ts.map +1 -1
- package/dist/runtime-bundle-hash.js +19 -0
- package/dist/server.d.ts +223 -56
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +263 -125
- package/package.json +13 -12
|
@@ -64,6 +64,27 @@ interface MountOptions {
|
|
|
64
64
|
readonly codeStore?: CodeStore;
|
|
65
65
|
/** Operator-configured public origin for absolute URL composition. */
|
|
66
66
|
readonly publicBaseUrl?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Origin the content-addressable routes (`/code/*`, `/contract/*`)
|
|
69
|
+
* are reached at — an edge-cached asset host when the deployment
|
|
70
|
+
* fronts static paths separately (ggui#522). Defaults to
|
|
71
|
+
* {@link publicBaseUrl}, then the request host. Session-API URLs
|
|
72
|
+
* (`/events`, `/stream`) never use it: those are dynamic and stay on
|
|
73
|
+
* the public origin.
|
|
74
|
+
*/
|
|
75
|
+
readonly codeBaseUrl?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Strict-CSP module-variant minter (ggui#522 slice 2) — the /state
|
|
78
|
+
* read re-mints the `codeModuleUrl` twin alongside `codeUrl`. Only
|
|
79
|
+
* consulted when an explicit {@link codeBaseUrl}/{@link publicBaseUrl}
|
|
80
|
+
* base resolved: the variant embeds absolute shim URLs on the SAME
|
|
81
|
+
* origin, and a request-derived base has no shim family behind it.
|
|
82
|
+
*/
|
|
83
|
+
readonly mintCodeModuleUrl?: (args: {
|
|
84
|
+
readonly code: string;
|
|
85
|
+
readonly hash: string;
|
|
86
|
+
readonly base: string;
|
|
87
|
+
}) => string | undefined;
|
|
67
88
|
/** Live-mode credential minter — fresh trio on every /state read. */
|
|
68
89
|
readonly mintBootstrap?: (sessionId: string, appId: string) => {
|
|
69
90
|
wsUrl: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-renders-routes.d.ts","sourceRoot":"","sources":["../src/api-renders-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAQ9F,OAAO,EAGL,KAAK,sBAAsB,EAC5B,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,sBAAsB,CAAC,WAAW,CAAC,CAAC;IACzD,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,qEAAqE;IACrE,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,KACV;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,oEAAoE;IACpE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,MAAM,CAAC;IACzC,iDAAiD;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"api-renders-routes.d.ts","sourceRoot":"","sources":["../src/api-renders-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAQ9F,OAAO,EAGL,KAAK,sBAAsB,EAC5B,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,sBAAsB,CAAC,WAAW,CAAC,CAAC;IACzD,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;OAMG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE;QAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;KACvB,KAAK,MAAM,GAAG,SAAS,CAAC;IACzB,qEAAqE;IACrE,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,KACV;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,oEAAoE;IACpE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,MAAM,CAAC;IACzC,iDAAiD;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CA8W9D"}
|
|
@@ -52,7 +52,11 @@ import { composeSessionApiUrls, MCP_APP_AI_GGUI_RENDER_META_KEY, } from "@ggui-a
|
|
|
52
52
|
* Returns nothing — the routes self-register.
|
|
53
53
|
*/
|
|
54
54
|
export function mountApiRendersRoutes(opts) {
|
|
55
|
-
const { app, renderStore, secret, appMetadataStore, themeId, themeMode, codeStore, publicBaseUrl, mintBootstrap, resolveRuntimeUrl, logger, } = opts;
|
|
55
|
+
const { app, renderStore, secret, appMetadataStore, themeId, themeMode, codeStore, publicBaseUrl, codeBaseUrl, mintBootstrap, resolveRuntimeUrl, logger, } = opts;
|
|
56
|
+
// The origin the content-addressable routes are reached at (asset
|
|
57
|
+
// host if fronted separately, else the public origin); the request
|
|
58
|
+
// host is the last resort for local/tunnel deployments.
|
|
59
|
+
const staticBase = codeBaseUrl ?? publicBaseUrl;
|
|
56
60
|
app.get("/api/sessions/:sessionId/state", async (req, res) => {
|
|
57
61
|
// CORS on EVERY response, gates included: cross-origin frames can
|
|
58
62
|
// only read a status when the response carries ACAO. Without it a
|
|
@@ -173,6 +177,7 @@ export function mountApiRendersRoutes(opts) {
|
|
|
173
177
|
// and need the URL to mount/refresh the static-component variant.
|
|
174
178
|
let renderCodeUrl;
|
|
175
179
|
let renderCodeHash;
|
|
180
|
+
let renderCodeModuleUrl;
|
|
176
181
|
let renderContractHash;
|
|
177
182
|
let renderValidatorsUrl;
|
|
178
183
|
if (!isSystem && !isMcpApps && codeStore) {
|
|
@@ -183,10 +188,19 @@ export function mountApiRendersRoutes(opts) {
|
|
|
183
188
|
await codeStore.put(hash, code);
|
|
184
189
|
renderCodeHash = hash;
|
|
185
190
|
const requestHost = req.get("host") ?? "";
|
|
186
|
-
const base =
|
|
187
|
-
?
|
|
191
|
+
const base = staticBase !== undefined
|
|
192
|
+
? staticBase.replace(/\/$/, "")
|
|
188
193
|
: `${req.protocol}://${requestHost}`;
|
|
189
194
|
renderCodeUrl = `${base}/code/${hash}.js`;
|
|
195
|
+
// Strict-CSP module-variant twin — only on an explicit base
|
|
196
|
+
// (see MountOptions.mintCodeModuleUrl for why).
|
|
197
|
+
if (staticBase !== undefined) {
|
|
198
|
+
renderCodeModuleUrl = opts.mintCodeModuleUrl?.({
|
|
199
|
+
code,
|
|
200
|
+
hash,
|
|
201
|
+
base,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
190
204
|
}
|
|
191
205
|
catch {
|
|
192
206
|
// Silent — the caller falls back to live-mode delivery, and
|
|
@@ -205,8 +219,8 @@ export function mountApiRendersRoutes(opts) {
|
|
|
205
219
|
await codeStore.put(bundle.contractHash, bundle.bundleSource);
|
|
206
220
|
renderContractHash = bundle.contractHash;
|
|
207
221
|
const requestHost = req.get("host") ?? "";
|
|
208
|
-
const base =
|
|
209
|
-
?
|
|
222
|
+
const base = staticBase !== undefined
|
|
223
|
+
? staticBase.replace(/\/$/, "")
|
|
210
224
|
: `${req.protocol}://${requestHost}`;
|
|
211
225
|
renderValidatorsUrl = `${base}/contract/${bundle.contractHash}.js`;
|
|
212
226
|
}
|
|
@@ -251,6 +265,9 @@ export function mountApiRendersRoutes(opts) {
|
|
|
251
265
|
? {
|
|
252
266
|
codeUrl: renderCodeUrl,
|
|
253
267
|
...(renderCodeHash !== undefined ? { codeHash: renderCodeHash } : {}),
|
|
268
|
+
...(renderCodeModuleUrl !== undefined
|
|
269
|
+
? { codeModuleUrl: renderCodeModuleUrl }
|
|
270
|
+
: {}),
|
|
254
271
|
}
|
|
255
272
|
: {}),
|
|
256
273
|
...(renderContractHash !== undefined && renderValidatorsUrl !== undefined
|
package/dist/build-mcp.d.ts
CHANGED
|
@@ -126,13 +126,44 @@ export interface BuildMcpServerOptions {
|
|
|
126
126
|
* `resources/read` later.
|
|
127
127
|
*/
|
|
128
128
|
readonly extraResources?: ReadonlyArray<(server: McpServer) => void>;
|
|
129
|
+
/**
|
|
130
|
+
* Withhold every handler's per-result bootstrap MATERIAL from tool
|
|
131
|
+
* results — the read-plane-only posture.
|
|
132
|
+
*
|
|
133
|
+
* By default a successful tool result carries the handler's
|
|
134
|
+
* `resultMeta` — for `ggui_render` / `ggui_update` that is the
|
|
135
|
+
* `ai.ggui/render` bootstrap a host may mount DIRECTLY without any
|
|
136
|
+
* further round-trip, plus the spec-canonical pointer `_meta.ui.
|
|
137
|
+
* resourceUri`. Setting this makes the server publish only the
|
|
138
|
+
* durable IDENTITY: `structuredContent.resourceUri` (the `ui://`
|
|
139
|
+
* locator) and — the same value, on the wire slot MCP Apps hosts
|
|
140
|
+
* read — `_meta.ui.resourceUri` (+ the legacy flat `ui/resourceUri`).
|
|
141
|
+
* `resultMeta` is never invoked (no bootstrap token is minted for a
|
|
142
|
+
* slice nobody receives); the pointer is derived from the validated
|
|
143
|
+
* OUTPUT itself, the single source of truth `resultMeta` reuses.
|
|
144
|
+
* A host MUST resolve every view by an authenticated `resources/read`
|
|
145
|
+
* — the persisted-locator path — before it can mount anything.
|
|
146
|
+
*
|
|
147
|
+
* Why the pointer stays (ggui#537): the identity IS the pointer.
|
|
148
|
+
* Spec-canonical hosts (claude.ai, Claude Desktop, `@ggui-ai/
|
|
149
|
+
* mcp-apps-react`'s chat-helpers, the OSS samples) mount the
|
|
150
|
+
* per-render self-contained shell that `_meta.ui.resourceUri` names —
|
|
151
|
+
* a `resources/read` the HOST performs, exactly the read-plane path
|
|
152
|
+
* this posture wants. The first arm (f8c93405d) stripped `_meta`
|
|
153
|
+
* wholesale, which took the pointer with it and left every such host
|
|
154
|
+
* with the declaration-level static shell and a result it could not
|
|
155
|
+
* mount from ("Waiting for tool result…", prod 2026-08-16→17).
|
|
156
|
+
*
|
|
157
|
+
* That is a deployment posture, not a debug switch: a hosted
|
|
158
|
+
* deployment that wants "views mount only through the read plane"
|
|
159
|
+
* (thread-scoped ownership checks, fresh per-read credentials, no
|
|
160
|
+
* inlined bootstrap material crossing a chat transcript) states it
|
|
161
|
+
* here by construction, and any host that still expects the inlined
|
|
162
|
+
* bootstrap fails loudly (a locator it cannot resolve) instead of
|
|
163
|
+
* silently mounting stale material. `structuredContent` and `content`
|
|
164
|
+
* are untouched; only `_meta` is withheld.
|
|
165
|
+
*/
|
|
166
|
+
readonly withholdResultMeta?: boolean;
|
|
129
167
|
}
|
|
130
|
-
/**
|
|
131
|
-
* Build a fresh MCP server with every handler registered.
|
|
132
|
-
*
|
|
133
|
-
* `getContext` is a late-binding accessor so the HTTP layer can thread
|
|
134
|
-
* per-request context (via AsyncLocalStorage or a closure) without
|
|
135
|
-
* leaking the shape into this module.
|
|
136
|
-
*/
|
|
137
168
|
export declare function buildMcpServer(info: ServerInfo, handlers: ReadonlyArray<SharedHandler<ZodRawShape, ZodRawShape>>, getContext: () => HandlerContext, logger: Logger, opts?: BuildMcpServerOptions): McpServer;
|
|
138
169
|
//# sourceMappingURL=build-mcp.d.ts.map
|
package/dist/build-mcp.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-mcp.d.ts","sourceRoot":"","sources":["../src/build-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,OAAO,EAAK,KAAK,WAAW,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EAEL,KAAK,iCAAiC,EACvC,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iCAAiC,CAAC;IAC3D;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;;;OAQG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IAC5D;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,aAAa,CAAC,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IAElE;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"build-mcp.d.ts","sourceRoot":"","sources":["../src/build-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,OAAO,EAAK,KAAK,WAAW,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EAEL,KAAK,iCAAiC,EACvC,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iCAAiC,CAAC;IAC3D;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;;;OAQG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IAC5D;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,aAAa,CAAC,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IAElE;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC,CAAC;IAErE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoCG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;CACvC;AAyBD,wBAAgB,cAAc,CAC5B,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,EAChE,UAAU,EAAE,MAAM,cAAc,EAChC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,qBAA0B,GAC/B,SAAS,CAwPX"}
|
package/dist/build-mcp.js
CHANGED
|
@@ -22,6 +22,23 @@ import { installMcpAppsOutbound, } from './mcp-apps-outbound.js';
|
|
|
22
22
|
* per-request context (via AsyncLocalStorage or a closure) without
|
|
23
23
|
* leaking the shape into this module.
|
|
24
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* The identity-only `_meta` a withholding server publishes (ggui#537):
|
|
27
|
+
* when the validated output carries a `ui://` `resourceUri` (the durable
|
|
28
|
+
* locator `ggui_render` / `ggui_update` surface on structuredContent),
|
|
29
|
+
* mirror it onto the spec-canonical `_meta.ui.resourceUri` slot MCP
|
|
30
|
+
* Apps hosts read (+ the legacy flat key), and nothing else. Same value
|
|
31
|
+
* `resultMeta` would have stamped, without invoking it — no bootstrap
|
|
32
|
+
* material, no minted token. `undefined` for outputs without a locator.
|
|
33
|
+
*/
|
|
34
|
+
function identityPointerMeta(validated) {
|
|
35
|
+
if (!isRecord(validated))
|
|
36
|
+
return undefined;
|
|
37
|
+
const uri = validated['resourceUri'];
|
|
38
|
+
if (typeof uri !== 'string' || !uri.startsWith('ui://'))
|
|
39
|
+
return undefined;
|
|
40
|
+
return { ui: { resourceUri: uri }, 'ui/resourceUri': uri };
|
|
41
|
+
}
|
|
25
42
|
export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
26
43
|
const server = new McpServer({
|
|
27
44
|
name: info.name,
|
|
@@ -128,8 +145,12 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
|
128
145
|
const validated = z.object(handler.outputSchema).parse(data);
|
|
129
146
|
// Per-result `_meta` — NOT merged into structuredContent, so
|
|
130
147
|
// agents that typecheck against the tool signature never see
|
|
131
|
-
// it. This is where view-only bootstrap material lives.
|
|
132
|
-
|
|
148
|
+
// it. This is where view-only bootstrap material lives. Under
|
|
149
|
+
// the withhold posture only the identity pointer is published,
|
|
150
|
+
// derived from the output (see `withholdResultMeta`).
|
|
151
|
+
const meta = opts.withholdResultMeta === true
|
|
152
|
+
? identityPointerMeta(validated)
|
|
153
|
+
: await handler.resultMeta?.(data, input, ctx);
|
|
133
154
|
logger.info('tool_invoked', {
|
|
134
155
|
tool: handler.name,
|
|
135
156
|
appId: ctx.appId,
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict-CSP module-variant delivery (ggui#522 slice 2).
|
|
3
|
+
*
|
|
4
|
+
* The `/code/<hash>.js` route serves STORED component bytes — compiled
|
|
5
|
+
* ESM whose imports are still BARE specifiers (`react`,
|
|
6
|
+
* `@ggui-ai/design/primitives`, …). A browser cannot import that
|
|
7
|
+
* standalone; the renderer historically bridged the gap in-frame by
|
|
8
|
+
* rewriting bare specifiers to `data:` shims and instantiating the
|
|
9
|
+
* module from a `blob:` URL — both of which a strict host CSP refuses.
|
|
10
|
+
*
|
|
11
|
+
* This module adds the server-side twin of that bridge: a SECOND,
|
|
12
|
+
* immutable route `/code/<hash>.m<rt>.js` serving the SAME stored bytes
|
|
13
|
+
* with the rewrite already applied — every bare specifier resolved to a
|
|
14
|
+
* static shim asset under `/_ggui/shims/<rt>/<name>.js` on the same
|
|
15
|
+
* origin. A frame whose `script-src` allows only the asset origin can
|
|
16
|
+
* `import()` the variant URL directly: no `blob:`, no `data:`, no
|
|
17
|
+
* `'unsafe-eval'`.
|
|
18
|
+
*
|
|
19
|
+
* `<rt>` — the 12-hex content hash of the runtime bundle — is the
|
|
20
|
+
* variant KEY, and it lives in the PATH deliberately: the asset CDN
|
|
21
|
+
* (CloudFront `CACHING_OPTIMIZED`) ignores query strings, so a
|
|
22
|
+
* query-keyed variant would collapse to one cached body across
|
|
23
|
+
* runtime versions. The hash versions the whole family: the shim
|
|
24
|
+
* files ship in the runtime bundle's dist, so any shim-affecting
|
|
25
|
+
* change also changes the bundle bytes, which changes `<rt>`, which
|
|
26
|
+
* changes every variant URL — immutable caching stays sound.
|
|
27
|
+
*
|
|
28
|
+
* The route serves only the CURRENT `<rt>`: after a redeploy, a
|
|
29
|
+
* CDN-evicted old-variant URL 404s (`no-store`) and the renderer falls
|
|
30
|
+
* back to its raw-bytes ladder — honest degradation, never stale shims.
|
|
31
|
+
*
|
|
32
|
+
* 3rd-party gadget packages (GG.8.2) have no static shim (their
|
|
33
|
+
* exports are derived per-render from the generated code); a module
|
|
34
|
+
* importing one is NOT asset-deliverable. Both ends respect that: the
|
|
35
|
+
* minter declines to stamp `codeModuleUrl`, and the route declines
|
|
36
|
+
* (404) any body whose rewrite leaves a bare specifier standing.
|
|
37
|
+
*/
|
|
38
|
+
import type { Express } from "express";
|
|
39
|
+
import type { Logger } from "./logger.js";
|
|
40
|
+
/**
|
|
41
|
+
* Mint the `codeModuleUrl` for one render's component code, or decline.
|
|
42
|
+
*
|
|
43
|
+
* `undefined` ⇒ the code is not asset-deliverable (it imports a
|
|
44
|
+
* package with no static shim) — the slice then carries only the raw
|
|
45
|
+
* `codeUrl`/`codeB64` carriers and the renderer uses its blob ladder.
|
|
46
|
+
*/
|
|
47
|
+
export type MintCodeModuleUrl = (args: {
|
|
48
|
+
/** RAW stored component code (markers included — stripped here). */
|
|
49
|
+
readonly code: string;
|
|
50
|
+
/** `sha256(code)` — the same hash `codeUrl` is composed from. */
|
|
51
|
+
readonly hash: string;
|
|
52
|
+
/** Code base URL, no trailing slash (the emitter already trims). */
|
|
53
|
+
readonly base: string;
|
|
54
|
+
}) => string | undefined;
|
|
55
|
+
/** The variant URL for (hash, rt) under `base` — ONE composition site. */
|
|
56
|
+
export declare function composeCodeModuleUrl(args: {
|
|
57
|
+
readonly base: string;
|
|
58
|
+
readonly hash: string;
|
|
59
|
+
readonly runtimeHash: string;
|
|
60
|
+
}): string;
|
|
61
|
+
/**
|
|
62
|
+
* Build the minter every `codeUrl` emitter injects. Coverability is
|
|
63
|
+
* decided from the code's own import list: every bare specifier must
|
|
64
|
+
* have a static shim ({@link ASSET_SHIM_FOR_SPECIFIER}) or the mint
|
|
65
|
+
* declines. Markers are stripped first — their embedded JSON can
|
|
66
|
+
* contain strings the import scanner would misread.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createCodeModuleUrlMinter(args: {
|
|
69
|
+
readonly runtimeHash: string;
|
|
70
|
+
}): MintCodeModuleUrl;
|
|
71
|
+
/**
|
|
72
|
+
* Rewrite stored component bytes into the servable asset-module
|
|
73
|
+
* variant — the same preprocessing the renderer's blob ladder applies
|
|
74
|
+
* (strip markers, hoist imports) plus the `asset-url` specifier
|
|
75
|
+
* rewrite. Declines when any bare specifier survives (no static shim
|
|
76
|
+
* exists for it) instead of serving a module that fails at eval.
|
|
77
|
+
*/
|
|
78
|
+
export declare function rewriteToAssetModule(args: {
|
|
79
|
+
readonly code: string;
|
|
80
|
+
readonly shimBaseUrl: string;
|
|
81
|
+
}): {
|
|
82
|
+
readonly ok: true;
|
|
83
|
+
readonly code: string;
|
|
84
|
+
} | {
|
|
85
|
+
readonly ok: false;
|
|
86
|
+
readonly leftover: readonly string[];
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Read the static shim files emitted by the iframe-runtime build
|
|
90
|
+
* (`dist/shims/<name>.js`) into memory, keyed by file name. Captured
|
|
91
|
+
* ONCE at composition — the immutable shim route must keep serving the
|
|
92
|
+
* exact bytes its URL family was stamped for, even across an in-place
|
|
93
|
+
* rebuild of the dist (same guarantee the hashed bundle route makes by
|
|
94
|
+
* serving captured bytes).
|
|
95
|
+
*
|
|
96
|
+
* `undefined` when the directory is missing/empty (bundle built by an
|
|
97
|
+
* older toolchain) — callers then skip the whole variant family.
|
|
98
|
+
*/
|
|
99
|
+
export declare function captureShimSources(shimsDir: string): ReadonlyMap<string, Buffer> | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Mount `GET <urlPrefix>/:rt/:file` serving the captured shim sources
|
|
102
|
+
* with the immutable posture of the hashed-bundle route. Only the
|
|
103
|
+
* CURRENT `runtimeHash` is served; any other `rt` 404s `no-store` so a
|
|
104
|
+
* stale URL can never lock wrong bytes into a cache.
|
|
105
|
+
*/
|
|
106
|
+
export declare function mountShimRoutes(opts: {
|
|
107
|
+
readonly app: Express;
|
|
108
|
+
readonly urlPrefix: string;
|
|
109
|
+
readonly runtimeHash: string;
|
|
110
|
+
readonly shims: ReadonlyMap<string, Buffer>;
|
|
111
|
+
}): void;
|
|
112
|
+
/**
|
|
113
|
+
* Options for the `/code/<hash>.m<rt>.js` variant route
|
|
114
|
+
* (mounted by `mountCodeRoutes` BEFORE the plain routes).
|
|
115
|
+
*/
|
|
116
|
+
export interface CodeModuleVariantOptions {
|
|
117
|
+
/** 12-hex content hash of the runtime bundle currently served. */
|
|
118
|
+
readonly runtimeHash: string;
|
|
119
|
+
/**
|
|
120
|
+
* Absolute base of THIS build's shim directory
|
|
121
|
+
* (`<codeBaseUrl><urlPrefix>/<runtimeHash>`), no trailing slash —
|
|
122
|
+
* the rewrite embeds it into every import.
|
|
123
|
+
*/
|
|
124
|
+
readonly shimBaseUrl: string;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* RegExp route for the variant path. A RegExp (not a string pattern)
|
|
128
|
+
* on purpose: two params separated by literal dots inside one path
|
|
129
|
+
* segment sit exactly on path-to-regexp's least-stable parsing ground,
|
|
130
|
+
* and a silent non-match here would read as "variant never works".
|
|
131
|
+
*/
|
|
132
|
+
export declare const CODE_MODULE_VARIANT_ROUTE: RegExp;
|
|
133
|
+
/**
|
|
134
|
+
* Mount the module-variant route. Serving posture mirrors the plain
|
|
135
|
+
* code route (immutable on success, `no-store` on every failure); the
|
|
136
|
+
* two decline arms are:
|
|
137
|
+
*
|
|
138
|
+
* - `rt` ≠ the current runtime hash → 404. The shims for another
|
|
139
|
+
* build are unknowable here; the renderer falls back to raw bytes.
|
|
140
|
+
* - leftover bare specifier after rewrite → 404. The module imports
|
|
141
|
+
* a package with no static shim (3rd-party gadget); serving it
|
|
142
|
+
* would fail at eval inside the frame instead of falling back.
|
|
143
|
+
*/
|
|
144
|
+
export declare function mountCodeModuleVariantRoute(opts: {
|
|
145
|
+
readonly app: Express;
|
|
146
|
+
readonly codeStore: import("@ggui-ai/mcp-server-core").CodeStore;
|
|
147
|
+
readonly variant: CodeModuleVariantOptions;
|
|
148
|
+
readonly logger: Logger;
|
|
149
|
+
}): void;
|
|
150
|
+
//# sourceMappingURL=code-module-variant.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-module-variant.d.ts","sourceRoot":"","sources":["../src/code-module-variant.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAU1D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE;IACrC,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,KAAK,MAAM,GAAG,SAAS,CAAC;AAEzB,0EAA0E;AAC1E,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,GAAG,MAAM,CAET;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,GAAG,iBAAiB,CASpB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAQ9G;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GACf,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAkBzC;AAQD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7C,GAAG,IAAI,CAyCP;AAED;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC,kEAAkE;IAClE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,QAAkD,CAAC;AAEzF;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE;IAChD,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,OAAO,0BAA0B,EAAE,SAAS,CAAC;IACjE,QAAQ,CAAC,OAAO,EAAE,wBAAwB,CAAC;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,GAAG,IAAI,CA8DP"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict-CSP module-variant delivery (ggui#522 slice 2).
|
|
3
|
+
*
|
|
4
|
+
* The `/code/<hash>.js` route serves STORED component bytes — compiled
|
|
5
|
+
* ESM whose imports are still BARE specifiers (`react`,
|
|
6
|
+
* `@ggui-ai/design/primitives`, …). A browser cannot import that
|
|
7
|
+
* standalone; the renderer historically bridged the gap in-frame by
|
|
8
|
+
* rewriting bare specifiers to `data:` shims and instantiating the
|
|
9
|
+
* module from a `blob:` URL — both of which a strict host CSP refuses.
|
|
10
|
+
*
|
|
11
|
+
* This module adds the server-side twin of that bridge: a SECOND,
|
|
12
|
+
* immutable route `/code/<hash>.m<rt>.js` serving the SAME stored bytes
|
|
13
|
+
* with the rewrite already applied — every bare specifier resolved to a
|
|
14
|
+
* static shim asset under `/_ggui/shims/<rt>/<name>.js` on the same
|
|
15
|
+
* origin. A frame whose `script-src` allows only the asset origin can
|
|
16
|
+
* `import()` the variant URL directly: no `blob:`, no `data:`, no
|
|
17
|
+
* `'unsafe-eval'`.
|
|
18
|
+
*
|
|
19
|
+
* `<rt>` — the 12-hex content hash of the runtime bundle — is the
|
|
20
|
+
* variant KEY, and it lives in the PATH deliberately: the asset CDN
|
|
21
|
+
* (CloudFront `CACHING_OPTIMIZED`) ignores query strings, so a
|
|
22
|
+
* query-keyed variant would collapse to one cached body across
|
|
23
|
+
* runtime versions. The hash versions the whole family: the shim
|
|
24
|
+
* files ship in the runtime bundle's dist, so any shim-affecting
|
|
25
|
+
* change also changes the bundle bytes, which changes `<rt>`, which
|
|
26
|
+
* changes every variant URL — immutable caching stays sound.
|
|
27
|
+
*
|
|
28
|
+
* The route serves only the CURRENT `<rt>`: after a redeploy, a
|
|
29
|
+
* CDN-evicted old-variant URL 404s (`no-store`) and the renderer falls
|
|
30
|
+
* back to its raw-bytes ladder — honest degradation, never stale shims.
|
|
31
|
+
*
|
|
32
|
+
* 3rd-party gadget packages (GG.8.2) have no static shim (their
|
|
33
|
+
* exports are derived per-render from the generated code); a module
|
|
34
|
+
* importing one is NOT asset-deliverable. Both ends respect that: the
|
|
35
|
+
* minter declines to stamp `codeModuleUrl`, and the route declines
|
|
36
|
+
* (404) any body whose rewrite leaves a bare specifier standing.
|
|
37
|
+
*/
|
|
38
|
+
import fs from "node:fs";
|
|
39
|
+
import path from "node:path";
|
|
40
|
+
import { rewriteImports, findBareImportSpecifiers, stripMarkers, ASSET_SHIM_FOR_SPECIFIER, } from "@ggui-ai/design/rendering";
|
|
41
|
+
import { hoistImports } from "@ggui-ai/design/module-loader";
|
|
42
|
+
/** The variant URL for (hash, rt) under `base` — ONE composition site. */
|
|
43
|
+
export function composeCodeModuleUrl(args) {
|
|
44
|
+
return `${args.base}/code/${args.hash}.m${args.runtimeHash}.js`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build the minter every `codeUrl` emitter injects. Coverability is
|
|
48
|
+
* decided from the code's own import list: every bare specifier must
|
|
49
|
+
* have a static shim ({@link ASSET_SHIM_FOR_SPECIFIER}) or the mint
|
|
50
|
+
* declines. Markers are stripped first — their embedded JSON can
|
|
51
|
+
* contain strings the import scanner would misread.
|
|
52
|
+
*/
|
|
53
|
+
export function createCodeModuleUrlMinter(args) {
|
|
54
|
+
const { runtimeHash } = args;
|
|
55
|
+
return ({ code, hash, base }) => {
|
|
56
|
+
const bare = findBareImportSpecifiers(stripMarkers(code));
|
|
57
|
+
if (!bare.every((spec) => spec in ASSET_SHIM_FOR_SPECIFIER)) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return composeCodeModuleUrl({ base, hash, runtimeHash });
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Rewrite stored component bytes into the servable asset-module
|
|
65
|
+
* variant — the same preprocessing the renderer's blob ladder applies
|
|
66
|
+
* (strip markers, hoist imports) plus the `asset-url` specifier
|
|
67
|
+
* rewrite. Declines when any bare specifier survives (no static shim
|
|
68
|
+
* exists for it) instead of serving a module that fails at eval.
|
|
69
|
+
*/
|
|
70
|
+
export function rewriteToAssetModule(args) {
|
|
71
|
+
const rewritten = rewriteImports(hoistImports(stripMarkers(args.code)), {
|
|
72
|
+
mode: "asset-url",
|
|
73
|
+
shimBaseUrl: args.shimBaseUrl,
|
|
74
|
+
});
|
|
75
|
+
const leftover = findBareImportSpecifiers(rewritten);
|
|
76
|
+
if (leftover.length > 0)
|
|
77
|
+
return { ok: false, leftover };
|
|
78
|
+
return { ok: true, code: rewritten };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Read the static shim files emitted by the iframe-runtime build
|
|
82
|
+
* (`dist/shims/<name>.js`) into memory, keyed by file name. Captured
|
|
83
|
+
* ONCE at composition — the immutable shim route must keep serving the
|
|
84
|
+
* exact bytes its URL family was stamped for, even across an in-place
|
|
85
|
+
* rebuild of the dist (same guarantee the hashed bundle route makes by
|
|
86
|
+
* serving captured bytes).
|
|
87
|
+
*
|
|
88
|
+
* `undefined` when the directory is missing/empty (bundle built by an
|
|
89
|
+
* older toolchain) — callers then skip the whole variant family.
|
|
90
|
+
*/
|
|
91
|
+
export function captureShimSources(shimsDir) {
|
|
92
|
+
let entries;
|
|
93
|
+
try {
|
|
94
|
+
entries = fs.readdirSync(shimsDir);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const out = new Map();
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (!entry.endsWith(".js"))
|
|
102
|
+
continue;
|
|
103
|
+
try {
|
|
104
|
+
out.set(entry, fs.readFileSync(path.join(shimsDir, entry)));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// A vanishing file mid-capture degrades to "that shim 404s";
|
|
108
|
+
// the renderer's fallback ladder covers it.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out.size > 0 ? out : undefined;
|
|
112
|
+
}
|
|
113
|
+
/** File-segment shape of a shim asset (`react.js`, `jsx-runtime.js`). */
|
|
114
|
+
const SHIM_FILE_REGEX = /^[a-z][a-z-]*\.js$/;
|
|
115
|
+
/** 12-hex runtime-hash path segment. */
|
|
116
|
+
const RUNTIME_HASH_REGEX = /^[a-f0-9]{12}$/;
|
|
117
|
+
/**
|
|
118
|
+
* Mount `GET <urlPrefix>/:rt/:file` serving the captured shim sources
|
|
119
|
+
* with the immutable posture of the hashed-bundle route. Only the
|
|
120
|
+
* CURRENT `runtimeHash` is served; any other `rt` 404s `no-store` so a
|
|
121
|
+
* stale URL can never lock wrong bytes into a cache.
|
|
122
|
+
*/
|
|
123
|
+
export function mountShimRoutes(opts) {
|
|
124
|
+
const { app, urlPrefix, runtimeHash, shims } = opts;
|
|
125
|
+
app.get(`${urlPrefix}/:rt/:file`, (req, res) => {
|
|
126
|
+
// Same CORS rationale as the runtime bundle: module fetches from a
|
|
127
|
+
// sandboxed `srcdoc` iframe (`null` origin) always run CORS mode.
|
|
128
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
129
|
+
const rt = req.params["rt"];
|
|
130
|
+
const file = req.params["file"];
|
|
131
|
+
if (typeof rt !== "string" ||
|
|
132
|
+
!RUNTIME_HASH_REGEX.test(rt) ||
|
|
133
|
+
typeof file !== "string" ||
|
|
134
|
+
!SHIM_FILE_REGEX.test(file)) {
|
|
135
|
+
res.setHeader("Cache-Control", "no-store");
|
|
136
|
+
res.status(400).json({
|
|
137
|
+
error: {
|
|
138
|
+
code: "invalid_request",
|
|
139
|
+
message: "shim path must be /<12-hex runtime hash>/<name>.js",
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const source = rt === runtimeHash ? shims.get(file) : undefined;
|
|
145
|
+
if (source === undefined) {
|
|
146
|
+
res.setHeader("Cache-Control", "no-store");
|
|
147
|
+
res.status(404).json({
|
|
148
|
+
error: {
|
|
149
|
+
code: "not_found",
|
|
150
|
+
message: rt === runtimeHash
|
|
151
|
+
? "unknown shim module"
|
|
152
|
+
: "shim family not served by this runtime build",
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
158
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
159
|
+
res.status(200).send(source);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* RegExp route for the variant path. A RegExp (not a string pattern)
|
|
164
|
+
* on purpose: two params separated by literal dots inside one path
|
|
165
|
+
* segment sit exactly on path-to-regexp's least-stable parsing ground,
|
|
166
|
+
* and a silent non-match here would read as "variant never works".
|
|
167
|
+
*/
|
|
168
|
+
export const CODE_MODULE_VARIANT_ROUTE = /^\/code\/([a-f0-9]{64})\.m([a-f0-9]{12})\.js$/;
|
|
169
|
+
/**
|
|
170
|
+
* Mount the module-variant route. Serving posture mirrors the plain
|
|
171
|
+
* code route (immutable on success, `no-store` on every failure); the
|
|
172
|
+
* two decline arms are:
|
|
173
|
+
*
|
|
174
|
+
* - `rt` ≠ the current runtime hash → 404. The shims for another
|
|
175
|
+
* build are unknowable here; the renderer falls back to raw bytes.
|
|
176
|
+
* - leftover bare specifier after rewrite → 404. The module imports
|
|
177
|
+
* a package with no static shim (3rd-party gadget); serving it
|
|
178
|
+
* would fail at eval inside the frame instead of falling back.
|
|
179
|
+
*/
|
|
180
|
+
export function mountCodeModuleVariantRoute(opts) {
|
|
181
|
+
const { app, codeStore, variant, logger } = opts;
|
|
182
|
+
app.get(CODE_MODULE_VARIANT_ROUTE, async (req, res) => {
|
|
183
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
184
|
+
// RegExp routes surface capture groups as positional params.
|
|
185
|
+
const hash = req.params["0"];
|
|
186
|
+
const rt = req.params["1"];
|
|
187
|
+
if (typeof hash !== "string" || typeof rt !== "string") {
|
|
188
|
+
res.setHeader("Cache-Control", "no-store");
|
|
189
|
+
res.status(400).json({
|
|
190
|
+
error: { code: "invalid_request", message: "malformed variant path" },
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (rt !== variant.runtimeHash) {
|
|
195
|
+
res.setHeader("Cache-Control", "no-store");
|
|
196
|
+
res.status(404).json({
|
|
197
|
+
error: {
|
|
198
|
+
code: "not_found",
|
|
199
|
+
message: "variant family not served by this runtime build",
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const code = await codeStore.get(hash);
|
|
206
|
+
if (code === null) {
|
|
207
|
+
res.setHeader("Cache-Control", "no-store");
|
|
208
|
+
res.status(404).json({
|
|
209
|
+
error: { code: "not_found", message: "unknown code hash" },
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const rewritten = rewriteToAssetModule({
|
|
214
|
+
code,
|
|
215
|
+
shimBaseUrl: variant.shimBaseUrl,
|
|
216
|
+
});
|
|
217
|
+
if (!rewritten.ok) {
|
|
218
|
+
logger.warn("code_module_variant_declined", {
|
|
219
|
+
hash,
|
|
220
|
+
leftover: [...rewritten.leftover],
|
|
221
|
+
});
|
|
222
|
+
res.setHeader("Cache-Control", "no-store");
|
|
223
|
+
res.status(404).json({
|
|
224
|
+
error: {
|
|
225
|
+
code: "not_found",
|
|
226
|
+
message: "module not asset-deliverable (bare imports without static shims)",
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
232
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
233
|
+
res.status(200).send(rewritten.code);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
logger.warn("code_module_variant_failed", { hash, error: String(err) });
|
|
237
|
+
res.setHeader("Cache-Control", "no-store");
|
|
238
|
+
res.status(500).json({
|
|
239
|
+
error: { code: "internal", message: "variant fetch failed" },
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
package/dist/code-routes.d.ts
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import type { CodeStore } from "@ggui-ai/mcp-server-core";
|
|
31
31
|
import type { Express } from "express";
|
|
32
32
|
import type { Logger } from "./logger.js";
|
|
33
|
+
import { type CodeModuleVariantOptions } from "./code-module-variant.js";
|
|
33
34
|
interface MountOptions {
|
|
34
35
|
/** Express app to mount onto. */
|
|
35
36
|
readonly app: Express;
|
|
@@ -37,10 +38,19 @@ interface MountOptions {
|
|
|
37
38
|
readonly codeStore: CodeStore;
|
|
38
39
|
/** Structured logger for fetch-failure warnings. */
|
|
39
40
|
readonly logger: Logger;
|
|
41
|
+
/**
|
|
42
|
+
* Strict-CSP module-variant family (`/code/<hash>.m<rt>.js`,
|
|
43
|
+
* ggui#522 slice 2). Present ⇒ the variant route mounts BEFORE the
|
|
44
|
+
* plain routes — ordering is load-bearing: `/code/:hash.js`'s param
|
|
45
|
+
* would otherwise swallow the variant path (`:hash` = `<hash>.m<rt>`)
|
|
46
|
+
* and 400 it at the hash gate.
|
|
47
|
+
*/
|
|
48
|
+
readonly moduleVariant?: CodeModuleVariantOptions;
|
|
40
49
|
}
|
|
41
50
|
/**
|
|
42
|
-
* Mount `GET /code/:hash.js` + `GET /contract/:hash.js`
|
|
43
|
-
*
|
|
51
|
+
* Mount `GET /code/:hash.js` + `GET /contract/:hash.js` (+ the
|
|
52
|
+
* module-variant route when configured) onto the express app. Returns
|
|
53
|
+
* nothing — the routes self-register.
|
|
44
54
|
*/
|
|
45
55
|
export declare function mountCodeRoutes(opts: MountOptions): void;
|
|
46
56
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"code-routes.d.ts","sourceRoot":"","sources":["../src/code-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAC1D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"code-routes.d.ts","sourceRoot":"","sources":["../src/code-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAC1D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAEL,KAAK,wBAAwB,EAC9B,MAAM,0BAA0B,CAAC;AAElC,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,wDAAwD;IACxD,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,oDAAoD;IACpD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,wBAAwB,CAAC;CACnD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAqDxD"}
|