@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 +21 -0
- package/README.md +29 -0
- package/dist/block-ui.d.ts +114 -0
- package/dist/block-ui.d.ts.map +1 -0
- package/dist/block-ui.js +173 -0
- package/dist/card-mount.d.ts +92 -0
- package/dist/card-mount.d.ts.map +1 -0
- package/dist/card-mount.js +103 -0
- package/dist/ggui-render.d.ts +120 -0
- package/dist/ggui-render.d.ts.map +1 -0
- package/dist/ggui-render.js +85 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/narrowing.d.ts +9 -0
- package/dist/narrowing.d.ts.map +1 -0
- package/dist/narrowing.js +8 -0
- package/package.json +66 -0
- package/src/block-ui.ts +205 -0
- package/src/card-mount.ts +149 -0
- package/src/ggui-render.ts +168 -0
- package/src/index.ts +36 -0
- package/src/narrowing.ts +19 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ggui-render.d.ts","sourceRoot":"","sources":["../src/ggui-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAgB,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAExE,OAAO,EACL,qBAAqB,EACrB,aAAa,GACd,MAAM,yCAAyC,CAAC;AACjD,YAAY,EACV,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,yCAAyC,CAAC;AAEjD;;;GAGG;AACH,eAAO,MAAM,oBAAoB,kBAAkC,CAAC;AAKpE,oFAAoF;AACpF,MAAM,WAAW,oBAAoB;IACnC,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,mBAAmB,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,IAAI,EAAE,SAAS,GAAG,SAAS,GAC1B,oBAAoB,GAAG,SAAS,CAUlC;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC,GAC/C,oBAAoB,GAAG,SAAS,CAElC;AAED,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,GAAG,oBAAoB,GAAG,SAAS,CAIlF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,oBAAoB,GAC3B,oBAAoB,GAAG,SAAS,CAOlC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { asGguiRenderBootstrap, gguiShellHtml, MCP_APP_AI_GGUI_RENDER_META_KEY, } from "@ggui-ai/protocol/integrations/mcp-apps";
|
|
2
|
+
import { isJsonObject } from "./block-ui.js";
|
|
3
|
+
export { asGguiRenderBootstrap, gguiShellHtml, } from "@ggui-ai/protocol/integrations/mcp-apps";
|
|
4
|
+
/**
|
|
5
|
+
* The `_meta` key the ggui render bootstrap rides on. Alias of the
|
|
6
|
+
* protocol package's own constant — one spelling, owned upstream.
|
|
7
|
+
*/
|
|
8
|
+
export const GGUI_RENDER_META_KEY = MCP_APP_AI_GGUI_RENDER_META_KEY;
|
|
9
|
+
/** The `ui://` scheme prefix every ggui render resource uri carries. */
|
|
10
|
+
const UI_SCHEME = "ui://";
|
|
11
|
+
/**
|
|
12
|
+
* A tool result's `uiData` (+ its `_meta`, when carried) → a ggui render
|
|
13
|
+
* descriptor, or `undefined` for anything that is not one.
|
|
14
|
+
*
|
|
15
|
+
* The `ui://` scheme gate is deliberate: `uiData` is a general-purpose channel
|
|
16
|
+
* (every `structuredContent` of a `_meta.ui`-stamped tool lands there), so a
|
|
17
|
+
* bare `resourceUri` string is not on its own a claim of generative UI.
|
|
18
|
+
*/
|
|
19
|
+
export function asGguiRender(uiData, meta) {
|
|
20
|
+
if (!isJsonObject(uiData))
|
|
21
|
+
return undefined;
|
|
22
|
+
const resourceUri = uiData.resourceUri;
|
|
23
|
+
if (typeof resourceUri !== "string" || !resourceUri.startsWith(UI_SCHEME))
|
|
24
|
+
return undefined;
|
|
25
|
+
const bootstrap = asGguiRenderBootstrap(meta);
|
|
26
|
+
return {
|
|
27
|
+
resourceUri,
|
|
28
|
+
...(typeof uiData.sessionId === "string" ? { sessionId: uiData.sessionId } : {}),
|
|
29
|
+
...(bootstrap ? { bootstrap } : {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A live `tool-result` AgBlock → its ggui render descriptor, if it is one.
|
|
34
|
+
*
|
|
35
|
+
* NOTE: `@ggui-ai/protocol/integrations/mcp-apps` exports a helper of the
|
|
36
|
+
* same name that narrows a spec-canonical MCP `CallToolResult` instead. This
|
|
37
|
+
* one is the silverprotocol-side twin — the input is the FOLDED block, whose
|
|
38
|
+
* `uiData`/`_meta` carriage is `@silverprotocol/core`'s contract, not ggui's.
|
|
39
|
+
*/
|
|
40
|
+
export function toolResultGguiRender(block) {
|
|
41
|
+
return asGguiRender(block.uiData, block._meta);
|
|
42
|
+
}
|
|
43
|
+
/** An untyped (persisted-snapshot) block → its ggui render descriptor, if it is one. */
|
|
44
|
+
export function blockGguiRender(block) {
|
|
45
|
+
if (!isJsonObject(block))
|
|
46
|
+
return undefined;
|
|
47
|
+
if (block.type !== "tool-result")
|
|
48
|
+
return undefined;
|
|
49
|
+
return asGguiRender(block.uiData, block._meta);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A ggui render descriptor → the mountable resource the host's existing
|
|
53
|
+
* mcp-ui path already knows how to mount, or `undefined` when the descriptor
|
|
54
|
+
* carries no bootstrap (history cards, and any fold that dropped `_meta`).
|
|
55
|
+
*
|
|
56
|
+
* The `uri` is the render's REAL `resourceUri` — the shell is the payload, not
|
|
57
|
+
* a renaming of the resource.
|
|
58
|
+
*
|
|
59
|
+
* The shell is built `background: 'transparent'`: every guuey host that
|
|
60
|
+
* mounts through this adapter (widget, portal web, Studio) draws its own
|
|
61
|
+
* card chrome around the iframe, so the host page composits behind the card.
|
|
62
|
+
* The upstream default (`'surface'`) is for standalone served documents —
|
|
63
|
+
* see `GguiShellHtmlOptions` in `@ggui-ai/protocol/integrations/mcp-apps`.
|
|
64
|
+
*
|
|
65
|
+
* **On `_meta` being required to MOUNT (but never to RECOGNISE).** Recognition
|
|
66
|
+
* — "this tool result is a ggui card" — is keyed on `uiData.resourceUri` alone
|
|
67
|
+
* and never waits for anything (see {@link asGguiRender}); nothing in this
|
|
68
|
+
* package is blocked on an upstream change. Mounting is different, and the
|
|
69
|
+
* requirement is ggui's, not ours: its runtime rejects a slice without
|
|
70
|
+
* `runtimeUrl` AND without at least one mode discriminator (`wsUrl`+`wsToken`,
|
|
71
|
+
* `codeUrl`, or `kind`) as `MALFORMED_BOOTSTRAP` and renders nothing. `uiData`
|
|
72
|
+
* carries none of those fields, so a bootstrap-less descriptor could only ever
|
|
73
|
+
* produce a blank frame; returning `undefined` and letting the host show its
|
|
74
|
+
* own placeholder is the honest answer, not a deferral. `@silverprotocol/core`'s
|
|
75
|
+
* `Reducer` is what puts `_meta` on the block for a live turn, in-repo, today.
|
|
76
|
+
*/
|
|
77
|
+
export function gguiRenderResource(render) {
|
|
78
|
+
if (!render.bootstrap)
|
|
79
|
+
return undefined;
|
|
80
|
+
return {
|
|
81
|
+
uri: render.resourceUri,
|
|
82
|
+
mimeType: "text/html",
|
|
83
|
+
text: gguiShellHtml(render.bootstrap, { background: "transparent" }),
|
|
84
|
+
};
|
|
85
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @guuey/mcp-apps-host — the MCP Apps (SEP-1865) Host role: view-mount
|
|
3
|
+
* narrowing, ui:// locator rehydration, sandbox-trust channels. See README +
|
|
4
|
+
* the conformance map (guuey#123).
|
|
5
|
+
*/
|
|
6
|
+
export { asResourcePayload, asUiResource, blockUiResource, isJsonObject, resourceHtml, scanProviderRawForUiResource, snapshotUiResource, toolResultUiResource, uiLocator, type McpUiResourcePayload, } from "./block-ui.js";
|
|
7
|
+
export { asGguiRender, asGguiRenderBootstrap, blockGguiRender, gguiRenderResource, gguiShellHtml, toolResultGguiRender, GGUI_RENDER_META_KEY, type GguiRenderBootstrap, type GguiRenderDescriptor, type GguiShellHtmlOptions, } from "./ggui-render.js";
|
|
8
|
+
export { snapshotViewMount, toolResultViewMount, type UiResourceReader, type ViewMount, type ViewMountChannel, } from "./card-mount.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,4BAA4B,EAC5B,kBAAkB,EAClB,oBAAoB,EACpB,SAAS,EACT,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @guuey/mcp-apps-host — the MCP Apps (SEP-1865) Host role: view-mount
|
|
3
|
+
* narrowing, ui:// locator rehydration, sandbox-trust channels. See README +
|
|
4
|
+
* the conformance map (guuey#123).
|
|
5
|
+
*/
|
|
6
|
+
export { asResourcePayload, asUiResource, blockUiResource, isJsonObject, resourceHtml, scanProviderRawForUiResource, snapshotUiResource, toolResultUiResource, uiLocator, } from "./block-ui.js";
|
|
7
|
+
export { asGguiRender, asGguiRenderBootstrap, blockGguiRender, gguiRenderResource, gguiShellHtml, toolResultGguiRender, GGUI_RENDER_META_KEY, } from "./ggui-render.js";
|
|
8
|
+
export { snapshotViewMount, toolResultViewMount, } from "./card-mount.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol-free subpath (`@guuey/mcp-apps-host/narrowing`): ONLY the
|
|
3
|
+
* recognition/narrowing helpers, so lean consumers (e.g. `@guuey/threads`'
|
|
4
|
+
* persistence projection) never pull `@ggui-ai/protocol` into their runtime
|
|
5
|
+
* graph through the barrel (which re-exports the ggui render arm until its
|
|
6
|
+
* retirement — conformance-map step 4).
|
|
7
|
+
*/
|
|
8
|
+
export { asResourcePayload, asUiResource, blockUiResource, isJsonObject, resourceHtml, scanProviderRawForUiResource, snapshotUiResource, toolResultUiResource, uiLocator, type McpUiResourcePayload, } from "./block-ui.js";
|
|
9
|
+
//# sourceMappingURL=narrowing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"narrowing.d.ts","sourceRoot":"","sources":["../src/narrowing.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,4BAA4B,EAC5B,kBAAkB,EAClB,oBAAoB,EACpB,SAAS,EACT,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol-free subpath (`@guuey/mcp-apps-host/narrowing`): ONLY the
|
|
3
|
+
* recognition/narrowing helpers, so lean consumers (e.g. `@guuey/threads`'
|
|
4
|
+
* persistence projection) never pull `@ggui-ai/protocol` into their runtime
|
|
5
|
+
* graph through the barrel (which re-exports the ggui render arm until its
|
|
6
|
+
* retirement — conformance-map step 4).
|
|
7
|
+
*/
|
|
8
|
+
export { asResourcePayload, asUiResource, blockUiResource, isJsonObject, resourceHtml, scanProviderRawForUiResource, snapshotUiResource, toolResultUiResource, uiLocator, } from "./block-ui.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@guuey/mcp-apps-host",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "The MCP Apps (SEP-1865) Host role for guuey's chat surfaces — view-mount narrowing across UI channels, ui:// locator rehydration by resources/read, and the sandbox-trust channel contract. Vendor-neutral: any spec-following MCP App mounts through it.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"src",
|
|
12
|
+
"!src/**/*.test.ts",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"react-native": "./src/index.ts",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./narrowing": {
|
|
23
|
+
"react-native": "./src/narrowing.ts",
|
|
24
|
+
"types": "./dist/narrowing.d.ts",
|
|
25
|
+
"import": "./dist/narrowing.js",
|
|
26
|
+
"default": "./dist/narrowing.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@ggui-ai/protocol": "0.6.3",
|
|
31
|
+
"@silverprotocol/core": "0.4.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^24.0.0",
|
|
35
|
+
"typescript": "^5.0.0",
|
|
36
|
+
"vitest": "^3.0.0"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"guuey",
|
|
43
|
+
"mcp",
|
|
44
|
+
"mcp-apps",
|
|
45
|
+
"sep-1865",
|
|
46
|
+
"host",
|
|
47
|
+
"generative-ui",
|
|
48
|
+
"view"
|
|
49
|
+
],
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/withguuey/guuey-sdks.git",
|
|
53
|
+
"directory": "packages/mcp-apps-host"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://guuey.com",
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/loqu-co/guuey/issues"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsc -p tsconfig.build.json",
|
|
61
|
+
"dev": "tsc --watch",
|
|
62
|
+
"typecheck": "tsc --noEmit",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"test:watch": "vitest"
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/block-ui.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
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
|
+
/**
|
|
25
|
+
* A narrowed MCP UI resource payload — the `resources/read` `contents[]`
|
|
26
|
+
* entry shape of SEP-1865 (uri + mimeType + text|blob), which is also the
|
|
27
|
+
* pre-SEP mcp-ui embedded `resource` content-part shape. (No `_meta.ui.resource`
|
|
28
|
+
* path exists in the spec; recognition rides `uiData`/content parts.)
|
|
29
|
+
*/
|
|
30
|
+
export interface McpUiResourcePayload {
|
|
31
|
+
uri: string;
|
|
32
|
+
mimeType?: string;
|
|
33
|
+
text?: string;
|
|
34
|
+
blob?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Narrow an opaque `JsonValue` to a plain (non-array) JSON object. */
|
|
38
|
+
export function isJsonObject(v: JsonValue | undefined): v is { [key: string]: JsonValue } {
|
|
39
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A JSON object → an MCP UI resource, if it has a `uri` plus renderable
|
|
44
|
+
* payload (`text` or base64 `blob`). Returns `undefined` for anything else.
|
|
45
|
+
*/
|
|
46
|
+
export function asResourcePayload(v: JsonValue | undefined): McpUiResourcePayload | undefined {
|
|
47
|
+
if (!isJsonObject(v)) return undefined;
|
|
48
|
+
if (typeof v.uri !== "string") return undefined;
|
|
49
|
+
if (typeof v.text !== "string" && typeof v.blob !== "string") return undefined;
|
|
50
|
+
return {
|
|
51
|
+
uri: v.uri,
|
|
52
|
+
...(typeof v.mimeType === "string" ? { mimeType: v.mimeType } : {}),
|
|
53
|
+
...(typeof v.text === "string" ? { text: v.text } : {}),
|
|
54
|
+
...(typeof v.blob === "string" ? { blob: v.blob } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Does a `tool-result` block's `uiData` carry an MCP embedded UI resource?
|
|
60
|
+
* Accepts the resource inlined directly, or wrapped as `{ resource: {...} }`
|
|
61
|
+
* (the shape an MCP `resource` content part carries). No `ui://` scheme gate
|
|
62
|
+
* here on purpose: `uiData` is the explicit *surface* channel (the server
|
|
63
|
+
* stamped `_meta.ui`), so any resource on it is meant to render.
|
|
64
|
+
*/
|
|
65
|
+
export function asUiResource(uiData: JsonValue | undefined): McpUiResourcePayload | undefined {
|
|
66
|
+
if (!isJsonObject(uiData)) return undefined;
|
|
67
|
+
const direct = asResourcePayload(uiData);
|
|
68
|
+
if (direct) return direct;
|
|
69
|
+
return asResourcePayload(uiData.resource);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Scan a `provider-raw` block's `raw` (the vendor tool_result content part)
|
|
74
|
+
* for a *generative-UI* resource. Unlike {@link asUiResource}, this path IS
|
|
75
|
+
* gated on the `ui://` scheme: `provider-raw` degradation is a lossy catch-all,
|
|
76
|
+
* so a plain file/text resource riding it is NOT a UI to mount — only the
|
|
77
|
+
* mcp-ui `ui://` convention is.
|
|
78
|
+
*/
|
|
79
|
+
export function scanProviderRawForUiResource(
|
|
80
|
+
raw: JsonValue | undefined,
|
|
81
|
+
): McpUiResourcePayload | undefined {
|
|
82
|
+
if (!isJsonObject(raw)) return undefined;
|
|
83
|
+
const candidate =
|
|
84
|
+
raw.resource !== undefined ? asResourcePayload(raw.resource) : asResourcePayload(raw);
|
|
85
|
+
if (!candidate) return undefined;
|
|
86
|
+
return candidate.uri.startsWith("ui://") ? candidate : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Extract a mountable UI resource from an opaque AgBlock-shaped `JsonValue`
|
|
91
|
+
* (used for persisted card snapshot parts, which arrive untyped). Dispatches
|
|
92
|
+
* by `block.type`:
|
|
93
|
+
* - `tool-result` → its `uiData` surface channel, then a `ui://` resource
|
|
94
|
+
* degraded into a `provider-raw` content part — the SAME two channels
|
|
95
|
+
* the live path ({@link toolResultUiResource}) mounts. The write side
|
|
96
|
+
* (`nocode-runtime`'s `uiCardArtifactsFromMessages`, guuey#86) persists
|
|
97
|
+
* card rows for both, so the snapshot arm must mount both or
|
|
98
|
+
* provider-raw-only cards rehydrate as placeholders.
|
|
99
|
+
* - `provider-raw` → a `ui://` resource hiding in `raw`
|
|
100
|
+
* - `resource` → a first-class embedded resource (gated on `ui://`)
|
|
101
|
+
* Everything else → `undefined`.
|
|
102
|
+
*/
|
|
103
|
+
export function blockUiResource(block: JsonValue): McpUiResourcePayload | undefined {
|
|
104
|
+
if (!isJsonObject(block)) return undefined;
|
|
105
|
+
switch (block.type) {
|
|
106
|
+
case "tool-result": {
|
|
107
|
+
const fromUiData = asUiResource(block.uiData);
|
|
108
|
+
if (fromUiData) return fromUiData;
|
|
109
|
+
if (Array.isArray(block.content)) {
|
|
110
|
+
for (const part of block.content) {
|
|
111
|
+
if (isJsonObject(part) && part.type === "provider-raw") {
|
|
112
|
+
const found = scanProviderRawForUiResource(part.raw);
|
|
113
|
+
if (found) return found;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
case "provider-raw":
|
|
120
|
+
return scanProviderRawForUiResource(block.raw);
|
|
121
|
+
case "resource": {
|
|
122
|
+
const r = asResourcePayload(block.resource);
|
|
123
|
+
return r && r.uri.startsWith("ui://") ? r : undefined;
|
|
124
|
+
}
|
|
125
|
+
default:
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A live `tool-result` AgBlock → its mountable UI resource, checking BOTH
|
|
132
|
+
* channels the Claude facet uses:
|
|
133
|
+
* 1. the `uiData` surface channel (server stamped `_meta.ui`), and
|
|
134
|
+
* 2. an embedded `ui://` resource that degraded into a `provider-raw`
|
|
135
|
+
* content part inside the tool result (MCP `resource` parts do NOT survive
|
|
136
|
+
* as first-class `resource` AgBlocks here).
|
|
137
|
+
* First-class `resource` content parts are intentionally not scanned in this
|
|
138
|
+
* typed live path (the Claude facet never emits them); the untyped card path
|
|
139
|
+
* ({@link blockUiResource}) covers them for other facets' persisted snapshots.
|
|
140
|
+
*/
|
|
141
|
+
export function toolResultUiResource(
|
|
142
|
+
block: Extract<AgBlock, { type: "tool-result" }>,
|
|
143
|
+
): McpUiResourcePayload | undefined {
|
|
144
|
+
const fromUiData = asUiResource(block.uiData);
|
|
145
|
+
if (fromUiData) return fromUiData;
|
|
146
|
+
for (const part of block.content) {
|
|
147
|
+
if (part.type === "provider-raw") {
|
|
148
|
+
const found = scanProviderRawForUiResource(part.raw);
|
|
149
|
+
if (found) return found;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A persisted `HistoryCard`'s `cardSnapshot` → a mountable UI resource. The
|
|
157
|
+
* snapshot is the verbatim `AgArtifact` the pod stored (`{ parts: AgBlock[] }`),
|
|
158
|
+
* so walk its `parts` for the first block that yields a resource; fall back to
|
|
159
|
+
* treating the snapshot root itself as a block.
|
|
160
|
+
*
|
|
161
|
+
* NOTE (`no-ggui-tools`): a ggui-rendered card carries NO inline HTML resource —
|
|
162
|
+
* its UI rides `_meta.ggui.bootstrap` and mounts via `@ggui-ai/react`'s
|
|
163
|
+
* `McpAppIframe`. That branch is OUT OF SCOPE for v1 (deferred-pending-capture).
|
|
164
|
+
* So a real ggui card resolves to `undefined` here and renders as the host's
|
|
165
|
+
* coherent placeholder, not a broken mount.
|
|
166
|
+
*/
|
|
167
|
+
/**
|
|
168
|
+
* A `tool-result` block's `uiData.resourceUri` when it is a `ui://` locator
|
|
169
|
+
* (MCP-Apps durable identity), else `undefined`. Vendor-neutral: ggui renders
|
|
170
|
+
* are one producer of this shape.
|
|
171
|
+
*/
|
|
172
|
+
export function uiLocator(uiData: JsonValue | undefined): string | undefined {
|
|
173
|
+
if (!isJsonObject(uiData)) return undefined;
|
|
174
|
+
const uri = uiData.resourceUri;
|
|
175
|
+
return typeof uri === "string" && uri.startsWith("ui://") ? uri : undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function snapshotUiResource(cardSnapshot: JsonValue): McpUiResourcePayload | undefined {
|
|
179
|
+
if (!isJsonObject(cardSnapshot)) return undefined;
|
|
180
|
+
const parts = cardSnapshot.parts;
|
|
181
|
+
if (Array.isArray(parts)) {
|
|
182
|
+
for (const part of parts) {
|
|
183
|
+
const found = blockUiResource(part);
|
|
184
|
+
if (found) return found;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return blockUiResource(cardSnapshot);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The resource's HTML: inline `text` wins; else base64-decode `blob`. `atob`
|
|
192
|
+
* alone yields a Latin-1 string (mojibake on multibyte UTF-8), so decode via
|
|
193
|
+
* bytes + `TextDecoder`. Invalid base64 → `undefined` (no renderable payload).
|
|
194
|
+
*/
|
|
195
|
+
export function resourceHtml(resource: McpUiResourcePayload): string | undefined {
|
|
196
|
+
if (resource.text !== undefined) return resource.text;
|
|
197
|
+
if (resource.blob !== undefined) {
|
|
198
|
+
try {
|
|
199
|
+
return new TextDecoder().decode(Uint8Array.from(atob(resource.blob), (c) => c.charCodeAt(0)));
|
|
200
|
+
} catch {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
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, type McpUiResourcePayload } from "./block-ui.js";
|
|
37
|
+
import { GGUI_RENDER_META_KEY, gguiRenderResource, toolResultGguiRender } from "./ggui-render.js";
|
|
38
|
+
import type { AgBlock, JsonValue } from "@silverprotocol/core";
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
/** Does the block's `_meta` carry the ggui render key at all (valid or not)? */
|
|
42
|
+
function blockCarriesGguiMetaKey(block: Extract<AgBlock, { type: "tool-result" }>): boolean {
|
|
43
|
+
const meta = block._meta;
|
|
44
|
+
return (
|
|
45
|
+
typeof meta === "object" && meta !== null && !Array.isArray(meta) && GGUI_RENDER_META_KEY in meta
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Which generative-UI channel produced a mount. See this module's header. */
|
|
50
|
+
export type ViewMountChannel = "inline" | "ggui" | "locator";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A mountable card, or the locator to fetch one with.
|
|
54
|
+
*
|
|
55
|
+
* `"inline"` — the server's own HTML, untrusted tenant content.
|
|
56
|
+
* `"ggui"` — a shell that boots the ggui runtime from a platform-pinned
|
|
57
|
+
* origin, and therefore needs a host page whose CSP allows that origin.
|
|
58
|
+
* `"locator"` — no mount material in hand, only the durable `ui://`
|
|
59
|
+
* identity (guuey#122): the host resolves it with a fresh, authenticated
|
|
60
|
+
* `resources/read` of the uri ({@link UiResourceReader}) — the spec-consistent
|
|
61
|
+
* template fetch, vendor-neutral. (The spec defers persistence/restoration
|
|
62
|
+
* itself; a full remount additionally owes the View `ui/notifications/tool-input`
|
|
63
|
+
* + its tool result — see the conformance map.) Until a reader is wired, the honest render
|
|
64
|
+
* is the host's own placeholder, never a stale mount.
|
|
65
|
+
*/
|
|
66
|
+
export type ViewMount =
|
|
67
|
+
| {
|
|
68
|
+
channel: "inline" | "ggui";
|
|
69
|
+
/** The payload an mcp-ui host mounts, identical in shape for both channels. */
|
|
70
|
+
resource: McpUiResourcePayload;
|
|
71
|
+
}
|
|
72
|
+
| {
|
|
73
|
+
channel: "locator";
|
|
74
|
+
/** The persisted `uiData.resourceUri` (`ui://` scheme) to re-fetch. */
|
|
75
|
+
resourceUri: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolves a `"locator"` mount by a fresh `resources/read` of the uri over
|
|
80
|
+
* an AUTHENTICATED channel the HOST owns — guuey must enforce its own
|
|
81
|
+
* user-ownership before fetching on a user's behalf, and a deny is
|
|
82
|
+
* byte-identical to a miss (`undefined` → placeholder, never an error
|
|
83
|
+
* surface). The reader returns a full {@link ViewMount} because only the
|
|
84
|
+
* transport knows which sandbox trust the fetched HTML needs (a ggui shell
|
|
85
|
+
* wants the ggui-CSP page; arbitrary tenant HTML wants the self-only page).
|
|
86
|
+
*/
|
|
87
|
+
export type UiResourceReader = (resourceUri: string) => Promise<ViewMount | undefined>;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A live `tool-result` block → the card to mount, across both channels.
|
|
91
|
+
* `undefined` when the block carries no generative UI at all (or carries a
|
|
92
|
+
* ggui render whose bootstrap did not reach us — see `ggui-render.ts`).
|
|
93
|
+
*/
|
|
94
|
+
export function toolResultViewMount(
|
|
95
|
+
block: Extract<AgBlock, { type: "tool-result" }>,
|
|
96
|
+
): ViewMount | undefined {
|
|
97
|
+
const inline = toolResultUiResource(block);
|
|
98
|
+
if (inline) return { resource: inline, channel: "inline" };
|
|
99
|
+
const ggui = toolResultGguiRender(block);
|
|
100
|
+
const resource = ggui ? gguiRenderResource(ggui) : undefined;
|
|
101
|
+
if (resource) return { resource, channel: "ggui" };
|
|
102
|
+
// A live locator whose mount material didn't reach us (a fold that
|
|
103
|
+
// dropped `_meta`): re-fetch works on live turns too — the resource is
|
|
104
|
+
// freshly minted (guuey#122). One diagnostic when `_meta` DID carry the
|
|
105
|
+
// vendor key but failed validation — a producer bug would otherwise be
|
|
106
|
+
// indistinguishable from a meta-less fold (blank UI, zero errors).
|
|
107
|
+
if (ggui && !ggui.bootstrap && blockCarriesGguiMetaKey(block)) {
|
|
108
|
+
console.warn(
|
|
109
|
+
`mcp-apps-host: tool result ${block.toolCallId} carries a malformed ggui render bootstrap — degrading to the locator channel`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const locator = uiLocator(block.uiData);
|
|
113
|
+
return locator !== undefined ? { channel: "locator", resourceUri: locator } : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A persisted `HistoryCard`'s `cardSnapshot` → the card to mount.
|
|
118
|
+
*
|
|
119
|
+
* There is deliberately NO bootstrap arm here (guuey#122): persistence
|
|
120
|
+
* strips tool-result `_meta` (see `@guuey/threads`' fold-rows), and a
|
|
121
|
+
* foreign snapshot that still carries one holds an expired `wsToken` — a
|
|
122
|
+
* dead mount. A persisted `ui://` locator resolves to the `"locator"`
|
|
123
|
+
* channel instead: rehydration is a fresh `resources/read` of the uri,
|
|
124
|
+
* the spec-consistent template fetch, vendor-neutral.
|
|
125
|
+
*/
|
|
126
|
+
export function snapshotViewMount(cardSnapshot: JsonValue): ViewMount | undefined {
|
|
127
|
+
const inline = snapshotUiResource(cardSnapshot);
|
|
128
|
+
if (inline) return { resource: inline, channel: "inline" };
|
|
129
|
+
for (const block of snapshotBlocks(cardSnapshot)) {
|
|
130
|
+
if (typeof block !== "object" || block === null || Array.isArray(block)) continue;
|
|
131
|
+
if (block.type !== "tool-result") continue;
|
|
132
|
+
const locator = uiLocator(block.uiData);
|
|
133
|
+
if (locator !== undefined) return { channel: "locator", resourceUri: locator };
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The blocks to scan inside a card snapshot: the stored `AgArtifact`'s `parts`
|
|
140
|
+
* when present, then the snapshot root itself — exactly `snapshotUiResource`'s own
|
|
141
|
+
* walk order, so both channels see the same candidates in the same order.
|
|
142
|
+
*/
|
|
143
|
+
function snapshotBlocks(cardSnapshot: JsonValue): JsonValue[] {
|
|
144
|
+
if (typeof cardSnapshot !== "object" || cardSnapshot === null || Array.isArray(cardSnapshot)) {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
const parts = cardSnapshot.parts;
|
|
148
|
+
return Array.isArray(parts) ? [...parts, cardSnapshot] : [cardSnapshot];
|
|
149
|
+
}
|