@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 CHANGED
@@ -27,6 +27,24 @@ role's client-side narrowing and mount contract:
27
27
  this assembly over guuey's platform proxy.
28
28
  - **Sandbox-trust channels** (`ViewMountChannel`): which sandbox host page a
29
29
  payload may mount in, until per-resource declared-CSP construction lands.
30
+ - **The host itself** (`attachViewHost`, and `<GuueyView>` from
31
+ `@guuey/mcp-apps-host/react`): a spec-following view opens with the
32
+ `ui/initialize` App handshake and **blocks on it** — mount material alone
33
+ renders a blank frame forever. `attachViewHost(iframe, config)` answers
34
+ that handshake for any embedder (framework-agnostic, one call); the React
35
+ component additionally owns iframe creation, lifecycle, and the safe
36
+ sandbox default (`allow-scripts` **without** `allow-same-origin` — with
37
+ it, agent-generated HTML would run as your origin — plus
38
+ `allow="clipboard-write"` so generated copy buttons work). Configurable
39
+ where a host genuinely varies: `hostCapabilities` (default `{}` —
40
+ advertise only what you implement), an optional `tools/call` relay hook
41
+ (privilege boundary, default off; pair with `createMcpUiActionRelay`),
42
+ `hostContext`, and the negotiation timeout. While a frame negotiates, the
43
+ state is labeled (`negotiating` / `connected` / `no-handshake`), never a
44
+ blank page.
45
+ - **Resolved-only mount walk** (`resolveViewMount`): chain it off
46
+ `toolResultViewMount`/`snapshotViewMount` with your reader and render
47
+ only `ResolvedViewMount`s — the locator round-trip is folded in.
30
48
 
31
49
  Vendor-neutral by principle: ggui's MCP Apps run on any spec-following host
32
50
  (claude.ai, chatgpt.com, guuey) precisely because they follow the spec; this
@@ -0,0 +1,95 @@
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
+ /** The wire arms a relay hands back to the sandbox — re-narrowed, never trusted. */
16
+ export type McpToolCallContent = {
17
+ type: "text";
18
+ text: string;
19
+ } | {
20
+ type: "image";
21
+ data: string;
22
+ mimeType: string;
23
+ } | {
24
+ type: "resource";
25
+ resource: {
26
+ uri: string;
27
+ mimeType?: string;
28
+ } & ({
29
+ text: string;
30
+ } | {
31
+ blob: string;
32
+ });
33
+ };
34
+ /**
35
+ * `structuredContent` is protocol-open by design (the MCP spec types it as
36
+ * an arbitrary JSON object) — the index signature is the honest wire type,
37
+ * not an erasure of a known shape.
38
+ */
39
+ export type McpToolStructuredContent = {
40
+ [key: string]: unknown;
41
+ };
42
+ /**
43
+ * The SEP-1865 CallToolResult surface a host hands back to the sandbox.
44
+ * A `type` alias, deliberately: the MCP SDK's own result types carry Zod
45
+ * passthrough index signatures, and only type aliases (never interfaces)
46
+ * get the implicit index signature that makes this assignable to them.
47
+ */
48
+ export type McpToolCallResult = {
49
+ content: McpToolCallContent[];
50
+ isError?: boolean;
51
+ structuredContent?: McpToolStructuredContent;
52
+ };
53
+ /**
54
+ * The runtime-action tools a card sandbox may relay — the client-side twin
55
+ * of the server allowlist (defense in depth: the proxy enforces it again).
56
+ */
57
+ export declare const UI_ACTION_TOOLS: ReadonlySet<string>;
58
+ /** The in-band answer for anything the relay cannot (or will not) do. */
59
+ export declare const UI_ACTION_UNAVAILABLE_TEXT = "This action isn't available right now.";
60
+ /**
61
+ * The in-band `isError` result for an action that cannot be performed —
62
+ * the relay's own refusals use it, and `attachViewHost` posts it when an
63
+ * embedder-supplied relay hook rejects (the view is always answered).
64
+ */
65
+ export declare function unavailableToolCallResult(): McpToolCallResult;
66
+ /**
67
+ * Narrow an untrusted `tools/call` answer to the arms the sandbox may see.
68
+ * Unknown content arms are DROPPED (never forwarded opaque); a value that
69
+ * is not result-shaped at all is `undefined` (the relay answers in-band).
70
+ */
71
+ export declare function asToolCallResult(value: unknown): McpToolCallResult | undefined;
72
+ /** The host-supplied transport {@link createMcpUiActionRelay} assembles over. */
73
+ export interface CreateMcpUiActionRelayDeps {
74
+ /**
75
+ * One `tools/call` bound to the mounted card's locator `uri` over the
76
+ * host's authenticated channel. Returns the raw result (narrowed here),
77
+ * or `undefined` when the upstream denied/lost the session. Throwing is
78
+ * treated as unavailable.
79
+ */
80
+ callTool: (uri: string, name: string, args: McpToolStructuredContent | undefined) => Promise<unknown>;
81
+ }
82
+ /** The request shape a mounted card's `onCallTool` bridge produces. */
83
+ export interface UiActionRequest {
84
+ /** The mounted card's persisted `ui://` locator — the action's scope. */
85
+ resourceUri: string;
86
+ name: string;
87
+ arguments?: McpToolStructuredContent;
88
+ }
89
+ /**
90
+ * Assemble the sandbox-facing action relay from a host transport. The
91
+ * returned function is shaped for an `onCallTool` bridge: it always
92
+ * resolves (never rejects), answering in-band.
93
+ */
94
+ export declare function createMcpUiActionRelay(deps: CreateMcpUiActionRelayDeps): (request: UiActionRequest) => Promise<McpToolCallResult>;
95
+ //# sourceMappingURL=action.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,oFAAoF;AACpF,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACjD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,CAC3C;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAChB;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CACnB,CAAC;CACH,CAAC;AAEN;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;CAC9C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAE9C,CAAC;AAEH,yEAAyE;AACzE,eAAO,MAAM,0BAA0B,2CACG,CAAC;AAE3C;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,iBAAiB,CAK7D;AAkCD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,GAAG,SAAS,CAgB9E;AAED,iFAAiF;AACjF,MAAM,WAAW,0BAA0B;IACzC;;;;;OAKG;IACH,QAAQ,EAAE,CACR,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,wBAAwB,GAAG,SAAS,KACvC,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED,uEAAuE;AACvE,MAAM,WAAW,eAAe;IAC9B,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,wBAAwB,CAAC;CACtC;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,0BAA0B,GAC/B,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAa1D"}
package/dist/action.js ADDED
@@ -0,0 +1,111 @@
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 runtime-action tools a card sandbox may relay — the client-side twin
17
+ * of the server allowlist (defense in depth: the proxy enforces it again).
18
+ */
19
+ export const UI_ACTION_TOOLS = new Set([
20
+ "ggui_runtime_submit_action",
21
+ ]);
22
+ /** The in-band answer for anything the relay cannot (or will not) do. */
23
+ export const UI_ACTION_UNAVAILABLE_TEXT = "This action isn't available right now.";
24
+ /**
25
+ * The in-band `isError` result for an action that cannot be performed —
26
+ * the relay's own refusals use it, and `attachViewHost` posts it when an
27
+ * embedder-supplied relay hook rejects (the view is always answered).
28
+ */
29
+ export function unavailableToolCallResult() {
30
+ return {
31
+ content: [{ type: "text", text: UI_ACTION_UNAVAILABLE_TEXT }],
32
+ isError: true,
33
+ };
34
+ }
35
+ function isJsonObjectLike(value) {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+ function asContentArm(value) {
39
+ if (!isJsonObjectLike(value))
40
+ return undefined;
41
+ const type = value["type"];
42
+ if (type === "text" && typeof value["text"] === "string") {
43
+ return { type: "text", text: value["text"] };
44
+ }
45
+ if (type === "image" &&
46
+ typeof value["data"] === "string" &&
47
+ typeof value["mimeType"] === "string") {
48
+ return { type: "image", data: value["data"], mimeType: value["mimeType"] };
49
+ }
50
+ if (type === "resource" && isJsonObjectLike(value["resource"])) {
51
+ const res = value["resource"];
52
+ if (typeof res["uri"] !== "string")
53
+ return undefined;
54
+ const mimeType = typeof res["mimeType"] === "string" ? { mimeType: res["mimeType"] } : {};
55
+ if (typeof res["text"] === "string") {
56
+ return { type: "resource", resource: { uri: res["uri"], ...mimeType, text: res["text"] } };
57
+ }
58
+ if (typeof res["blob"] === "string") {
59
+ return { type: "resource", resource: { uri: res["uri"], ...mimeType, blob: res["blob"] } };
60
+ }
61
+ }
62
+ return undefined;
63
+ }
64
+ /**
65
+ * Narrow an untrusted `tools/call` answer to the arms the sandbox may see.
66
+ * Unknown content arms are DROPPED (never forwarded opaque); a value that
67
+ * is not result-shaped at all is `undefined` (the relay answers in-band).
68
+ */
69
+ export function asToolCallResult(value) {
70
+ if (!isJsonObjectLike(value))
71
+ return undefined;
72
+ const rawContent = value["content"];
73
+ if (!Array.isArray(rawContent))
74
+ return undefined;
75
+ const content = [];
76
+ for (const entry of rawContent) {
77
+ const arm = asContentArm(entry);
78
+ if (arm)
79
+ content.push(arm);
80
+ }
81
+ return {
82
+ content,
83
+ ...(value["isError"] === true ? { isError: true } : {}),
84
+ ...(isJsonObjectLike(value["structuredContent"])
85
+ ? { structuredContent: value["structuredContent"] }
86
+ : {}),
87
+ };
88
+ }
89
+ /**
90
+ * Assemble the sandbox-facing action relay from a host transport. The
91
+ * returned function is shaped for an `onCallTool` bridge: it always
92
+ * resolves (never rejects), answering in-band.
93
+ */
94
+ export function createMcpUiActionRelay(deps) {
95
+ return async (request) => {
96
+ if (!UI_ACTION_TOOLS.has(request.name))
97
+ return unavailableToolCallResult();
98
+ if (!request.resourceUri.startsWith("ui://"))
99
+ return unavailableToolCallResult();
100
+ let raw;
101
+ try {
102
+ raw = await deps.callTool(request.resourceUri, request.name, request.arguments);
103
+ }
104
+ catch {
105
+ return unavailableToolCallResult(); // transport failure == unavailable, in-band
106
+ }
107
+ if (raw === undefined)
108
+ return unavailableToolCallResult();
109
+ return asToolCallResult(raw) ?? unavailableToolCallResult();
110
+ };
111
+ }
@@ -97,4 +97,22 @@ export declare function toolResultViewMount(block: Extract<AgBlock, {
97
97
  * the spec-consistent template fetch, vendor-neutral.
98
98
  */
99
99
  export declare function snapshotViewMount(cardSnapshot: JsonValue): ViewMount | undefined;
100
+ /**
101
+ * The resolved-only convenience over the mount union (guuey#186 G6): every
102
+ * consumer that renders was writing the same two-call walk — narrow the
103
+ * union, then feed the `"locator"` arm to a reader. This collapses it:
104
+ *
105
+ * - already-resolved mounts pass through untouched (no reader round-trip);
106
+ * - a `"locator"` arm resolves via the reader — or the honest `undefined`
107
+ * (placeholder) when no reader is wired: never a stale mount;
108
+ * - a reader that answers with ANOTHER locator is treated as a miss. The
109
+ * {@link UiResourceReader} contract says a read yields mount material or
110
+ * nothing (guuey#127); a locator answer would loop, so the honest
111
+ * reading is "could not resolve", not recursion.
112
+ *
113
+ * Takes `ViewMount | undefined` so it chains directly off
114
+ * `toolResultViewMount`/`snapshotViewMount` without a narrowing dance at
115
+ * the call site.
116
+ */
117
+ export declare function resolveViewMount(mount: ViewMount | undefined, reader?: UiResourceReader): Promise<ResolvedViewMount | undefined>;
100
118
  //# sourceMappingURL=card-mount.d.ts.map
@@ -1 +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,GAAG,iBAAiB,GAAG,gBAAgB,CAAC;AAE7D;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,EAAE,oBAAoB,CAAC;CAChC;AAED,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,SAAS,CAAC;IACnB,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;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"}
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,GAAG,iBAAiB,GAAG,gBAAgB,CAAC;AAE7D;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,EAAE,oBAAoB,CAAC;CAChC;AAED,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,SAAS,CAAC;IACnB,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;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;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,MAAM,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAKxC"}
@@ -89,6 +89,31 @@ export function snapshotViewMount(cardSnapshot) {
89
89
  }
90
90
  return undefined;
91
91
  }
92
+ /**
93
+ * The resolved-only convenience over the mount union (guuey#186 G6): every
94
+ * consumer that renders was writing the same two-call walk — narrow the
95
+ * union, then feed the `"locator"` arm to a reader. This collapses it:
96
+ *
97
+ * - already-resolved mounts pass through untouched (no reader round-trip);
98
+ * - a `"locator"` arm resolves via the reader — or the honest `undefined`
99
+ * (placeholder) when no reader is wired: never a stale mount;
100
+ * - a reader that answers with ANOTHER locator is treated as a miss. The
101
+ * {@link UiResourceReader} contract says a read yields mount material or
102
+ * nothing (guuey#127); a locator answer would loop, so the honest
103
+ * reading is "could not resolve", not recursion.
104
+ *
105
+ * Takes `ViewMount | undefined` so it chains directly off
106
+ * `toolResultViewMount`/`snapshotViewMount` without a narrowing dance at
107
+ * the call site.
108
+ */
109
+ export async function resolveViewMount(mount, reader) {
110
+ if (mount === undefined || mount.channel !== "locator")
111
+ return mount;
112
+ if (reader === undefined)
113
+ return undefined;
114
+ const read = await reader(mount.resourceUri);
115
+ return read === undefined || read.channel === "locator" ? undefined : read;
116
+ }
92
117
  /**
93
118
  * The blocks to scan inside a card snapshot: the stored `AgArtifact`'s `parts`
94
119
  * when present, then the snapshot root itself — exactly `snapshotUiResource`'s own
package/dist/index.d.ts CHANGED
@@ -5,6 +5,10 @@
5
5
  */
6
6
  export { asResourcePayload, asUiResource, blockUiResource, isJsonObject, resourceHtml, scanProviderRawForUiResource, snapshotUiResource, toolResultUiResource, uiLocator, type McpUiResourcePayload, } from "./block-ui.js";
7
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 LocatorViewMount, type ResolvedViewMount, type UiResourceReader, type ViewMount, type ViewMountChannel, } from "./card-mount.js";
8
+ export { resolveViewMount, snapshotViewMount, toolResultViewMount, type LocatorViewMount, type ResolvedViewMount, type UiResourceReader, type ViewMount, type ViewMountChannel, } from "./card-mount.js";
9
9
  export { createMcpUiResourceReader, uiResourceChannel, type CreateMcpUiResourceReaderDeps, type McpResourceReadResult, } from "./reader.js";
10
+ export { asToolCallResult, createMcpUiActionRelay, unavailableToolCallResult, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, type CreateMcpUiActionRelayDeps, type McpToolCallContent, type McpToolCallResult, type McpToolStructuredContent, type UiActionRequest, } from "./action.js";
11
+ export { initializeResult, initialViewHostState, teardownMessage, toolCallResponse, TOOLS_CALL_METHOD, viewHostElapsed, viewHostReceive, type ViewHostBehavior, type ViewHostEffect, type ViewHostOutbound, type ViewHostPhase, type ViewHostInfo, type ViewHostState, type ViewHostTransition, type ViewRequestId, } from "./view-host-protocol.js";
12
+ export { attachViewHost, viewDocumentHtml, type AttachViewHostConfig, type ViewFrameLike, type ViewHostEvents, } from "./view-host.js";
13
+ export { attachSandboxPageDelivery, isSandboxProxyReady, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, type SandboxPageDeliveryConfig, } from "./sandbox-page.js";
10
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAC"}
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,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EACf,0BAA0B,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,6BAA6B,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -5,5 +5,9 @@
5
5
  */
6
6
  export { asResourcePayload, asUiResource, blockUiResource, isJsonObject, resourceHtml, scanProviderRawForUiResource, snapshotUiResource, toolResultUiResource, uiLocator, } from "./block-ui.js";
7
7
  export { asGguiRender, asGguiRenderBootstrap, blockGguiRender, gguiRenderResource, gguiShellHtml, toolResultGguiRender, GGUI_RENDER_META_KEY, } from "./ggui-render.js";
8
- export { snapshotViewMount, toolResultViewMount, } from "./card-mount.js";
8
+ export { resolveViewMount, snapshotViewMount, toolResultViewMount, } from "./card-mount.js";
9
9
  export { createMcpUiResourceReader, uiResourceChannel, } from "./reader.js";
10
+ export { asToolCallResult, createMcpUiActionRelay, unavailableToolCallResult, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, } from "./action.js";
11
+ export { initializeResult, initialViewHostState, teardownMessage, toolCallResponse, TOOLS_CALL_METHOD, viewHostElapsed, viewHostReceive, } from "./view-host-protocol.js";
12
+ export { attachViewHost, viewDocumentHtml, } from "./view-host.js";
13
+ export { attachSandboxPageDelivery, isSandboxProxyReady, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, } from "./sandbox-page.js";
@@ -0,0 +1,94 @@
1
+ /**
2
+ * React entry point (`@guuey/mcp-apps-host/react`).
3
+ *
4
+ * `<GuueyView>` is the one React-coupled surface — the root subpath stays
5
+ * React-free (narrowing, rehydration, the relay, `attachViewHost` itself),
6
+ * so server-side consumers never import React at all. The component is a
7
+ * CONVENIENCE composition of the framework-agnostic primitive: iframe
8
+ * creation + the sandbox invariant + the handshake + lifecycle. A host
9
+ * that needs a different composition (a transcript renderer, a non-React
10
+ * surface) uses `attachViewHost` directly.
11
+ *
12
+ * ## Sandbox posture (invariant, not preference)
13
+ *
14
+ * `sandbox="allow-scripts"` WITHOUT `allow-same-origin`: a `srcdoc` frame
15
+ * inherits its embedder's origin, so granting both would run
16
+ * agent-generated HTML AS the embedding page — reach into its DOM and its
17
+ * signed-in session, an XSS by construction. Dropping `allow-same-origin`
18
+ * puts the document in an opaque origin instead. Extra flags ride ON TOP
19
+ * via {@link GuueyViewProps.dangerouslyAddSandboxFlags} — named the way it
20
+ * is because every flag it adds widens what agent-generated HTML can do.
21
+ * `allow="clipboard-write"` is delegated by default: generated views own
22
+ * copy buttons, and without the delegation every one of them silently
23
+ * no-ops inside the opaque origin.
24
+ *
25
+ * ## Who paints which state
26
+ *
27
+ * While `"negotiating"`, the component shows a small non-blocking status
28
+ * line — never a bare blank frame (guuey#186 audit). On `"connected"` the
29
+ * view owns its pixels (and its own failures) and the line disappears. On
30
+ * `"no-handshake"` the meaning is channel-aware: a `"ggui"` shell always
31
+ * negotiates, so silence is a boot failure and is labeled as one; an
32
+ * `"inline"` card is arbitrary tenant HTML with no handshake obligation,
33
+ * so the status line simply retires and the document stands as rendered.
34
+ */
35
+ import { type CSSProperties, type ReactNode } from "react";
36
+ import { type AttachViewHostConfig } from "./view-host.js";
37
+ import type { ViewHostPhase } from "./view-host-protocol.js";
38
+ import type { ResolvedViewMount } from "./card-mount.js";
39
+ export { attachViewHost, viewDocumentHtml } from "./view-host.js";
40
+ export type { AttachViewHostConfig, ViewFrameLike, ViewHostEvents } from "./view-host.js";
41
+ export { attachSandboxPageDelivery, isSandboxProxyReady, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, type SandboxPageDeliveryConfig, } from "./sandbox-page.js";
42
+ export type { ViewHostPhase } from "./view-host-protocol.js";
43
+ export type { ResolvedViewMount, ViewMount, ViewMountChannel } from "./card-mount.js";
44
+ export interface GuueyViewProps extends Pick<AttachViewHostConfig, "hostCapabilities" | "hostInfo" | "hostContext" | "onCallTool" | "negotiationTimeoutMs"> {
45
+ /** The resolved card to mount (see `toolResultViewMount`/`resolveViewMount`). */
46
+ mount: ResolvedViewMount;
47
+ /**
48
+ * Opt into the TWO-ORIGIN mount: instead of `srcdoc`, the frame loads
49
+ * this host-served sandbox page (guuey's `/mcp-app-sandbox` pattern — the
50
+ * caller builds the full URL, channel/app query included) and the
51
+ * document is delivered over the page's relay protocol
52
+ * (`attachSandboxPageDelivery`). Why: a `srcdoc` frame INHERITS the
53
+ * embedder's CSP, so its egress confinement is whatever the page happens
54
+ * to carry; the sandbox page is served WITH the per-request CSP that
55
+ * confines the mount — and the untrusted document lands in the page's
56
+ * own inner opaque frame, never in this one. In this mode the frame's
57
+ * `sandbox` gains `allow-same-origin` — REQUIRED and safe: the frame
58
+ * holds the cross-origin RELAY PAGE (which must run as its real origin
59
+ * for its CSP + referrer checks to mean anything), never agent HTML.
60
+ * The page must be a genuinely different origin; a same-origin URL is
61
+ * refused with a labeled state, never mounted.
62
+ */
63
+ sandboxPageUrl?: string;
64
+ /**
65
+ * Sandbox flags appended to the safe default (`allow-scripts`). Every
66
+ * entry widens what agent-generated HTML may do — `allow-same-origin`
67
+ * in particular hands it the embedder's origin. Prefer leaving unset.
68
+ * In `sandboxPageUrl` mode these forward to the INNER frame via the
69
+ * page's relay (which strips `allow-same-origin` regardless).
70
+ */
71
+ dangerouslyAddSandboxFlags?: string[];
72
+ /** Permissions-Policy delegation for the frame. Default `clipboard-write`. */
73
+ allow?: string;
74
+ /** Accessible frame title. Default "Generated view". */
75
+ title?: string;
76
+ className?: string;
77
+ style?: CSSProperties;
78
+ /** Observe the negotiation phase (the same states the default UI labels). */
79
+ onPhaseChange?: (phase: ViewHostPhase) => void;
80
+ /**
81
+ * Replace the default status/failure line for a phase. Return `null` for
82
+ * "render nothing". The default: a quiet "Negotiating with view…" line
83
+ * while `"negotiating"`; a labeled failure for `"no-handshake"` on the
84
+ * `"ggui"` channel; nothing once `"connected"` (the view owns its
85
+ * pixels) and nothing for a silent `"inline"` card.
86
+ */
87
+ renderStatus?: (phase: ViewHostPhase) => ReactNode;
88
+ }
89
+ /**
90
+ * Mount a resolved view and play the MCP Apps Host for it. See the module
91
+ * docblock for the sandbox and state contracts.
92
+ */
93
+ export declare function GuueyView(props: GuueyViewProps): ReactNode;
94
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,EAAwC,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjG,OAAO,EAAoC,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE7F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClE,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC1F,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,6BAA6B,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAKtF,MAAM,WAAW,cACf,SAAQ,IAAI,CACV,oBAAoB,EACpB,kBAAkB,GAAG,UAAU,GAAG,aAAa,GAAG,YAAY,GAAG,sBAAsB,CACxF;IACD,iFAAiF;IACjF,KAAK,EAAE,iBAAiB,CAAC;IACzB;;;;;;;;;;;;;;;OAeG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,0BAA0B,CAAC,EAAE,MAAM,EAAE,CAAC;IACtC,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,SAAS,CAAC;CACpD;AA+BD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,CAiI1D"}
package/dist/react.js ADDED
@@ -0,0 +1,158 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * React entry point (`@guuey/mcp-apps-host/react`).
4
+ *
5
+ * `<GuueyView>` is the one React-coupled surface — the root subpath stays
6
+ * React-free (narrowing, rehydration, the relay, `attachViewHost` itself),
7
+ * so server-side consumers never import React at all. The component is a
8
+ * CONVENIENCE composition of the framework-agnostic primitive: iframe
9
+ * creation + the sandbox invariant + the handshake + lifecycle. A host
10
+ * that needs a different composition (a transcript renderer, a non-React
11
+ * surface) uses `attachViewHost` directly.
12
+ *
13
+ * ## Sandbox posture (invariant, not preference)
14
+ *
15
+ * `sandbox="allow-scripts"` WITHOUT `allow-same-origin`: a `srcdoc` frame
16
+ * inherits its embedder's origin, so granting both would run
17
+ * agent-generated HTML AS the embedding page — reach into its DOM and its
18
+ * signed-in session, an XSS by construction. Dropping `allow-same-origin`
19
+ * puts the document in an opaque origin instead. Extra flags ride ON TOP
20
+ * via {@link GuueyViewProps.dangerouslyAddSandboxFlags} — named the way it
21
+ * is because every flag it adds widens what agent-generated HTML can do.
22
+ * `allow="clipboard-write"` is delegated by default: generated views own
23
+ * copy buttons, and without the delegation every one of them silently
24
+ * no-ops inside the opaque origin.
25
+ *
26
+ * ## Who paints which state
27
+ *
28
+ * While `"negotiating"`, the component shows a small non-blocking status
29
+ * line — never a bare blank frame (guuey#186 audit). On `"connected"` the
30
+ * view owns its pixels (and its own failures) and the line disappears. On
31
+ * `"no-handshake"` the meaning is channel-aware: a `"ggui"` shell always
32
+ * negotiates, so silence is a boot failure and is labeled as one; an
33
+ * `"inline"` card is arbitrary tenant HTML with no handshake obligation,
34
+ * so the status line simply retires and the document stands as rendered.
35
+ */
36
+ import { useEffect, useMemo, useRef, useState } from "react";
37
+ import { attachViewHost, viewDocumentHtml } from "./view-host.js";
38
+ import { attachSandboxPageDelivery } from "./sandbox-page.js";
39
+ export { attachViewHost, viewDocumentHtml } from "./view-host.js";
40
+ export { attachSandboxPageDelivery, isSandboxProxyReady, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, } from "./sandbox-page.js";
41
+ /** Accessible name for a mounted view when the caller has nothing better. */
42
+ const DEFAULT_TITLE = "Generated view";
43
+ const statusLineStyle = {
44
+ position: "absolute",
45
+ insetInlineStart: 8,
46
+ insetBlockEnd: 8,
47
+ margin: 0,
48
+ padding: "2px 8px",
49
+ fontSize: 12,
50
+ lineHeight: "18px",
51
+ opacity: 0.65,
52
+ pointerEvents: "none",
53
+ };
54
+ function defaultStatus(phase, channel) {
55
+ if (phase === "negotiating") {
56
+ return _jsx("p", { style: statusLineStyle, children: "Negotiating with view\u2026" });
57
+ }
58
+ if (phase === "no-handshake" && channel === "ggui") {
59
+ // A ggui shell negotiates unconditionally before painting, so silence
60
+ // here is a boot failure with no other author — label it (role=alert
61
+ // so it is announced, not just drawn).
62
+ return (_jsx("p", { role: "alert", style: { ...statusLineStyle, opacity: 1, pointerEvents: "auto" }, children: "This view did not start \u2014 it never negotiated with the host." }));
63
+ }
64
+ return null;
65
+ }
66
+ /**
67
+ * Mount a resolved view and play the MCP Apps Host for it. See the module
68
+ * docblock for the sandbox and state contracts.
69
+ */
70
+ export function GuueyView(props) {
71
+ const { mount, sandboxPageUrl, dangerouslyAddSandboxFlags, allow, title, className, style, onPhaseChange, renderStatus, ...hostConfig } = props;
72
+ const frameRef = useRef(null);
73
+ const [phase, setPhase] = useState("negotiating");
74
+ const html = viewDocumentHtml(mount.resource);
75
+ // Vet the sandbox page once per URL. Same-origin is REFUSED (the widget's
76
+ // ResourceMount precedent, generalized): the whole point of the page is
77
+ // being a different origin — same-origin would hand the relay page (and
78
+ // through `allow-same-origin`, everything it can reach) the embedder's
79
+ // own origin.
80
+ const sandboxPage = useMemo(() => {
81
+ if (sandboxPageUrl === undefined)
82
+ return undefined;
83
+ let url;
84
+ try {
85
+ url = new URL(sandboxPageUrl);
86
+ }
87
+ catch {
88
+ return "refused";
89
+ }
90
+ if (typeof window !== "undefined" && url.origin === window.location.origin)
91
+ return "refused";
92
+ return url;
93
+ }, [sandboxPageUrl]);
94
+ const page = sandboxPage instanceof URL ? sandboxPage : undefined;
95
+ // The attachment is keyed to the mounted DOCUMENT, not to every render's
96
+ // fresh callback identities — host config rides a ref so the effect's
97
+ // dependency list is honestly just the document identity.
98
+ const latest = useRef({ hostConfig, onPhaseChange, dangerouslyAddSandboxFlags });
99
+ latest.current = { hostConfig, onPhaseChange, dangerouslyAddSandboxFlags };
100
+ useEffect(() => {
101
+ // Keyed to the same identity the frame is (the resource uri): a new
102
+ // document boots fresh, and the previous negotiation's phase must not
103
+ // paper over it.
104
+ setPhase("negotiating");
105
+ const frame = frameRef.current;
106
+ if (frame === null || html === undefined)
107
+ return;
108
+ if (sandboxPageUrl !== undefined && page === undefined)
109
+ return; // refused config — nothing mounts
110
+ const resourceUri = mount.resource.uri;
111
+ const detachHost = attachViewHost(frame, {
112
+ ...latest.current.hostConfig,
113
+ resourceUri,
114
+ onPhaseChange: (next) => {
115
+ setPhase(next);
116
+ latest.current.onPhaseChange?.(next);
117
+ },
118
+ });
119
+ if (page === undefined)
120
+ return detachHost;
121
+ // Two-origin mode: the page announces readiness, the document is
122
+ // delivered over its relay (re-delivered on a reload's re-announce),
123
+ // and the view-host handshake crosses the same relay transparently.
124
+ const flags = latest.current.dangerouslyAddSandboxFlags;
125
+ const detachDelivery = attachSandboxPageDelivery(frame, {
126
+ pageOrigin: page.origin,
127
+ html,
128
+ ...(flags !== undefined && flags.length > 0
129
+ ? { sandbox: ["allow-scripts", ...flags].join(" ") }
130
+ : {}),
131
+ });
132
+ return () => {
133
+ detachDelivery();
134
+ detachHost();
135
+ };
136
+ }, [mount.resource.uri, html, sandboxPageUrl, page]);
137
+ if (html === undefined) {
138
+ // A resolved mount with no document is producer-side breakage; an
139
+ // empty frame would be a lie. Label it, in the same voice as the
140
+ // no-handshake state.
141
+ return (_jsx("div", { className: className, style: { position: "relative", ...style }, children: _jsx("p", { role: "alert", style: { ...statusLineStyle, opacity: 1, pointerEvents: "auto" }, children: "This view could not be displayed \u2014 its resource carries no document." }) }));
142
+ }
143
+ if (sandboxPageUrl !== undefined && page === undefined) {
144
+ // A malformed or SAME-ORIGIN sandbox page is a configuration state, not
145
+ // a property of the card — refused, labeled, never mounted.
146
+ return (_jsx("div", { className: className, style: { position: "relative", ...style }, children: _jsx("p", { role: "alert", style: { ...statusLineStyle, opacity: 1, pointerEvents: "auto" }, children: "Interactive view unavailable \u2014 the sandbox page is not usable from this origin." }) }));
147
+ }
148
+ return (_jsxs("div", { className: className, style: { position: "relative", ...style }, children: [_jsx("iframe", { ref: frameRef, ...(page !== undefined ? { src: page.href } : { srcDoc: html }), title: title ?? DEFAULT_TITLE,
149
+ // srcdoc mode: the INVARIANT — agent HTML in an opaque origin, extra
150
+ // flags only widen knowingly. Page mode: the frame holds the
151
+ // cross-origin RELAY PAGE, which must run as its real origin
152
+ // (`allow-same-origin`) for its CSP/referrer machinery to exist at
153
+ // all; the agent HTML lands in the page's own inner opaque frame,
154
+ // and the caller's extra flags travel to THAT frame via the relay.
155
+ sandbox: page !== undefined
156
+ ? "allow-scripts allow-same-origin allow-forms"
157
+ : ["allow-scripts", ...(dangerouslyAddSandboxFlags ?? [])].join(" "), allow: allow ?? "clipboard-write", style: { display: "block", width: "100%", height: "100%", border: 0 } }, `${page?.href ?? "srcdoc"}::${mount.resource.uri}`), renderStatus !== undefined ? renderStatus(phase) : defaultStatus(phase, mount.channel)] }));
158
+ }