@ggui-ai/mcp-server 0.1.0-rc.3 → 0.2.0-alpha.3
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 -1
- package/dist/build-mcp.d.ts +8 -8
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +53 -12
- package/dist/console-auth.d.ts +7 -7
- package/dist/console-auth.d.ts.map +1 -1
- package/dist/console-auth.js +4 -4
- package/dist/console-cache.d.ts +2 -2
- package/dist/console-cache.d.ts.map +1 -1
- package/dist/console-cache.js +10 -10
- package/dist/console-headers.d.ts +2 -2
- package/dist/console-headers.js +2 -2
- package/dist/console-payloads.d.ts +3 -3
- package/dist/console-payloads.d.ts.map +1 -1
- package/dist/console-payloads.js +10 -10
- package/dist/console-timeline.d.ts +12 -30
- package/dist/console-timeline.d.ts.map +1 -1
- package/dist/console-timeline.js +44 -45
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/instructions-presets.d.ts +1 -1
- package/dist/instructions-presets.d.ts.map +1 -1
- package/dist/instructions-presets.js +40 -22
- package/dist/llm-backed-negotiator.d.ts +6 -6
- package/dist/llm-backed-negotiator.d.ts.map +1 -1
- package/dist/llm-backed-negotiator.js +92 -91
- package/dist/mcp-apps-inbound.d.ts +3 -3
- package/dist/mcp-apps-inbound.d.ts.map +1 -1
- package/dist/mcp-apps-inbound.js +28 -23
- package/dist/mcp-apps-outbound.d.ts +136 -121
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +384 -414
- package/dist/mcp-mounts.d.ts +21 -20
- package/dist/mcp-mounts.d.ts.map +1 -1
- package/dist/mcp-mounts.js +25 -29
- package/dist/{session-channel.d.ts → render-channel.d.ts} +145 -102
- package/dist/render-channel.d.ts.map +1 -0
- package/dist/{session-channel.js → render-channel.js} +481 -462
- package/dist/schema-compat.d.ts +11 -11
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +6 -6
- package/dist/server.d.ts +224 -219
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1498 -1963
- package/dist/storage.d.ts +7 -7
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +12 -12
- package/package.json +12 -11
- package/dist/render-gate.d.ts +0 -87
- package/dist/render-gate.d.ts.map +0 -1
- package/dist/render-gate.js +0 -77
- package/dist/render-rate-limit.d.ts +0 -59
- package/dist/render-rate-limit.d.ts.map +0 -1
- package/dist/render-rate-limit.js +0 -73
- package/dist/render-signing.d.ts +0 -98
- package/dist/render-signing.d.ts.map +0 -1
- package/dist/render-signing.js +0 -113
- package/dist/session-channel.d.ts.map +0 -1
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP Apps outbound wiring — the server-side half of the
|
|
3
|
-
* `
|
|
3
|
+
* `ggui_render -> ui://ggui/render -> iframe -> live channel` delivery path.
|
|
4
4
|
*
|
|
5
5
|
* Responsibilities of this module (and nothing else):
|
|
6
6
|
*
|
|
7
7
|
* 1. Advertise the `io.modelcontextprotocol/ui` capability on every
|
|
8
8
|
* fresh `McpServer` instance so MCP Apps hosts know the server
|
|
9
9
|
* speaks the UI extension.
|
|
10
|
-
* 2. Register `ui://ggui/
|
|
10
|
+
* 2. Register `ui://ggui/render` as a static resource servable via
|
|
11
11
|
* MCP `resources/read`. The resource body is the thin-shell HTML
|
|
12
12
|
* hosts sandbox-render when they see `_meta.ui.resourceUri` on a
|
|
13
|
-
* `
|
|
13
|
+
* `ggui_render` result.
|
|
14
14
|
*
|
|
15
15
|
* Boundary discipline:
|
|
16
16
|
*
|
|
@@ -31,28 +31,29 @@
|
|
|
31
31
|
* build target — per the design lock, the shell is served by the
|
|
32
32
|
* same `@ggui-ai/mcp-server` instance that mints the bootstrap.
|
|
33
33
|
*/
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
34
|
+
import { deriveBundleOrigins, deriveContractBundle, derivePublicEnvProjection, deriveRenderMeta, findBlueprintExact, } from "@ggui-ai/mcp-server-handlers/renders";
|
|
35
|
+
import { deriveContextDefault } from "@ggui-ai/protocol";
|
|
36
|
+
import { GGUI_RENDER_RESOURCE_MIME, GGUI_RENDER_RESOURCE_URI, MCP_APPS_UI_CAPABILITY, MCP_APP_AI_GGUI_RENDER_META_KEY, deriveContextName, } from "@ggui-ai/protocol/integrations/mcp-apps";
|
|
37
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
38
|
+
import { registerAppResource } from "@modelcontextprotocol/ext-apps/server";
|
|
39
|
+
import { createHash } from "node:crypto";
|
|
39
40
|
/**
|
|
40
|
-
* Thin-shell body served from `ui://ggui/
|
|
41
|
+
* Thin-shell body served from `ui://ggui/render` (C8 pivot).
|
|
41
42
|
*
|
|
42
43
|
* **Architectural role.** The shell is a ~30 LOC bootstrap wrapper -
|
|
43
|
-
* it runs a minimal `ui/initialize` preflight to read
|
|
44
|
-
* `
|
|
45
|
-
* `@ggui-ai/iframe-runtime` bundle from that URL. Every rendering
|
|
46
|
-
* concern - WS open, subscribe,
|
|
44
|
+
* it runs a minimal `ui/initialize` preflight to read the
|
|
45
|
+
* `ai.ggui/render.runtimeUrl` slice field, then dynamic-script-loads
|
|
46
|
+
* the `@ggui-ai/iframe-runtime` bundle from that URL. Every rendering
|
|
47
|
+
* concern - WS open, subscribe, render mount, component code eval,
|
|
47
48
|
* adapter install - belongs to the renderer bundle (shipped C7a-d),
|
|
48
49
|
* not here.
|
|
49
50
|
*
|
|
50
51
|
* **Why bootstrap-driven URL.** `srcdoc` iframes have `about:srcdoc`
|
|
51
52
|
* as their URL, so relative paths can't resolve to the MCP server's
|
|
52
|
-
* HTTP listener. The server-controlled `runtimeUrl` lands on
|
|
53
|
-
* `
|
|
54
|
-
* the first `ui/initialize` response - origin-agnostic, works
|
|
55
|
-
* OSS same-origin AND hosted-cloud CDN deployments.
|
|
53
|
+
* HTTP listener. The server-controlled `runtimeUrl` lands on the
|
|
54
|
+
* `ai.ggui/render.runtimeUrl` slice field and the shell picks it up
|
|
55
|
+
* from the first `ui/initialize` response - origin-agnostic, works
|
|
56
|
+
* under OSS same-origin AND hosted-cloud CDN deployments.
|
|
56
57
|
*
|
|
57
58
|
* **Why `<script type="module">`.** `@ggui-ai/iframe-runtime` is bundled as
|
|
58
59
|
* ESM (its own runtime.ts:5 declares the contract: "the thin-shell
|
|
@@ -70,7 +71,8 @@ import { MCP_APPS_UI_CAPABILITY, GGUI_SESSION_RESOURCE_URI, GGUI_SESSION_RESOURC
|
|
|
70
71
|
* `postMessage({type:'ggui:bootstrap-failed', reason, message}, '*')`:
|
|
71
72
|
*
|
|
72
73
|
* - `BOOTSTRAP_META_MISSING` - `ui/initialize` returned without a
|
|
73
|
-
* valid `
|
|
74
|
+
* valid `ai.ggui/render` slice OR without a `runtimeUrl` field on
|
|
75
|
+
* it.
|
|
74
76
|
* - `BUNDLE_FETCH_FAILED` - `<script src>` errored (network failure,
|
|
75
77
|
* 404, CSP reject with an observable `error` event).
|
|
76
78
|
*
|
|
@@ -117,19 +119,30 @@ import { MCP_APPS_UI_CAPABILITY, GGUI_SESSION_RESOURCE_URI, GGUI_SESSION_RESOURC
|
|
|
117
119
|
* `mcp-apps-outbound.test.ts` drift test recomputes the hash and fails
|
|
118
120
|
* loudly if they diverge.
|
|
119
121
|
*/
|
|
120
|
-
const
|
|
122
|
+
const GGUI_RENDER_SHELL_SCRIPT_BODY = `
|
|
121
123
|
(function(){'use strict';
|
|
122
124
|
// MCP Apps shell. Speaks the canonical postMessage protocol from
|
|
123
125
|
// @modelcontextprotocol/ext-apps:
|
|
124
126
|
// 1. iframe -> host: ui/initialize
|
|
125
127
|
// 2. iframe -> host: ui/notifications/initialized
|
|
126
128
|
// 3. host -> iframe: ui/notifications/tool-result (per CallToolResult)
|
|
127
|
-
// On tool-result,
|
|
128
|
-
//
|
|
129
|
-
//
|
|
129
|
+
// On tool-result, read the slice envelope from _meta (spec-canonical
|
|
130
|
+
// CallToolResult _meta at the top level, or the first-party
|
|
131
|
+
// params.toolOutput._meta shape), set window.__GGUI_META__ to the
|
|
132
|
+
// envelope, fetch runtime as blob, inject as script.
|
|
130
133
|
// Runtime auto-mounts inline. No nested iframe (claudemcpcontent.com
|
|
131
134
|
// CSP frame-src forbids cross-origin frames). State machine matches
|
|
132
135
|
// buildSelfContainedShell so the same runtime mounts both paths.
|
|
136
|
+
//
|
|
137
|
+
// R5 (2026-05-26): the historic /r/<shortCode> HTTP fallback was
|
|
138
|
+
// removed along with the bearer-by-obscurity model -- hosts that strip
|
|
139
|
+
// _meta no longer have a recovery path here. Spec-canonical hosts
|
|
140
|
+
// deliver _meta inline and are unaffected.
|
|
141
|
+
//
|
|
142
|
+
// Phase B (2026-05-27): the historic two-slice envelope
|
|
143
|
+
// (\`ai.ggui/session\` + \`ai.ggui/stack-item\`) collapsed to ONE flat
|
|
144
|
+
// \`ai.ggui/render\` slice. runtimeUrl now lives directly on the
|
|
145
|
+
// render slice; renderId is the canonical identity.
|
|
133
146
|
var rpcId=1,pending={};
|
|
134
147
|
var rootEl=document.getElementById('ggui-root');
|
|
135
148
|
rootEl.style.cssText='display:flex;flex-direction:column;height:100%;min-height:300px;margin:0';
|
|
@@ -148,29 +161,6 @@ function postRpc(method,params){
|
|
|
148
161
|
catch(e){delete pending[id];rej(e);}
|
|
149
162
|
});
|
|
150
163
|
}
|
|
151
|
-
function readToolResult(p){
|
|
152
|
-
if(!p)return null;
|
|
153
|
-
var sc=p.structuredContent;
|
|
154
|
-
if(sc&&typeof sc.url==='string'&&typeof sc.shortCode==='string'){
|
|
155
|
-
return {url:sc.url,shortCode:sc.shortCode};
|
|
156
|
-
}
|
|
157
|
-
if(Array.isArray(p.content)){
|
|
158
|
-
for(var i=0;i<p.content.length;i++){
|
|
159
|
-
var c=p.content[i];
|
|
160
|
-
if(c&&c.type==='text'&&typeof c.text==='string'){
|
|
161
|
-
try{var j=JSON.parse(c.text);
|
|
162
|
-
if(j&&typeof j.url==='string'&&typeof j.shortCode==='string'){
|
|
163
|
-
return {url:j.url,shortCode:j.shortCode};
|
|
164
|
-
}
|
|
165
|
-
}catch(e){}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
function deriveBase(url){
|
|
172
|
-
try{var u=new URL(url);return u.origin;}catch(e){return null;}
|
|
173
|
-
}
|
|
174
164
|
function postBootstrapFailed(reason,message){
|
|
175
165
|
// Surface every shell-layer bootstrap-failure path as a typed
|
|
176
166
|
// RendererBootFailedMessage envelope so hosts pinning the C9
|
|
@@ -179,24 +169,23 @@ function postBootstrapFailed(reason,message){
|
|
|
179
169
|
// times out. Reason codes match BootstrapFailureReason.
|
|
180
170
|
try{window.parent.postMessage({type:'ggui:bootstrap-failed',reason:reason,message:message},'*');}catch(e){}
|
|
181
171
|
}
|
|
182
|
-
async function
|
|
172
|
+
async function mountFromMeta(envelope){
|
|
183
173
|
if(mounted)return;
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// runtime
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if(!bootstrap||typeof bootstrap.runtimeUrl!=='string'){
|
|
174
|
+
// Slice envelope shape (Phase B): { "ai.ggui/render": { renderId,
|
|
175
|
+
// appId, runtimeUrl, ... } }. runtimeUrl on the render slice is
|
|
176
|
+
// the only load-bearing field at the shell layer — it tells us
|
|
177
|
+
// which iframe-runtime bundle to fetch. Everything else is
|
|
178
|
+
// optional; the runtime decides at boot time based on the meta
|
|
179
|
+
// it reads from window.__GGUI_META__.
|
|
180
|
+
var renderSlice=envelope&&envelope['ai.ggui/render'];
|
|
181
|
+
var runtimeUrl=renderSlice&&renderSlice.runtimeUrl;
|
|
182
|
+
if(!envelope||typeof runtimeUrl!=='string'){
|
|
194
183
|
setOverlay('Bootstrap payload malformed.');
|
|
195
184
|
postBootstrapFailed('BOOTSTRAP_MALFORMED','Bootstrap payload malformed.');
|
|
196
185
|
return;
|
|
197
186
|
}
|
|
198
187
|
setOverlay('Loading UI…');
|
|
199
|
-
window.
|
|
188
|
+
window.__GGUI_META__=envelope;
|
|
200
189
|
// Load the runtime bundle via a direct cross-origin script tag
|
|
201
190
|
// (governed by CSP script-src) instead of fetch + Blob (governed by
|
|
202
191
|
// CSP connect-src). claude.ai's claudemcpcontent.com iframe CSP
|
|
@@ -216,7 +205,7 @@ async function mountFromBootstrap(bootstrap){
|
|
|
216
205
|
// console -- the bundle ships ACAO=* so credentialed mode is
|
|
217
206
|
// unnecessary.
|
|
218
207
|
s.crossOrigin='anonymous';
|
|
219
|
-
s.src=
|
|
208
|
+
s.src=runtimeUrl;
|
|
220
209
|
s.onload=function(){mounted=true;};
|
|
221
210
|
s.onerror=function(e){
|
|
222
211
|
var msg='Runtime bundle failed to load: '+(e&&e.message||'script error');
|
|
@@ -231,56 +220,34 @@ async function mountFromBootstrap(bootstrap){
|
|
|
231
220
|
postBootstrapFailed('BUNDLE_FETCH_FAILED',msg);
|
|
232
221
|
}
|
|
233
222
|
}
|
|
234
|
-
|
|
235
|
-
if(mounted)return;
|
|
236
|
-
var base=deriveBase(toolResult.url);
|
|
237
|
-
if(!base){setOverlay('Invalid URL in tool result.');return;}
|
|
238
|
-
setOverlay('Loading UI…');
|
|
239
|
-
var bootstrap;
|
|
240
|
-
try{
|
|
241
|
-
var bRes=await fetch(base+'/api/bootstrap/'+encodeURIComponent(toolResult.shortCode),{
|
|
242
|
-
cache:'no-store',
|
|
243
|
-
headers:{accept:'application/json'},
|
|
244
|
-
});
|
|
245
|
-
if(!bRes.ok){setOverlay('Bootstrap fetch failed: HTTP '+bRes.status);return;}
|
|
246
|
-
bootstrap=await bRes.json();
|
|
247
|
-
}catch(e){setOverlay('Bootstrap fetch error: '+(e&&e.message||e));return;}
|
|
248
|
-
return mountFromBootstrap(bootstrap);
|
|
249
|
-
}
|
|
250
|
-
function readBootstrapFromInitResult(result){
|
|
223
|
+
function readMetaFromInitResult(result){
|
|
251
224
|
if(!result||typeof result!=='object')return null;
|
|
252
225
|
var toolOutput=result.toolOutput;
|
|
253
226
|
if(!toolOutput||typeof toolOutput!=='object')return null;
|
|
254
227
|
var meta=toolOutput._meta;
|
|
255
228
|
if(!meta||typeof meta!=='object')return null;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// wsUrl+token). Requiring componentCode at the shell layer turned
|
|
265
|
-
// every Path B inline-bootstrap delivery into a silent reject.
|
|
266
|
-
if(typeof b.runtimeUrl!=='string')return null;
|
|
267
|
-
return b;
|
|
229
|
+
// Single slice-envelope key (Phase B: ai.ggui/render). Only the
|
|
230
|
+
// render slice's runtimeUrl is load-bearing at the shell layer;
|
|
231
|
+
// the runtime reads everything else off window.__GGUI_META__
|
|
232
|
+
// after we set it.
|
|
233
|
+
var renderSlice=meta['ai.ggui/render'];
|
|
234
|
+
if(!renderSlice||typeof renderSlice!=='object')return null;
|
|
235
|
+
if(typeof renderSlice.runtimeUrl!=='string')return null;
|
|
236
|
+
return meta;
|
|
268
237
|
}
|
|
269
|
-
function
|
|
238
|
+
function hasAiGguiMetaPlaceholder(result){
|
|
270
239
|
// Detect the protocol-violation case where the host signaled
|
|
271
|
-
// "I tried to deliver
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
// sitting in the Path-A waiting state forever.
|
|
240
|
+
// "I tried to deliver meta" (toolOutput._meta carries an
|
|
241
|
+
// ai.ggui/render key) but it's malformed. Fail-fast with
|
|
242
|
+
// BOOTSTRAP_META_MISSING rather than waiting forever.
|
|
275
243
|
if(!result||typeof result!=='object')return false;
|
|
276
244
|
var toolOutput=result.toolOutput;
|
|
277
245
|
if(!toolOutput||typeof toolOutput!=='object')return false;
|
|
278
246
|
var meta=toolOutput._meta;
|
|
279
247
|
if(!meta||typeof meta!=='object')return false;
|
|
280
|
-
|
|
281
|
-
return !!ggui&&typeof ggui==='object';
|
|
248
|
+
return 'ai.ggui/render' in meta;
|
|
282
249
|
}
|
|
283
|
-
function
|
|
250
|
+
function readMetaFromCallToolResult(params){
|
|
284
251
|
// MCP Apps spec (specification/2026-01-26/apps.mdx:1145-1155):
|
|
285
252
|
// ui/notifications/tool-result
|
|
286
253
|
// params: CallToolResult // Standard MCP type
|
|
@@ -288,20 +255,14 @@ function readBootstrapFromCallToolResult(params){
|
|
|
288
255
|
// level (NOT under params.toolOutput, which is where the
|
|
289
256
|
// first-party McpAppIframe convention wraps it). Spec-compliant
|
|
290
257
|
// hosts (Claude Desktop, claude.ai Connector, Claude Code) deliver
|
|
291
|
-
//
|
|
258
|
+
// slice-envelope material here.
|
|
292
259
|
if(!params||typeof params!=='object')return null;
|
|
293
260
|
var meta=params._meta;
|
|
294
261
|
if(!meta||typeof meta!=='object')return null;
|
|
295
|
-
var
|
|
296
|
-
if(!
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
// Same loosening as readBootstrapFromInitResult / mountFromBootstrap:
|
|
300
|
-
// only runtimeUrl is required at the shell layer. componentCode is
|
|
301
|
-
// optional (absent for GguiBootstrapMeta-shaped bootstraps that
|
|
302
|
-
// open WS via wsUrl+token).
|
|
303
|
-
if(typeof b.runtimeUrl!=='string')return null;
|
|
304
|
-
return b;
|
|
262
|
+
var renderSlice=meta['ai.ggui/render'];
|
|
263
|
+
if(!renderSlice||typeof renderSlice!=='object')return null;
|
|
264
|
+
if(typeof renderSlice.runtimeUrl!=='string')return null;
|
|
265
|
+
return meta;
|
|
305
266
|
}
|
|
306
267
|
window.addEventListener('message',function(ev){
|
|
307
268
|
var m=ev&&ev.data;
|
|
@@ -315,20 +276,19 @@ window.addEventListener('message',function(ev){
|
|
|
315
276
|
// Spec-compliant hosts: m.params IS the CallToolResult; _meta is
|
|
316
277
|
// at the top level. Try this FIRST so Claude Desktop / claude.ai
|
|
317
278
|
// Connector / Claude Code land here.
|
|
318
|
-
var specB=
|
|
319
|
-
if(specB){
|
|
320
|
-
// First-party McpAppIframe convention:
|
|
279
|
+
var specB=readMetaFromCallToolResult(m.params);
|
|
280
|
+
if(specB){mountFromMeta(specB);return;}
|
|
281
|
+
// First-party McpAppIframe convention: slice envelope nested under
|
|
321
282
|
// params.toolOutput._meta. First-party hosts (Studio, Portal,
|
|
322
283
|
// console) use this shape for both init-response and post-init
|
|
323
284
|
// notification.
|
|
324
|
-
var bb=
|
|
325
|
-
if(bb){
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
if(tr)mount(tr);
|
|
285
|
+
var bb=readMetaFromInitResult(m.params);
|
|
286
|
+
if(bb){mountFromMeta(bb);return;}
|
|
287
|
+
// R5 (2026-05-26) -- the /r/<shortCode> HTTP fallback was removed
|
|
288
|
+
// along with the bearer-by-obscurity model. Hosts that strip
|
|
289
|
+
// _meta on the tool-result wire have no fallback path here;
|
|
290
|
+
// spec-canonical hosts deliver meta inline and land in the two
|
|
291
|
+
// branches above.
|
|
332
292
|
}
|
|
333
293
|
});
|
|
334
294
|
setOverlay('Initializing…');
|
|
@@ -342,26 +302,24 @@ postRpc('ui/initialize',{
|
|
|
342
302
|
}).then(function(result){
|
|
343
303
|
clearTimeout(initTimer);
|
|
344
304
|
postNotification('ui/notifications/initialized',{});
|
|
345
|
-
// Path B:
|
|
346
|
-
// <McpAppIframe>'s first-party dispatch deliver
|
|
305
|
+
// Path B: slice envelope inline in ui/initialize result. Hosts
|
|
306
|
+
// using <McpAppIframe>'s first-party dispatch deliver meta here
|
|
347
307
|
// and never send a separate ui/notifications/tool-result.
|
|
348
|
-
var b=
|
|
349
|
-
if(b){
|
|
308
|
+
var b=readMetaFromInitResult(result);
|
|
309
|
+
if(b){mountFromMeta(b);return;}
|
|
350
310
|
// Protocol-violation surface: host signalled an attempt
|
|
351
|
-
// (toolOutput._meta.ggui
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if(hasGguiMetaPlaceholder(result)){
|
|
357
|
-
var msg='ui/initialize result missing _meta.ggui.bootstrap or runtimeUrl';
|
|
311
|
+
// (toolOutput._meta carries ai.ggui/render) but it's malformed.
|
|
312
|
+
// Fail fast so hosts pinning a typed error envelope don't sit on
|
|
313
|
+
// the Path-A waiting overlay forever.
|
|
314
|
+
if(hasAiGguiMetaPlaceholder(result)){
|
|
315
|
+
var msg='ui/initialize result missing valid ai.ggui/render.runtimeUrl';
|
|
358
316
|
setOverlay(msg);
|
|
359
317
|
postBootstrapFailed('BOOTSTRAP_META_MISSING',msg);
|
|
360
318
|
return;
|
|
361
319
|
}
|
|
362
320
|
// Path A: wait for the host to send ui/notifications/tool-result
|
|
363
321
|
// with structuredContent.{url, shortCode}. MCP Apps hosts that don't
|
|
364
|
-
// implement the reading-B inline-
|
|
322
|
+
// implement the reading-B inline-meta convention land here.
|
|
365
323
|
setOverlay('Waiting for tool result…');
|
|
366
324
|
}).catch(function(e){
|
|
367
325
|
clearTimeout(initTimer);
|
|
@@ -369,13 +327,13 @@ postRpc('ui/initialize',{
|
|
|
369
327
|
});
|
|
370
328
|
})();
|
|
371
329
|
`;
|
|
372
|
-
export const
|
|
373
|
-
<html lang="en" style="height:100%"><head><meta charset="utf-8"><title>ggui
|
|
330
|
+
export const GGUI_RENDER_SHELL_HTML = `<!doctype html>
|
|
331
|
+
<html lang="en" style="height:100%"><head><meta charset="utf-8"><title>ggui render</title></head>
|
|
374
332
|
<body style="margin:0;height:100%;min-height:480px"><div id="ggui-root" data-ggui-shell="thin" style="height:100%;min-height:480px"></div>
|
|
375
|
-
<script>${
|
|
333
|
+
<script>${GGUI_RENDER_SHELL_SCRIPT_BODY}</script></body></html>`;
|
|
376
334
|
/**
|
|
377
335
|
* CSP `script-src` source expression that authorises the inline
|
|
378
|
-
* `<script>` block of {@link
|
|
336
|
+
* `<script>` block of {@link GGUI_RENDER_SHELL_HTML} when it executes
|
|
379
337
|
* inside an iframe whose CSP is inherited from a parent host.
|
|
380
338
|
*
|
|
381
339
|
* # Why this exists
|
|
@@ -412,11 +370,11 @@ export const GGUI_SESSION_SHELL_HTML = `<!doctype html>
|
|
|
412
370
|
* path where the host is `<McpAppIframe>` and the parent SPA owns
|
|
413
371
|
* the CSP it inherits.
|
|
414
372
|
*/
|
|
415
|
-
export const
|
|
416
|
-
.update(
|
|
417
|
-
.digest(
|
|
373
|
+
export const GGUI_RENDER_SHELL_SCRIPT_HASH = `'sha256-${createHash("sha256")
|
|
374
|
+
.update(GGUI_RENDER_SHELL_SCRIPT_BODY)
|
|
375
|
+
.digest("base64")}'`;
|
|
418
376
|
/**
|
|
419
|
-
* Register `ui://ggui/
|
|
377
|
+
* Register `ui://ggui/render` as a readable resource on an `McpServer`.
|
|
420
378
|
*
|
|
421
379
|
* The resource is STATIC - `resources/read` always returns the same
|
|
422
380
|
* body. Per-session state lives on the live channel, not in the resource.
|
|
@@ -448,13 +406,27 @@ export const GGUI_SESSION_SHELL_SCRIPT_HASH = `'sha256-${createHash('sha256')
|
|
|
448
406
|
* back to the host's default CSP (fine for same-origin hosts;
|
|
449
407
|
* restrictive for cross-origin claude.ai-style hosts).
|
|
450
408
|
*/
|
|
451
|
-
function buildCspMeta(publicBaseUrl
|
|
452
|
-
|
|
409
|
+
function buildCspMeta(publicBaseUrl,
|
|
410
|
+
/**
|
|
411
|
+
* Local-dev fallback: when `publicBaseUrl` is absent (first-party
|
|
412
|
+
* same-origin deployments, e.g. `ggui serve` on `127.0.0.1`), derive
|
|
413
|
+
* the CSP block from `runtimeUrl`. The runtime + WS + state endpoints
|
|
414
|
+
* all live on the runtime's origin in same-origin deployments, so
|
|
415
|
+
* declaring it covers every fetch the canvas iframe makes.
|
|
416
|
+
*
|
|
417
|
+
* Without this fallback, local dev with cross-origin sandbox proxies
|
|
418
|
+
* (sample-agent's `:7790/sandbox.html` writing the canvas HTML that
|
|
419
|
+
* references `:6786/_ggui/iframe-runtime.js`) trips a `script-src`
|
|
420
|
+
* violation that blanks the iframe — verified live 2026-05-27.
|
|
421
|
+
*/
|
|
422
|
+
runtimeUrl) {
|
|
423
|
+
const source = publicBaseUrl ?? runtimeUrl;
|
|
424
|
+
if (!source)
|
|
453
425
|
return undefined;
|
|
454
426
|
try {
|
|
455
|
-
const parsed = new URL(
|
|
427
|
+
const parsed = new URL(source);
|
|
456
428
|
const origin = parsed.origin;
|
|
457
|
-
const wsScheme = parsed.protocol ===
|
|
429
|
+
const wsScheme = parsed.protocol === "https:" ? "wss:" : "ws:";
|
|
458
430
|
const wsOrigin = `${wsScheme}//${parsed.host}`;
|
|
459
431
|
return {
|
|
460
432
|
ui: {
|
|
@@ -469,7 +441,7 @@ function buildCspMeta(publicBaseUrl) {
|
|
|
469
441
|
return undefined;
|
|
470
442
|
}
|
|
471
443
|
}
|
|
472
|
-
export function
|
|
444
|
+
export function registerGguiRenderResource(server, shellHtml = GGUI_RENDER_SHELL_HTML, publicBaseUrl) {
|
|
473
445
|
let cspMeta;
|
|
474
446
|
if (publicBaseUrl) {
|
|
475
447
|
try {
|
|
@@ -484,7 +456,7 @@ export function registerGguiSessionResource(server, shellHtml = GGUI_SESSION_SHE
|
|
|
484
456
|
// diagnosis case). Declare BOTH schemes so the same physical
|
|
485
457
|
// origin is reachable via HTTPS (`/api/bootstrap`, `/_ggui/
|
|
486
458
|
// iframe-runtime.js`) AND wss (live-channel subscribe).
|
|
487
|
-
const wsScheme = parsed.protocol ===
|
|
459
|
+
const wsScheme = parsed.protocol === "https:" ? "wss:" : "ws:";
|
|
488
460
|
const wsOrigin = `${wsScheme}//${parsed.host}`;
|
|
489
461
|
cspMeta = {
|
|
490
462
|
ui: {
|
|
@@ -503,17 +475,23 @@ export function registerGguiSessionResource(server, shellHtml = GGUI_SESSION_SHE
|
|
|
503
475
|
cspMeta = undefined;
|
|
504
476
|
}
|
|
505
477
|
}
|
|
506
|
-
|
|
478
|
+
// `registerAppResource` (from `@modelcontextprotocol/ext-apps/server`)
|
|
479
|
+
// defaults `mimeType` to `RESOURCE_MIME_TYPE` — the same
|
|
480
|
+
// `text/html;profile=mcp-app` value `GGUI_RENDER_RESOURCE_MIME`
|
|
481
|
+
// carries. Letting the canonical helper own the default means the
|
|
482
|
+
// mimeType string lives in ONE place across the ecosystem (the SDK)
|
|
483
|
+
// rather than duplicated in our protocol package.
|
|
484
|
+
registerAppResource(server, "ggui-render", GGUI_RENDER_RESOURCE_URI, {
|
|
507
485
|
// `title` / `description` show up in MCP clients that surface
|
|
508
486
|
// resource metadata. Short + concrete.
|
|
509
|
-
title:
|
|
510
|
-
description:
|
|
511
|
-
mimeType:
|
|
487
|
+
title: "ggui render",
|
|
488
|
+
description: "Thin-shell iframe bundle that bootstraps a ggui render. MCP Apps hosts fetch this when they see `_meta.ui.resourceUri` on a ggui_render result.",
|
|
489
|
+
mimeType: GGUI_RENDER_RESOURCE_MIME,
|
|
512
490
|
}, async (uri) => ({
|
|
513
491
|
contents: [
|
|
514
492
|
{
|
|
515
493
|
uri: uri.href,
|
|
516
|
-
mimeType:
|
|
494
|
+
mimeType: GGUI_RENDER_RESOURCE_MIME,
|
|
517
495
|
text: shellHtml,
|
|
518
496
|
...(cspMeta !== undefined ? { _meta: cspMeta } : {}),
|
|
519
497
|
},
|
|
@@ -542,7 +520,7 @@ export function advertiseMcpAppsUiCapability(server) {
|
|
|
542
520
|
* Build the self-contained shell HTML for a given session.
|
|
543
521
|
*
|
|
544
522
|
* The returned HTML is a complete, standalone document: it inlines the
|
|
545
|
-
* compiled component (base64) + session ids in a `window.
|
|
523
|
+
* compiled component (base64) + session ids in a `window.__GGUI_META__`
|
|
546
524
|
* global, then loads the iframe-runtime bundle via `<script type="module"
|
|
547
525
|
* src={runtimeUrl}>`. The runtime takes over synchronously on import,
|
|
548
526
|
* mounts the component, and the iframe paints WITHOUT any further server
|
|
@@ -569,79 +547,41 @@ export function buildSelfContainedShell(opts) {
|
|
|
569
547
|
// (e.g. codeUrl + live-mode credentials for an iframe that mounts
|
|
570
548
|
// statically but subscribes for updates); the iframe-runtime parser
|
|
571
549
|
// picks per its priority order.
|
|
572
|
-
const isSystem = typeof opts.systemKind ===
|
|
573
|
-
const hasCodeUrl = typeof opts.codeUrl ===
|
|
574
|
-
const hasLive = typeof opts.wsUrl ===
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
throw new Error('buildSelfContainedShell: canvasMode is mutually exclusive with `systemKind`, `codeUrl`, and `stackItemId` — canvas iframes are session-scoped and render via the live channel, not a pre-pinned stack item');
|
|
579
|
-
}
|
|
580
|
-
if (isCanvas && !hasLive) {
|
|
581
|
-
throw new Error('buildSelfContainedShell: canvasMode requires the live-mode trio (`wsUrl` + `token` + `expiresAt`) — the canvas iframe receives stack items via the live channel, not static inlining');
|
|
582
|
-
}
|
|
550
|
+
const isSystem = typeof opts.systemKind === "string" && opts.systemKind.length > 0;
|
|
551
|
+
const hasCodeUrl = typeof opts.codeUrl === "string" && opts.codeUrl.length > 0;
|
|
552
|
+
const hasLive = typeof opts.wsUrl === "string" &&
|
|
553
|
+
opts.wsUrl.length > 0 &&
|
|
554
|
+
typeof opts.token === "string" &&
|
|
555
|
+
opts.token.length > 0;
|
|
583
556
|
if (!isSystem && !hasCodeUrl && !hasLive) {
|
|
584
|
-
throw new Error(
|
|
557
|
+
throw new Error("buildSelfContainedShell: at least one of `codeUrl`, `systemKind`, or live-mode (`wsUrl` + `token`) must be set");
|
|
585
558
|
}
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
// inline
|
|
589
|
-
//
|
|
590
|
-
// `
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const bootstrap = {
|
|
596
|
-
sessionId: opts.sessionId,
|
|
559
|
+
// Build the single render slice (Phase B: ai.ggui/render collapsed
|
|
560
|
+
// the prior ai.ggui/session + ai.ggui/stack-item pair into one flat
|
|
561
|
+
// shape). The inline global carries the SAME shape as the wire
|
|
562
|
+
// `_meta` envelope so the iframe-runtime's `parseMetaFromGlobal`
|
|
563
|
+
// defers to the same `parseMcpAppAiGguiRenderMeta` parser the
|
|
564
|
+
// postMessage paths use. `runtimeUrl` is required across all modes
|
|
565
|
+
// (the shell-bundled script tag fetches the runtime from there).
|
|
566
|
+
const render = {
|
|
567
|
+
renderId: opts.renderId,
|
|
597
568
|
appId: opts.appId,
|
|
598
569
|
runtimeUrl: opts.runtimeUrl,
|
|
599
|
-
// Static-content discriminators — system-card and codeUrl are
|
|
600
|
-
// mutually exclusive (the iframe-runtime rejects the both-set mix
|
|
601
|
-
// as MALFORMED). Live-mode credentials (wsUrl/token below) may
|
|
602
|
-
// coexist with either or stand alone.
|
|
603
|
-
...(isSystem ? { kind: opts.systemKind } : {}),
|
|
604
|
-
...(!isSystem && hasCodeUrl
|
|
605
|
-
? {
|
|
606
|
-
codeUrl: opts.codeUrl,
|
|
607
|
-
...(opts.codeHash !== undefined ? { codeHash: opts.codeHash } : {}),
|
|
608
|
-
}
|
|
609
|
-
: {}),
|
|
610
|
-
...(opts.stackItemId !== undefined ? { stackItemId: opts.stackItemId } : {}),
|
|
611
|
-
...(isCanvas ? { canvasMode: true } : {}),
|
|
612
570
|
...(opts.themeId !== undefined ? { themeId: opts.themeId } : {}),
|
|
613
571
|
...(opts.themeMode !== undefined ? { themeMode: opts.themeMode } : {}),
|
|
614
|
-
...(opts.propsJson !== undefined ? { propsJson: opts.propsJson } : {}),
|
|
615
572
|
...(opts.appCallableTools !== undefined && opts.appCallableTools.length > 0
|
|
616
573
|
? { appCallableTools: opts.appCallableTools }
|
|
617
574
|
: {}),
|
|
618
|
-
...(opts.actionNextSteps !== undefined &&
|
|
619
|
-
Object.keys(opts.actionNextSteps).length > 0
|
|
620
|
-
? { actionNextSteps: opts.actionNextSteps }
|
|
621
|
-
: {}),
|
|
622
|
-
...(opts.contextSlots !== undefined && opts.contextSlots.length > 0
|
|
623
|
-
? { contextSlots: opts.contextSlots }
|
|
624
|
-
: {}),
|
|
625
575
|
...(opts.permissionsPolicy !== undefined && opts.permissionsPolicy.length > 0
|
|
626
576
|
? { permissionsPolicy: opts.permissionsPolicy }
|
|
627
577
|
: {}),
|
|
628
578
|
// Wrapper catalog the iframe-runtime dynamic-imports at boot.
|
|
629
|
-
// Symmetric with `
|
|
579
|
+
// Symmetric with the `ai.ggui/render` wire-slice `gadgets` field.
|
|
630
580
|
// Without this forward, the self-contained shell path
|
|
631
581
|
// (/r/<shortCode>, resources/read) would render as STDLIB-only —
|
|
632
582
|
// wrapper-using contracts (Leaflet, Mapbox) destructure unknown
|
|
633
583
|
// hooks at runtime.
|
|
634
|
-
...(opts.gadgets !== undefined && opts.gadgets.length > 0
|
|
635
|
-
? { gadgets: opts.gadgets }
|
|
636
|
-
: {}),
|
|
637
|
-
// Precompiled, eval-free contract validators. Symmetric forward
|
|
638
|
-
// for the self-contained shell — the renderer iframe's strict CSP
|
|
639
|
-
// blocks runtime `ajv.compile()`, so it loads these modules via
|
|
640
|
-
// `blob:` import. Omitted when the contract declares no
|
|
641
|
-
// runtime-validated schema.
|
|
642
|
-
...(opts.compiledValidators !== undefined
|
|
643
|
-
? { compiledValidators: opts.compiledValidators }
|
|
644
|
-
: {}),
|
|
584
|
+
...(opts.gadgets !== undefined && opts.gadgets.length > 0 ? { gadgets: opts.gadgets } : {}),
|
|
645
585
|
// Server-filtered public env values that declared wrappers'
|
|
646
586
|
// `requires` cover. Symmetric forward; without it, wrappers
|
|
647
587
|
// calling `getPublicEnv()` throw at hook-mount on the
|
|
@@ -649,14 +589,54 @@ export function buildSelfContainedShell(opts) {
|
|
|
649
589
|
...(opts.publicEnv !== undefined && Object.keys(opts.publicEnv).length > 0
|
|
650
590
|
? { publicEnv: opts.publicEnv }
|
|
651
591
|
: {}),
|
|
652
|
-
// Live-mode trio.
|
|
653
|
-
// (`wsUrl XOR
|
|
592
|
+
// Live-mode trio. The iframe-runtime rejects half-live envelopes
|
|
593
|
+
// (`wsUrl XOR wsToken` MALFORMED), so we forward all three together
|
|
654
594
|
// or none at all — the caller is responsible for pairing them at
|
|
655
595
|
// mint time. `expiresAt` is degrade-able (past-due → static-only)
|
|
656
|
-
// but is part of the live trio at emit time.
|
|
596
|
+
// but is part of the live trio at emit time. The `opts.token`
|
|
597
|
+
// input is renamed to `wsToken` on the slice for wire-field parity.
|
|
657
598
|
...(opts.wsUrl !== undefined ? { wsUrl: opts.wsUrl } : {}),
|
|
658
|
-
...(opts.token !== undefined ? {
|
|
599
|
+
...(opts.token !== undefined ? { wsToken: opts.token } : {}),
|
|
659
600
|
...(opts.expiresAt !== undefined ? { expiresAt: opts.expiresAt } : {}),
|
|
601
|
+
// Polling fallback URL — lights up `@ggui-ai/live-channel`'s
|
|
602
|
+
// events-polling transport when WS is unavailable. Absent ⇒
|
|
603
|
+
// WS-only mode (legacy behavior). See SelfContainedShellInputs
|
|
604
|
+
// .pollingUrl for the URL shape.
|
|
605
|
+
...(opts.pollingUrl !== undefined ? { pollingUrl: opts.pollingUrl } : {}),
|
|
606
|
+
...(opts.lastSequence !== undefined ? { lastSequence: opts.lastSequence } : {}),
|
|
607
|
+
// Visible-bits surface — what the iframe is mounting right now.
|
|
608
|
+
// Static-content discriminators (codeUrl / kind) are mutually
|
|
609
|
+
// exclusive; the iframe-runtime rejects the both-set mix.
|
|
610
|
+
...(isSystem ? { kind: opts.systemKind } : {}),
|
|
611
|
+
...(!isSystem && hasCodeUrl
|
|
612
|
+
? {
|
|
613
|
+
codeUrl: opts.codeUrl,
|
|
614
|
+
...(opts.codeHash !== undefined ? { codeHash: opts.codeHash } : {}),
|
|
615
|
+
}
|
|
616
|
+
: {}),
|
|
617
|
+
...(opts.propsJson !== undefined ? { propsJson: opts.propsJson } : {}),
|
|
618
|
+
...(opts.actionNextSteps !== undefined && Object.keys(opts.actionNextSteps).length > 0
|
|
619
|
+
? { actionNextSteps: opts.actionNextSteps }
|
|
620
|
+
: {}),
|
|
621
|
+
...(opts.contextSlots !== undefined && opts.contextSlots.length > 0
|
|
622
|
+
? { contextSlots: opts.contextSlots }
|
|
623
|
+
: {}),
|
|
624
|
+
// Content-addressable contract-validator bundle. Iframe-runtime
|
|
625
|
+
// fetches `validatorsUrl` + dynamic-imports to resolve
|
|
626
|
+
// validators. Omitted when the contract declares no
|
|
627
|
+
// runtime-validated schema OR when the server has no CodeStore
|
|
628
|
+
// wired for the bundle write.
|
|
629
|
+
...(opts.contractHash !== undefined && opts.validatorsUrl !== undefined
|
|
630
|
+
? {
|
|
631
|
+
contractHash: opts.contractHash,
|
|
632
|
+
validatorsUrl: opts.validatorsUrl,
|
|
633
|
+
}
|
|
634
|
+
: {}),
|
|
635
|
+
};
|
|
636
|
+
// Same wire shape as `_meta` — the iframe-runtime's global parser
|
|
637
|
+
// reuses `parseMcpAppAiGguiRenderMeta` for the single render slice.
|
|
638
|
+
const bootstrap = {
|
|
639
|
+
[MCP_APP_AI_GGUI_RENDER_META_KEY]: render,
|
|
660
640
|
};
|
|
661
641
|
// JSON.stringify produces valid JS, but `<` / `>` / `&` / `U+2028`
|
|
662
642
|
// / `U+2029` can break HTML or JS parsers when embedded inline.
|
|
@@ -664,131 +644,131 @@ export function buildSelfContainedShell(opts) {
|
|
|
664
644
|
// that JSON allows but JS parsers historically choked on; modern
|
|
665
645
|
// engines accept them in strings but the escape is cheap insurance.
|
|
666
646
|
const json = JSON.stringify(bootstrap)
|
|
667
|
-
.replace(/</g,
|
|
668
|
-
.replace(/>/g,
|
|
669
|
-
.replace(/&/g,
|
|
670
|
-
.replace(/\u2028/g,
|
|
671
|
-
.replace(/\u2029/g,
|
|
647
|
+
.replace(/</g, "\\u003c")
|
|
648
|
+
.replace(/>/g, "\\u003e")
|
|
649
|
+
.replace(/&/g, "\\u0026")
|
|
650
|
+
.replace(/\u2028/g, "\\u2028")
|
|
651
|
+
.replace(/\u2029/g, "\\u2029");
|
|
672
652
|
// HTML-escape the runtimeUrl for the `src` attribute. Server
|
|
673
653
|
// operators control this string but a defensive escape avoids any
|
|
674
654
|
// surprise if a future code path lets user-derived data flow here.
|
|
675
655
|
const safeRuntimeUrl = opts.runtimeUrl
|
|
676
|
-
.replace(/&/g,
|
|
677
|
-
.replace(/"/g,
|
|
678
|
-
.replace(/</g,
|
|
679
|
-
.replace(/>/g,
|
|
656
|
+
.replace(/&/g, "&")
|
|
657
|
+
.replace(/"/g, """)
|
|
658
|
+
.replace(/</g, "<")
|
|
659
|
+
.replace(/>/g, ">");
|
|
680
660
|
return `<!doctype html>
|
|
681
|
-
<html lang="en"><head><meta charset="utf-8"><title>ggui
|
|
661
|
+
<html lang="en"><head><meta charset="utf-8"><title>ggui render</title></head>
|
|
682
662
|
<body>
|
|
683
663
|
<div id="ggui-root" data-ggui-shell="self-contained"></div>
|
|
684
|
-
<script>window.
|
|
664
|
+
<script>window.__GGUI_META__ = ${json};</script>
|
|
685
665
|
<script type="module" crossorigin="anonymous" src="${safeRuntimeUrl}"></script>
|
|
686
666
|
</body></html>`;
|
|
687
667
|
}
|
|
688
668
|
/**
|
|
689
|
-
* Minimal "loading" HTML served when a per-
|
|
690
|
-
* for a
|
|
669
|
+
* Minimal "loading" HTML served when a per-render resource is fetched
|
|
670
|
+
* for a render whose visible-bits surface has no componentCode yet
|
|
691
671
|
* (placeholder, generation in flight). Renders a tiny status surface
|
|
692
672
|
* so hosts that pin lifecycle selectors don't see a blank document.
|
|
693
673
|
*
|
|
694
|
-
* Hosts SHOULD re-fetch when they observe additional `
|
|
695
|
-
* results on the same
|
|
696
|
-
* value stays stable across
|
|
674
|
+
* Hosts SHOULD re-fetch when they observe additional `ggui_render`
|
|
675
|
+
* results on the same render — the per-call `_meta.ui.resourceUri`
|
|
676
|
+
* value stays stable across commits for a render, so re-fetching the
|
|
697
677
|
* same URI returns fresher HTML on the second try.
|
|
698
678
|
*
|
|
699
679
|
* @public
|
|
700
680
|
*/
|
|
701
|
-
export function buildSelfContainedLoadingShell(
|
|
681
|
+
export function buildSelfContainedLoadingShell(renderId) {
|
|
702
682
|
return `<!doctype html>
|
|
703
|
-
<html lang="en"><head><meta charset="utf-8"><title>ggui
|
|
683
|
+
<html lang="en"><head><meta charset="utf-8"><title>ggui render</title></head>
|
|
704
684
|
<body>
|
|
705
|
-
<div id="ggui-root" data-ggui-shell="loading" data-ggui-
|
|
706
|
-
.replace(/&/g,
|
|
707
|
-
.replace(/"/g,
|
|
708
|
-
.replace(/</g,
|
|
709
|
-
.replace(/>/g,
|
|
685
|
+
<div id="ggui-root" data-ggui-shell="loading" data-ggui-render-id="${renderId
|
|
686
|
+
.replace(/&/g, "&")
|
|
687
|
+
.replace(/"/g, """)
|
|
688
|
+
.replace(/</g, "<")
|
|
689
|
+
.replace(/>/g, ">")}">Generating UI…</div>
|
|
710
690
|
</body></html>`;
|
|
711
691
|
}
|
|
712
|
-
function
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
id: entry.id,
|
|
727
|
-
kind: entry.kind,
|
|
728
|
-
...(props !== undefined ? { props } : {}),
|
|
729
|
-
source: entry,
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
continue;
|
|
733
|
-
}
|
|
734
|
-
const code = entry.componentCode;
|
|
735
|
-
if (typeof code === 'string' && code.length > 0) {
|
|
692
|
+
function pickComponentFromRender(render) {
|
|
693
|
+
if (!render)
|
|
694
|
+
return null;
|
|
695
|
+
if (render.type === "mcpApps")
|
|
696
|
+
return null;
|
|
697
|
+
const propsRaw = "props" in render ? render.props : undefined;
|
|
698
|
+
const props = propsRaw !== undefined &&
|
|
699
|
+
propsRaw !== null &&
|
|
700
|
+
typeof propsRaw === "object" &&
|
|
701
|
+
!Array.isArray(propsRaw)
|
|
702
|
+
? propsRaw
|
|
703
|
+
: undefined;
|
|
704
|
+
if (render.type === "system") {
|
|
705
|
+
if (typeof render.kind === "string" && render.kind.length > 0) {
|
|
736
706
|
return {
|
|
737
|
-
id:
|
|
738
|
-
|
|
707
|
+
id: render.id,
|
|
708
|
+
kind: render.kind,
|
|
739
709
|
...(props !== undefined ? { props } : {}),
|
|
740
|
-
source:
|
|
710
|
+
source: render,
|
|
741
711
|
};
|
|
742
712
|
}
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
const code = render.componentCode;
|
|
716
|
+
if (typeof code === "string" && code.length > 0) {
|
|
717
|
+
return {
|
|
718
|
+
id: render.id,
|
|
719
|
+
componentCode: code,
|
|
720
|
+
...(props !== undefined ? { props } : {}),
|
|
721
|
+
source: render,
|
|
722
|
+
};
|
|
743
723
|
}
|
|
744
724
|
return null;
|
|
745
725
|
}
|
|
746
726
|
/**
|
|
747
|
-
* Register a `ui://ggui/
|
|
748
|
-
* `resources/read` request is resolved by looking up the
|
|
749
|
-
* store
|
|
750
|
-
*
|
|
727
|
+
* Register a `ui://ggui/render/{renderId}` resource template. Each
|
|
728
|
+
* `resources/read` request is resolved by looking up the render in the
|
|
729
|
+
* store and returning the self-contained shell with that
|
|
730
|
+
* componentCode inlined.
|
|
751
731
|
*
|
|
752
|
-
* Per-call `_meta.ui.resourceUri` (stamped by `
|
|
753
|
-
* pins the URI to a specific
|
|
754
|
-
* than the static `ui://ggui/
|
|
732
|
+
* Per-call `_meta.ui.resourceUri` (stamped by `ggui_render.resultMeta`)
|
|
733
|
+
* pins the URI to a specific renderId; hosts fetch THAT URI rather
|
|
734
|
+
* than the static `ui://ggui/render` one. Both registrations co-exist:
|
|
755
735
|
* legacy postMessage shell at the static URI, self-contained shell at
|
|
756
736
|
* the templated URI.
|
|
757
737
|
*
|
|
758
738
|
* Failure modes:
|
|
759
|
-
* -
|
|
760
|
-
*
|
|
761
|
-
* -
|
|
762
|
-
* -
|
|
739
|
+
* - Render not found → loading shell (host re-fetches; absent
|
|
740
|
+
* render is a transient state immediately after `ggui_render`).
|
|
741
|
+
* - Render found, no componentCode yet → loading shell.
|
|
742
|
+
* - Render found, componentCode present → self-contained shell.
|
|
763
743
|
*
|
|
764
744
|
* Returns nothing; mutates the server in place.
|
|
765
745
|
*
|
|
766
746
|
* @public
|
|
767
747
|
*/
|
|
768
|
-
export function
|
|
748
|
+
export function registerGguiRenderResourceTemplate(server, opts) {
|
|
769
749
|
// TWO templates registered against the same handler core:
|
|
770
750
|
//
|
|
771
|
-
// 1. Single-segment legacy URI — `ui://ggui/
|
|
751
|
+
// 1. Single-segment legacy URI — `ui://ggui/render/{renderId}`.
|
|
772
752
|
// Pre-resume-contract chats in claude.ai's history persisted
|
|
773
753
|
// this shape; we keep the registration so historical messages
|
|
774
|
-
// still rehydrate (loading shell on
|
|
754
|
+
// still rehydrate (loading shell on render miss).
|
|
775
755
|
//
|
|
776
|
-
// 2. Two-segment resume URI — `ui://ggui/
|
|
777
|
-
// {blueprintKey}`. Stamped by every
|
|
756
|
+
// 2. Two-segment resume URI — `ui://ggui/render/{renderId}/
|
|
757
|
+
// {blueprintKey}`. Stamped by every render since the resume
|
|
778
758
|
// contract landed. Carries enough state for the handler to do:
|
|
779
|
-
// (a) parallel
|
|
759
|
+
// (a) parallel render + blueprint registry lookup (no data
|
|
780
760
|
// dependency between them), (b) registry-only fallback when
|
|
781
|
-
// the
|
|
761
|
+
// the render is gone but the blueprint is still cached
|
|
782
762
|
// (renders the original card with default props/context
|
|
783
763
|
// instead of the dead loading shell).
|
|
784
|
-
const legacyTemplate = new ResourceTemplate(`${
|
|
785
|
-
// No list-callback — the resource set is unbounded per
|
|
786
|
-
// count, and `resources/list` would leak
|
|
764
|
+
const legacyTemplate = new ResourceTemplate(`${GGUI_RENDER_RESOURCE_URI}/{renderId}`, {
|
|
765
|
+
// No list-callback — the resource set is unbounded per render
|
|
766
|
+
// count, and `resources/list` would leak render ids across
|
|
787
767
|
// tenants. Hosts discover specific URIs via per-call `_meta.ui.
|
|
788
768
|
// resourceUri` instead.
|
|
789
769
|
list: undefined,
|
|
790
770
|
});
|
|
791
|
-
const resumeTemplate = new ResourceTemplate(`${
|
|
771
|
+
const resumeTemplate = new ResourceTemplate(`${GGUI_RENDER_RESOURCE_URI}/{renderId}/{blueprintKey}`, { list: undefined });
|
|
792
772
|
// CSP-meta block forwarded on every shell response when the
|
|
793
773
|
// template was wired with `publicBaseUrl`. claude.ai's iframe
|
|
794
774
|
// applies the host's restrictive default (`connect-src 'none'`)
|
|
@@ -796,8 +776,8 @@ export function registerGguiSessionResourceTemplate(server, opts) {
|
|
|
796
776
|
// without that the `<script type="module" src=runtimeUrl>` tag
|
|
797
777
|
// fails with a generic "script error" since cross-origin script
|
|
798
778
|
// loading is blocked. Same shape declared on the static
|
|
799
|
-
// `ui://ggui/
|
|
800
|
-
const templateCspMeta = buildCspMeta(opts.publicBaseUrl);
|
|
779
|
+
// `ui://ggui/render` resource; this is the per-call mirror.
|
|
780
|
+
const templateCspMeta = buildCspMeta(opts.publicBaseUrl, opts.runtimeUrl);
|
|
801
781
|
/**
|
|
802
782
|
* Merge gadget-declared origins from
|
|
803
783
|
* {@link deriveBundleOrigins} into the base `templateCspMeta`. The
|
|
@@ -816,10 +796,7 @@ export function registerGguiSessionResourceTemplate(server, opts) {
|
|
|
816
796
|
return {
|
|
817
797
|
ui: {
|
|
818
798
|
csp: {
|
|
819
|
-
connectDomains: [
|
|
820
|
-
...templateCspMeta.ui.csp.connectDomains,
|
|
821
|
-
...gadgetOrigins.connect,
|
|
822
|
-
],
|
|
799
|
+
connectDomains: [...templateCspMeta.ui.csp.connectDomains, ...gadgetOrigins.connect],
|
|
823
800
|
resourceDomains: [
|
|
824
801
|
...templateCspMeta.ui.csp.resourceDomains,
|
|
825
802
|
...gadgetOrigins.script,
|
|
@@ -833,117 +810,93 @@ export function registerGguiSessionResourceTemplate(server, opts) {
|
|
|
833
810
|
contents: [
|
|
834
811
|
{
|
|
835
812
|
uri: uri.href,
|
|
836
|
-
mimeType:
|
|
813
|
+
mimeType: GGUI_RENDER_RESOURCE_MIME,
|
|
837
814
|
text,
|
|
838
815
|
...(cspMeta !== undefined ? { _meta: cspMeta } : {}),
|
|
839
816
|
},
|
|
840
817
|
],
|
|
841
818
|
});
|
|
842
|
-
const loadingShell = (uri,
|
|
819
|
+
const loadingShell = (uri, renderId) => shellContents(uri, buildSelfContainedLoadingShell(renderId));
|
|
843
820
|
// Single shared handler powers both templates. `blueprintKey` is
|
|
844
821
|
// optional in the variables map — present for the resume URI shape,
|
|
845
822
|
// absent for the legacy single-segment shape.
|
|
846
823
|
async function handle(uri, variables) {
|
|
847
|
-
const
|
|
848
|
-
const
|
|
849
|
-
if (typeof
|
|
850
|
-
return loadingShell(uri,
|
|
824
|
+
const renderIdRaw = variables["renderId"];
|
|
825
|
+
const renderId = Array.isArray(renderIdRaw) ? renderIdRaw[0] : renderIdRaw;
|
|
826
|
+
if (typeof renderId !== "string" || renderId.length === 0) {
|
|
827
|
+
return loadingShell(uri, "unknown");
|
|
851
828
|
}
|
|
852
|
-
const blueprintKeyRaw = variables[
|
|
853
|
-
const blueprintKey = Array.isArray(blueprintKeyRaw)
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
const [session, blueprint] = await Promise.all([
|
|
864
|
-
opts.sessionStore.get(sessionId),
|
|
829
|
+
const blueprintKeyRaw = variables["blueprintKey"];
|
|
830
|
+
const blueprintKey = Array.isArray(blueprintKeyRaw) ? blueprintKeyRaw[0] : blueprintKeyRaw;
|
|
831
|
+
const hasResumeKey = typeof blueprintKey === "string" && blueprintKey.length > 0;
|
|
832
|
+
// Parallel lookup. The render and the blueprint registry are
|
|
833
|
+
// independent — even though the render's componentCode could feed
|
|
834
|
+
// the renderable directly, we ALSO want the blueprint entry as a
|
|
835
|
+
// registry-only fallback when the render is gone but the blueprint
|
|
836
|
+
// is still cached (chat-history rehydrate after render TTL or
|
|
837
|
+
// process restart).
|
|
838
|
+
const [stored, blueprint] = await Promise.all([
|
|
839
|
+
opts.renderStore.get(renderId),
|
|
865
840
|
hasResumeKey && opts.vectorStore && opts.defaultAppIdFallback
|
|
866
|
-
? findBlueprintExact({ vectorStore: opts.vectorStore }, opts.defaultAppIdFallback,
|
|
841
|
+
? findBlueprintExact({ vectorStore: opts.vectorStore }, opts.defaultAppIdFallback, "template", blueprintKey)
|
|
867
842
|
: Promise.resolve(null),
|
|
868
843
|
]);
|
|
869
|
-
//
|
|
870
|
-
//
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
//
|
|
877
|
-
// Falls through to the legacy single-item path when `mintBootstrap`
|
|
878
|
-
// isn't wired (canvas can't function without WS credentials, and
|
|
879
|
-
// serving a pinned stack item is strictly better than a dead
|
|
880
|
-
// loading shell).
|
|
881
|
-
if (session && session.mcpAppsMode === 'canvas' && opts.mintBootstrap) {
|
|
882
|
-
let creds = null;
|
|
883
|
-
try {
|
|
884
|
-
creds = await opts.mintBootstrap(sessionId, session.appId);
|
|
885
|
-
}
|
|
886
|
-
catch {
|
|
887
|
-
creds = null;
|
|
888
|
-
}
|
|
889
|
-
if (creds) {
|
|
890
|
-
const html = buildSelfContainedShell({
|
|
891
|
-
sessionId,
|
|
892
|
-
appId: session.appId,
|
|
893
|
-
canvasMode: true,
|
|
894
|
-
runtimeUrl: opts.runtimeUrl,
|
|
895
|
-
wsUrl: creds.wsUrl,
|
|
896
|
-
token: creds.token,
|
|
897
|
-
expiresAt: creds.expiresAt,
|
|
898
|
-
...(opts.themeId !== undefined ? { themeId: opts.themeId } : {}),
|
|
899
|
-
...(opts.themeMode !== undefined ? { themeMode: opts.themeMode } : {}),
|
|
900
|
-
});
|
|
901
|
-
return shellContents(uri, html);
|
|
902
|
-
}
|
|
903
|
-
// mintBootstrap returned null / threw — fall through to legacy
|
|
904
|
-
// path below. Logs are silent at this layer; deployment-level
|
|
905
|
-
// metrics (cloud) catch the degradation rate.
|
|
906
|
-
}
|
|
907
|
-
// Happy path: session present, top stack item renderable. Mount
|
|
908
|
-
// with the live state (current props, current contextSpec values).
|
|
909
|
-
if (session) {
|
|
910
|
-
const top = pickTopComponentItem(session.stack);
|
|
911
|
-
if (top) {
|
|
912
|
-
// Project the active stack item to the transport-agnostic
|
|
913
|
-
// bootstrap view — same source of truth `push.ts` and
|
|
844
|
+
// Happy path: render present and renderable. Mount with the live
|
|
845
|
+
// state (current props, current contextSpec values).
|
|
846
|
+
if (stored) {
|
|
847
|
+
const picked = pickComponentFromRender(stored.render);
|
|
848
|
+
if (picked) {
|
|
849
|
+
// Project the active render to the transport-agnostic bootstrap
|
|
850
|
+
// view — same source of truth the render-mutation handler and
|
|
914
851
|
// `/r/<shortCode>` consume. Carries permissionsPolicy when
|
|
915
852
|
// clientCapabilities declares permissions. The MCP Apps
|
|
916
853
|
// resource path emits this only into the inline bootstrap
|
|
917
854
|
// (the browser-enforced gate ultimately comes from the host's
|
|
918
855
|
// `allow=""` attribute when the host translates
|
|
919
856
|
// `_meta.ui.permissions` — set by McpAppIframe consumers).
|
|
920
|
-
const view =
|
|
921
|
-
const isSystem =
|
|
922
|
-
// Static-component delivery via codeUrl
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
//
|
|
857
|
+
const view = deriveRenderMeta(picked.source);
|
|
858
|
+
const isSystem = picked.kind !== undefined;
|
|
859
|
+
// Static-component delivery via codeUrl. The compiled-component
|
|
860
|
+
// path mints a content-addressable URL the iframe-runtime
|
|
861
|
+
// fetches at boot; the loading shell takes over when codeStore +
|
|
862
|
+
// codeBaseUrl aren't wired.
|
|
926
863
|
let codeUrl;
|
|
927
864
|
let codeHash;
|
|
865
|
+
let contractHash;
|
|
866
|
+
let validatorsUrl;
|
|
928
867
|
if (!isSystem && opts.codeStore && opts.codeBaseUrl) {
|
|
929
868
|
try {
|
|
930
|
-
const hash = opts.codeStore.hashOf(
|
|
931
|
-
await opts.codeStore.put(hash,
|
|
869
|
+
const hash = opts.codeStore.hashOf(picked.componentCode);
|
|
870
|
+
await opts.codeStore.put(hash, picked.componentCode);
|
|
932
871
|
codeHash = hash;
|
|
933
|
-
const base = opts.codeBaseUrl.replace(/\/$/,
|
|
872
|
+
const base = opts.codeBaseUrl.replace(/\/$/, "");
|
|
934
873
|
codeUrl = `${base}/code/${hash}.js`;
|
|
935
874
|
}
|
|
936
875
|
catch {
|
|
937
876
|
// Silent — falls through to loading shell below.
|
|
938
877
|
}
|
|
878
|
+
// Content-addressable contract-validator bundle (#109).
|
|
879
|
+
try {
|
|
880
|
+
const bundle = await deriveContractBundle(picked.source);
|
|
881
|
+
if (bundle) {
|
|
882
|
+
await opts.codeStore.put(bundle.contractHash, bundle.bundleSource);
|
|
883
|
+
contractHash = bundle.contractHash;
|
|
884
|
+
const base = opts.codeBaseUrl.replace(/\/$/, "");
|
|
885
|
+
validatorsUrl = `${base}/contract/${bundle.contractHash}.js`;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
catch {
|
|
889
|
+
// Silent — bundle write failure degrades to no client-side
|
|
890
|
+
// validators (server-side gate is authoritative).
|
|
891
|
+
}
|
|
939
892
|
}
|
|
940
893
|
if (!isSystem && codeUrl === undefined) {
|
|
941
|
-
// Compiled-component
|
|
942
|
-
// emit the loading shell so the operator can refresh once
|
|
894
|
+
// Compiled-component render but no codeUrl channel available
|
|
895
|
+
// — emit the loading shell so the operator can refresh once
|
|
943
896
|
// codeStore is wired. Direct-render `/r/<shortCode>` falls
|
|
944
897
|
// through to live-mode instead; this MCP-resource path has
|
|
945
898
|
// no WS-mode fallback (resources/read is one-shot).
|
|
946
|
-
return loadingShell(uri,
|
|
899
|
+
return loadingShell(uri, renderId);
|
|
947
900
|
}
|
|
948
901
|
// Project the wrapper catalog AND the union-filtered
|
|
949
902
|
// publicEnv onto the inline bootstrap so the resource-served
|
|
@@ -953,47 +906,66 @@ export function registerGguiSessionResourceTemplate(server, opts) {
|
|
|
953
906
|
let resourcePublicEnv;
|
|
954
907
|
if (opts.appMetadataStore) {
|
|
955
908
|
try {
|
|
956
|
-
const appRecord = await opts.appMetadataStore.get(
|
|
957
|
-
resourcePublicEnv = derivePublicEnvProjection(
|
|
909
|
+
const appRecord = await opts.appMetadataStore.get(stored.appId);
|
|
910
|
+
resourcePublicEnv = derivePublicEnvProjection(picked.source, appRecord?.publicEnv);
|
|
958
911
|
}
|
|
959
912
|
catch {
|
|
960
913
|
// Silent — wrappers calling getPublicEnv throw clearly.
|
|
961
914
|
}
|
|
962
915
|
}
|
|
916
|
+
// Live-channel bootstrap — when the operator wired
|
|
917
|
+
// {@link GguiRenderResourceTemplateOptions.mintWsToken}, mint a
|
|
918
|
+
// wsToken for this render so the iframe-runtime opens a
|
|
919
|
+
// WebSocket on mount and receives `props_update` frames.
|
|
920
|
+
// Without this, the resource shell renders in static-component
|
|
921
|
+
// mode only — `ggui_update` server-side mutations never
|
|
922
|
+
// visibly reach the live iframe (hosts must re-fetch
|
|
923
|
+
// `resources/read` after every update tool result to see new
|
|
924
|
+
// state).
|
|
925
|
+
let wsUrl;
|
|
926
|
+
let wsToken;
|
|
927
|
+
if (opts.mintWsToken) {
|
|
928
|
+
try {
|
|
929
|
+
const minted = opts.mintWsToken(renderId, stored.appId);
|
|
930
|
+
wsUrl = minted.wsUrl;
|
|
931
|
+
wsToken = minted.token;
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
// Silent — falls back to static-component mode.
|
|
935
|
+
}
|
|
936
|
+
}
|
|
963
937
|
const html = buildSelfContainedShell({
|
|
964
|
-
|
|
965
|
-
appId:
|
|
938
|
+
renderId,
|
|
939
|
+
appId: stored.appId,
|
|
966
940
|
...(isSystem
|
|
967
|
-
? { systemKind:
|
|
941
|
+
? { systemKind: picked.kind }
|
|
968
942
|
: {
|
|
969
943
|
codeUrl: codeUrl,
|
|
970
944
|
...(codeHash !== undefined ? { codeHash } : {}),
|
|
971
945
|
}),
|
|
972
946
|
runtimeUrl: opts.runtimeUrl,
|
|
973
|
-
|
|
947
|
+
...(wsUrl !== undefined && wsToken !== undefined
|
|
948
|
+
? { wsUrl, token: wsToken }
|
|
949
|
+
: {}),
|
|
974
950
|
...(opts.themeId !== undefined ? { themeId: opts.themeId } : {}),
|
|
975
951
|
...(opts.themeMode !== undefined ? { themeMode: opts.themeMode } : {}),
|
|
976
952
|
...(view.propsJson !== undefined ? { propsJson: view.propsJson } : {}),
|
|
977
|
-
...(view.actionNextSteps !== undefined
|
|
978
|
-
|
|
979
|
-
: {}),
|
|
980
|
-
...(view.contextSlots !== undefined
|
|
981
|
-
? { contextSlots: view.contextSlots }
|
|
982
|
-
: {}),
|
|
953
|
+
...(view.actionNextSteps !== undefined ? { actionNextSteps: view.actionNextSteps } : {}),
|
|
954
|
+
...(view.contextSlots !== undefined ? { contextSlots: view.contextSlots } : {}),
|
|
983
955
|
...(view.permissionsPolicy !== undefined
|
|
984
956
|
? { permissionsPolicy: view.permissionsPolicy }
|
|
985
957
|
: {}),
|
|
986
|
-
...(view.gadgets !== undefined &&
|
|
987
|
-
view.gadgets.length > 0
|
|
958
|
+
...(view.gadgets !== undefined && view.gadgets.length > 0
|
|
988
959
|
? { gadgets: view.gadgets }
|
|
989
960
|
: {}),
|
|
990
|
-
...(
|
|
991
|
-
? {
|
|
961
|
+
...(contractHash !== undefined && validatorsUrl !== undefined
|
|
962
|
+
? { contractHash, validatorsUrl }
|
|
992
963
|
: {}),
|
|
993
|
-
...(resourcePublicEnv !== undefined &&
|
|
994
|
-
Object.keys(resourcePublicEnv).length > 0
|
|
964
|
+
...(resourcePublicEnv !== undefined && Object.keys(resourcePublicEnv).length > 0
|
|
995
965
|
? { publicEnv: resourcePublicEnv }
|
|
996
966
|
: {}),
|
|
967
|
+
// R6 — ledger cursor stamp for polling-cursor alignment.
|
|
968
|
+
lastSequence: stored.eventSequence,
|
|
997
969
|
});
|
|
998
970
|
// Augment per-call CSP with gadget-declared bundle / style /
|
|
999
971
|
// API origins. Without this, claude.ai's iframe CSP only allows
|
|
@@ -1004,11 +976,11 @@ export function registerGguiSessionResourceTemplate(server, opts) {
|
|
|
1004
976
|
// "Something went wrong." The /r/<shortCode> HTTP path already
|
|
1005
977
|
// derives these via deriveBundleOrigins; this is the per-call
|
|
1006
978
|
// resource mirror.
|
|
1007
|
-
const gadgetOrigins = deriveBundleOrigins(
|
|
979
|
+
const gadgetOrigins = deriveBundleOrigins(picked.source);
|
|
1008
980
|
return shellContents(uri, html, augmentCspMeta(gadgetOrigins));
|
|
1009
981
|
}
|
|
1010
982
|
}
|
|
1011
|
-
// Registry-only fallback:
|
|
983
|
+
// Registry-only fallback: render is gone (TTL / restart) but the
|
|
1012
984
|
// blueprint is still in the registry. Synthesize the shell from
|
|
1013
985
|
// the blueprint's componentCode + propsSpec defaults — strictly
|
|
1014
986
|
// worse than the live mount (no current props, no preserved
|
|
@@ -1016,7 +988,7 @@ export function registerGguiSessionResourceTemplate(server, opts) {
|
|
|
1016
988
|
// shell.
|
|
1017
989
|
if (blueprint && opts.defaultAppIdFallback) {
|
|
1018
990
|
const html = await buildShellFromBlueprint({
|
|
1019
|
-
|
|
991
|
+
renderId,
|
|
1020
992
|
appId: opts.defaultAppIdFallback,
|
|
1021
993
|
blueprint,
|
|
1022
994
|
runtimeUrl: opts.runtimeUrl,
|
|
@@ -1030,27 +1002,27 @@ export function registerGguiSessionResourceTemplate(server, opts) {
|
|
|
1030
1002
|
}
|
|
1031
1003
|
// Fallthrough to loading shell when codeStore isn't wired.
|
|
1032
1004
|
}
|
|
1033
|
-
return loadingShell(uri,
|
|
1005
|
+
return loadingShell(uri, renderId);
|
|
1034
1006
|
}
|
|
1035
|
-
server.registerResource(
|
|
1036
|
-
title:
|
|
1037
|
-
description:
|
|
1038
|
-
mimeType:
|
|
1007
|
+
server.registerResource("ggui-render-self-contained", legacyTemplate, {
|
|
1008
|
+
title: "ggui render (self-contained, legacy URI)",
|
|
1009
|
+
description: "Per-render self-contained shell — single-segment URI shape predating the resume contract. Falls back to loading shell when the render is gone (no blueprintKey to do registry-only render).",
|
|
1010
|
+
mimeType: GGUI_RENDER_RESOURCE_MIME,
|
|
1039
1011
|
}, handle);
|
|
1040
|
-
server.registerResource(
|
|
1041
|
-
title:
|
|
1042
|
-
description:
|
|
1043
|
-
mimeType:
|
|
1012
|
+
server.registerResource("ggui-session-self-contained-resume", resumeTemplate, {
|
|
1013
|
+
title: "ggui render (self-contained, resume URI)",
|
|
1014
|
+
description: "Per-render self-contained shell — two-segment URI shape carrying both renderId AND blueprintKey. Resource handler runs Promise.all over render + registry; falls back to registry-only static render when the render has been evicted but the blueprint is still cached.",
|
|
1015
|
+
mimeType: GGUI_RENDER_RESOURCE_MIME,
|
|
1044
1016
|
}, handle);
|
|
1045
1017
|
}
|
|
1046
1018
|
/**
|
|
1047
|
-
* Synthesize a shell from a registry-only blueprint (no live
|
|
1048
|
-
* Used when chat-history rehydrate finds the
|
|
1019
|
+
* Synthesize a shell from a registry-only blueprint (no live render).
|
|
1020
|
+
* Used when chat-history rehydrate finds the render evicted but the
|
|
1049
1021
|
* blueprint registry still holds the entry. Renders the same
|
|
1050
|
-
* componentCode the original
|
|
1022
|
+
* componentCode the original commit generated, seeded with the
|
|
1051
1023
|
* contract's declared `propsSpec` defaults + `contextSpec` defaults.
|
|
1052
1024
|
* Live state (the user's interactive edits, last-known context
|
|
1053
|
-
* values) is lost in this path; that's the cost of
|
|
1025
|
+
* values) is lost in this path; that's the cost of render eviction.
|
|
1054
1026
|
*
|
|
1055
1027
|
* Internal — exported nowhere because the only safe trigger path is
|
|
1056
1028
|
* inside the resource handler with the resume URI shape (URI carries
|
|
@@ -1062,14 +1034,12 @@ async function buildShellFromBlueprint(args) {
|
|
|
1062
1034
|
return undefined;
|
|
1063
1035
|
}
|
|
1064
1036
|
const contract = blueprint.contract ?? {};
|
|
1065
|
-
const propsSpec =
|
|
1037
|
+
const propsSpec = "props" in contract && contract.props !== undefined
|
|
1066
1038
|
? contract.props
|
|
1067
1039
|
: undefined;
|
|
1068
|
-
const propsJson = propsSpec
|
|
1069
|
-
? JSON.stringify(deriveDefaultPropsValues(propsSpec))
|
|
1070
|
-
: undefined;
|
|
1040
|
+
const propsJson = propsSpec ? JSON.stringify(deriveDefaultPropsValues(propsSpec)) : undefined;
|
|
1071
1041
|
const contextSlots = deriveDefaultContextSlots(contract.contextSpec);
|
|
1072
|
-
const actionNextSteps =
|
|
1042
|
+
const actionNextSteps = "actionSpec" in contract && contract.actionSpec !== undefined
|
|
1073
1043
|
? deriveWiredActionToolsFromSpec(contract.actionSpec)
|
|
1074
1044
|
: undefined;
|
|
1075
1045
|
let codeUrl;
|
|
@@ -1077,14 +1047,14 @@ async function buildShellFromBlueprint(args) {
|
|
|
1077
1047
|
try {
|
|
1078
1048
|
codeHash = args.codeStore.hashOf(blueprint.componentCode);
|
|
1079
1049
|
await args.codeStore.put(codeHash, blueprint.componentCode);
|
|
1080
|
-
const base = args.codeBaseUrl.replace(/\/$/,
|
|
1050
|
+
const base = args.codeBaseUrl.replace(/\/$/, "");
|
|
1081
1051
|
codeUrl = `${base}/code/${codeHash}.js`;
|
|
1082
1052
|
}
|
|
1083
1053
|
catch {
|
|
1084
1054
|
return undefined;
|
|
1085
1055
|
}
|
|
1086
1056
|
return buildSelfContainedShell({
|
|
1087
|
-
|
|
1057
|
+
renderId: args.renderId,
|
|
1088
1058
|
appId: args.appId,
|
|
1089
1059
|
codeUrl,
|
|
1090
1060
|
codeHash,
|
|
@@ -1113,11 +1083,11 @@ function deriveDefaultContextSlots(spec) {
|
|
|
1113
1083
|
return undefined;
|
|
1114
1084
|
const collected = [];
|
|
1115
1085
|
for (const [name, entry] of Object.entries(spec)) {
|
|
1116
|
-
if (!entry || typeof entry !==
|
|
1086
|
+
if (!entry || typeof entry !== "object")
|
|
1117
1087
|
continue;
|
|
1118
1088
|
if (entry.schema === undefined || entry.schema === null)
|
|
1119
1089
|
continue;
|
|
1120
|
-
if (typeof entry.schema !==
|
|
1090
|
+
if (typeof entry.schema !== "object")
|
|
1121
1091
|
continue;
|
|
1122
1092
|
const fallback = deriveContextDefault(entry);
|
|
1123
1093
|
collected.push({
|
|
@@ -1134,8 +1104,8 @@ function deriveWiredActionToolsFromSpec(spec) {
|
|
|
1134
1104
|
const collected = {};
|
|
1135
1105
|
for (const [name, entry] of Object.entries(spec)) {
|
|
1136
1106
|
if (entry &&
|
|
1137
|
-
typeof entry ===
|
|
1138
|
-
typeof entry.nextStep ===
|
|
1107
|
+
typeof entry === "object" &&
|
|
1108
|
+
typeof entry.nextStep === "string" &&
|
|
1139
1109
|
entry.nextStep.length > 0) {
|
|
1140
1110
|
collected[name] = entry.nextStep;
|
|
1141
1111
|
}
|
|
@@ -1144,20 +1114,20 @@ function deriveWiredActionToolsFromSpec(spec) {
|
|
|
1144
1114
|
}
|
|
1145
1115
|
/**
|
|
1146
1116
|
* Apply the full MCP Apps outbound wiring to a fresh `McpServer` - both
|
|
1147
|
-
* the capability advertisement and the `ui://ggui/
|
|
1117
|
+
* the capability advertisement and the `ui://ggui/render` resource. The
|
|
1148
1118
|
* single entry-point `build-mcp.ts` calls so request-path wiring stays
|
|
1149
1119
|
* one line.
|
|
1150
1120
|
*
|
|
1151
|
-
* When `selfContained` is supplied, ALSO registers the per-
|
|
1152
|
-
* `ui://ggui/
|
|
1121
|
+
* When `selfContained` is supplied, ALSO registers the per-render
|
|
1122
|
+
* `ui://ggui/render/{renderId}` resource template that serves the
|
|
1153
1123
|
* self-contained shell (the path third-party MCP Apps hosts use). The
|
|
1154
1124
|
* legacy static URI registration is unconditional — first-party hosts
|
|
1155
1125
|
* (Studio, Portal, console) still rely on the postMessage path.
|
|
1156
1126
|
*/
|
|
1157
1127
|
export function installMcpAppsOutbound(server, opts = {}) {
|
|
1158
1128
|
advertiseMcpAppsUiCapability(server);
|
|
1159
|
-
|
|
1129
|
+
registerGguiRenderResource(server, opts.shellHtml, opts.publicBaseUrl);
|
|
1160
1130
|
if (opts.selfContained) {
|
|
1161
|
-
|
|
1131
|
+
registerGguiRenderResourceTemplate(server, opts.selfContained);
|
|
1162
1132
|
}
|
|
1163
1133
|
}
|