@ggui-ai/mcp-server 0.2.0-alpha.3 → 0.3.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/admin-blueprints-transport.d.ts.map +1 -1
  2. package/dist/admin-blueprints-transport.js +2 -1
  3. package/dist/admin-oauth-providers-transport.d.ts.map +1 -1
  4. package/dist/admin-oauth-providers-transport.js +7 -5
  5. package/dist/api-renders-routes.d.ts +85 -0
  6. package/dist/api-renders-routes.d.ts.map +1 -0
  7. package/dist/api-renders-routes.js +372 -0
  8. package/dist/build-mcp.d.ts +1 -1
  9. package/dist/build-mcp.d.ts.map +1 -1
  10. package/dist/build-mcp.js +39 -5
  11. package/dist/code-routes.d.ts +47 -0
  12. package/dist/code-routes.d.ts.map +1 -0
  13. package/dist/code-routes.js +81 -0
  14. package/dist/code-store-fs.js +2 -2
  15. package/dist/console-auth.d.ts +10 -10
  16. package/dist/console-auth.d.ts.map +1 -1
  17. package/dist/console-auth.js +5 -5
  18. package/dist/console-blueprint-routes.d.ts +71 -0
  19. package/dist/console-blueprint-routes.d.ts.map +1 -0
  20. package/dist/console-blueprint-routes.js +348 -0
  21. package/dist/console-chat-routes.d.ts +80 -0
  22. package/dist/console-chat-routes.d.ts.map +1 -0
  23. package/dist/console-chat-routes.js +182 -0
  24. package/dist/console-config-routes.d.ts +37 -0
  25. package/dist/console-config-routes.d.ts.map +1 -0
  26. package/dist/console-config-routes.js +91 -0
  27. package/dist/console-headers.d.ts +1 -1
  28. package/dist/console-info-routes.d.ts +84 -0
  29. package/dist/console-info-routes.d.ts.map +1 -0
  30. package/dist/console-info-routes.js +135 -0
  31. package/dist/console-keys-routes.d.ts +50 -0
  32. package/dist/console-keys-routes.d.ts.map +1 -0
  33. package/dist/console-keys-routes.js +222 -0
  34. package/dist/console-llm-keys-routes.d.ts +47 -0
  35. package/dist/console-llm-keys-routes.d.ts.map +1 -0
  36. package/dist/console-llm-keys-routes.js +443 -0
  37. package/dist/console-mcp-tools-routes.d.ts +41 -0
  38. package/dist/console-mcp-tools-routes.d.ts.map +1 -0
  39. package/dist/console-mcp-tools-routes.js +60 -0
  40. package/dist/console-registry-routes.d.ts +66 -0
  41. package/dist/console-registry-routes.d.ts.map +1 -0
  42. package/dist/console-registry-routes.js +276 -0
  43. package/dist/console-session-routes.d.ts +89 -0
  44. package/dist/console-session-routes.d.ts.map +1 -0
  45. package/dist/console-session-routes.js +385 -0
  46. package/dist/console-sessions-routes.d.ts +52 -0
  47. package/dist/console-sessions-routes.d.ts.map +1 -0
  48. package/dist/console-sessions-routes.js +106 -0
  49. package/dist/console-static-routes.d.ts +54 -0
  50. package/dist/console-static-routes.d.ts.map +1 -0
  51. package/dist/console-static-routes.js +190 -0
  52. package/dist/console-theme-routes.d.ts +3 -3
  53. package/dist/console-theme-routes.js +1 -1
  54. package/dist/console-timeline.d.ts +5 -5
  55. package/dist/console-timeline.d.ts.map +1 -1
  56. package/dist/console-timeline.js +27 -26
  57. package/dist/console-welcome.js +2 -2
  58. package/dist/email-login.d.ts.map +1 -1
  59. package/dist/email-login.js +2 -3
  60. package/dist/ggui-session-channel/action-ingress.d.ts +54 -0
  61. package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -0
  62. package/dist/ggui-session-channel/action-ingress.js +228 -0
  63. package/dist/ggui-session-channel/channel-subscriptions.d.ts +97 -0
  64. package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -0
  65. package/dist/ggui-session-channel/channel-subscriptions.js +224 -0
  66. package/dist/ggui-session-channel/internal-types.d.ts +102 -0
  67. package/dist/ggui-session-channel/internal-types.d.ts.map +1 -0
  68. package/dist/ggui-session-channel/internal-types.js +6 -0
  69. package/dist/ggui-session-channel/outbound.d.ts +81 -0
  70. package/dist/ggui-session-channel/outbound.d.ts.map +1 -0
  71. package/dist/ggui-session-channel/outbound.js +174 -0
  72. package/dist/ggui-session-channel/socket-router.d.ts +38 -0
  73. package/dist/ggui-session-channel/socket-router.d.ts.map +1 -0
  74. package/dist/ggui-session-channel/socket-router.js +213 -0
  75. package/dist/ggui-session-channel/subscribe.d.ts +165 -0
  76. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -0
  77. package/dist/ggui-session-channel/subscribe.js +370 -0
  78. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +40 -0
  79. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -0
  80. package/dist/ggui-session-channel/subscriber-lifecycle.js +123 -0
  81. package/dist/ggui-session-channel.d.ts +425 -0
  82. package/dist/ggui-session-channel.d.ts.map +1 -0
  83. package/dist/ggui-session-channel.js +262 -0
  84. package/dist/health-routes.d.ts +76 -0
  85. package/dist/health-routes.d.ts.map +1 -0
  86. package/dist/health-routes.js +145 -0
  87. package/dist/index.d.ts +10 -11
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +8 -9
  90. package/dist/instructions-presets.d.ts +3 -3
  91. package/dist/instructions-presets.js +25 -25
  92. package/dist/llm-backed-negotiator.d.ts +68 -67
  93. package/dist/llm-backed-negotiator.d.ts.map +1 -1
  94. package/dist/llm-backed-negotiator.js +82 -248
  95. package/dist/mcp-apps-outbound.d.ts +47 -48
  96. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  97. package/dist/mcp-apps-outbound.js +204 -183
  98. package/dist/mcp-endpoint-routes.d.ts +88 -0
  99. package/dist/mcp-endpoint-routes.d.ts.map +1 -0
  100. package/dist/mcp-endpoint-routes.js +359 -0
  101. package/dist/mcp-mounts.d.ts +2 -76
  102. package/dist/mcp-mounts.d.ts.map +1 -1
  103. package/dist/mcp-mounts.js +0 -76
  104. package/dist/oauth-as-routes.d.ts +60 -0
  105. package/dist/oauth-as-routes.d.ts.map +1 -0
  106. package/dist/oauth-as-routes.js +82 -0
  107. package/dist/oauth-clients-routes.d.ts +39 -0
  108. package/dist/oauth-clients-routes.d.ts.map +1 -0
  109. package/dist/oauth-clients-routes.js +87 -0
  110. package/dist/oauth-login-types.d.ts +1 -20
  111. package/dist/oauth-login-types.d.ts.map +1 -1
  112. package/dist/oauth-login-types.js +30 -7
  113. package/dist/oauth-login.d.ts.map +1 -1
  114. package/dist/oauth-login.js +3 -2
  115. package/dist/oauth-providers-store.d.ts.map +1 -1
  116. package/dist/oauth-providers-store.js +5 -5
  117. package/dist/oauth.d.ts +9 -8
  118. package/dist/oauth.d.ts.map +1 -1
  119. package/dist/oauth.js +41 -19
  120. package/dist/pairing-transport.d.ts.map +1 -1
  121. package/dist/pairing-transport.js +2 -1
  122. package/dist/request-context.d.ts +2 -2
  123. package/dist/request-context.js +2 -2
  124. package/dist/reserved-validators.d.ts.map +1 -1
  125. package/dist/reserved-validators.js +9 -1
  126. package/dist/route-param.d.ts +9 -0
  127. package/dist/route-param.d.ts.map +1 -0
  128. package/dist/route-param.js +10 -0
  129. package/dist/runtime-bundle-route.d.ts +43 -0
  130. package/dist/runtime-bundle-route.d.ts.map +1 -0
  131. package/dist/runtime-bundle-route.js +80 -0
  132. package/dist/schema-compat.d.ts +64 -62
  133. package/dist/schema-compat.d.ts.map +1 -1
  134. package/dist/schema-compat.js +23 -51
  135. package/dist/server.d.ts +179 -193
  136. package/dist/server.d.ts.map +1 -1
  137. package/dist/server.js +644 -3759
  138. package/dist/storage.d.ts +5 -5
  139. package/dist/storage.d.ts.map +1 -1
  140. package/dist/storage.js +5 -5
  141. package/dist/thread-transport.d.ts.map +1 -1
  142. package/dist/thread-transport.js +4 -3
  143. package/dist/user-session-auth.d.ts +7 -21
  144. package/dist/user-session-auth.d.ts.map +1 -1
  145. package/dist/user-session-auth.js +7 -28
  146. package/package.json +16 -15
  147. package/dist/mcp-apps-inbound.d.ts +0 -86
  148. package/dist/mcp-apps-inbound.d.ts.map +0 -1
  149. package/dist/mcp-apps-inbound.js +0 -283
  150. package/dist/render-channel.d.ts +0 -694
  151. package/dist/render-channel.d.ts.map +0 -1
  152. package/dist/render-channel.js +0 -1775
@@ -32,8 +32,8 @@
32
32
  * same `@ggui-ai/mcp-server` instance that mints the bootstrap.
33
33
  */
34
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";
35
+ import { deriveContextDefault, isRecord } 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, MCP_APP_BOOTSTRAP_FAILED_TYPE, deriveContextName, } from "@ggui-ai/protocol/integrations/mcp-apps";
37
37
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
38
38
  import { registerAppResource } from "@modelcontextprotocol/ext-apps/server";
39
39
  import { createHash } from "node:crypto";
@@ -41,23 +41,25 @@ import { createHash } from "node:crypto";
41
41
  * Thin-shell body served from `ui://ggui/render` (C8 pivot).
42
42
  *
43
43
  * **Architectural role.** The shell is a ~30 LOC bootstrap wrapper -
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,
48
- * adapter install - belongs to the renderer bundle (shipped C7a-d),
49
- * not here.
44
+ * it runs the `ui/initialize` + `ui/notifications/initialized`
45
+ * handshake, then waits for the host's spec-canonical
46
+ * `ui/notifications/tool-result` notification carrying the
47
+ * `ai.ggui/render` slice in `_meta`, and dynamic-script-loads the
48
+ * `@ggui-ai/iframe-runtime` bundle from the slice's `runtimeUrl`.
49
+ * Every rendering concern - WS open, subscribe, render mount,
50
+ * component code eval, adapter install - belongs to the renderer
51
+ * bundle (shipped C7a-d), not here.
50
52
  *
51
53
  * **Why bootstrap-driven URL.** `srcdoc` iframes have `about:srcdoc`
52
54
  * as their URL, so relative paths can't resolve to the MCP server's
53
55
  * HTTP listener. The server-controlled `runtimeUrl` lands on the
54
56
  * `ai.ggui/render.runtimeUrl` slice field and the shell picks it up
55
- * from the first `ui/initialize` response - origin-agnostic, works
57
+ * from the tool-result `_meta` slice - origin-agnostic, works
56
58
  * under OSS same-origin AND hosted-cloud CDN deployments.
57
59
  *
58
60
  * **Why `<script type="module">`.** `@ggui-ai/iframe-runtime` is bundled as
59
61
  * ESM (its own runtime.ts:5 declares the contract: "the thin-shell
60
- * HTML loads it via `<script type="module" src=".../renderer.js">`").
62
+ * HTML loads it via `<script type="module" src=".../iframe-runtime.js">`").
61
63
  * Loading the bundle as a classic `<script src=...>` throws
62
64
  * `SyntaxError: Unexpected token 'export'` synchronously when the
63
65
  * browser parses the bundle - the renderer never executes, the
@@ -70,31 +72,30 @@ import { createHash } from "node:crypto";
70
72
  * surface to the parent via
71
73
  * `postMessage({type:'ggui:bootstrap-failed', reason, message}, '*')`:
72
74
  *
73
- * - `BOOTSTRAP_META_MISSING` - `ui/initialize` returned without a
74
- * valid `ai.ggui/render` slice OR without a `runtimeUrl` field on
75
- * it.
75
+ * - `MALFORMED_BOOTSTRAP` - the tool-result `_meta` slice arrived
76
+ * without a valid `ai.ggui/render` envelope or `runtimeUrl`.
76
77
  * - `BUNDLE_FETCH_FAILED` - `<script src>` errored (network failure,
77
78
  * 404, CSP reject with an observable `error` event).
78
79
  *
79
- * Post-renderer failures (WS handshake / auth / session-mismatch) are
80
+ * Post-renderer failures (WS handshake / auth / render-mismatch) are
80
81
  * the renderer bundle's responsibility - `runtime.ts::postBootFailure`
81
- * emits the same `ggui:bootstrap-failed` envelope AND, for post-WS-open
82
- * failures, a `_ggui:contract-error` envelope on the live channel per
83
- * `ContractErrorCode` additions (C8 Commit 1/3).
82
+ * emits the same `ggui:bootstrap-failed` envelope.
84
83
  *
85
84
  * **Adapter boundary unchanged.** The preflight's `message` listener
86
85
  * routes ONLY responses to its own pending JSON-RPC ids. MCP Apps
87
86
  * lifecycle notifications from the host (`ui/notifications/message`,
88
87
  * `ui/update-model-context`) are dropped. This mirrors the pre-C8
89
- * posture - the shell still MUST NOT mutate session state from
88
+ * posture - the shell still MUST NOT mutate render state from
90
89
  * arbitrary host messages. ADAPTER BOUNDARY enforced by the
91
90
  * `pending[m.id]` route-by-id check.
92
91
  *
93
92
  * **Double `ui/initialize` is intentional.** The shell runs a minimal
94
- * preflight solely to fetch `runtimeUrl`; the renderer bundle's
95
- * autostart path runs its own `ui/initialize` for the full bootstrap
96
- * parse. MCP Apps hosts handle repeats idempotently - the preflight's
97
- * cost is a single postMessage round-trip.
93
+ * preflight to complete the MCP Apps handshake (hosts release the
94
+ * tool-result notification only after `ui/notifications/initialized`);
95
+ * the renderer bundle's autostart path runs its own `ui/initialize`
96
+ * for the full bootstrap parse. MCP Apps hosts handle repeats
97
+ * idempotently - the preflight's cost is a single postMessage
98
+ * round-trip.
98
99
  *
99
100
  * Exported as a string constant for tests; not part of the public
100
101
  * package API. Vanilla JS, no build step, no external deps. The shell
@@ -104,9 +105,9 @@ import { createHash } from "node:crypto";
104
105
  /**
105
106
  * Inline body of the thin shell's bootstrap `<script>` block.
106
107
  *
107
- * Split from {@link GGUI_SESSION_SHELL_HTML} so the exact bytes the
108
+ * Split from {@link GGUI_RENDER_SHELL_HTML} so the exact bytes the
108
109
  * browser sees inside the `<script>...</script>` tag are addressable
109
- * for CSP-hash purposes — see {@link GGUI_SESSION_SHELL_SCRIPT_HASH}.
110
+ * for CSP-hash purposes — see {@link GGUI_RENDER_SHELL_SCRIPT_HASH}.
110
111
  *
111
112
  * The browser's CSP `'sha256-...'` source-expression is computed over
112
113
  * the literal text content of the `<script>` element (everything
@@ -114,10 +115,11 @@ import { createHash } from "node:crypto";
114
115
  * whitespace inside). Concatenating this constant unchanged into the
115
116
  * shell HTML means the runtime hash and the constant-time hash agree.
116
117
  *
117
- * NEVER mutate this constant without also bumping
118
- * {@link GGUI_SESSION_SHELL_SCRIPT_HASH} — the
119
- * `mcp-apps-outbound.test.ts` drift test recomputes the hash and fails
120
- * loudly if they diverge.
118
+ * {@link GGUI_RENDER_SHELL_SCRIPT_HASH} is DERIVED from this constant
119
+ * at module load, so edits here propagate to the CSP hash
120
+ * automatically. The `mcp-apps-outbound.test.ts` drift test recomputes
121
+ * the hash from the assembled shell HTML and fails loudly if the body
122
+ * and the HTML assembly ever disagree.
121
123
  */
122
124
  const GGUI_RENDER_SHELL_SCRIPT_BODY = `
123
125
  (function(){'use strict';
@@ -127,9 +129,8 @@ const GGUI_RENDER_SHELL_SCRIPT_BODY = `
127
129
  // 2. iframe -> host: ui/notifications/initialized
128
130
  // 3. host -> iframe: ui/notifications/tool-result (per CallToolResult)
129
131
  // 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.
132
+ // CallToolResult _meta at the top level), set window.__GGUI_META__ to
133
+ // the envelope, fetch runtime as blob, inject as script.
133
134
  // Runtime auto-mounts inline. No nested iframe (claudemcpcontent.com
134
135
  // CSP frame-src forbids cross-origin frames). State machine matches
135
136
  // buildSelfContainedShell so the same runtime mounts both paths.
@@ -142,7 +143,7 @@ const GGUI_RENDER_SHELL_SCRIPT_BODY = `
142
143
  // Phase B (2026-05-27): the historic two-slice envelope
143
144
  // (\`ai.ggui/session\` + \`ai.ggui/stack-item\`) collapsed to ONE flat
144
145
  // \`ai.ggui/render\` slice. runtimeUrl now lives directly on the
145
- // render slice; renderId is the canonical identity.
146
+ // render slice; sessionId is the canonical identity.
146
147
  var rpcId=1,pending={};
147
148
  var rootEl=document.getElementById('ggui-root');
148
149
  rootEl.style.cssText='display:flex;flex-direction:column;height:100%;min-height:300px;margin:0';
@@ -167,11 +168,11 @@ function postBootstrapFailed(reason,message){
167
168
  // error pane (McpAppIframe onError, IframeErrorPane) see the
168
169
  // failure instead of staring at the inert overlay until the test
169
170
  // times out. Reason codes match BootstrapFailureReason.
170
- try{window.parent.postMessage({type:'ggui:bootstrap-failed',reason:reason,message:message},'*');}catch(e){}
171
+ try{window.parent.postMessage({type:'${MCP_APP_BOOTSTRAP_FAILED_TYPE}',reason:reason,message:message},'*');}catch(e){}
171
172
  }
172
173
  async function mountFromMeta(envelope){
173
174
  if(mounted)return;
174
- // Slice envelope shape (Phase B): { "ai.ggui/render": { renderId,
175
+ // Slice envelope shape (Phase B): { "ai.ggui/render": { sessionId,
175
176
  // appId, runtimeUrl, ... } }. runtimeUrl on the render slice is
176
177
  // the only load-bearing field at the shell layer — it tells us
177
178
  // which iframe-runtime bundle to fetch. Everything else is
@@ -181,7 +182,7 @@ async function mountFromMeta(envelope){
181
182
  var runtimeUrl=renderSlice&&renderSlice.runtimeUrl;
182
183
  if(!envelope||typeof runtimeUrl!=='string'){
183
184
  setOverlay('Bootstrap payload malformed.');
184
- postBootstrapFailed('BOOTSTRAP_MALFORMED','Bootstrap payload malformed.');
185
+ postBootstrapFailed('MALFORMED_BOOTSTRAP','Bootstrap payload malformed.');
185
186
  return;
186
187
  }
187
188
  setOverlay('Loading UI…');
@@ -220,42 +221,17 @@ async function mountFromMeta(envelope){
220
221
  postBootstrapFailed('BUNDLE_FETCH_FAILED',msg);
221
222
  }
222
223
  }
223
- function readMetaFromInitResult(result){
224
- if(!result||typeof result!=='object')return null;
225
- var toolOutput=result.toolOutput;
226
- if(!toolOutput||typeof toolOutput!=='object')return null;
227
- var meta=toolOutput._meta;
228
- if(!meta||typeof meta!=='object')return null;
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;
237
- }
238
- function hasAiGguiMetaPlaceholder(result){
239
- // Detect the protocol-violation case where the host signaled
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.
243
- if(!result||typeof result!=='object')return false;
244
- var toolOutput=result.toolOutput;
245
- if(!toolOutput||typeof toolOutput!=='object')return false;
246
- var meta=toolOutput._meta;
247
- if(!meta||typeof meta!=='object')return false;
248
- return 'ai.ggui/render' in meta;
249
- }
250
224
  function readMetaFromCallToolResult(params){
251
225
  // MCP Apps spec (specification/2026-01-26/apps.mdx:1145-1155):
252
226
  // ui/notifications/tool-result
253
227
  // params: CallToolResult // Standard MCP type
254
228
  // So params IS the CallToolResult and _meta lives at the top
255
- // level (NOT under params.toolOutput, which is where the
256
- // first-party McpAppIframe convention wraps it). Spec-compliant
257
- // hosts (Claude Desktop, claude.ai Connector, Claude Code) deliver
258
- // slice-envelope material here.
229
+ // level. Spec-compliant hosts (Claude Desktop, claude.ai
230
+ // Connector, Claude Code) deliver slice-envelope material here.
231
+ // Single slice-envelope key (Phase B: ai.ggui/render). Only the
232
+ // render slice's runtimeUrl is load-bearing at the shell layer;
233
+ // the runtime reads everything else off window.__GGUI_META__
234
+ // after we set it.
259
235
  if(!params||typeof params!=='object')return null;
260
236
  var meta=params._meta;
261
237
  if(!meta||typeof meta!=='object')return null;
@@ -274,21 +250,14 @@ window.addEventListener('message',function(ev){
274
250
  }
275
251
  if(m.method==='ui/notifications/tool-result'){
276
252
  // Spec-compliant hosts: m.params IS the CallToolResult; _meta is
277
- // at the top level. Try this FIRST so Claude Desktop / claude.ai
278
- // Connector / Claude Code land here.
279
- var specB=readMetaFromCallToolResult(m.params);
280
- if(specB){mountFromMeta(specB);return;}
281
- // First-party McpAppIframe convention: slice envelope nested under
282
- // params.toolOutput._meta. First-party hosts (Studio, Portal,
283
- // console) use this shape for both init-response and post-init
284
- // notification.
285
- var bb=readMetaFromInitResult(m.params);
286
- if(bb){mountFromMeta(bb);return;}
253
+ // at the top level.
254
+ var specMeta=readMetaFromCallToolResult(m.params);
255
+ if(specMeta){mountFromMeta(specMeta);return;}
287
256
  // R5 (2026-05-26) -- the /r/<shortCode> HTTP fallback was removed
288
257
  // along with the bearer-by-obscurity model. Hosts that strip
289
258
  // _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.
259
+ // spec-canonical hosts deliver meta inline and land in the branch
260
+ // above.
292
261
  }
293
262
  });
294
263
  setOverlay('Initializing…');
@@ -297,29 +266,15 @@ var initTimer=setTimeout(function(){
297
266
  },3000);
298
267
  postRpc('ui/initialize',{
299
268
  appCapabilities:{},
300
- appInfo:{name:'ggui-session',version:'1.0.0'},
269
+ appInfo:{name:'ggui-render',version:'1.0.0'},
301
270
  protocolVersion:'2026-01-26'
302
- }).then(function(result){
271
+ }).then(function(){
303
272
  clearTimeout(initTimer);
304
273
  postNotification('ui/notifications/initialized',{});
305
- // Path B: slice envelope inline in ui/initialize result. Hosts
306
- // using <McpAppIframe>'s first-party dispatch deliver meta here
307
- // and never send a separate ui/notifications/tool-result.
308
- var b=readMetaFromInitResult(result);
309
- if(b){mountFromMeta(b);return;}
310
- // Protocol-violation surface: host signalled an attempt
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';
316
- setOverlay(msg);
317
- postBootstrapFailed('BOOTSTRAP_META_MISSING',msg);
318
- return;
319
- }
320
- // Path A: wait for the host to send ui/notifications/tool-result
321
- // with structuredContent.{url, shortCode}. MCP Apps hosts that don't
322
- // implement the reading-B inline-meta convention land here.
274
+ // Wait for the host to send ui/notifications/tool-result carrying
275
+ // the slice envelope in _meta — the spec-canonical delivery channel.
276
+ // The ui/initialize result itself carries no slice meta (the
277
+ // McpUiInitializeResult schema defines no such field).
323
278
  setOverlay('Waiting for tool result…');
324
279
  }).catch(function(e){
325
280
  clearTimeout(initTimer);
@@ -327,9 +282,50 @@ postRpc('ui/initialize',{
327
282
  });
328
283
  })();
329
284
  `;
285
+ // `--ggui-color-surface` is injected at `:root` on this document's
286
+ // `<head>` by the iframe-runtime at boot (react-renderer.ts ->
287
+ // `<style id="ggui-theme-vars">`), so the `var()` resolves to the
288
+ // active theme's exact per-mode surface color at runtime. The static
289
+ // `#1e293b` fallback (the dark-mode surface) covers the pre-resolve
290
+ // first paint + browsers that drop unresolved custom properties.
291
+ //
292
+ // # Why paint the document background here (Safari white-canvas fix)
293
+ //
294
+ // This shell is ALWAYS the **top-level document of a standalone served
295
+ // iframe** (`ui://ggui/render`, the `/r/<shortCode>` viewer, the
296
+ // sandbox-proxy inner-iframe `srcdoc`). It is NEVER inlined into a host
297
+ // page — the host wrapper (`<McpAppIframe>`) only ever loads it AS an
298
+ // iframe document, never as part of its own DOM. So painting `html` /
299
+ // `body` here is structurally unreachable by inline embedding and
300
+ // cannot impose a background on a host page.
301
+ //
302
+ // The design scope root (`.ggui-rcr-*`) and the sandbox-proxy outer
303
+ // document stay `background-color: transparent` by design (so a host
304
+ // themeing the chat surface shows through AROUND the rendered content).
305
+ // But nothing painted the rendered-content document's OWN backdrop:
306
+ // Chrome composited the transparent iframe document over the dark host
307
+ // app behind it (looked dark), while Safari renders a transparent
308
+ // iframe document's backdrop as the opaque UA `Canvas` color (white) —
309
+ // the per-browser divergence the bug reported. The component itself
310
+ // themed correctly (its scoped vars resolve); only the page behind it
311
+ // diverged. Painting the served document's own surface here removes the
312
+ // dependency on a browser honoring iframe transparency.
313
+ //
314
+ // Value-resolution only — no `--ggui-*` token added or renamed.
315
+ const GGUI_RENDER_SHELL_SURFACE = `var(--ggui-color-surface, #1e293b)`;
316
+ // `#ggui-root` here is LOAD-BEARING for the shell script (NOT a React
317
+ // mount target): the inline script grabs it as `rootEl` for the
318
+ // pre-mount overlays ("Initializing…", "Waiting for tool result…",
319
+ // bootstrap-failure messages) and clears it before injecting the
320
+ // runtime bundle. The runtime itself mounts into its own
321
+ // `<ul data-ggui-session-root>` appended to `document.body`
322
+ // (iframe-runtime `status-dom.ts#ensureStatusDom`), so `#ggui-root`
323
+ // stays empty after a successful mount — that is expected, not a bug.
324
+ // The self-contained shell (`buildSelfContainedShell`) has no overlay
325
+ // script and therefore no anchor div at all.
330
326
  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>
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>
327
+ <html lang="en" style="height:100%;background-color:${GGUI_RENDER_SHELL_SURFACE}"><head><meta charset="utf-8"><title>ggui render</title></head>
328
+ <body style="margin:0;height:100%;min-height:480px;background-color:${GGUI_RENDER_SHELL_SURFACE}"><div id="ggui-root" data-ggui-shell="thin" style="height:100%;min-height:480px"></div>
333
329
  <script>${GGUI_RENDER_SHELL_SCRIPT_BODY}</script></body></html>`;
334
330
  /**
335
331
  * CSP `script-src` source expression that authorises the inline
@@ -359,7 +355,7 @@ export const GGUI_RENDER_SHELL_HTML = `<!doctype html>
359
355
  * # Where it gets used
360
356
  *
361
357
  * `console-headers.ts::DEVTOOL_CSP` appends this expression to its
362
- * `script-src` directive. Hosted closed-runtime session-resource
358
+ * `script-src` directive. Hosted closed-runtime render-resource
363
359
  * endpoints have their own CSP and serve the same shell — that path
364
360
  * needs the same expression added; tracked separately.
365
361
  *
@@ -377,7 +373,7 @@ export const GGUI_RENDER_SHELL_SCRIPT_HASH = `'sha256-${createHash("sha256")
377
373
  * Register `ui://ggui/render` as a readable resource on an `McpServer`.
378
374
  *
379
375
  * The resource is STATIC - `resources/read` always returns the same
380
- * body. Per-session state lives on the live channel, not in the resource.
376
+ * body. Per-render state lives on the live channel, not in the resource.
381
377
  *
382
378
  * When `publicBaseUrl` is supplied, the resource content carries
383
379
  * `_meta.ui.csp.{connectDomains,resourceDomains}` per the MCP Apps spec
@@ -412,10 +408,10 @@ function buildCspMeta(publicBaseUrl,
412
408
  * same-origin deployments, e.g. `ggui serve` on `127.0.0.1`), derive
413
409
  * the CSP block from `runtimeUrl`. The runtime + WS + state endpoints
414
410
  * all live on the runtime's origin in same-origin deployments, so
415
- * declaring it covers every fetch the canvas iframe makes.
411
+ * declaring it covers every fetch the sandboxed iframe makes.
416
412
  *
417
413
  * Without this fallback, local dev with cross-origin sandbox proxies
418
- * (sample-agent's `:7790/sandbox.html` writing the canvas HTML that
414
+ * (sample-agent's `:7790/sandbox.html` writing the sandbox HTML that
419
415
  * references `:6786/_ggui/iframe-runtime.js`) trips a `script-src`
420
416
  * violation that blanks the iframe — verified live 2026-05-27.
421
417
  */
@@ -517,10 +513,10 @@ export function advertiseMcpAppsUiCapability(server) {
517
513
  });
518
514
  }
519
515
  /**
520
- * Build the self-contained shell HTML for a given session.
516
+ * Build the self-contained shell HTML for a given render.
521
517
  *
522
518
  * The returned HTML is a complete, standalone document: it inlines the
523
- * compiled component (base64) + session ids in a `window.__GGUI_META__`
519
+ * compiled component (base64) + the render id in a `window.__GGUI_META__`
524
520
  * global, then loads the iframe-runtime bundle via `<script type="module"
525
521
  * src={runtimeUrl}>`. The runtime takes over synchronously on import,
526
522
  * mounts the component, and the iframe paints WITHOUT any further server
@@ -564,14 +560,15 @@ export function buildSelfContainedShell(opts) {
564
560
  // postMessage paths use. `runtimeUrl` is required across all modes
565
561
  // (the shell-bundled script tag fetches the runtime from there).
566
562
  const render = {
567
- renderId: opts.renderId,
563
+ sessionId: opts.sessionId,
568
564
  appId: opts.appId,
569
565
  runtimeUrl: opts.runtimeUrl,
570
566
  ...(opts.themeId !== undefined ? { themeId: opts.themeId } : {}),
571
567
  ...(opts.themeMode !== undefined ? { themeMode: opts.themeMode } : {}),
572
- ...(opts.appCallableTools !== undefined && opts.appCallableTools.length > 0
573
- ? { appCallableTools: opts.appCallableTools }
574
- : {}),
568
+ // Per-app theme overlay — same field the MCP-Apps `_meta` slice
569
+ // carries, forwarded onto the inline bootstrap so the self-contained
570
+ // shell applies the operator overlay too.
571
+ ...(opts.theme !== undefined ? { theme: opts.theme } : {}),
575
572
  ...(opts.permissionsPolicy !== undefined && opts.permissionsPolicy.length > 0
576
573
  ? { permissionsPolicy: opts.permissionsPolicy }
577
574
  : {}),
@@ -615,9 +612,6 @@ export function buildSelfContainedShell(opts) {
615
612
  }
616
613
  : {}),
617
614
  ...(opts.propsJson !== undefined ? { propsJson: opts.propsJson } : {}),
618
- ...(opts.actionNextSteps !== undefined && Object.keys(opts.actionNextSteps).length > 0
619
- ? { actionNextSteps: opts.actionNextSteps }
620
- : {}),
621
615
  ...(opts.contextSlots !== undefined && opts.contextSlots.length > 0
622
616
  ? { contextSlots: opts.contextSlots }
623
617
  : {}),
@@ -657,10 +651,26 @@ export function buildSelfContainedShell(opts) {
657
651
  .replace(/"/g, "&quot;")
658
652
  .replace(/</g, "&lt;")
659
653
  .replace(/>/g, "&gt;");
654
+ // Standalone served-iframe document — paint its own surface backdrop
655
+ // so the canvas behind the rendered component never depends on a
656
+ // browser honoring iframe transparency (the Safari white-canvas bug).
657
+ // Same gating rationale as `GGUI_RENDER_SHELL_HTML`: this is ALWAYS a
658
+ // top-level iframe document (the `__GGUI_META__` self-contained shell
659
+ // for claude.ai / per-render resource shells), never inlined into a
660
+ // host page, so painting `html`/`body` here cannot regress inline
661
+ // embedding. `var(--ggui-color-surface, …)` resolves to the active
662
+ // theme's per-mode surface once the iframe-runtime injects the `:root`
663
+ // theme vars; the static dark fallback covers pre-resolve paint.
664
+ // No anchor div: the iframe-runtime never mounts into a server-
665
+ // provided container — it appends its own `<ul data-ggui-session-root>`
666
+ // mount target to `document.body` at boot (iframe-runtime
667
+ // `status-dom.ts#ensureStatusDom`). The thin postMessage shell's
668
+ // `#ggui-root` is different: its inline script uses that div for
669
+ // pre-mount overlays, which this shell has none of (the runtime boots
670
+ // synchronously from the inline `__GGUI_META__` global).
660
671
  return `<!doctype html>
661
- <html lang="en"><head><meta charset="utf-8"><title>ggui render</title></head>
662
- <body>
663
- <div id="ggui-root" data-ggui-shell="self-contained"></div>
672
+ <html lang="en" style="background-color:${GGUI_RENDER_SHELL_SURFACE}"><head><meta charset="utf-8"><title>ggui render</title></head>
673
+ <body style="background-color:${GGUI_RENDER_SHELL_SURFACE}">
664
674
  <script>window.__GGUI_META__ = ${json};</script>
665
675
  <script type="module" crossorigin="anonymous" src="${safeRuntimeUrl}"></script>
666
676
  </body></html>`;
@@ -678,29 +688,27 @@ export function buildSelfContainedShell(opts) {
678
688
  *
679
689
  * @public
680
690
  */
681
- export function buildSelfContainedLoadingShell(renderId) {
691
+ export function buildSelfContainedLoadingShell(sessionId) {
692
+ // Standalone served-iframe loading document — same surface-backdrop
693
+ // paint + gating as the thin/self-contained shells so the canvas is
694
+ // dark while the render is still in flight (no Safari white flash).
682
695
  return `<!doctype html>
683
- <html lang="en"><head><meta charset="utf-8"><title>ggui render</title></head>
684
- <body>
685
- <div id="ggui-root" data-ggui-shell="loading" data-ggui-render-id="${renderId
696
+ <html lang="en" style="background-color:${GGUI_RENDER_SHELL_SURFACE}"><head><meta charset="utf-8"><title>ggui render</title></head>
697
+ <body style="background-color:${GGUI_RENDER_SHELL_SURFACE}">
698
+ <div id="ggui-root" data-ggui-shell="loading" data-ggui-session-id="${sessionId
686
699
  .replace(/&/g, "&amp;")
687
700
  .replace(/"/g, "&quot;")
688
701
  .replace(/</g, "&lt;")
689
702
  .replace(/>/g, "&gt;")}">Generating UI…</div>
690
703
  </body></html>`;
691
704
  }
692
- function pickComponentFromRender(render) {
705
+ function pickComponentFromGguiSession(render) {
693
706
  if (!render)
694
707
  return null;
695
708
  if (render.type === "mcpApps")
696
709
  return null;
697
710
  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;
711
+ const props = isRecord(propsRaw) ? propsRaw : undefined;
704
712
  if (render.type === "system") {
705
713
  if (typeof render.kind === "string" && render.kind.length > 0) {
706
714
  return {
@@ -724,22 +732,22 @@ function pickComponentFromRender(render) {
724
732
  return null;
725
733
  }
726
734
  /**
727
- * Register a `ui://ggui/render/{renderId}` resource template. Each
735
+ * Register a `ui://ggui/render/{sessionId}` resource template. Each
728
736
  * `resources/read` request is resolved by looking up the render in the
729
737
  * store and returning the self-contained shell with that
730
738
  * componentCode inlined.
731
739
  *
732
740
  * Per-call `_meta.ui.resourceUri` (stamped by `ggui_render.resultMeta`)
733
- * pins the URI to a specific renderId; hosts fetch THAT URI rather
741
+ * pins the URI to a specific sessionId; hosts fetch THAT URI rather
734
742
  * than the static `ui://ggui/render` one. Both registrations co-exist:
735
743
  * legacy postMessage shell at the static URI, self-contained shell at
736
744
  * the templated URI.
737
745
  *
738
746
  * Failure modes:
739
- * - Render not found → loading shell (host re-fetches; absent
747
+ * - GguiSession not found → loading shell (host re-fetches; absent
740
748
  * 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.
749
+ * - GguiSession found, no componentCode yet → loading shell.
750
+ * - GguiSession found, componentCode present → self-contained shell.
743
751
  *
744
752
  * Returns nothing; mutates the server in place.
745
753
  *
@@ -748,12 +756,12 @@ function pickComponentFromRender(render) {
748
756
  export function registerGguiRenderResourceTemplate(server, opts) {
749
757
  // TWO templates registered against the same handler core:
750
758
  //
751
- // 1. Single-segment legacy URI — `ui://ggui/render/{renderId}`.
759
+ // 1. Single-segment legacy URI — `ui://ggui/render/{sessionId}`.
752
760
  // Pre-resume-contract chats in claude.ai's history persisted
753
761
  // this shape; we keep the registration so historical messages
754
762
  // still rehydrate (loading shell on render miss).
755
763
  //
756
- // 2. Two-segment resume URI — `ui://ggui/render/{renderId}/
764
+ // 2. Two-segment resume URI — `ui://ggui/render/{sessionId}/
757
765
  // {blueprintKey}`. Stamped by every render since the resume
758
766
  // contract landed. Carries enough state for the handler to do:
759
767
  // (a) parallel render + blueprint registry lookup (no data
@@ -761,14 +769,14 @@ export function registerGguiRenderResourceTemplate(server, opts) {
761
769
  // the render is gone but the blueprint is still cached
762
770
  // (renders the original card with default props/context
763
771
  // instead of the dead loading shell).
764
- const legacyTemplate = new ResourceTemplate(`${GGUI_RENDER_RESOURCE_URI}/{renderId}`, {
772
+ const legacyTemplate = new ResourceTemplate(`${GGUI_RENDER_RESOURCE_URI}/{sessionId}`, {
765
773
  // No list-callback — the resource set is unbounded per render
766
774
  // count, and `resources/list` would leak render ids across
767
775
  // tenants. Hosts discover specific URIs via per-call `_meta.ui.
768
776
  // resourceUri` instead.
769
777
  list: undefined,
770
778
  });
771
- const resumeTemplate = new ResourceTemplate(`${GGUI_RENDER_RESOURCE_URI}/{renderId}/{blueprintKey}`, { list: undefined });
779
+ const resumeTemplate = new ResourceTemplate(`${GGUI_RENDER_RESOURCE_URI}/{sessionId}/{blueprintKey}`, { list: undefined });
772
780
  // CSP-meta block forwarded on every shell response when the
773
781
  // template was wired with `publicBaseUrl`. claude.ai's iframe
774
782
  // applies the host's restrictive default (`connect-src 'none'`)
@@ -782,7 +790,7 @@ export function registerGguiRenderResourceTemplate(server, opts) {
782
790
  * Merge gadget-declared origins from
783
791
  * {@link deriveBundleOrigins} into the base `templateCspMeta`. The
784
792
  * base only carries the publicBaseUrl origin (HTTPS + WSS); without
785
- * the per-stack-item augmentation, gadget bundle / style / API
793
+ * the per-render augmentation, gadget bundle / style / API
786
794
  * origins (Leaflet tiles, Mapbox API, Stripe SDK, …) are blocked by
787
795
  * claude.ai's iframe CSP and the component fails to render. Returns
788
796
  * `undefined` when there's no base CSP at all (publicBaseUrl
@@ -816,14 +824,14 @@ export function registerGguiRenderResourceTemplate(server, opts) {
816
824
  },
817
825
  ],
818
826
  });
819
- const loadingShell = (uri, renderId) => shellContents(uri, buildSelfContainedLoadingShell(renderId));
827
+ const loadingShell = (uri, sessionId) => shellContents(uri, buildSelfContainedLoadingShell(sessionId));
820
828
  // Single shared handler powers both templates. `blueprintKey` is
821
829
  // optional in the variables map — present for the resume URI shape,
822
830
  // absent for the legacy single-segment shape.
823
831
  async function handle(uri, variables) {
824
- const renderIdRaw = variables["renderId"];
825
- const renderId = Array.isArray(renderIdRaw) ? renderIdRaw[0] : renderIdRaw;
826
- if (typeof renderId !== "string" || renderId.length === 0) {
832
+ const sessionIdRaw = variables["sessionId"];
833
+ const sessionId = Array.isArray(sessionIdRaw) ? sessionIdRaw[0] : sessionIdRaw;
834
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
827
835
  return loadingShell(uri, "unknown");
828
836
  }
829
837
  const blueprintKeyRaw = variables["blueprintKey"];
@@ -836,15 +844,18 @@ export function registerGguiRenderResourceTemplate(server, opts) {
836
844
  // is still cached (chat-history rehydrate after render TTL or
837
845
  // process restart).
838
846
  const [stored, blueprint] = await Promise.all([
839
- opts.renderStore.get(renderId),
840
- hasResumeKey && opts.vectorStore && opts.defaultAppIdFallback
841
- ? findBlueprintExact({ vectorStore: opts.vectorStore }, opts.defaultAppIdFallback, "template", blueprintKey)
847
+ opts.renderStore.get(sessionId),
848
+ hasResumeKey && opts.vectorStore && opts.index && opts.defaultAppIdFallback
849
+ ? findBlueprintExact({ vectorStore: opts.vectorStore, index: opts.index }, opts.defaultAppIdFallback, "template",
850
+ // Resume URI carries only a contract hash — omit variantKey
851
+ // so the lookup resolves the default variant.
852
+ blueprintKey)
842
853
  : Promise.resolve(null),
843
854
  ]);
844
855
  // Happy path: render present and renderable. Mount with the live
845
856
  // state (current props, current contextSpec values).
846
857
  if (stored) {
847
- const picked = pickComponentFromRender(stored.render);
858
+ const picked = pickComponentFromGguiSession(stored.render);
848
859
  if (picked) {
849
860
  // Project the active render to the transport-agnostic bootstrap
850
861
  // view — same source of truth the render-mutation handler and
@@ -890,14 +901,14 @@ export function registerGguiRenderResourceTemplate(server, opts) {
890
901
  // validators (server-side gate is authoritative).
891
902
  }
892
903
  }
893
- if (!isSystem && codeUrl === undefined) {
894
- // Compiled-component render but no codeUrl channel available
895
- // — emit the loading shell so the operator can refresh once
896
- // codeStore is wired. Direct-render `/r/<shortCode>` falls
897
- // through to live-mode instead; this MCP-resource path has
898
- // no WS-mode fallback (resources/read is one-shot).
899
- return loadingShell(uri, renderId);
900
- }
904
+ // The codeUrl gate is applied AFTER the live-channel mint below, so
905
+ // a render with no static codeUrl still mounts via live-mode
906
+ // (wsUrl + wsToken) instead of stalling on the loading shell —
907
+ // parity with the `/r/<shortCode>` path. (See the gate after the
908
+ // mint.) This matters for deployments that wire `mintWsToken` but no
909
+ // `codeStore`/`codeBaseUrl` (e.g. the cloud pod): the agent-server
910
+ // inlines THIS resource, so without the fallback every cloud render
911
+ // hung on the dead "Generating UI…" shell.
901
912
  // Project the wrapper catalog AND the union-filtered
902
913
  // publicEnv onto the inline bootstrap so the resource-served
903
914
  // iframe matches the MCP-Apps postMessage path. Without this,
@@ -924,33 +935,59 @@ export function registerGguiRenderResourceTemplate(server, opts) {
924
935
  // state).
925
936
  let wsUrl;
926
937
  let wsToken;
938
+ let wsExpiresAt;
927
939
  if (opts.mintWsToken) {
928
940
  try {
929
- const minted = opts.mintWsToken(renderId, stored.appId);
941
+ const minted = opts.mintWsToken(sessionId, stored.appId);
930
942
  wsUrl = minted.wsUrl;
931
943
  wsToken = minted.token;
944
+ // Forward the token TTL so the iframe-runtime can degrade to
945
+ // static-only mode once it lapses (parity with the render-tool
946
+ // slice projection, render.ts). Dropping it left the live-mode
947
+ // resource shell unable to know when its WS token expired.
948
+ wsExpiresAt = minted.expiresAt;
932
949
  }
933
950
  catch {
934
951
  // Silent — falls back to static-component mode.
935
952
  }
936
953
  }
954
+ // Mount-mode gate (moved below the live-channel mint): emit the
955
+ // loading shell ONLY when NEITHER a static codeUrl channel NOR a
956
+ // live-channel wsToken is available. A deployment that wires no
957
+ // codeStore (codeUrl === undefined) but DOES wire mintWsToken now
958
+ // mounts via live-mode rather than hanging on "Generating UI…".
959
+ if (!isSystem && codeUrl === undefined && (wsUrl === undefined || wsToken === undefined)) {
960
+ return loadingShell(uri, sessionId);
961
+ }
937
962
  const html = buildSelfContainedShell({
938
- renderId,
963
+ sessionId,
939
964
  appId: stored.appId,
940
965
  ...(isSystem
941
966
  ? { systemKind: picked.kind }
942
- : {
943
- codeUrl: codeUrl,
944
- ...(codeHash !== undefined ? { codeHash } : {}),
945
- }),
967
+ : codeUrl !== undefined
968
+ ? {
969
+ codeUrl,
970
+ ...(codeHash !== undefined ? { codeHash } : {}),
971
+ }
972
+ : // No static codeUrl → live-mode (wsUrl + token spread below)
973
+ // carries the render; buildSelfContainedShell accepts
974
+ // live-mode without codeUrl.
975
+ {}),
946
976
  runtimeUrl: opts.runtimeUrl,
947
977
  ...(wsUrl !== undefined && wsToken !== undefined
948
- ? { wsUrl, token: wsToken }
978
+ ? {
979
+ wsUrl,
980
+ token: wsToken,
981
+ ...(wsExpiresAt !== undefined ? { expiresAt: wsExpiresAt } : {}),
982
+ }
949
983
  : {}),
950
984
  ...(opts.themeId !== undefined ? { themeId: opts.themeId } : {}),
951
985
  ...(opts.themeMode !== undefined ? { themeMode: opts.themeMode } : {}),
986
+ // Per-app theme overlay projected by `deriveRenderMeta` from
987
+ // the render's `theme` sidecar — forwarded so the
988
+ // resource-served iframe matches the postMessage path.
989
+ ...(view.theme !== undefined ? { theme: view.theme } : {}),
952
990
  ...(view.propsJson !== undefined ? { propsJson: view.propsJson } : {}),
953
- ...(view.actionNextSteps !== undefined ? { actionNextSteps: view.actionNextSteps } : {}),
954
991
  ...(view.contextSlots !== undefined ? { contextSlots: view.contextSlots } : {}),
955
992
  ...(view.permissionsPolicy !== undefined
956
993
  ? { permissionsPolicy: view.permissionsPolicy }
@@ -988,7 +1025,7 @@ export function registerGguiRenderResourceTemplate(server, opts) {
988
1025
  // shell.
989
1026
  if (blueprint && opts.defaultAppIdFallback) {
990
1027
  const html = await buildShellFromBlueprint({
991
- renderId,
1028
+ sessionId,
992
1029
  appId: opts.defaultAppIdFallback,
993
1030
  blueprint,
994
1031
  runtimeUrl: opts.runtimeUrl,
@@ -1002,16 +1039,16 @@ export function registerGguiRenderResourceTemplate(server, opts) {
1002
1039
  }
1003
1040
  // Fallthrough to loading shell when codeStore isn't wired.
1004
1041
  }
1005
- return loadingShell(uri, renderId);
1042
+ return loadingShell(uri, sessionId);
1006
1043
  }
1007
1044
  server.registerResource("ggui-render-self-contained", legacyTemplate, {
1008
1045
  title: "ggui render (self-contained, legacy URI)",
1009
1046
  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
1047
  mimeType: GGUI_RENDER_RESOURCE_MIME,
1011
1048
  }, handle);
1012
- server.registerResource("ggui-session-self-contained-resume", resumeTemplate, {
1049
+ server.registerResource("ggui-render-self-contained-resume", resumeTemplate, {
1013
1050
  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.",
1051
+ description: "Per-render self-contained shell — two-segment URI shape carrying both sessionId 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
1052
  mimeType: GGUI_RENDER_RESOURCE_MIME,
1016
1053
  }, handle);
1017
1054
  }
@@ -1039,9 +1076,6 @@ async function buildShellFromBlueprint(args) {
1039
1076
  : undefined;
1040
1077
  const propsJson = propsSpec ? JSON.stringify(deriveDefaultPropsValues(propsSpec)) : undefined;
1041
1078
  const contextSlots = deriveDefaultContextSlots(contract.contextSpec);
1042
- const actionNextSteps = "actionSpec" in contract && contract.actionSpec !== undefined
1043
- ? deriveWiredActionToolsFromSpec(contract.actionSpec)
1044
- : undefined;
1045
1079
  let codeUrl;
1046
1080
  let codeHash;
1047
1081
  try {
@@ -1054,7 +1088,7 @@ async function buildShellFromBlueprint(args) {
1054
1088
  return undefined;
1055
1089
  }
1056
1090
  return buildSelfContainedShell({
1057
- renderId: args.renderId,
1091
+ sessionId: args.sessionId,
1058
1092
  appId: args.appId,
1059
1093
  codeUrl,
1060
1094
  codeHash,
@@ -1063,7 +1097,6 @@ async function buildShellFromBlueprint(args) {
1063
1097
  ...(args.themeMode !== undefined ? { themeMode: args.themeMode } : {}),
1064
1098
  ...(propsJson !== undefined ? { propsJson } : {}),
1065
1099
  ...(contextSlots !== undefined ? { contextSlots } : {}),
1066
- ...(actionNextSteps !== undefined ? { actionNextSteps } : {}),
1067
1100
  });
1068
1101
  }
1069
1102
  function deriveDefaultPropsValues(spec) {
@@ -1100,18 +1133,6 @@ function deriveDefaultContextSlots(spec) {
1100
1133
  }
1101
1134
  return collected.length > 0 ? collected : undefined;
1102
1135
  }
1103
- function deriveWiredActionToolsFromSpec(spec) {
1104
- const collected = {};
1105
- for (const [name, entry] of Object.entries(spec)) {
1106
- if (entry &&
1107
- typeof entry === "object" &&
1108
- typeof entry.nextStep === "string" &&
1109
- entry.nextStep.length > 0) {
1110
- collected[name] = entry.nextStep;
1111
- }
1112
- }
1113
- return Object.keys(collected).length > 0 ? collected : undefined;
1114
- }
1115
1136
  /**
1116
1137
  * Apply the full MCP Apps outbound wiring to a fresh `McpServer` - both
1117
1138
  * the capability advertisement and the `ui://ggui/render` resource. The
@@ -1119,7 +1140,7 @@ function deriveWiredActionToolsFromSpec(spec) {
1119
1140
  * one line.
1120
1141
  *
1121
1142
  * When `selfContained` is supplied, ALSO registers the per-render
1122
- * `ui://ggui/render/{renderId}` resource template that serves the
1143
+ * `ui://ggui/render/{sessionId}` resource template that serves the
1123
1144
  * self-contained shell (the path third-party MCP Apps hosts use). The
1124
1145
  * legacy static URI registration is unconditional — first-party hosts
1125
1146
  * (Studio, Portal, console) still rely on the postMessage path.