@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.
Files changed (59) hide show
  1. package/README.md +1 -1
  2. package/dist/build-mcp.d.ts +8 -8
  3. package/dist/build-mcp.d.ts.map +1 -1
  4. package/dist/build-mcp.js +53 -12
  5. package/dist/console-auth.d.ts +7 -7
  6. package/dist/console-auth.d.ts.map +1 -1
  7. package/dist/console-auth.js +4 -4
  8. package/dist/console-cache.d.ts +2 -2
  9. package/dist/console-cache.d.ts.map +1 -1
  10. package/dist/console-cache.js +10 -10
  11. package/dist/console-headers.d.ts +2 -2
  12. package/dist/console-headers.js +2 -2
  13. package/dist/console-payloads.d.ts +3 -3
  14. package/dist/console-payloads.d.ts.map +1 -1
  15. package/dist/console-payloads.js +10 -10
  16. package/dist/console-timeline.d.ts +12 -30
  17. package/dist/console-timeline.d.ts.map +1 -1
  18. package/dist/console-timeline.js +44 -45
  19. package/dist/index.d.ts +7 -7
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +5 -5
  22. package/dist/instructions-presets.d.ts +1 -1
  23. package/dist/instructions-presets.d.ts.map +1 -1
  24. package/dist/instructions-presets.js +40 -22
  25. package/dist/llm-backed-negotiator.d.ts +6 -6
  26. package/dist/llm-backed-negotiator.d.ts.map +1 -1
  27. package/dist/llm-backed-negotiator.js +92 -91
  28. package/dist/mcp-apps-inbound.d.ts +3 -3
  29. package/dist/mcp-apps-inbound.d.ts.map +1 -1
  30. package/dist/mcp-apps-inbound.js +28 -23
  31. package/dist/mcp-apps-outbound.d.ts +136 -121
  32. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  33. package/dist/mcp-apps-outbound.js +384 -414
  34. package/dist/mcp-mounts.d.ts +21 -20
  35. package/dist/mcp-mounts.d.ts.map +1 -1
  36. package/dist/mcp-mounts.js +25 -29
  37. package/dist/{session-channel.d.ts → render-channel.d.ts} +145 -102
  38. package/dist/render-channel.d.ts.map +1 -0
  39. package/dist/{session-channel.js → render-channel.js} +481 -462
  40. package/dist/schema-compat.d.ts +11 -11
  41. package/dist/schema-compat.d.ts.map +1 -1
  42. package/dist/schema-compat.js +6 -6
  43. package/dist/server.d.ts +224 -219
  44. package/dist/server.d.ts.map +1 -1
  45. package/dist/server.js +1498 -1963
  46. package/dist/storage.d.ts +7 -7
  47. package/dist/storage.d.ts.map +1 -1
  48. package/dist/storage.js +12 -12
  49. package/package.json +12 -11
  50. package/dist/render-gate.d.ts +0 -87
  51. package/dist/render-gate.d.ts.map +0 -1
  52. package/dist/render-gate.js +0 -77
  53. package/dist/render-rate-limit.d.ts +0 -59
  54. package/dist/render-rate-limit.d.ts.map +0 -1
  55. package/dist/render-rate-limit.js +0 -73
  56. package/dist/render-signing.d.ts +0 -98
  57. package/dist/render-signing.d.ts.map +0 -1
  58. package/dist/render-signing.js +0 -113
  59. 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
- * `ggui_push -> ui://ggui/session -> iframe -> live channel` delivery path.
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/session` as a static resource servable via
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
- * `ggui_push` result.
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 { createHash } from 'node:crypto';
35
- import { ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
36
- import { deriveContextDefault, } from '@ggui-ai/protocol';
37
- import { deriveStackItemBootstrapView, derivePublicEnvProjection, deriveBundleOrigins, findBlueprintExact, } from '@ggui-ai/mcp-server-handlers/session-mutations';
38
- import { MCP_APPS_UI_CAPABILITY, GGUI_SESSION_RESOURCE_URI, GGUI_SESSION_RESOURCE_MIME, deriveContextName, } from '@ggui-ai/protocol/integrations/mcp-apps';
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/session` (C8 pivot).
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
- * `_meta.ggui.bootstrap.runtimeUrl`, then dynamic-script-loads the
45
- * `@ggui-ai/iframe-runtime` bundle from that URL. Every rendering
46
- * concern - WS open, subscribe, stack render, component code eval,
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
- * `_meta.ggui.bootstrap.runtimeUrl` and the shell picks it up from
54
- * the first `ui/initialize` response - origin-agnostic, works under
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 `_meta.ggui.bootstrap` OR without a `runtimeUrl` field.
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 GGUI_SESSION_SHELL_SCRIPT_BODY = `
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, derive base URL + shortCode from structuredContent,
128
- // fetch JSON bootstrap (componentCode + ids + runtimeUrl), set
129
- // window.__GGUI_BOOTSTRAP__, fetch runtime as blob, inject as script.
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 mountFromBootstrap(bootstrap){
172
+ async function mountFromMeta(envelope){
183
173
  if(mounted)return;
184
- // runtimeUrl is the only load-bearing field at the shell layer —
185
- // it tells us which iframe-runtime bundle to fetch. componentCode
186
- // is OPTIONAL: present in self-contained-mode bootstraps (the
187
- // runtime renders the inlined code without WS), absent in
188
- // GguiBootstrapMeta-shaped bootstraps (the runtime opens the WS
189
- // subscription via wsUrl+token to receive the stack). Either is
190
- // valid; the runtime decides at boot time. Rejecting on missing
191
- // componentCode silently breaks every Path B GguiBootstrapMeta
192
- // delivery, which is the whole point of the inline-bootstrap path.
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.__GGUI_BOOTSTRAP__=bootstrap;
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=bootstrap.runtimeUrl;
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
- async function mount(toolResult){
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
- var ggui=meta.ggui;
257
- if(!ggui||typeof ggui!=='object')return null;
258
- var b=ggui.bootstrap;
259
- if(!b||typeof b!=='object')return null;
260
- // Same loosening as mountFromBootstrap: only runtimeUrl is required
261
- // here. GguiBootstrapMeta forwarded by first-party McpAppIframe
262
- // hosts omits componentCode by design (the runtime fetches the
263
- // stack via
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 hasGguiMetaPlaceholder(result){
238
+ function hasAiGguiMetaPlaceholder(result){
270
239
  // Detect the protocol-violation case where the host signaled
271
- // "I tried to deliver bootstrap" (toolOutput._meta.ggui exists)
272
- // but the bootstrap field itself is absent or malformed. In that
273
- // shape, we fail-fast with BOOTSTRAP_META_MISSING rather than
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
- var ggui=meta.ggui;
281
- return !!ggui&&typeof ggui==='object';
248
+ return 'ai.ggui/render' in meta;
282
249
  }
283
- function readBootstrapFromCallToolResult(params){
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
- // bootstrap material here.
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 ggui=meta.ggui;
296
- if(!ggui||typeof ggui!=='object')return null;
297
- var b=ggui.bootstrap;
298
- if(!b||typeof b!=='object')return null;
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=readBootstrapFromCallToolResult(m.params);
319
- if(specB){mountFromBootstrap(specB);return;}
320
- // First-party McpAppIframe convention: bootstrap nested under
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=readBootstrapFromInitResult(m.params);
325
- if(bb){mountFromBootstrap(bb);return;}
326
- // Path A (last-resort fallback): host posts structuredContent.
327
- // {url, shortCode}; shell fetches /api/bootstrap/<shortCode>
328
- // over HTTP. The OSS server doesn't mount that endpoint, so this
329
- // path is effectively dead unless a hosted operator wires it.
330
- var tr=readToolResult(m.params);
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: bootstrap inline in ui/initialize result. Hosts using
346
- // <McpAppIframe>'s first-party dispatch deliver bootstrap meta here
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=readBootstrapFromInitResult(result);
349
- if(b){mountFromBootstrap(b);return;}
308
+ var b=readMetaFromInitResult(result);
309
+ if(b){mountFromMeta(b);return;}
350
310
  // Protocol-violation surface: host signalled an attempt
351
- // (toolOutput._meta.ggui exists) but bootstrap is missing or
352
- // malformed. Fail fast so hosts pinning a typed error envelope
353
- // don't sit on the Path-A waiting overlay forever. Only fires
354
- // when the meta placeholder is present — a fully absent _meta.ggui
355
- // still falls through to Path A waiting.
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-bootstrap convention land here.
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 GGUI_SESSION_SHELL_HTML = `<!doctype html>
373
- <html lang="en" style="height:100%"><head><meta charset="utf-8"><title>ggui session</title></head>
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>${GGUI_SESSION_SHELL_SCRIPT_BODY}</script></body></html>`;
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 GGUI_SESSION_SHELL_HTML} when it executes
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 GGUI_SESSION_SHELL_SCRIPT_HASH = `'sha256-${createHash('sha256')
416
- .update(GGUI_SESSION_SHELL_SCRIPT_BODY)
417
- .digest('base64')}'`;
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/session` as a readable resource on an `McpServer`.
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
- if (!publicBaseUrl)
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(publicBaseUrl);
427
+ const parsed = new URL(source);
456
428
  const origin = parsed.origin;
457
- const wsScheme = parsed.protocol === 'https:' ? 'wss:' : 'ws:';
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 registerGguiSessionResource(server, shellHtml = GGUI_SESSION_SHELL_HTML, publicBaseUrl) {
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 === 'https:' ? 'wss:' : 'ws:';
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
- server.registerResource('ggui-session', GGUI_SESSION_RESOURCE_URI, {
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: 'ggui session',
510
- description: 'Thin-shell iframe bundle that bootstraps a ggui session. MCP Apps hosts fetch this when they see `_meta.ui.resourceUri` on a ggui_push result.',
511
- mimeType: GGUI_SESSION_RESOURCE_MIME,
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: GGUI_SESSION_RESOURCE_MIME,
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.__GGUI_BOOTSTRAP__`
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 === 'string' && opts.systemKind.length > 0;
573
- const hasCodeUrl = typeof opts.codeUrl === 'string' && opts.codeUrl.length > 0;
574
- const hasLive = typeof opts.wsUrl === 'string' && opts.wsUrl.length > 0
575
- && typeof opts.token === 'string' && opts.token.length > 0;
576
- const isCanvas = opts.canvasMode === true;
577
- if (isCanvas && (isSystem || hasCodeUrl || opts.stackItemId !== undefined)) {
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('buildSelfContainedShell: at least one of `codeUrl`, `systemKind`, or live-mode (`wsUrl` + `token`) must be set');
557
+ throw new Error("buildSelfContainedShell: at least one of `codeUrl`, `systemKind`, or live-mode (`wsUrl` + `token`) must be set");
585
558
  }
586
- // Inject `runtimeUrl` + the three bootstrap-derivation fields
587
- // (`appCallableTools`, `actionNextSteps`, `contextSlots`) into the
588
- // inline bootstrap so the iframe-runtime's bootstrap validator
589
- // (which requires `runtimeUrl` across all modes + makes
590
- // `appCallableTools`/`actionNextSteps`/`contextSlots` observable on
591
- // the self-contained path) accepts the envelope. Omitting
592
- // `runtimeUrl` from the inline JSON makes every `/r/<shortCode>`
593
- // direct-preview a blank white page because `parseBootstrap`
594
- // rejects it as MALFORMED.
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 `_meta.ggui.bootstrap`'s `gadgets` field.
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. parseBootstrap rejects half-live envelopes
653
- // (`wsUrl XOR token` MALFORMED), so we forward all three together
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 ? { token: opts.token } : {}),
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, '\\u003c')
668
- .replace(/>/g, '\\u003e')
669
- .replace(/&/g, '\\u0026')
670
- .replace(/\u2028/g, '\\u2028')
671
- .replace(/\u2029/g, '\\u2029');
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, '&amp;')
677
- .replace(/"/g, '&quot;')
678
- .replace(/</g, '&lt;')
679
- .replace(/>/g, '&gt;');
656
+ .replace(/&/g, "&amp;")
657
+ .replace(/"/g, "&quot;")
658
+ .replace(/</g, "&lt;")
659
+ .replace(/>/g, "&gt;");
680
660
  return `<!doctype html>
681
- <html lang="en"><head><meta charset="utf-8"><title>ggui session</title></head>
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.__GGUI_BOOTSTRAP__ = ${json};</script>
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-session resource is fetched
690
- * for a session whose top stack item has no componentCode yet
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 `ggui_push`
695
- * results on the same session — the per-call `_meta.ui.resourceUri`
696
- * value stays stable across pushes for a session, so re-fetching the
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(sessionId) {
681
+ export function buildSelfContainedLoadingShell(renderId) {
702
682
  return `<!doctype html>
703
- <html lang="en"><head><meta charset="utf-8"><title>ggui session</title></head>
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-session-id="${sessionId
706
- .replace(/&/g, '&amp;')
707
- .replace(/"/g, '&quot;')
708
- .replace(/</g, '&lt;')
709
- .replace(/>/g, '&gt;')}">Generating UI…</div>
685
+ <div id="ggui-root" data-ggui-shell="loading" data-ggui-render-id="${renderId
686
+ .replace(/&/g, "&amp;")
687
+ .replace(/"/g, "&quot;")
688
+ .replace(/</g, "&lt;")
689
+ .replace(/>/g, "&gt;")}">Generating UI…</div>
710
690
  </body></html>`;
711
691
  }
712
- function pickTopComponentItem(stack) {
713
- for (let i = stack.length - 1; i >= 0; i -= 1) {
714
- const entry = stack[i];
715
- if (!entry || entry.type === 'mcpApps')
716
- continue;
717
- const props = entry.props !== undefined &&
718
- entry.props !== null &&
719
- typeof entry.props === 'object' &&
720
- !Array.isArray(entry.props)
721
- ? entry.props
722
- : undefined;
723
- if (entry.type === 'system') {
724
- if (typeof entry.kind === 'string' && entry.kind.length > 0) {
725
- return {
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: entry.id,
738
- componentCode: code,
707
+ id: render.id,
708
+ kind: render.kind,
739
709
  ...(props !== undefined ? { props } : {}),
740
- source: entry,
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/session/{sessionId}` resource template. Each
748
- * `resources/read` request is resolved by looking up the session in the
749
- * store, picking the topmost component stack item, and returning the
750
- * self-contained shell with that componentCode inlined.
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 `ggui_push.resultMeta`)
753
- * pins the URI to a specific sessionId; hosts fetch THAT URI rather
754
- * than the static `ui://ggui/session` one. Both registrations co-exist:
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
- * - Session not found → loading shell (host re-fetches; absent
760
- * session is a transient state immediately after `ggui_push`).
761
- * - Session found, no componentCode yet → loading shell.
762
- * - Session found, componentCode present → self-contained shell.
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 registerGguiSessionResourceTemplate(server, opts) {
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/session/{sessionId}`.
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 session miss).
754
+ // still rehydrate (loading shell on render miss).
775
755
  //
776
- // 2. Two-segment resume URI — `ui://ggui/session/{sessionId}/
777
- // {blueprintKey}`. Stamped by every push since the resume
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 session + blueprint registry lookup (no data
759
+ // (a) parallel render + blueprint registry lookup (no data
780
760
  // dependency between them), (b) registry-only fallback when
781
- // the session is gone but the blueprint is still cached
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(`${GGUI_SESSION_RESOURCE_URI}/{sessionId}`, {
785
- // No list-callback — the resource set is unbounded per session
786
- // count, and `resources/list` would leak session ids across
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(`${GGUI_SESSION_RESOURCE_URI}/{sessionId}/{blueprintKey}`, { list: undefined });
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/session` resource; this is the per-call mirror.
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: GGUI_SESSION_RESOURCE_MIME,
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, sessionId) => shellContents(uri, buildSelfContainedLoadingShell(sessionId));
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 sessionIdRaw = variables['sessionId'];
848
- const sessionId = Array.isArray(sessionIdRaw) ? sessionIdRaw[0] : sessionIdRaw;
849
- if (typeof sessionId !== 'string' || sessionId.length === 0) {
850
- return loadingShell(uri, 'unknown');
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['blueprintKey'];
853
- const blueprintKey = Array.isArray(blueprintKeyRaw)
854
- ? blueprintKeyRaw[0]
855
- : blueprintKeyRaw;
856
- const hasResumeKey = typeof blueprintKey === 'string' && blueprintKey.length > 0;
857
- // Parallel lookup. The session and the blueprint registry are
858
- // independent — even though `session.stack[top].componentCode`
859
- // could feed the renderable directly, we ALSO want the blueprint
860
- // entry as a registry-only fallback when the session is gone but
861
- // the blueprint is still cached (chat-history rehydrate after
862
- // session TTL or process restart).
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, 'template', blueprintKey)
841
+ ? findBlueprintExact({ vectorStore: opts.vectorStore }, opts.defaultAppIdFallback, "template", blueprintKey)
867
842
  : Promise.resolve(null),
868
843
  ]);
869
- // Canvas-mode branch: session-scoped
870
- // iframe that renders {@link CanvasShell} and subscribes to
871
- // the live channel for stack-item delivery. The bootstrap carries
872
- // `canvasMode: true` + live-mode trio (wsUrl/token/expiresAt) and
873
- // NO stackItemId / codeUrl / systemKind — the iframe-runtime
874
- // mounts the canvas and waits for `push` envelopes rather than
875
- // rendering a pinned static item.
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 = deriveStackItemBootstrapView(top.source);
921
- const isSystem = top.kind !== undefined;
922
- // Static-component delivery via codeUrl (T3-1, 2026-05-13). The
923
- // compiled-component path mints a content-addressable URL the
924
- // iframe-runtime fetches at boot; the loading shell takes over
925
- // when codeStore + codeBaseUrl aren't wired.
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(top.componentCode);
931
- await opts.codeStore.put(hash, top.componentCode);
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 item but no codeUrl channel available —
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, sessionId);
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(session.appId);
957
- resourcePublicEnv = derivePublicEnvProjection(top.source, appRecord?.publicEnv);
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
- sessionId,
965
- appId: session.appId,
938
+ renderId,
939
+ appId: stored.appId,
966
940
  ...(isSystem
967
- ? { systemKind: top.kind }
941
+ ? { systemKind: picked.kind }
968
942
  : {
969
943
  codeUrl: codeUrl,
970
944
  ...(codeHash !== undefined ? { codeHash } : {}),
971
945
  }),
972
946
  runtimeUrl: opts.runtimeUrl,
973
- stackItemId: top.id,
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
- ? { actionNextSteps: view.actionNextSteps }
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
- ...(view.compiledValidators !== undefined
991
- ? { compiledValidators: view.compiledValidators }
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(top.source);
979
+ const gadgetOrigins = deriveBundleOrigins(picked.source);
1008
980
  return shellContents(uri, html, augmentCspMeta(gadgetOrigins));
1009
981
  }
1010
982
  }
1011
- // Registry-only fallback: session is gone (TTL / restart) but the
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
- sessionId,
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, sessionId);
1005
+ return loadingShell(uri, renderId);
1034
1006
  }
1035
- server.registerResource('ggui-session-self-contained', legacyTemplate, {
1036
- title: 'ggui session (self-contained, legacy URI)',
1037
- description: 'Per-session self-contained shell — single-segment URI shape predating the resume contract. Falls back to loading shell when the session is gone (no blueprintKey to do registry-only render).',
1038
- mimeType: GGUI_SESSION_RESOURCE_MIME,
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('ggui-session-self-contained-resume', resumeTemplate, {
1041
- title: 'ggui session (self-contained, resume URI)',
1042
- description: 'Per-session self-contained shell — two-segment URI shape carrying both sessionId AND blueprintKey. Resource handler runs Promise.all over session + registry; falls back to registry-only static render when the session has been evicted but the blueprint is still cached.',
1043
- mimeType: GGUI_SESSION_RESOURCE_MIME,
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 session).
1048
- * Used when chat-history rehydrate finds the session evicted but 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 push generated, seeded with the
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 session eviction.
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 = 'props' in contract && contract.props !== undefined
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 = 'actionSpec' in contract && contract.actionSpec !== undefined
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
- sessionId: args.sessionId,
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 !== 'object')
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 !== 'object')
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 === 'object' &&
1138
- typeof entry.nextStep === 'string' &&
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/session` resource. The
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-session
1152
- * `ui://ggui/session/{sessionId}` resource template that serves the
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
- registerGguiSessionResource(server, opts.shellHtml, opts.publicBaseUrl);
1129
+ registerGguiRenderResource(server, opts.shellHtml, opts.publicBaseUrl);
1160
1130
  if (opts.selfContained) {
1161
- registerGguiSessionResourceTemplate(server, opts.selfContained);
1131
+ registerGguiRenderResourceTemplate(server, opts.selfContained);
1162
1132
  }
1163
1133
  }