@guuey/mcp-apps-host 0.3.1 → 0.5.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/README.md +18 -0
- package/dist/action.d.ts +95 -0
- package/dist/action.d.ts.map +1 -0
- package/dist/action.js +111 -0
- package/dist/card-mount.d.ts +18 -0
- package/dist/card-mount.d.ts.map +1 -1
- package/dist/card-mount.js +25 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/react.d.ts +94 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +158 -0
- package/dist/sandbox-page.d.ts +75 -0
- package/dist/sandbox-page.d.ts.map +1 -0
- package/dist/sandbox-page.js +40 -0
- package/dist/view-host-protocol.d.ts +167 -0
- package/dist/view-host-protocol.d.ts.map +1 -0
- package/dist/view-host-protocol.js +168 -0
- package/dist/view-host.d.ts +119 -0
- package/dist/view-host.d.ts.map +1 -0
- package/dist/view-host.js +143 -0
- package/package.json +21 -4
- package/src/action.ts +171 -0
- package/src/card-mount.ts +27 -0
- package/src/index.ts +44 -0
- package/src/react.tsx +268 -0
- package/src/sandbox-page.ts +116 -0
- package/src/view-host-protocol.ts +299 -0
- package/src/view-host.ts +241 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `attachViewHost` — the DOM glue around `view-host-protocol.ts`'s pure
|
|
3
|
+
* machine (guuey#186 Gap 1). Framework-agnostic: any embedder with an
|
|
4
|
+
* iframe can play the MCP Apps Host role with one call; `react.tsx` is one
|
|
5
|
+
* convenience composition of exactly this, and a future full transcript
|
|
6
|
+
* renderer composes the same primitive differently.
|
|
7
|
+
*
|
|
8
|
+
* This module is glue by DESIGN — every decision (what to answer, what to
|
|
9
|
+
* refuse, when the negotiation window lapses) lives in the machine, which
|
|
10
|
+
* the Node-only publish gate can test. What genuinely needs a browser is
|
|
11
|
+
* this file's five moves: listen, identity-filter, post, time, detach —
|
|
12
|
+
* covered by the monorepo's Playwright leg (`e2e/`), which the guuey-sdks
|
|
13
|
+
* mirror deliberately does not carry.
|
|
14
|
+
*
|
|
15
|
+
* ## The identity filter (security invariant)
|
|
16
|
+
*
|
|
17
|
+
* Messages are matched by `event.source === frame.contentWindow`, NEVER by
|
|
18
|
+
* `event.origin`: a view frame runs `sandbox="allow-scripts"` WITHOUT
|
|
19
|
+
* `allow-same-origin`, so its origin is opaque — every message it posts
|
|
20
|
+
* carries `"null"`, a value every other sandboxed frame on the page
|
|
21
|
+
* shares, identifying nobody. The window handle is the only identity that
|
|
22
|
+
* names the frame; a frame with no `contentWindow` matches nothing rather
|
|
23
|
+
* than everything. Responses target `'*'` for the same reason: an opaque
|
|
24
|
+
* origin is not addressable by name, and the handshake payload carries no
|
|
25
|
+
* secrets — it is the result the spec defines for any host.
|
|
26
|
+
*
|
|
27
|
+
* Seeded from ggui's console `surface-host.ts` (donated, guuey#186 audit);
|
|
28
|
+
* re-derived here against the pure machine + our own tests.
|
|
29
|
+
*/
|
|
30
|
+
import { initialViewHostState, teardownMessage, toolCallResponse, viewHostElapsed, viewHostReceive, } from "./view-host-protocol.js";
|
|
31
|
+
import { unavailableToolCallResult, } from "./action.js";
|
|
32
|
+
const DEFAULT_HOST_INFO = { name: "guuey-view-host", version: "1" };
|
|
33
|
+
const DEFAULT_NEGOTIATION_TIMEOUT_MS = 8000;
|
|
34
|
+
/** Derived + configured context, per the {@link AttachViewHostConfig.hostContext} contract. */
|
|
35
|
+
function hostContextFor(frame, config) {
|
|
36
|
+
return {
|
|
37
|
+
locale: typeof navigator !== "undefined" ? navigator.language : "en-US",
|
|
38
|
+
...(frame.clientWidth > 0 && frame.clientHeight > 0
|
|
39
|
+
? { containerDimensions: { width: frame.clientWidth, height: frame.clientHeight } }
|
|
40
|
+
: {}),
|
|
41
|
+
...config.hostContext,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function behaviorFor(frame, config) {
|
|
45
|
+
const relayWired = config.onCallTool !== undefined && config.resourceUri !== undefined;
|
|
46
|
+
return {
|
|
47
|
+
hostInfo: config.hostInfo ?? DEFAULT_HOST_INFO,
|
|
48
|
+
hostCapabilities: {
|
|
49
|
+
...(relayWired ? { serverTools: {} } : {}),
|
|
50
|
+
...config.hostCapabilities,
|
|
51
|
+
},
|
|
52
|
+
hostContext: hostContextFor(frame, config),
|
|
53
|
+
toolRelay: relayWired,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Attach the Host role to a mounted view frame. Returns a detach function;
|
|
58
|
+
* call it before the frame unmounts — it stops listening and posts the
|
|
59
|
+
* spec-mannered `ui/resource-teardown` farewell through the CACHED window
|
|
60
|
+
* handle (post-removal, `frame.contentWindow` is already null).
|
|
61
|
+
*/
|
|
62
|
+
export function attachViewHost(frame, config = {}) {
|
|
63
|
+
const cachedWindow = frame.contentWindow;
|
|
64
|
+
let state = initialViewHostState();
|
|
65
|
+
const setState = (next) => {
|
|
66
|
+
const phaseChanged = next.phase !== state.phase;
|
|
67
|
+
state = next;
|
|
68
|
+
if (phaseChanged)
|
|
69
|
+
config.onPhaseChange?.(next.phase);
|
|
70
|
+
};
|
|
71
|
+
const post = (message) => {
|
|
72
|
+
frame.contentWindow?.postMessage(message, "*");
|
|
73
|
+
};
|
|
74
|
+
const relay = (id, name, args) => {
|
|
75
|
+
const { onCallTool, resourceUri } = config;
|
|
76
|
+
// The machine only emits the effect when the relay is wired (behavior
|
|
77
|
+
// is derived from this same config), so these are invariants, not
|
|
78
|
+
// runtime branches a view can steer.
|
|
79
|
+
if (onCallTool === undefined || resourceUri === undefined)
|
|
80
|
+
return;
|
|
81
|
+
onCallTool({ resourceUri, name, ...(args === undefined ? {} : { arguments: args }) }).then((result) => post(toolCallResponse(id, result)),
|
|
82
|
+
// A relay hook that rejects (createMcpUiActionRelay never does, but
|
|
83
|
+
// the hook is embedder code) still owes the view an answer — the
|
|
84
|
+
// same in-band unavailable the relay itself uses, never a hang.
|
|
85
|
+
() => post(toolCallResponse(id, unavailableToolCallResult())));
|
|
86
|
+
};
|
|
87
|
+
const onMessage = (event) => {
|
|
88
|
+
if (frame.contentWindow === null || event.source !== frame.contentWindow)
|
|
89
|
+
return;
|
|
90
|
+
const { state: next, effects } = viewHostReceive(state, behaviorFor(frame, config), event.data);
|
|
91
|
+
setState(next);
|
|
92
|
+
for (const effect of effects) {
|
|
93
|
+
if (effect.kind === "respond")
|
|
94
|
+
post(effect.message);
|
|
95
|
+
else
|
|
96
|
+
relay(effect.id, effect.name, effect.arguments);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
// One listener, two subscription paths: the injectable seam for Node
|
|
100
|
+
// tests, and `window` — whose lib.dom listener typing wants the concrete
|
|
101
|
+
// `MessageEvent` — for the browser default.
|
|
102
|
+
const subscribe = () => {
|
|
103
|
+
const { events } = config;
|
|
104
|
+
if (events !== undefined) {
|
|
105
|
+
events.addEventListener("message", onMessage);
|
|
106
|
+
return () => events.removeEventListener("message", onMessage);
|
|
107
|
+
}
|
|
108
|
+
const domListener = (event) => onMessage(event);
|
|
109
|
+
window.addEventListener("message", domListener);
|
|
110
|
+
return () => window.removeEventListener("message", domListener);
|
|
111
|
+
};
|
|
112
|
+
const unsubscribe = subscribe();
|
|
113
|
+
const timeoutMs = config.negotiationTimeoutMs ?? DEFAULT_NEGOTIATION_TIMEOUT_MS;
|
|
114
|
+
const timer = timeoutMs > 0 ? setTimeout(() => setState(viewHostElapsed(state)), timeoutMs) : undefined;
|
|
115
|
+
return () => {
|
|
116
|
+
if (timer !== undefined)
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
unsubscribe();
|
|
119
|
+
cachedWindow?.postMessage(teardownMessage(), "*");
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The document a {@link McpUiResourcePayload} mounts: `text` verbatim, or
|
|
124
|
+
* `blob` base64-decoded as UTF-8. `undefined` when the payload carries
|
|
125
|
+
* neither — nothing to put in `srcdoc`.
|
|
126
|
+
*/
|
|
127
|
+
export function viewDocumentHtml(resource) {
|
|
128
|
+
if (typeof resource.text === "string")
|
|
129
|
+
return resource.text;
|
|
130
|
+
if (typeof resource.blob === "string") {
|
|
131
|
+
try {
|
|
132
|
+
const bytes = Uint8Array.from(atob(resource.blob), (c) => c.charCodeAt(0));
|
|
133
|
+
return new TextDecoder().decode(bytes);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Malformed base64 is producer-side wire data, not an embedder bug —
|
|
137
|
+
// the honest answer is "no document" (the same labeled state a
|
|
138
|
+
// payload with neither field gets), not a render-time throw.
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guuey/mcp-apps-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -24,14 +24,31 @@
|
|
|
24
24
|
"types": "./dist/narrowing.d.ts",
|
|
25
25
|
"import": "./dist/narrowing.js",
|
|
26
26
|
"default": "./dist/narrowing.js"
|
|
27
|
+
},
|
|
28
|
+
"./react": {
|
|
29
|
+
"react-native": "./src/react.tsx",
|
|
30
|
+
"types": "./dist/react.d.ts",
|
|
31
|
+
"import": "./dist/react.js",
|
|
32
|
+
"default": "./dist/react.js"
|
|
27
33
|
}
|
|
28
34
|
},
|
|
29
35
|
"dependencies": {
|
|
30
|
-
"@ggui-ai/protocol": "0.
|
|
36
|
+
"@ggui-ai/protocol": "0.9.0",
|
|
37
|
+
"@modelcontextprotocol/ext-apps": "1.7.5",
|
|
31
38
|
"@silverprotocol/core": "0.4.1"
|
|
32
39
|
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"react": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
33
48
|
"devDependencies": {
|
|
34
49
|
"@types/node": "^24.0.0",
|
|
50
|
+
"@types/react": "^19.0.0",
|
|
51
|
+
"react": "^19.0.0",
|
|
35
52
|
"typescript": "^5.0.0",
|
|
36
53
|
"vitest": "^3.0.0"
|
|
37
54
|
},
|
|
@@ -52,10 +69,10 @@
|
|
|
52
69
|
"url": "git+https://github.com/withguuey/guuey-sdks.git",
|
|
53
70
|
"directory": "packages/mcp-apps-host"
|
|
54
71
|
},
|
|
55
|
-
"homepage": "https://guuey.com",
|
|
56
72
|
"bugs": {
|
|
57
|
-
"url": "https://github.com/
|
|
73
|
+
"url": "https://github.com/withguuey/guuey-sdks/issues"
|
|
58
74
|
},
|
|
75
|
+
"homepage": "https://guuey.com",
|
|
59
76
|
"scripts": {
|
|
60
77
|
"build": "tsc -p tsconfig.build.json",
|
|
61
78
|
"dev": "tsc --watch",
|
package/src/action.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Host role's ACTION side (guuey#158) — the `tools/call` sibling of
|
|
3
|
+
* `reader.ts`. A mounted card's sandbox posts a runtime action to the host
|
|
4
|
+
* (SEP-1865 / ggui's relay-host contract); the host relays it over an
|
|
5
|
+
* AUTHENTICATED transport it owns, and hands the result back in-band. Two
|
|
6
|
+
* invariants mirror the reader:
|
|
7
|
+
*
|
|
8
|
+
* - the relay NEVER throws into the sandbox bridge: allowlist miss,
|
|
9
|
+
* transport failure, and un-narrowable answers all collapse to an
|
|
10
|
+
* in-band `isError` result the card can display;
|
|
11
|
+
* - runtime re-narrowing, not trust: transports are host-supplied and the
|
|
12
|
+
* upstream answer is wire data — every content arm is re-checked before
|
|
13
|
+
* it crosses into the sandbox.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The wire arms a relay hands back to the sandbox — re-narrowed, never trusted. */
|
|
17
|
+
export type McpToolCallContent =
|
|
18
|
+
| { type: "text"; text: string }
|
|
19
|
+
| { type: "image"; data: string; mimeType: string }
|
|
20
|
+
| {
|
|
21
|
+
type: "resource";
|
|
22
|
+
resource: { uri: string; mimeType?: string } & (
|
|
23
|
+
| { text: string }
|
|
24
|
+
| { blob: string }
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `structuredContent` is protocol-open by design (the MCP spec types it as
|
|
30
|
+
* an arbitrary JSON object) — the index signature is the honest wire type,
|
|
31
|
+
* not an erasure of a known shape.
|
|
32
|
+
*/
|
|
33
|
+
export type McpToolStructuredContent = {
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The SEP-1865 CallToolResult surface a host hands back to the sandbox.
|
|
39
|
+
* A `type` alias, deliberately: the MCP SDK's own result types carry Zod
|
|
40
|
+
* passthrough index signatures, and only type aliases (never interfaces)
|
|
41
|
+
* get the implicit index signature that makes this assignable to them.
|
|
42
|
+
*/
|
|
43
|
+
export type McpToolCallResult = {
|
|
44
|
+
content: McpToolCallContent[];
|
|
45
|
+
isError?: boolean;
|
|
46
|
+
structuredContent?: McpToolStructuredContent;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The runtime-action tools a card sandbox may relay — the client-side twin
|
|
51
|
+
* of the server allowlist (defense in depth: the proxy enforces it again).
|
|
52
|
+
*/
|
|
53
|
+
export const UI_ACTION_TOOLS: ReadonlySet<string> = new Set([
|
|
54
|
+
"ggui_runtime_submit_action",
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/** The in-band answer for anything the relay cannot (or will not) do. */
|
|
58
|
+
export const UI_ACTION_UNAVAILABLE_TEXT =
|
|
59
|
+
"This action isn't available right now.";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The in-band `isError` result for an action that cannot be performed —
|
|
63
|
+
* the relay's own refusals use it, and `attachViewHost` posts it when an
|
|
64
|
+
* embedder-supplied relay hook rejects (the view is always answered).
|
|
65
|
+
*/
|
|
66
|
+
export function unavailableToolCallResult(): McpToolCallResult {
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: UI_ACTION_UNAVAILABLE_TEXT }],
|
|
69
|
+
isError: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isJsonObjectLike(value: unknown): value is McpToolStructuredContent {
|
|
74
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function asContentArm(value: unknown): McpToolCallContent | undefined {
|
|
78
|
+
if (!isJsonObjectLike(value)) return undefined;
|
|
79
|
+
const type = value["type"];
|
|
80
|
+
if (type === "text" && typeof value["text"] === "string") {
|
|
81
|
+
return { type: "text", text: value["text"] };
|
|
82
|
+
}
|
|
83
|
+
if (
|
|
84
|
+
type === "image" &&
|
|
85
|
+
typeof value["data"] === "string" &&
|
|
86
|
+
typeof value["mimeType"] === "string"
|
|
87
|
+
) {
|
|
88
|
+
return { type: "image", data: value["data"], mimeType: value["mimeType"] };
|
|
89
|
+
}
|
|
90
|
+
if (type === "resource" && isJsonObjectLike(value["resource"])) {
|
|
91
|
+
const res = value["resource"];
|
|
92
|
+
if (typeof res["uri"] !== "string") return undefined;
|
|
93
|
+
const mimeType =
|
|
94
|
+
typeof res["mimeType"] === "string" ? { mimeType: res["mimeType"] } : {};
|
|
95
|
+
if (typeof res["text"] === "string") {
|
|
96
|
+
return { type: "resource", resource: { uri: res["uri"], ...mimeType, text: res["text"] } };
|
|
97
|
+
}
|
|
98
|
+
if (typeof res["blob"] === "string") {
|
|
99
|
+
return { type: "resource", resource: { uri: res["uri"], ...mimeType, blob: res["blob"] } };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Narrow an untrusted `tools/call` answer to the arms the sandbox may see.
|
|
107
|
+
* Unknown content arms are DROPPED (never forwarded opaque); a value that
|
|
108
|
+
* is not result-shaped at all is `undefined` (the relay answers in-band).
|
|
109
|
+
*/
|
|
110
|
+
export function asToolCallResult(value: unknown): McpToolCallResult | undefined {
|
|
111
|
+
if (!isJsonObjectLike(value)) return undefined;
|
|
112
|
+
const rawContent = value["content"];
|
|
113
|
+
if (!Array.isArray(rawContent)) return undefined;
|
|
114
|
+
const content: McpToolCallContent[] = [];
|
|
115
|
+
for (const entry of rawContent) {
|
|
116
|
+
const arm = asContentArm(entry);
|
|
117
|
+
if (arm) content.push(arm);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
content,
|
|
121
|
+
...(value["isError"] === true ? { isError: true } : {}),
|
|
122
|
+
...(isJsonObjectLike(value["structuredContent"])
|
|
123
|
+
? { structuredContent: value["structuredContent"] }
|
|
124
|
+
: {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The host-supplied transport {@link createMcpUiActionRelay} assembles over. */
|
|
129
|
+
export interface CreateMcpUiActionRelayDeps {
|
|
130
|
+
/**
|
|
131
|
+
* One `tools/call` bound to the mounted card's locator `uri` over the
|
|
132
|
+
* host's authenticated channel. Returns the raw result (narrowed here),
|
|
133
|
+
* or `undefined` when the upstream denied/lost the session. Throwing is
|
|
134
|
+
* treated as unavailable.
|
|
135
|
+
*/
|
|
136
|
+
callTool: (
|
|
137
|
+
uri: string,
|
|
138
|
+
name: string,
|
|
139
|
+
args: McpToolStructuredContent | undefined,
|
|
140
|
+
) => Promise<unknown>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The request shape a mounted card's `onCallTool` bridge produces. */
|
|
144
|
+
export interface UiActionRequest {
|
|
145
|
+
/** The mounted card's persisted `ui://` locator — the action's scope. */
|
|
146
|
+
resourceUri: string;
|
|
147
|
+
name: string;
|
|
148
|
+
arguments?: McpToolStructuredContent;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Assemble the sandbox-facing action relay from a host transport. The
|
|
153
|
+
* returned function is shaped for an `onCallTool` bridge: it always
|
|
154
|
+
* resolves (never rejects), answering in-band.
|
|
155
|
+
*/
|
|
156
|
+
export function createMcpUiActionRelay(
|
|
157
|
+
deps: CreateMcpUiActionRelayDeps,
|
|
158
|
+
): (request: UiActionRequest) => Promise<McpToolCallResult> {
|
|
159
|
+
return async (request) => {
|
|
160
|
+
if (!UI_ACTION_TOOLS.has(request.name)) return unavailableToolCallResult();
|
|
161
|
+
if (!request.resourceUri.startsWith("ui://")) return unavailableToolCallResult();
|
|
162
|
+
let raw: unknown;
|
|
163
|
+
try {
|
|
164
|
+
raw = await deps.callTool(request.resourceUri, request.name, request.arguments);
|
|
165
|
+
} catch {
|
|
166
|
+
return unavailableToolCallResult(); // transport failure == unavailable, in-band
|
|
167
|
+
}
|
|
168
|
+
if (raw === undefined) return unavailableToolCallResult();
|
|
169
|
+
return asToolCallResult(raw) ?? unavailableToolCallResult();
|
|
170
|
+
};
|
|
171
|
+
}
|
package/src/card-mount.ts
CHANGED
|
@@ -143,6 +143,33 @@ export function snapshotViewMount(cardSnapshot: JsonValue): ViewMount | undefine
|
|
|
143
143
|
return undefined;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* The resolved-only convenience over the mount union (guuey#186 G6): every
|
|
148
|
+
* consumer that renders was writing the same two-call walk — narrow the
|
|
149
|
+
* union, then feed the `"locator"` arm to a reader. This collapses it:
|
|
150
|
+
*
|
|
151
|
+
* - already-resolved mounts pass through untouched (no reader round-trip);
|
|
152
|
+
* - a `"locator"` arm resolves via the reader — or the honest `undefined`
|
|
153
|
+
* (placeholder) when no reader is wired: never a stale mount;
|
|
154
|
+
* - a reader that answers with ANOTHER locator is treated as a miss. The
|
|
155
|
+
* {@link UiResourceReader} contract says a read yields mount material or
|
|
156
|
+
* nothing (guuey#127); a locator answer would loop, so the honest
|
|
157
|
+
* reading is "could not resolve", not recursion.
|
|
158
|
+
*
|
|
159
|
+
* Takes `ViewMount | undefined` so it chains directly off
|
|
160
|
+
* `toolResultViewMount`/`snapshotViewMount` without a narrowing dance at
|
|
161
|
+
* the call site.
|
|
162
|
+
*/
|
|
163
|
+
export async function resolveViewMount(
|
|
164
|
+
mount: ViewMount | undefined,
|
|
165
|
+
reader?: UiResourceReader,
|
|
166
|
+
): Promise<ResolvedViewMount | undefined> {
|
|
167
|
+
if (mount === undefined || mount.channel !== "locator") return mount;
|
|
168
|
+
if (reader === undefined) return undefined;
|
|
169
|
+
const read = await reader(mount.resourceUri);
|
|
170
|
+
return read === undefined || read.channel === "locator" ? undefined : read;
|
|
171
|
+
}
|
|
172
|
+
|
|
146
173
|
/**
|
|
147
174
|
* The blocks to scan inside a card snapshot: the stored `AgArtifact`'s `parts`
|
|
148
175
|
* when present, then the snapshot root itself — exactly `snapshotUiResource`'s own
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export {
|
|
|
28
28
|
type GguiShellHtmlOptions,
|
|
29
29
|
} from "./ggui-render.js";
|
|
30
30
|
export {
|
|
31
|
+
resolveViewMount,
|
|
31
32
|
snapshotViewMount,
|
|
32
33
|
toolResultViewMount,
|
|
33
34
|
type LocatorViewMount,
|
|
@@ -42,3 +43,46 @@ export {
|
|
|
42
43
|
type CreateMcpUiResourceReaderDeps,
|
|
43
44
|
type McpResourceReadResult,
|
|
44
45
|
} from "./reader.js";
|
|
46
|
+
export {
|
|
47
|
+
asToolCallResult,
|
|
48
|
+
createMcpUiActionRelay,
|
|
49
|
+
unavailableToolCallResult,
|
|
50
|
+
UI_ACTION_TOOLS,
|
|
51
|
+
UI_ACTION_UNAVAILABLE_TEXT,
|
|
52
|
+
type CreateMcpUiActionRelayDeps,
|
|
53
|
+
type McpToolCallContent,
|
|
54
|
+
type McpToolCallResult,
|
|
55
|
+
type McpToolStructuredContent,
|
|
56
|
+
type UiActionRequest,
|
|
57
|
+
} from "./action.js";
|
|
58
|
+
export {
|
|
59
|
+
initializeResult,
|
|
60
|
+
initialViewHostState,
|
|
61
|
+
teardownMessage,
|
|
62
|
+
toolCallResponse,
|
|
63
|
+
TOOLS_CALL_METHOD,
|
|
64
|
+
viewHostElapsed,
|
|
65
|
+
viewHostReceive,
|
|
66
|
+
type ViewHostBehavior,
|
|
67
|
+
type ViewHostEffect,
|
|
68
|
+
type ViewHostOutbound,
|
|
69
|
+
type ViewHostPhase,
|
|
70
|
+
type ViewHostInfo,
|
|
71
|
+
type ViewHostState,
|
|
72
|
+
type ViewHostTransition,
|
|
73
|
+
type ViewRequestId,
|
|
74
|
+
} from "./view-host-protocol.js";
|
|
75
|
+
export {
|
|
76
|
+
attachViewHost,
|
|
77
|
+
viewDocumentHtml,
|
|
78
|
+
type AttachViewHostConfig,
|
|
79
|
+
type ViewFrameLike,
|
|
80
|
+
type ViewHostEvents,
|
|
81
|
+
} from "./view-host.js";
|
|
82
|
+
export {
|
|
83
|
+
attachSandboxPageDelivery,
|
|
84
|
+
isSandboxProxyReady,
|
|
85
|
+
SANDBOX_PROXY_READY_METHOD,
|
|
86
|
+
SANDBOX_RESOURCE_READY_METHOD,
|
|
87
|
+
type SandboxPageDeliveryConfig,
|
|
88
|
+
} from "./sandbox-page.js";
|