@guuey/mcp-apps-host 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Loqu, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # @guuey/mcp-apps-host
2
+
3
+ The **Host** role of [MCP Apps (SEP-1865)](https://github.com/modelcontextprotocol/ext-apps)
4
+ for guuey's chat surfaces — and for any host that mounts MCP Apps views.
5
+
6
+ The spec's constant refrain is "Host MUST …". This package implements that
7
+ role's client-side narrowing and mount contract:
8
+
9
+ - **View-mount dispatch** (`toolResultViewMount`, `snapshotViewMount`): one
10
+ narrowing that answers "what, if anything, does this block mount?" across
11
+ the UI channels a transcript carries — an inline `ui://` resource payload,
12
+ a vendor fast-path (ggui's render shell, until its retirement per the
13
+ conformance map), or a bare `ui://` **locator**.
14
+ - **Locator rehydration**: a persisted `ui://` locator remounts by a fresh,
15
+ authenticated `resources/read` of the uri — the spec-consistent template
16
+ fetch (the spec itself defers persistence/restoration; a full remount
17
+ additionally owes the View `ui/notifications/tool-input` + its tool
18
+ result) — never by replaying stored mount material. The read transport is injected
19
+ (`UiResourceReader`); the host owns auth and user-ownership enforcement,
20
+ and a deny is byte-identical to a miss.
21
+ - **Sandbox-trust channels** (`ViewMountChannel`): which sandbox host page a
22
+ payload may mount in, until per-resource declared-CSP construction lands.
23
+
24
+ Vendor-neutral by principle: ggui's MCP Apps run on any spec-following host
25
+ (claude.ai, chatgpt.com, guuey) precisely because they follow the spec; this
26
+ package is guuey's OSS host-side support for the same spec.
27
+
28
+ Conformance status and roadmap: `docs/development/mcp-apps-host-conformance.md`
29
+ in the guuey monorepo (guuey#123).
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Pure block-walk / resource-narrowing helpers for a block-preserving agent
3
+ * transcript — no React, no DOM, so the narrowing logic stays unit-testable in
4
+ * isolation (this package's vitest runs a `node` environment) and can be shared
5
+ * by every host renderer (Studio's `AgentBlocks`, Portal-web's agent chat).
6
+ *
7
+ * The pod's AgJSON wire carries generative-UI payloads on `tool.done` events,
8
+ * which the reducer folds onto `tool-result` blocks. Two channels reach us:
9
+ *
10
+ * 1. **`uiData`** — the MCP-Apps *surface* channel. The pod's Claude facet
11
+ * routes a tool result's `structuredContent` here when the server stamped
12
+ * `_meta.ui`. Any resource here is intended as UI.
13
+ * 2. **`provider-raw` content blocks** — an MCP embedded `resource` content
14
+ * part does NOT survive as a first-class `resource` AgBlock in the Claude
15
+ * facet; it degrades to `{ type:'provider-raw', vendor, raw:<part> }`. So a
16
+ * `ui://` resource can be hiding inside `provider-raw.raw` and must be
17
+ * scanned for defensively.
18
+ *
19
+ * The resource-narrowing (opaque `JsonValue` → typed payload) mirrors the
20
+ * proven `create-agentic-app` web template — structural validation, never a cast.
21
+ */
22
+ import type { AgBlock, JsonValue } from "@silverprotocol/core";
23
+ /**
24
+ * A narrowed MCP UI resource payload — the `resources/read` `contents[]`
25
+ * entry shape of SEP-1865 (uri + mimeType + text|blob), which is also the
26
+ * pre-SEP mcp-ui embedded `resource` content-part shape. (No `_meta.ui.resource`
27
+ * path exists in the spec; recognition rides `uiData`/content parts.)
28
+ */
29
+ export interface McpUiResourcePayload {
30
+ uri: string;
31
+ mimeType?: string;
32
+ text?: string;
33
+ blob?: string;
34
+ }
35
+ /** Narrow an opaque `JsonValue` to a plain (non-array) JSON object. */
36
+ export declare function isJsonObject(v: JsonValue | undefined): v is {
37
+ [key: string]: JsonValue;
38
+ };
39
+ /**
40
+ * A JSON object → an MCP UI resource, if it has a `uri` plus renderable
41
+ * payload (`text` or base64 `blob`). Returns `undefined` for anything else.
42
+ */
43
+ export declare function asResourcePayload(v: JsonValue | undefined): McpUiResourcePayload | undefined;
44
+ /**
45
+ * Does a `tool-result` block's `uiData` carry an MCP embedded UI resource?
46
+ * Accepts the resource inlined directly, or wrapped as `{ resource: {...} }`
47
+ * (the shape an MCP `resource` content part carries). No `ui://` scheme gate
48
+ * here on purpose: `uiData` is the explicit *surface* channel (the server
49
+ * stamped `_meta.ui`), so any resource on it is meant to render.
50
+ */
51
+ export declare function asUiResource(uiData: JsonValue | undefined): McpUiResourcePayload | undefined;
52
+ /**
53
+ * Scan a `provider-raw` block's `raw` (the vendor tool_result content part)
54
+ * for a *generative-UI* resource. Unlike {@link asUiResource}, this path IS
55
+ * gated on the `ui://` scheme: `provider-raw` degradation is a lossy catch-all,
56
+ * so a plain file/text resource riding it is NOT a UI to mount — only the
57
+ * mcp-ui `ui://` convention is.
58
+ */
59
+ export declare function scanProviderRawForUiResource(raw: JsonValue | undefined): McpUiResourcePayload | undefined;
60
+ /**
61
+ * Extract a mountable UI resource from an opaque AgBlock-shaped `JsonValue`
62
+ * (used for persisted card snapshot parts, which arrive untyped). Dispatches
63
+ * by `block.type`:
64
+ * - `tool-result` → its `uiData` surface channel, then a `ui://` resource
65
+ * degraded into a `provider-raw` content part — the SAME two channels
66
+ * the live path ({@link toolResultUiResource}) mounts. The write side
67
+ * (`nocode-runtime`'s `uiCardArtifactsFromMessages`, guuey#86) persists
68
+ * card rows for both, so the snapshot arm must mount both or
69
+ * provider-raw-only cards rehydrate as placeholders.
70
+ * - `provider-raw` → a `ui://` resource hiding in `raw`
71
+ * - `resource` → a first-class embedded resource (gated on `ui://`)
72
+ * Everything else → `undefined`.
73
+ */
74
+ export declare function blockUiResource(block: JsonValue): McpUiResourcePayload | undefined;
75
+ /**
76
+ * A live `tool-result` AgBlock → its mountable UI resource, checking BOTH
77
+ * channels the Claude facet uses:
78
+ * 1. the `uiData` surface channel (server stamped `_meta.ui`), and
79
+ * 2. an embedded `ui://` resource that degraded into a `provider-raw`
80
+ * content part inside the tool result (MCP `resource` parts do NOT survive
81
+ * as first-class `resource` AgBlocks here).
82
+ * First-class `resource` content parts are intentionally not scanned in this
83
+ * typed live path (the Claude facet never emits them); the untyped card path
84
+ * ({@link blockUiResource}) covers them for other facets' persisted snapshots.
85
+ */
86
+ export declare function toolResultUiResource(block: Extract<AgBlock, {
87
+ type: "tool-result";
88
+ }>): McpUiResourcePayload | undefined;
89
+ /**
90
+ * A persisted `HistoryCard`'s `cardSnapshot` → a mountable UI resource. The
91
+ * snapshot is the verbatim `AgArtifact` the pod stored (`{ parts: AgBlock[] }`),
92
+ * so walk its `parts` for the first block that yields a resource; fall back to
93
+ * treating the snapshot root itself as a block.
94
+ *
95
+ * NOTE (`no-ggui-tools`): a ggui-rendered card carries NO inline HTML resource —
96
+ * its UI rides `_meta.ggui.bootstrap` and mounts via `@ggui-ai/react`'s
97
+ * `McpAppIframe`. That branch is OUT OF SCOPE for v1 (deferred-pending-capture).
98
+ * So a real ggui card resolves to `undefined` here and renders as the host's
99
+ * coherent placeholder, not a broken mount.
100
+ */
101
+ /**
102
+ * A `tool-result` block's `uiData.resourceUri` when it is a `ui://` locator
103
+ * (MCP-Apps durable identity), else `undefined`. Vendor-neutral: ggui renders
104
+ * are one producer of this shape.
105
+ */
106
+ export declare function uiLocator(uiData: JsonValue | undefined): string | undefined;
107
+ export declare function snapshotUiResource(cardSnapshot: JsonValue): McpUiResourcePayload | undefined;
108
+ /**
109
+ * The resource's HTML: inline `text` wins; else base64-decode `blob`. `atob`
110
+ * alone yields a Latin-1 string (mojibake on multibyte UTF-8), so decode via
111
+ * bytes + `TextDecoder`. Invalid base64 → `undefined` (no renderable payload).
112
+ */
113
+ export declare function resourceHtml(resource: McpUiResourcePayload): string | undefined;
114
+ //# sourceMappingURL=block-ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-ui.d.ts","sourceRoot":"","sources":["../src/block-ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAE/D;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,CAAC,IAAI;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAExF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,oBAAoB,GAAG,SAAS,CAU5F;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,oBAAoB,GAAG,SAAS,CAK5F;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,SAAS,GAAG,SAAS,GACzB,oBAAoB,GAAG,SAAS,CAMlC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,GAAG,oBAAoB,GAAG,SAAS,CAyBlF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC,GAC/C,oBAAoB,GAAG,SAAS,CAUlC;AAED;;;;;;;;;;;GAWG;AACH;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAI3E;AAED,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,SAAS,GAAG,oBAAoB,GAAG,SAAS,CAU5F;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,oBAAoB,GAAG,MAAM,GAAG,SAAS,CAU/E"}
@@ -0,0 +1,173 @@
1
+ /** Narrow an opaque `JsonValue` to a plain (non-array) JSON object. */
2
+ export function isJsonObject(v) {
3
+ return typeof v === "object" && v !== null && !Array.isArray(v);
4
+ }
5
+ /**
6
+ * A JSON object → an MCP UI resource, if it has a `uri` plus renderable
7
+ * payload (`text` or base64 `blob`). Returns `undefined` for anything else.
8
+ */
9
+ export function asResourcePayload(v) {
10
+ if (!isJsonObject(v))
11
+ return undefined;
12
+ if (typeof v.uri !== "string")
13
+ return undefined;
14
+ if (typeof v.text !== "string" && typeof v.blob !== "string")
15
+ return undefined;
16
+ return {
17
+ uri: v.uri,
18
+ ...(typeof v.mimeType === "string" ? { mimeType: v.mimeType } : {}),
19
+ ...(typeof v.text === "string" ? { text: v.text } : {}),
20
+ ...(typeof v.blob === "string" ? { blob: v.blob } : {}),
21
+ };
22
+ }
23
+ /**
24
+ * Does a `tool-result` block's `uiData` carry an MCP embedded UI resource?
25
+ * Accepts the resource inlined directly, or wrapped as `{ resource: {...} }`
26
+ * (the shape an MCP `resource` content part carries). No `ui://` scheme gate
27
+ * here on purpose: `uiData` is the explicit *surface* channel (the server
28
+ * stamped `_meta.ui`), so any resource on it is meant to render.
29
+ */
30
+ export function asUiResource(uiData) {
31
+ if (!isJsonObject(uiData))
32
+ return undefined;
33
+ const direct = asResourcePayload(uiData);
34
+ if (direct)
35
+ return direct;
36
+ return asResourcePayload(uiData.resource);
37
+ }
38
+ /**
39
+ * Scan a `provider-raw` block's `raw` (the vendor tool_result content part)
40
+ * for a *generative-UI* resource. Unlike {@link asUiResource}, this path IS
41
+ * gated on the `ui://` scheme: `provider-raw` degradation is a lossy catch-all,
42
+ * so a plain file/text resource riding it is NOT a UI to mount — only the
43
+ * mcp-ui `ui://` convention is.
44
+ */
45
+ export function scanProviderRawForUiResource(raw) {
46
+ if (!isJsonObject(raw))
47
+ return undefined;
48
+ const candidate = raw.resource !== undefined ? asResourcePayload(raw.resource) : asResourcePayload(raw);
49
+ if (!candidate)
50
+ return undefined;
51
+ return candidate.uri.startsWith("ui://") ? candidate : undefined;
52
+ }
53
+ /**
54
+ * Extract a mountable UI resource from an opaque AgBlock-shaped `JsonValue`
55
+ * (used for persisted card snapshot parts, which arrive untyped). Dispatches
56
+ * by `block.type`:
57
+ * - `tool-result` → its `uiData` surface channel, then a `ui://` resource
58
+ * degraded into a `provider-raw` content part — the SAME two channels
59
+ * the live path ({@link toolResultUiResource}) mounts. The write side
60
+ * (`nocode-runtime`'s `uiCardArtifactsFromMessages`, guuey#86) persists
61
+ * card rows for both, so the snapshot arm must mount both or
62
+ * provider-raw-only cards rehydrate as placeholders.
63
+ * - `provider-raw` → a `ui://` resource hiding in `raw`
64
+ * - `resource` → a first-class embedded resource (gated on `ui://`)
65
+ * Everything else → `undefined`.
66
+ */
67
+ export function blockUiResource(block) {
68
+ if (!isJsonObject(block))
69
+ return undefined;
70
+ switch (block.type) {
71
+ case "tool-result": {
72
+ const fromUiData = asUiResource(block.uiData);
73
+ if (fromUiData)
74
+ return fromUiData;
75
+ if (Array.isArray(block.content)) {
76
+ for (const part of block.content) {
77
+ if (isJsonObject(part) && part.type === "provider-raw") {
78
+ const found = scanProviderRawForUiResource(part.raw);
79
+ if (found)
80
+ return found;
81
+ }
82
+ }
83
+ }
84
+ return undefined;
85
+ }
86
+ case "provider-raw":
87
+ return scanProviderRawForUiResource(block.raw);
88
+ case "resource": {
89
+ const r = asResourcePayload(block.resource);
90
+ return r && r.uri.startsWith("ui://") ? r : undefined;
91
+ }
92
+ default:
93
+ return undefined;
94
+ }
95
+ }
96
+ /**
97
+ * A live `tool-result` AgBlock → its mountable UI resource, checking BOTH
98
+ * channels the Claude facet uses:
99
+ * 1. the `uiData` surface channel (server stamped `_meta.ui`), and
100
+ * 2. an embedded `ui://` resource that degraded into a `provider-raw`
101
+ * content part inside the tool result (MCP `resource` parts do NOT survive
102
+ * as first-class `resource` AgBlocks here).
103
+ * First-class `resource` content parts are intentionally not scanned in this
104
+ * typed live path (the Claude facet never emits them); the untyped card path
105
+ * ({@link blockUiResource}) covers them for other facets' persisted snapshots.
106
+ */
107
+ export function toolResultUiResource(block) {
108
+ const fromUiData = asUiResource(block.uiData);
109
+ if (fromUiData)
110
+ return fromUiData;
111
+ for (const part of block.content) {
112
+ if (part.type === "provider-raw") {
113
+ const found = scanProviderRawForUiResource(part.raw);
114
+ if (found)
115
+ return found;
116
+ }
117
+ }
118
+ return undefined;
119
+ }
120
+ /**
121
+ * A persisted `HistoryCard`'s `cardSnapshot` → a mountable UI resource. The
122
+ * snapshot is the verbatim `AgArtifact` the pod stored (`{ parts: AgBlock[] }`),
123
+ * so walk its `parts` for the first block that yields a resource; fall back to
124
+ * treating the snapshot root itself as a block.
125
+ *
126
+ * NOTE (`no-ggui-tools`): a ggui-rendered card carries NO inline HTML resource —
127
+ * its UI rides `_meta.ggui.bootstrap` and mounts via `@ggui-ai/react`'s
128
+ * `McpAppIframe`. That branch is OUT OF SCOPE for v1 (deferred-pending-capture).
129
+ * So a real ggui card resolves to `undefined` here and renders as the host's
130
+ * coherent placeholder, not a broken mount.
131
+ */
132
+ /**
133
+ * A `tool-result` block's `uiData.resourceUri` when it is a `ui://` locator
134
+ * (MCP-Apps durable identity), else `undefined`. Vendor-neutral: ggui renders
135
+ * are one producer of this shape.
136
+ */
137
+ export function uiLocator(uiData) {
138
+ if (!isJsonObject(uiData))
139
+ return undefined;
140
+ const uri = uiData.resourceUri;
141
+ return typeof uri === "string" && uri.startsWith("ui://") ? uri : undefined;
142
+ }
143
+ export function snapshotUiResource(cardSnapshot) {
144
+ if (!isJsonObject(cardSnapshot))
145
+ return undefined;
146
+ const parts = cardSnapshot.parts;
147
+ if (Array.isArray(parts)) {
148
+ for (const part of parts) {
149
+ const found = blockUiResource(part);
150
+ if (found)
151
+ return found;
152
+ }
153
+ }
154
+ return blockUiResource(cardSnapshot);
155
+ }
156
+ /**
157
+ * The resource's HTML: inline `text` wins; else base64-decode `blob`. `atob`
158
+ * alone yields a Latin-1 string (mojibake on multibyte UTF-8), so decode via
159
+ * bytes + `TextDecoder`. Invalid base64 → `undefined` (no renderable payload).
160
+ */
161
+ export function resourceHtml(resource) {
162
+ if (resource.text !== undefined)
163
+ return resource.text;
164
+ if (resource.blob !== undefined) {
165
+ try {
166
+ return new TextDecoder().decode(Uint8Array.from(atob(resource.blob), (c) => c.charCodeAt(0)));
167
+ }
168
+ catch {
169
+ return undefined;
170
+ }
171
+ }
172
+ return undefined;
173
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The card-mount dispatcher: ONE narrowing that answers "what, if anything,
3
+ * does this block mount?" across BOTH generative-UI channels a guuey pod
4
+ * emits.
5
+ *
6
+ * 1. **inline mcp-ui resource** — `{uri, text|blob}` on `uiData`, or a
7
+ * `ui://` resource degraded into a `provider-raw` content part. Handled
8
+ * verbatim by `block-ui.ts`; this module does not touch that path, it
9
+ * only tries it FIRST.
10
+ * 2. **ggui render** — `uiData.resourceUri` + the `_meta["ai.ggui/render"]`
11
+ * bootstrap, mounted through ggui's self-contained shell. See
12
+ * `ggui-render.ts`.
13
+ *
14
+ * Both channels land on the SAME `McpUiResourcePayload`, which is the whole
15
+ * point: a host that already mounts inline resources through
16
+ * `@mcp-ui/client`'s `AppRenderer` in a second-origin sandbox gains ggui cards
17
+ * without a second mount mechanism, a second iframe contract, or a second
18
+ * security posture to review.
19
+ *
20
+ * Precedence is inline-first and deliberate: an inline resource is the
21
+ * server's explicit, self-sufficient HTML. A ggui render only ever wins when
22
+ * there is no inline resource to prefer, so this dispatcher can never change
23
+ * what an existing inline card renders.
24
+ *
25
+ * ## Why the CHANNEL is returned alongside the resource
26
+ *
27
+ * The payload alone cannot say where it came from — a ggui shell is a string
28
+ * of HTML like any other. But a host has one decision that genuinely depends
29
+ * on the origin of that HTML: WHICH sandbox host page to mount it in. A ggui
30
+ * shell must load ggui's runtime bundle and open its WSS, so it needs a page
31
+ * whose CSP names the ggui origins; an inline card is arbitrary tenant HTML
32
+ * and must keep the self-only page it has always had. Handing back the channel
33
+ * keeps that one narrowing in one place — the alternative was for every host
34
+ * to re-run `toolResultGguiRender` beside this call and ask again.
35
+ */
36
+ import { type McpUiResourcePayload } from "./block-ui.js";
37
+ import type { AgBlock, JsonValue } from "@silverprotocol/core";
38
+ /** Which generative-UI channel produced a mount. See this module's header. */
39
+ export type ViewMountChannel = "inline" | "ggui" | "locator";
40
+ /**
41
+ * A mountable card, or the locator to fetch one with.
42
+ *
43
+ * `"inline"` — the server's own HTML, untrusted tenant content.
44
+ * `"ggui"` — a shell that boots the ggui runtime from a platform-pinned
45
+ * origin, and therefore needs a host page whose CSP allows that origin.
46
+ * `"locator"` — no mount material in hand, only the durable `ui://`
47
+ * identity (guuey#122): the host resolves it with a fresh, authenticated
48
+ * `resources/read` of the uri ({@link UiResourceReader}) — the spec-consistent
49
+ * template fetch, vendor-neutral. (The spec defers persistence/restoration
50
+ * itself; a full remount additionally owes the View `ui/notifications/tool-input`
51
+ * + its tool result — see the conformance map.) Until a reader is wired, the honest render
52
+ * is the host's own placeholder, never a stale mount.
53
+ */
54
+ export type ViewMount = {
55
+ channel: "inline" | "ggui";
56
+ /** The payload an mcp-ui host mounts, identical in shape for both channels. */
57
+ resource: McpUiResourcePayload;
58
+ } | {
59
+ channel: "locator";
60
+ /** The persisted `uiData.resourceUri` (`ui://` scheme) to re-fetch. */
61
+ resourceUri: string;
62
+ };
63
+ /**
64
+ * Resolves a `"locator"` mount by a fresh `resources/read` of the uri over
65
+ * an AUTHENTICATED channel the HOST owns — guuey must enforce its own
66
+ * user-ownership before fetching on a user's behalf, and a deny is
67
+ * byte-identical to a miss (`undefined` → placeholder, never an error
68
+ * surface). The reader returns a full {@link ViewMount} because only the
69
+ * transport knows which sandbox trust the fetched HTML needs (a ggui shell
70
+ * wants the ggui-CSP page; arbitrary tenant HTML wants the self-only page).
71
+ */
72
+ export type UiResourceReader = (resourceUri: string) => Promise<ViewMount | undefined>;
73
+ /**
74
+ * A live `tool-result` block → the card to mount, across both channels.
75
+ * `undefined` when the block carries no generative UI at all (or carries a
76
+ * ggui render whose bootstrap did not reach us — see `ggui-render.ts`).
77
+ */
78
+ export declare function toolResultViewMount(block: Extract<AgBlock, {
79
+ type: "tool-result";
80
+ }>): ViewMount | undefined;
81
+ /**
82
+ * A persisted `HistoryCard`'s `cardSnapshot` → the card to mount.
83
+ *
84
+ * There is deliberately NO bootstrap arm here (guuey#122): persistence
85
+ * strips tool-result `_meta` (see `@guuey/threads`' fold-rows), and a
86
+ * foreign snapshot that still carries one holds an expired `wsToken` — a
87
+ * dead mount. A persisted `ui://` locator resolves to the `"locator"`
88
+ * channel instead: rehydration is a fresh `resources/read` of the uri,
89
+ * the spec-consistent template fetch, vendor-neutral.
90
+ */
91
+ export declare function snapshotViewMount(cardSnapshot: JsonValue): ViewMount | undefined;
92
+ //# sourceMappingURL=card-mount.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card-mount.d.ts","sourceRoot":"","sources":["../src/card-mount.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAuD,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAE/G,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAW/D,8EAA8E;AAC9E,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAE7D;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,SAAS,GACjB;IACE,OAAO,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,EAAE,oBAAoB,CAAC;CAChC,GACD;IACE,OAAO,EAAE,SAAS,CAAC;IACnB,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEN;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;AAEvF;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC,GAC/C,SAAS,GAAG,SAAS,CAkBvB;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAUhF"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The card-mount dispatcher: ONE narrowing that answers "what, if anything,
3
+ * does this block mount?" across BOTH generative-UI channels a guuey pod
4
+ * emits.
5
+ *
6
+ * 1. **inline mcp-ui resource** — `{uri, text|blob}` on `uiData`, or a
7
+ * `ui://` resource degraded into a `provider-raw` content part. Handled
8
+ * verbatim by `block-ui.ts`; this module does not touch that path, it
9
+ * only tries it FIRST.
10
+ * 2. **ggui render** — `uiData.resourceUri` + the `_meta["ai.ggui/render"]`
11
+ * bootstrap, mounted through ggui's self-contained shell. See
12
+ * `ggui-render.ts`.
13
+ *
14
+ * Both channels land on the SAME `McpUiResourcePayload`, which is the whole
15
+ * point: a host that already mounts inline resources through
16
+ * `@mcp-ui/client`'s `AppRenderer` in a second-origin sandbox gains ggui cards
17
+ * without a second mount mechanism, a second iframe contract, or a second
18
+ * security posture to review.
19
+ *
20
+ * Precedence is inline-first and deliberate: an inline resource is the
21
+ * server's explicit, self-sufficient HTML. A ggui render only ever wins when
22
+ * there is no inline resource to prefer, so this dispatcher can never change
23
+ * what an existing inline card renders.
24
+ *
25
+ * ## Why the CHANNEL is returned alongside the resource
26
+ *
27
+ * The payload alone cannot say where it came from — a ggui shell is a string
28
+ * of HTML like any other. But a host has one decision that genuinely depends
29
+ * on the origin of that HTML: WHICH sandbox host page to mount it in. A ggui
30
+ * shell must load ggui's runtime bundle and open its WSS, so it needs a page
31
+ * whose CSP names the ggui origins; an inline card is arbitrary tenant HTML
32
+ * and must keep the self-only page it has always had. Handing back the channel
33
+ * keeps that one narrowing in one place — the alternative was for every host
34
+ * to re-run `toolResultGguiRender` beside this call and ask again.
35
+ */
36
+ import { snapshotUiResource, toolResultUiResource, uiLocator } from "./block-ui.js";
37
+ import { GGUI_RENDER_META_KEY, gguiRenderResource, toolResultGguiRender } from "./ggui-render.js";
38
+ /** Does the block's `_meta` carry the ggui render key at all (valid or not)? */
39
+ function blockCarriesGguiMetaKey(block) {
40
+ const meta = block._meta;
41
+ return (typeof meta === "object" && meta !== null && !Array.isArray(meta) && GGUI_RENDER_META_KEY in meta);
42
+ }
43
+ /**
44
+ * A live `tool-result` block → the card to mount, across both channels.
45
+ * `undefined` when the block carries no generative UI at all (or carries a
46
+ * ggui render whose bootstrap did not reach us — see `ggui-render.ts`).
47
+ */
48
+ export function toolResultViewMount(block) {
49
+ const inline = toolResultUiResource(block);
50
+ if (inline)
51
+ return { resource: inline, channel: "inline" };
52
+ const ggui = toolResultGguiRender(block);
53
+ const resource = ggui ? gguiRenderResource(ggui) : undefined;
54
+ if (resource)
55
+ return { resource, channel: "ggui" };
56
+ // A live locator whose mount material didn't reach us (a fold that
57
+ // dropped `_meta`): re-fetch works on live turns too — the resource is
58
+ // freshly minted (guuey#122). One diagnostic when `_meta` DID carry the
59
+ // vendor key but failed validation — a producer bug would otherwise be
60
+ // indistinguishable from a meta-less fold (blank UI, zero errors).
61
+ if (ggui && !ggui.bootstrap && blockCarriesGguiMetaKey(block)) {
62
+ console.warn(`mcp-apps-host: tool result ${block.toolCallId} carries a malformed ggui render bootstrap — degrading to the locator channel`);
63
+ }
64
+ const locator = uiLocator(block.uiData);
65
+ return locator !== undefined ? { channel: "locator", resourceUri: locator } : undefined;
66
+ }
67
+ /**
68
+ * A persisted `HistoryCard`'s `cardSnapshot` → the card to mount.
69
+ *
70
+ * There is deliberately NO bootstrap arm here (guuey#122): persistence
71
+ * strips tool-result `_meta` (see `@guuey/threads`' fold-rows), and a
72
+ * foreign snapshot that still carries one holds an expired `wsToken` — a
73
+ * dead mount. A persisted `ui://` locator resolves to the `"locator"`
74
+ * channel instead: rehydration is a fresh `resources/read` of the uri,
75
+ * the spec-consistent template fetch, vendor-neutral.
76
+ */
77
+ export function snapshotViewMount(cardSnapshot) {
78
+ const inline = snapshotUiResource(cardSnapshot);
79
+ if (inline)
80
+ return { resource: inline, channel: "inline" };
81
+ for (const block of snapshotBlocks(cardSnapshot)) {
82
+ if (typeof block !== "object" || block === null || Array.isArray(block))
83
+ continue;
84
+ if (block.type !== "tool-result")
85
+ continue;
86
+ const locator = uiLocator(block.uiData);
87
+ if (locator !== undefined)
88
+ return { channel: "locator", resourceUri: locator };
89
+ }
90
+ return undefined;
91
+ }
92
+ /**
93
+ * The blocks to scan inside a card snapshot: the stored `AgArtifact`'s `parts`
94
+ * when present, then the snapshot root itself — exactly `snapshotUiResource`'s own
95
+ * walk order, so both channels see the same candidates in the same order.
96
+ */
97
+ function snapshotBlocks(cardSnapshot) {
98
+ if (typeof cardSnapshot !== "object" || cardSnapshot === null || Array.isArray(cardSnapshot)) {
99
+ return [];
100
+ }
101
+ const parts = cardSnapshot.parts;
102
+ return Array.isArray(parts) ? [...parts, cardSnapshot] : [cardSnapshot];
103
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The **ggui render** channel: narrowing + self-contained shell construction
3
+ * for a generative-UI card produced by the ggui MCP server (`ggui_render`).
4
+ *
5
+ * ## Where the pieces live (guuey#108 / ggui#427)
6
+ *
7
+ * The two halves of this channel have different owners, and the module is
8
+ * split along that line:
9
+ *
10
+ * - **ggui's wire contract** — what makes a `_meta` slice a mountable
11
+ * render bootstrap, and what the self-contained shell must contain — is
12
+ * OWNED by ggui and imported from
13
+ * `@ggui-ai/protocol/integrations/mcp-apps` ({@link asGguiRenderBootstrap},
14
+ * {@link gguiShellHtml}, the `ai.ggui/render` key). This package used to
15
+ * carry byte-compatible private copies (lifted upstream as ggui#427);
16
+ * re-exporting the originals means a shell-contract change lands here by
17
+ * bumping the pin, not by mirror-editing two repos.
18
+ * - **host/silverprotocol shapes** — the `uiData`-keyed RECOGNITION signal,
19
+ * the `AgBlock` tool-result narrowing, and the `McpUiResourcePayload`
20
+ * adapter onto the host's existing mcp-ui mount path — are guuey-side
21
+ * contracts and stay implemented here.
22
+ *
23
+ * ## Why recognition and mounting are separate
24
+ *
25
+ * A ggui render's `tool.done` carries two distinct signals:
26
+ *
27
+ * 1. **`uiData.resourceUri` is the RECOGNITION signal.** It is the only part
28
+ * of the render's identity that survives `@silverprotocol/core`'s fold
29
+ * (the reducer copies `uiData` — and, as of `@silverprotocol/core`
30
+ * 0.4.1 (workspace#9), `_meta` — onto the `tool-result` block).
31
+ * 2. **`_meta["ai.ggui/render"]` is the MOUNT MATERIAL.** Everything needed
32
+ * to boot the card — which runtime bundle to load, which live-channel to
33
+ * open, which props to seed — lives there and nowhere else.
34
+ *
35
+ * The shell {@link gguiShellHtml} builds is a string of HTML, so the ggui
36
+ * card rides the host's EXISTING mcp-ui mount path unchanged: it narrows to
37
+ * the same `McpUiResourcePayload` an inline resource does, so
38
+ * `@mcp-ui/client`'s `AppRenderer` posts it as `srcdoc` into the
39
+ * second-origin `mcp-app-sandbox.html` page — same double-iframe rule, same
40
+ * sandbox origin, same opaque inner frame. No second mount mechanism.
41
+ *
42
+ * NOT in scope here: rehydrating a ggui card from persisted history. The
43
+ * bootstrap's `wsToken` expires minutes after the render, so a stored
44
+ * bootstrap is dead on arrival — a history card resolves to the `locator`
45
+ * channel (`card-mount.ts`, guuey#122) and remounts by a fresh
46
+ * `resources/read`, never a bootstrap replay.
47
+ */
48
+ import type { AgBlock, JsonValue } from "@silverprotocol/core";
49
+ import { type GguiRenderBootstrap } from "@ggui-ai/protocol/integrations/mcp-apps";
50
+ import { type McpUiResourcePayload } from "./block-ui.js";
51
+ export { asGguiRenderBootstrap, gguiShellHtml, } from "@ggui-ai/protocol/integrations/mcp-apps";
52
+ export type { GguiRenderBootstrap, GguiShellHtmlOptions, } from "@ggui-ai/protocol/integrations/mcp-apps";
53
+ /**
54
+ * The `_meta` key the ggui render bootstrap rides on. Alias of the
55
+ * protocol package's own constant — one spelling, owned upstream.
56
+ */
57
+ export declare const GGUI_RENDER_META_KEY: "ai.ggui/render";
58
+ /** A ggui render recognised on a tool result: its resource uri + mount material. */
59
+ export interface GguiRenderDescriptor {
60
+ /** `uiData.resourceUri` — `ui://ggui/render/<sessionId>/<contractHash>`. */
61
+ resourceUri: string;
62
+ /** `uiData.sessionId`, when present. */
63
+ sessionId?: string;
64
+ /**
65
+ * The `_meta["ai.ggui/render"]` slice, when it reached us. Absent for a
66
+ * persisted history card and for any consumer folding without `fold.ts`'s
67
+ * `_meta` carriage — such a descriptor is recognised but NOT mountable.
68
+ */
69
+ bootstrap?: GguiRenderBootstrap;
70
+ }
71
+ /**
72
+ * A tool result's `uiData` (+ its `_meta`, when carried) → a ggui render
73
+ * descriptor, or `undefined` for anything that is not one.
74
+ *
75
+ * The `ui://` scheme gate is deliberate: `uiData` is a general-purpose channel
76
+ * (every `structuredContent` of a `_meta.ui`-stamped tool lands there), so a
77
+ * bare `resourceUri` string is not on its own a claim of generative UI.
78
+ */
79
+ export declare function asGguiRender(uiData: JsonValue | undefined, meta: JsonValue | undefined): GguiRenderDescriptor | undefined;
80
+ /**
81
+ * A live `tool-result` AgBlock → its ggui render descriptor, if it is one.
82
+ *
83
+ * NOTE: `@ggui-ai/protocol/integrations/mcp-apps` exports a helper of the
84
+ * same name that narrows a spec-canonical MCP `CallToolResult` instead. This
85
+ * one is the silverprotocol-side twin — the input is the FOLDED block, whose
86
+ * `uiData`/`_meta` carriage is `@silverprotocol/core`'s contract, not ggui's.
87
+ */
88
+ export declare function toolResultGguiRender(block: Extract<AgBlock, {
89
+ type: "tool-result";
90
+ }>): GguiRenderDescriptor | undefined;
91
+ /** An untyped (persisted-snapshot) block → its ggui render descriptor, if it is one. */
92
+ export declare function blockGguiRender(block: JsonValue): GguiRenderDescriptor | undefined;
93
+ /**
94
+ * A ggui render descriptor → the mountable resource the host's existing
95
+ * mcp-ui path already knows how to mount, or `undefined` when the descriptor
96
+ * carries no bootstrap (history cards, and any fold that dropped `_meta`).
97
+ *
98
+ * The `uri` is the render's REAL `resourceUri` — the shell is the payload, not
99
+ * a renaming of the resource.
100
+ *
101
+ * The shell is built `background: 'transparent'`: every guuey host that
102
+ * mounts through this adapter (widget, portal web, Studio) draws its own
103
+ * card chrome around the iframe, so the host page composits behind the card.
104
+ * The upstream default (`'surface'`) is for standalone served documents —
105
+ * see `GguiShellHtmlOptions` in `@ggui-ai/protocol/integrations/mcp-apps`.
106
+ *
107
+ * **On `_meta` being required to MOUNT (but never to RECOGNISE).** Recognition
108
+ * — "this tool result is a ggui card" — is keyed on `uiData.resourceUri` alone
109
+ * and never waits for anything (see {@link asGguiRender}); nothing in this
110
+ * package is blocked on an upstream change. Mounting is different, and the
111
+ * requirement is ggui's, not ours: its runtime rejects a slice without
112
+ * `runtimeUrl` AND without at least one mode discriminator (`wsUrl`+`wsToken`,
113
+ * `codeUrl`, or `kind`) as `MALFORMED_BOOTSTRAP` and renders nothing. `uiData`
114
+ * carries none of those fields, so a bootstrap-less descriptor could only ever
115
+ * produce a blank frame; returning `undefined` and letting the host show its
116
+ * own placeholder is the honest answer, not a deferral. `@silverprotocol/core`'s
117
+ * `Reducer` is what puts `_meta` on the block for a live turn, in-repo, today.
118
+ */
119
+ export declare function gguiRenderResource(render: GguiRenderDescriptor): McpUiResourcePayload | undefined;
120
+ //# sourceMappingURL=ggui-render.d.ts.map