@guuey/mcp-apps-host 0.3.1 → 0.4.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.
@@ -0,0 +1,89 @@
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
+ * Narrow an untrusted `tools/call` answer to the arms the sandbox may see.
62
+ * Unknown content arms are DROPPED (never forwarded opaque); a value that
63
+ * is not result-shaped at all is `undefined` (the relay answers in-band).
64
+ */
65
+ export declare function asToolCallResult(value: unknown): McpToolCallResult | undefined;
66
+ /** The host-supplied transport {@link createMcpUiActionRelay} assembles over. */
67
+ export interface CreateMcpUiActionRelayDeps {
68
+ /**
69
+ * One `tools/call` bound to the mounted card's locator `uri` over the
70
+ * host's authenticated channel. Returns the raw result (narrowed here),
71
+ * or `undefined` when the upstream denied/lost the session. Throwing is
72
+ * treated as unavailable.
73
+ */
74
+ callTool: (uri: string, name: string, args: McpToolStructuredContent | undefined) => Promise<unknown>;
75
+ }
76
+ /** The request shape a mounted card's `onCallTool` bridge produces. */
77
+ export interface UiActionRequest {
78
+ /** The mounted card's persisted `ui://` locator — the action's scope. */
79
+ resourceUri: string;
80
+ name: string;
81
+ arguments?: McpToolStructuredContent;
82
+ }
83
+ /**
84
+ * Assemble the sandbox-facing action relay from a host transport. The
85
+ * returned function is shaped for an `onCallTool` bridge: it always
86
+ * resolves (never rejects), answering in-band.
87
+ */
88
+ export declare function createMcpUiActionRelay(deps: CreateMcpUiActionRelayDeps): (request: UiActionRequest) => Promise<McpToolCallResult>;
89
+ //# 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;AAyC3C;;;;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,106 @@
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
+ function unavailable() {
25
+ return {
26
+ content: [{ type: "text", text: UI_ACTION_UNAVAILABLE_TEXT }],
27
+ isError: true,
28
+ };
29
+ }
30
+ function isJsonObjectLike(value) {
31
+ return typeof value === "object" && value !== null && !Array.isArray(value);
32
+ }
33
+ function asContentArm(value) {
34
+ if (!isJsonObjectLike(value))
35
+ return undefined;
36
+ const type = value["type"];
37
+ if (type === "text" && typeof value["text"] === "string") {
38
+ return { type: "text", text: value["text"] };
39
+ }
40
+ if (type === "image" &&
41
+ typeof value["data"] === "string" &&
42
+ typeof value["mimeType"] === "string") {
43
+ return { type: "image", data: value["data"], mimeType: value["mimeType"] };
44
+ }
45
+ if (type === "resource" && isJsonObjectLike(value["resource"])) {
46
+ const res = value["resource"];
47
+ if (typeof res["uri"] !== "string")
48
+ return undefined;
49
+ const mimeType = typeof res["mimeType"] === "string" ? { mimeType: res["mimeType"] } : {};
50
+ if (typeof res["text"] === "string") {
51
+ return { type: "resource", resource: { uri: res["uri"], ...mimeType, text: res["text"] } };
52
+ }
53
+ if (typeof res["blob"] === "string") {
54
+ return { type: "resource", resource: { uri: res["uri"], ...mimeType, blob: res["blob"] } };
55
+ }
56
+ }
57
+ return undefined;
58
+ }
59
+ /**
60
+ * Narrow an untrusted `tools/call` answer to the arms the sandbox may see.
61
+ * Unknown content arms are DROPPED (never forwarded opaque); a value that
62
+ * is not result-shaped at all is `undefined` (the relay answers in-band).
63
+ */
64
+ export function asToolCallResult(value) {
65
+ if (!isJsonObjectLike(value))
66
+ return undefined;
67
+ const rawContent = value["content"];
68
+ if (!Array.isArray(rawContent))
69
+ return undefined;
70
+ const content = [];
71
+ for (const entry of rawContent) {
72
+ const arm = asContentArm(entry);
73
+ if (arm)
74
+ content.push(arm);
75
+ }
76
+ return {
77
+ content,
78
+ ...(value["isError"] === true ? { isError: true } : {}),
79
+ ...(isJsonObjectLike(value["structuredContent"])
80
+ ? { structuredContent: value["structuredContent"] }
81
+ : {}),
82
+ };
83
+ }
84
+ /**
85
+ * Assemble the sandbox-facing action relay from a host transport. The
86
+ * returned function is shaped for an `onCallTool` bridge: it always
87
+ * resolves (never rejects), answering in-band.
88
+ */
89
+ export function createMcpUiActionRelay(deps) {
90
+ return async (request) => {
91
+ if (!UI_ACTION_TOOLS.has(request.name))
92
+ return unavailable();
93
+ if (!request.resourceUri.startsWith("ui://"))
94
+ return unavailable();
95
+ let raw;
96
+ try {
97
+ raw = await deps.callTool(request.resourceUri, request.name, request.arguments);
98
+ }
99
+ catch {
100
+ return unavailable(); // transport failure == unavailable, in-band
101
+ }
102
+ if (raw === undefined)
103
+ return unavailable();
104
+ return asToolCallResult(raw) ?? unavailable();
105
+ };
106
+ }
package/dist/index.d.ts CHANGED
@@ -7,4 +7,5 @@ export { asResourcePayload, asUiResource, blockUiResource, isJsonObject, resourc
7
7
  export { asGguiRender, asGguiRenderBootstrap, blockGguiRender, gguiRenderResource, gguiShellHtml, toolResultGguiRender, GGUI_RENDER_META_KEY, type GguiRenderBootstrap, type GguiRenderDescriptor, type GguiShellHtmlOptions, } from "./ggui-render.js";
8
8
  export { 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, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, type CreateMcpUiActionRelayDeps, type McpToolCallContent, type McpToolCallResult, type McpToolStructuredContent, type UiActionRequest, } from "./action.js";
10
11
  //# 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,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,eAAe,EACf,0BAA0B,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -7,3 +7,4 @@ export { asResourcePayload, asUiResource, blockUiResource, isJsonObject, resourc
7
7
  export { asGguiRender, asGguiRenderBootstrap, blockGguiRender, gguiRenderResource, gguiShellHtml, toolResultGguiRender, GGUI_RENDER_META_KEY, } from "./ggui-render.js";
8
8
  export { snapshotViewMount, toolResultViewMount, } from "./card-mount.js";
9
9
  export { createMcpUiResourceReader, uiResourceChannel, } from "./reader.js";
10
+ export { asToolCallResult, createMcpUiActionRelay, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, } from "./action.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guuey/mcp-apps-host",
3
- "version": "0.3.1",
3
+ "version": "0.4.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",
@@ -27,7 +27,7 @@
27
27
  }
28
28
  },
29
29
  "dependencies": {
30
- "@ggui-ai/protocol": "0.6.3",
30
+ "@ggui-ai/protocol": "0.9.0",
31
31
  "@silverprotocol/core": "0.4.1"
32
32
  },
33
33
  "devDependencies": {
@@ -52,10 +52,10 @@
52
52
  "url": "git+https://github.com/withguuey/guuey-sdks.git",
53
53
  "directory": "packages/mcp-apps-host"
54
54
  },
55
- "homepage": "https://guuey.com",
56
55
  "bugs": {
57
- "url": "https://github.com/loqu-co/guuey/issues"
56
+ "url": "https://github.com/withguuey/guuey-sdks/issues"
58
57
  },
58
+ "homepage": "https://guuey.com",
59
59
  "scripts": {
60
60
  "build": "tsc -p tsconfig.build.json",
61
61
  "dev": "tsc --watch",
package/src/action.ts ADDED
@@ -0,0 +1,166 @@
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
+ function unavailable(): McpToolCallResult {
62
+ return {
63
+ content: [{ type: "text", text: UI_ACTION_UNAVAILABLE_TEXT }],
64
+ isError: true,
65
+ };
66
+ }
67
+
68
+ function isJsonObjectLike(value: unknown): value is McpToolStructuredContent {
69
+ return typeof value === "object" && value !== null && !Array.isArray(value);
70
+ }
71
+
72
+ function asContentArm(value: unknown): McpToolCallContent | undefined {
73
+ if (!isJsonObjectLike(value)) return undefined;
74
+ const type = value["type"];
75
+ if (type === "text" && typeof value["text"] === "string") {
76
+ return { type: "text", text: value["text"] };
77
+ }
78
+ if (
79
+ type === "image" &&
80
+ typeof value["data"] === "string" &&
81
+ typeof value["mimeType"] === "string"
82
+ ) {
83
+ return { type: "image", data: value["data"], mimeType: value["mimeType"] };
84
+ }
85
+ if (type === "resource" && isJsonObjectLike(value["resource"])) {
86
+ const res = value["resource"];
87
+ if (typeof res["uri"] !== "string") return undefined;
88
+ const mimeType =
89
+ typeof res["mimeType"] === "string" ? { mimeType: res["mimeType"] } : {};
90
+ if (typeof res["text"] === "string") {
91
+ return { type: "resource", resource: { uri: res["uri"], ...mimeType, text: res["text"] } };
92
+ }
93
+ if (typeof res["blob"] === "string") {
94
+ return { type: "resource", resource: { uri: res["uri"], ...mimeType, blob: res["blob"] } };
95
+ }
96
+ }
97
+ return undefined;
98
+ }
99
+
100
+ /**
101
+ * Narrow an untrusted `tools/call` answer to the arms the sandbox may see.
102
+ * Unknown content arms are DROPPED (never forwarded opaque); a value that
103
+ * is not result-shaped at all is `undefined` (the relay answers in-band).
104
+ */
105
+ export function asToolCallResult(value: unknown): McpToolCallResult | undefined {
106
+ if (!isJsonObjectLike(value)) return undefined;
107
+ const rawContent = value["content"];
108
+ if (!Array.isArray(rawContent)) return undefined;
109
+ const content: McpToolCallContent[] = [];
110
+ for (const entry of rawContent) {
111
+ const arm = asContentArm(entry);
112
+ if (arm) content.push(arm);
113
+ }
114
+ return {
115
+ content,
116
+ ...(value["isError"] === true ? { isError: true } : {}),
117
+ ...(isJsonObjectLike(value["structuredContent"])
118
+ ? { structuredContent: value["structuredContent"] }
119
+ : {}),
120
+ };
121
+ }
122
+
123
+ /** The host-supplied transport {@link createMcpUiActionRelay} assembles over. */
124
+ export interface CreateMcpUiActionRelayDeps {
125
+ /**
126
+ * One `tools/call` bound to the mounted card's locator `uri` over the
127
+ * host's authenticated channel. Returns the raw result (narrowed here),
128
+ * or `undefined` when the upstream denied/lost the session. Throwing is
129
+ * treated as unavailable.
130
+ */
131
+ callTool: (
132
+ uri: string,
133
+ name: string,
134
+ args: McpToolStructuredContent | undefined,
135
+ ) => Promise<unknown>;
136
+ }
137
+
138
+ /** The request shape a mounted card's `onCallTool` bridge produces. */
139
+ export interface UiActionRequest {
140
+ /** The mounted card's persisted `ui://` locator — the action's scope. */
141
+ resourceUri: string;
142
+ name: string;
143
+ arguments?: McpToolStructuredContent;
144
+ }
145
+
146
+ /**
147
+ * Assemble the sandbox-facing action relay from a host transport. The
148
+ * returned function is shaped for an `onCallTool` bridge: it always
149
+ * resolves (never rejects), answering in-band.
150
+ */
151
+ export function createMcpUiActionRelay(
152
+ deps: CreateMcpUiActionRelayDeps,
153
+ ): (request: UiActionRequest) => Promise<McpToolCallResult> {
154
+ return async (request) => {
155
+ if (!UI_ACTION_TOOLS.has(request.name)) return unavailable();
156
+ if (!request.resourceUri.startsWith("ui://")) return unavailable();
157
+ let raw: unknown;
158
+ try {
159
+ raw = await deps.callTool(request.resourceUri, request.name, request.arguments);
160
+ } catch {
161
+ return unavailable(); // transport failure == unavailable, in-band
162
+ }
163
+ if (raw === undefined) return unavailable();
164
+ return asToolCallResult(raw) ?? unavailable();
165
+ };
166
+ }
package/src/index.ts CHANGED
@@ -42,3 +42,14 @@ export {
42
42
  type CreateMcpUiResourceReaderDeps,
43
43
  type McpResourceReadResult,
44
44
  } from "./reader.js";
45
+ export {
46
+ asToolCallResult,
47
+ createMcpUiActionRelay,
48
+ UI_ACTION_TOOLS,
49
+ UI_ACTION_UNAVAILABLE_TEXT,
50
+ type CreateMcpUiActionRelayDeps,
51
+ type McpToolCallContent,
52
+ type McpToolCallResult,
53
+ type McpToolStructuredContent,
54
+ type UiActionRequest,
55
+ } from "./action.js";