@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.
- package/dist/admin-blueprints-transport.d.ts.map +1 -1
- package/dist/admin-blueprints-transport.js +2 -1
- package/dist/admin-oauth-providers-transport.d.ts.map +1 -1
- package/dist/admin-oauth-providers-transport.js +7 -5
- package/dist/api-renders-routes.d.ts +85 -0
- package/dist/api-renders-routes.d.ts.map +1 -0
- package/dist/api-renders-routes.js +372 -0
- package/dist/build-mcp.d.ts +1 -1
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +39 -5
- package/dist/code-routes.d.ts +47 -0
- package/dist/code-routes.d.ts.map +1 -0
- package/dist/code-routes.js +81 -0
- package/dist/code-store-fs.js +2 -2
- package/dist/console-auth.d.ts +10 -10
- package/dist/console-auth.d.ts.map +1 -1
- package/dist/console-auth.js +5 -5
- package/dist/console-blueprint-routes.d.ts +71 -0
- package/dist/console-blueprint-routes.d.ts.map +1 -0
- package/dist/console-blueprint-routes.js +348 -0
- package/dist/console-chat-routes.d.ts +80 -0
- package/dist/console-chat-routes.d.ts.map +1 -0
- package/dist/console-chat-routes.js +182 -0
- package/dist/console-config-routes.d.ts +37 -0
- package/dist/console-config-routes.d.ts.map +1 -0
- package/dist/console-config-routes.js +91 -0
- package/dist/console-headers.d.ts +1 -1
- package/dist/console-info-routes.d.ts +84 -0
- package/dist/console-info-routes.d.ts.map +1 -0
- package/dist/console-info-routes.js +135 -0
- package/dist/console-keys-routes.d.ts +50 -0
- package/dist/console-keys-routes.d.ts.map +1 -0
- package/dist/console-keys-routes.js +222 -0
- package/dist/console-llm-keys-routes.d.ts +47 -0
- package/dist/console-llm-keys-routes.d.ts.map +1 -0
- package/dist/console-llm-keys-routes.js +443 -0
- package/dist/console-mcp-tools-routes.d.ts +41 -0
- package/dist/console-mcp-tools-routes.d.ts.map +1 -0
- package/dist/console-mcp-tools-routes.js +60 -0
- package/dist/console-registry-routes.d.ts +66 -0
- package/dist/console-registry-routes.d.ts.map +1 -0
- package/dist/console-registry-routes.js +276 -0
- package/dist/console-session-routes.d.ts +89 -0
- package/dist/console-session-routes.d.ts.map +1 -0
- package/dist/console-session-routes.js +385 -0
- package/dist/console-sessions-routes.d.ts +52 -0
- package/dist/console-sessions-routes.d.ts.map +1 -0
- package/dist/console-sessions-routes.js +106 -0
- package/dist/console-static-routes.d.ts +54 -0
- package/dist/console-static-routes.d.ts.map +1 -0
- package/dist/console-static-routes.js +190 -0
- package/dist/console-theme-routes.d.ts +3 -3
- package/dist/console-theme-routes.js +1 -1
- package/dist/console-timeline.d.ts +5 -5
- package/dist/console-timeline.d.ts.map +1 -1
- package/dist/console-timeline.js +27 -26
- package/dist/console-welcome.js +2 -2
- package/dist/email-login.d.ts.map +1 -1
- package/dist/email-login.js +2 -3
- package/dist/ggui-session-channel/action-ingress.d.ts +54 -0
- package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -0
- package/dist/ggui-session-channel/action-ingress.js +228 -0
- package/dist/ggui-session-channel/channel-subscriptions.d.ts +97 -0
- package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -0
- package/dist/ggui-session-channel/channel-subscriptions.js +224 -0
- package/dist/ggui-session-channel/internal-types.d.ts +102 -0
- package/dist/ggui-session-channel/internal-types.d.ts.map +1 -0
- package/dist/ggui-session-channel/internal-types.js +6 -0
- package/dist/ggui-session-channel/outbound.d.ts +81 -0
- package/dist/ggui-session-channel/outbound.d.ts.map +1 -0
- package/dist/ggui-session-channel/outbound.js +174 -0
- package/dist/ggui-session-channel/socket-router.d.ts +38 -0
- package/dist/ggui-session-channel/socket-router.d.ts.map +1 -0
- package/dist/ggui-session-channel/socket-router.js +213 -0
- package/dist/ggui-session-channel/subscribe.d.ts +165 -0
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -0
- package/dist/ggui-session-channel/subscribe.js +370 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +40 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.js +123 -0
- package/dist/ggui-session-channel.d.ts +425 -0
- package/dist/ggui-session-channel.d.ts.map +1 -0
- package/dist/ggui-session-channel.js +262 -0
- package/dist/health-routes.d.ts +76 -0
- package/dist/health-routes.d.ts.map +1 -0
- package/dist/health-routes.js +145 -0
- package/dist/index.d.ts +10 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -9
- package/dist/instructions-presets.d.ts +3 -3
- package/dist/instructions-presets.js +25 -25
- package/dist/llm-backed-negotiator.d.ts +68 -67
- package/dist/llm-backed-negotiator.d.ts.map +1 -1
- package/dist/llm-backed-negotiator.js +82 -248
- package/dist/mcp-apps-outbound.d.ts +47 -48
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +204 -183
- package/dist/mcp-endpoint-routes.d.ts +88 -0
- package/dist/mcp-endpoint-routes.d.ts.map +1 -0
- package/dist/mcp-endpoint-routes.js +359 -0
- package/dist/mcp-mounts.d.ts +2 -76
- package/dist/mcp-mounts.d.ts.map +1 -1
- package/dist/mcp-mounts.js +0 -76
- package/dist/oauth-as-routes.d.ts +60 -0
- package/dist/oauth-as-routes.d.ts.map +1 -0
- package/dist/oauth-as-routes.js +82 -0
- package/dist/oauth-clients-routes.d.ts +39 -0
- package/dist/oauth-clients-routes.d.ts.map +1 -0
- package/dist/oauth-clients-routes.js +87 -0
- package/dist/oauth-login-types.d.ts +1 -20
- package/dist/oauth-login-types.d.ts.map +1 -1
- package/dist/oauth-login-types.js +30 -7
- package/dist/oauth-login.d.ts.map +1 -1
- package/dist/oauth-login.js +3 -2
- package/dist/oauth-providers-store.d.ts.map +1 -1
- package/dist/oauth-providers-store.js +5 -5
- package/dist/oauth.d.ts +9 -8
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +41 -19
- package/dist/pairing-transport.d.ts.map +1 -1
- package/dist/pairing-transport.js +2 -1
- package/dist/request-context.d.ts +2 -2
- package/dist/request-context.js +2 -2
- package/dist/reserved-validators.d.ts.map +1 -1
- package/dist/reserved-validators.js +9 -1
- package/dist/route-param.d.ts +9 -0
- package/dist/route-param.d.ts.map +1 -0
- package/dist/route-param.js +10 -0
- package/dist/runtime-bundle-route.d.ts +43 -0
- package/dist/runtime-bundle-route.d.ts.map +1 -0
- package/dist/runtime-bundle-route.js +80 -0
- package/dist/schema-compat.d.ts +64 -62
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +23 -51
- package/dist/server.d.ts +179 -193
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +644 -3759
- package/dist/storage.d.ts +5 -5
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +5 -5
- package/dist/thread-transport.d.ts.map +1 -1
- package/dist/thread-transport.js +4 -3
- package/dist/user-session-auth.d.ts +7 -21
- package/dist/user-session-auth.d.ts.map +1 -1
- package/dist/user-session-auth.js +7 -28
- package/package.json +16 -15
- package/dist/mcp-apps-inbound.d.ts +0 -86
- package/dist/mcp-apps-inbound.d.ts.map +0 -1
- package/dist/mcp-apps-inbound.js +0 -283
- package/dist/render-channel.d.ts +0 -694
- package/dist/render-channel.d.ts.map +0 -1
- 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
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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
|
|
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=".../
|
|
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
|
-
* - `
|
|
74
|
-
* valid `ai.ggui/render`
|
|
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 /
|
|
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
|
|
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
|
|
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
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* `mcp-apps-outbound.test.ts` drift test recomputes
|
|
120
|
-
* loudly if
|
|
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,
|
|
131
|
-
//
|
|
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;
|
|
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:'
|
|
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": {
|
|
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('
|
|
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 (
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
// slice-
|
|
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.
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
291
|
-
//
|
|
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-
|
|
269
|
+
appInfo:{name:'ggui-render',version:'1.0.0'},
|
|
301
270
|
protocolVersion:'2026-01-26'
|
|
302
|
-
}).then(function(
|
|
271
|
+
}).then(function(){
|
|
303
272
|
clearTimeout(initTimer);
|
|
304
273
|
postNotification('ui/notifications/initialized',{});
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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) +
|
|
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
|
-
|
|
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
|
-
|
|
573
|
-
|
|
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, """)
|
|
658
652
|
.replace(/</g, "<")
|
|
659
653
|
.replace(/>/g, ">");
|
|
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(
|
|
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-
|
|
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, "&")
|
|
687
700
|
.replace(/"/g, """)
|
|
688
701
|
.replace(/</g, "<")
|
|
689
702
|
.replace(/>/g, ">")}">Generating UI…</div>
|
|
690
703
|
</body></html>`;
|
|
691
704
|
}
|
|
692
|
-
function
|
|
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
|
|
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/{
|
|
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
|
|
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
|
-
* -
|
|
747
|
+
* - GguiSession not found → loading shell (host re-fetches; absent
|
|
740
748
|
* render is a transient state immediately after `ggui_render`).
|
|
741
|
-
* -
|
|
742
|
-
* -
|
|
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/{
|
|
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/{
|
|
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}/{
|
|
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}/{
|
|
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-
|
|
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,
|
|
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
|
|
825
|
-
const
|
|
826
|
-
if (typeof
|
|
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(
|
|
840
|
-
hasResumeKey && opts.vectorStore && opts.defaultAppIdFallback
|
|
841
|
-
? findBlueprintExact({ vectorStore: opts.vectorStore }, opts.defaultAppIdFallback, "template",
|
|
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 =
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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(
|
|
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
|
-
|
|
963
|
+
sessionId,
|
|
939
964
|
appId: stored.appId,
|
|
940
965
|
...(isSystem
|
|
941
966
|
? { systemKind: picked.kind }
|
|
942
|
-
:
|
|
943
|
-
|
|
944
|
-
|
|
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
|
-
? {
|
|
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
|
-
|
|
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,
|
|
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-
|
|
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
|
|
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
|
-
|
|
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/{
|
|
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.
|