@guuey/mcp-apps-host 0.3.0 → 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.
- package/README.md +7 -0
- package/dist/action.d.ts +89 -0
- package/dist/action.d.ts.map +1 -0
- package/dist/action.js +106 -0
- package/dist/card-mount.d.ts +11 -3
- package/dist/card-mount.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/reader.d.ts +42 -0
- package/dist/reader.d.ts.map +1 -0
- package/dist/reader.js +45 -0
- package/package.json +4 -4
- package/src/action.ts +166 -0
- package/src/card-mount.ts +19 -11
- package/src/index.ts +19 -0
- package/src/reader.ts +98 -0
package/README.md
CHANGED
|
@@ -18,6 +18,13 @@ role's client-side narrowing and mount contract:
|
|
|
18
18
|
result) — never by replaying stored mount material. The read transport is injected
|
|
19
19
|
(`UiResourceReader`); the host owns auth and user-ownership enforcement,
|
|
20
20
|
and a deny is byte-identical to a miss.
|
|
21
|
+
- **Generic reader assembly** (`createMcpUiResourceReader`): hand it one
|
|
22
|
+
host-owned `resources/read` callable (a raw MCP client, an authenticated
|
|
23
|
+
proxy — no SDK dependency imposed) and get a `UiResourceReader` back, with
|
|
24
|
+
the trust rules built in: deny == miss == placeholder, and the sandbox
|
|
25
|
+
channel derives from the _requested_ locator uri, never the response
|
|
26
|
+
(`uiResourceChannel`). `@guuey/agent-client`'s `createUiResourceReader` is
|
|
27
|
+
this assembly over guuey's platform proxy.
|
|
21
28
|
- **Sandbox-trust channels** (`ViewMountChannel`): which sandbox host page a
|
|
22
29
|
payload may mount in, until per-resource declared-CSP construction lands.
|
|
23
30
|
|
package/dist/action.d.ts
ADDED
|
@@ -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/card-mount.d.ts
CHANGED
|
@@ -51,15 +51,23 @@ export type ViewMountChannel = "inline" | "ggui" | "locator";
|
|
|
51
51
|
* + its tool result — see the conformance map.) Until a reader is wired, the honest render
|
|
52
52
|
* is the host's own placeholder, never a stale mount.
|
|
53
53
|
*/
|
|
54
|
-
export type ViewMount =
|
|
54
|
+
export type ViewMount = ResolvedViewMount | LocatorViewMount;
|
|
55
|
+
/**
|
|
56
|
+
* A view with mount material in hand — the arms a host can render directly,
|
|
57
|
+
* and the ONLY arms a `UiResourceReader` resolves (guuey#127): a read either
|
|
58
|
+
* yields mount material or the honest placeholder, never another locator.
|
|
59
|
+
*/
|
|
60
|
+
export interface ResolvedViewMount {
|
|
55
61
|
channel: "inline" | "ggui";
|
|
56
62
|
/** The payload an mcp-ui host mounts, identical in shape for both channels. */
|
|
57
63
|
resource: McpUiResourcePayload;
|
|
58
|
-
}
|
|
64
|
+
}
|
|
65
|
+
/** The durable-identity arm: no mount material, only the uri to re-fetch. */
|
|
66
|
+
export interface LocatorViewMount {
|
|
59
67
|
channel: "locator";
|
|
60
68
|
/** The persisted `uiData.resourceUri` (`ui://` scheme) to re-fetch. */
|
|
61
69
|
resourceUri: string;
|
|
62
|
-
}
|
|
70
|
+
}
|
|
63
71
|
/**
|
|
64
72
|
* Resolves a `"locator"` mount by a fresh `resources/read` of the uri over
|
|
65
73
|
* an AUTHENTICATED channel the HOST owns — guuey must enforce its own
|
package/dist/card-mount.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,5 +5,7 @@
|
|
|
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 UiResourceReader, type ViewMount, type ViewMountChannel, } from "./card-mount.js";
|
|
8
|
+
export { snapshotViewMount, toolResultViewMount, type LocatorViewMount, type ResolvedViewMount, type UiResourceReader, type ViewMount, type ViewMountChannel, } from "./card-mount.js";
|
|
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";
|
|
9
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,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
|
@@ -6,3 +6,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
8
|
export { snapshotViewMount, toolResultViewMount, } from "./card-mount.js";
|
|
9
|
+
export { createMcpUiResourceReader, uiResourceChannel, } from "./reader.js";
|
|
10
|
+
export { asToolCallResult, createMcpUiActionRelay, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, } from "./action.js";
|
package/dist/reader.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ResolvedViewMount, ViewMountChannel } from "./card-mount.js";
|
|
2
|
+
/**
|
|
3
|
+
* One `resources/read` `contents[]` entry, structurally (SEP-1865). Kept a
|
|
4
|
+
* separate type from {@link McpUiResourcePayload} even though the shapes
|
|
5
|
+
* coincide today: this is the UNVALIDATED wire-side entry a transport hands
|
|
6
|
+
* over (phase 2 grows `_meta` here — per-resource declared CSP, conformance
|
|
7
|
+
* map step 3), while the payload is the narrowed thing a host mounts.
|
|
8
|
+
*/
|
|
9
|
+
export interface McpResourceReadResult {
|
|
10
|
+
uri: string;
|
|
11
|
+
mimeType?: string;
|
|
12
|
+
text?: string;
|
|
13
|
+
/** Base64 payload arm — hosts decode via {@link resourceHtml}. */
|
|
14
|
+
blob?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Phase-1 sandbox-trust channel resolution (conformance map, retirement
|
|
18
|
+
* step 3): a `ui://ggui/…` locator mounts through the ggui-CSP host page
|
|
19
|
+
* (`"ggui"` — the shell boots ggui's runtime bundle and opens its WSS),
|
|
20
|
+
* anything else through the self-only page (`"inline"` — arbitrary tenant
|
|
21
|
+
* HTML). Retired when servers declare runtime/ws origins per resource
|
|
22
|
+
* (`_meta.ui.csp` on the read result); until then the uri prefix is the only
|
|
23
|
+
* signal a host has BEFORE fetching mount material.
|
|
24
|
+
*/
|
|
25
|
+
export declare function uiResourceChannel(resourceUri: string): Exclude<ViewMountChannel, "locator">;
|
|
26
|
+
/** The host-supplied transport {@link createMcpUiResourceReader} assembles over. */
|
|
27
|
+
export interface CreateMcpUiResourceReaderDeps {
|
|
28
|
+
/**
|
|
29
|
+
* One `resources/read` of `uri` on the host's own MCP connection (or an
|
|
30
|
+
* equivalent authenticated proxy), returning the first `contents[]` entry —
|
|
31
|
+
* or `undefined` when the read yields none. Throwing is treated as a miss.
|
|
32
|
+
*/
|
|
33
|
+
readResource: (uri: string) => Promise<McpResourceReadResult | undefined>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Assemble a `UiResourceReader` from a host-supplied `resources/read`
|
|
37
|
+
* transport. The returned reader resolves only the mountable arms — a read
|
|
38
|
+
* either yields mount material or the honest placeholder, never another
|
|
39
|
+
* locator.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createMcpUiResourceReader(deps: CreateMcpUiResourceReaderDeps): (resourceUri: string) => Promise<ResolvedViewMount | undefined>;
|
|
42
|
+
//# sourceMappingURL=reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAE3E;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAE3F;AAED,oFAAoF;AACpF,MAAM,WAAW,6BAA6B;IAC5C;;;;OAIG;IACH,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAAC;CAC3E;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,6BAA6B,GAClC,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAsBjE"}
|
package/dist/reader.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase-1 sandbox-trust channel resolution (conformance map, retirement
|
|
3
|
+
* step 3): a `ui://ggui/…` locator mounts through the ggui-CSP host page
|
|
4
|
+
* (`"ggui"` — the shell boots ggui's runtime bundle and opens its WSS),
|
|
5
|
+
* anything else through the self-only page (`"inline"` — arbitrary tenant
|
|
6
|
+
* HTML). Retired when servers declare runtime/ws origins per resource
|
|
7
|
+
* (`_meta.ui.csp` on the read result); until then the uri prefix is the only
|
|
8
|
+
* signal a host has BEFORE fetching mount material.
|
|
9
|
+
*/
|
|
10
|
+
export function uiResourceChannel(resourceUri) {
|
|
11
|
+
return resourceUri.startsWith("ui://ggui/") ? "ggui" : "inline";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Assemble a `UiResourceReader` from a host-supplied `resources/read`
|
|
15
|
+
* transport. The returned reader resolves only the mountable arms — a read
|
|
16
|
+
* either yields mount material or the honest placeholder, never another
|
|
17
|
+
* locator.
|
|
18
|
+
*/
|
|
19
|
+
export function createMcpUiResourceReader(deps) {
|
|
20
|
+
return async (resourceUri) => {
|
|
21
|
+
let entry;
|
|
22
|
+
try {
|
|
23
|
+
entry = await deps.readResource(resourceUri);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined; // transport failure == deny == miss → placeholder
|
|
27
|
+
}
|
|
28
|
+
if (!entry)
|
|
29
|
+
return undefined;
|
|
30
|
+
// Runtime re-narrowing, not trust in the annotation: transports are
|
|
31
|
+
// host-supplied and may be plain JS. Same field rules as `asResourcePayload`,
|
|
32
|
+
// applied to the typed entry (which may carry extra wire fields — dropped).
|
|
33
|
+
if (typeof entry.uri !== "string")
|
|
34
|
+
return undefined;
|
|
35
|
+
if (typeof entry.text !== "string" && typeof entry.blob !== "string")
|
|
36
|
+
return undefined;
|
|
37
|
+
const resource = {
|
|
38
|
+
uri: entry.uri,
|
|
39
|
+
...(typeof entry.mimeType === "string" ? { mimeType: entry.mimeType } : {}),
|
|
40
|
+
...(typeof entry.text === "string" ? { text: entry.text } : {}),
|
|
41
|
+
...(typeof entry.blob === "string" ? { blob: entry.blob } : {}),
|
|
42
|
+
};
|
|
43
|
+
return { channel: uiResourceChannel(resourceUri), resource };
|
|
44
|
+
};
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guuey/mcp-apps-host",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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/
|
|
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/card-mount.ts
CHANGED
|
@@ -63,17 +63,25 @@ export type ViewMountChannel = "inline" | "ggui" | "locator";
|
|
|
63
63
|
* + its tool result — see the conformance map.) Until a reader is wired, the honest render
|
|
64
64
|
* is the host's own placeholder, never a stale mount.
|
|
65
65
|
*/
|
|
66
|
-
export type ViewMount =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
66
|
+
export type ViewMount = ResolvedViewMount | LocatorViewMount;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A view with mount material in hand — the arms a host can render directly,
|
|
70
|
+
* and the ONLY arms a `UiResourceReader` resolves (guuey#127): a read either
|
|
71
|
+
* yields mount material or the honest placeholder, never another locator.
|
|
72
|
+
*/
|
|
73
|
+
export interface ResolvedViewMount {
|
|
74
|
+
channel: "inline" | "ggui";
|
|
75
|
+
/** The payload an mcp-ui host mounts, identical in shape for both channels. */
|
|
76
|
+
resource: McpUiResourcePayload;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The durable-identity arm: no mount material, only the uri to re-fetch. */
|
|
80
|
+
export interface LocatorViewMount {
|
|
81
|
+
channel: "locator";
|
|
82
|
+
/** The persisted `uiData.resourceUri` (`ui://` scheme) to re-fetch. */
|
|
83
|
+
resourceUri: string;
|
|
84
|
+
}
|
|
77
85
|
|
|
78
86
|
/**
|
|
79
87
|
* Resolves a `"locator"` mount by a fresh `resources/read` of the uri over
|
package/src/index.ts
CHANGED
|
@@ -30,7 +30,26 @@ export {
|
|
|
30
30
|
export {
|
|
31
31
|
snapshotViewMount,
|
|
32
32
|
toolResultViewMount,
|
|
33
|
+
type LocatorViewMount,
|
|
34
|
+
type ResolvedViewMount,
|
|
33
35
|
type UiResourceReader,
|
|
34
36
|
type ViewMount,
|
|
35
37
|
type ViewMountChannel,
|
|
36
38
|
} from "./card-mount.js";
|
|
39
|
+
export {
|
|
40
|
+
createMcpUiResourceReader,
|
|
41
|
+
uiResourceChannel,
|
|
42
|
+
type CreateMcpUiResourceReaderDeps,
|
|
43
|
+
type McpResourceReadResult,
|
|
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";
|
package/src/reader.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generic SEP-1865 locator reader (guuey#127): resolve a persisted
|
|
3
|
+
* `ui://` locator ({@link LocatorViewMount}) into a mountable
|
|
4
|
+
* {@link ResolvedViewMount} by ONE fresh `resources/read` over a transport
|
|
5
|
+
* the HOST supplies.
|
|
6
|
+
*
|
|
7
|
+
* The transport is a structural callable — deliberately no
|
|
8
|
+
* `@modelcontextprotocol/sdk` dependency — because hosts hold wildly
|
|
9
|
+
* different connections: a raw MCP client (ggui's `with-guuey-web` sample
|
|
10
|
+
* keeps one for guest `tools/call` relay), an authenticated platform proxy
|
|
11
|
+
* (`@guuey/agent-client`'s `createUiResourceReader` is exactly this assembly
|
|
12
|
+
* over guuey's `GET /v1/threads/:threadId/ui-resource` route), or anything
|
|
13
|
+
* else that can perform one read. The host adapts whatever client it holds.
|
|
14
|
+
*
|
|
15
|
+
* Trust rules, identical to the platform reader and now in ONE place:
|
|
16
|
+
*
|
|
17
|
+
* - a transport deny, a miss, and a thrown transport error are all
|
|
18
|
+
* `undefined` → the host renders its placeholder, never an error surface
|
|
19
|
+
* (deny == miss — no oracle for which locators resolve);
|
|
20
|
+
* - enforcement (user-ownership, tenancy, credential handling) belongs
|
|
21
|
+
* INSIDE the transport, before any bytes come back — this assembly never
|
|
22
|
+
* sees a credential;
|
|
23
|
+
* - the sandbox-trust channel derives from the REQUESTED locator uri, never
|
|
24
|
+
* from the response — a server answering with a foreign `ui://ggui/…` uri
|
|
25
|
+
* must not steer its HTML into the ggui-CSP host page.
|
|
26
|
+
*/
|
|
27
|
+
import type { McpUiResourcePayload } from "./block-ui.js";
|
|
28
|
+
import type { ResolvedViewMount, ViewMountChannel } from "./card-mount.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One `resources/read` `contents[]` entry, structurally (SEP-1865). Kept a
|
|
32
|
+
* separate type from {@link McpUiResourcePayload} even though the shapes
|
|
33
|
+
* coincide today: this is the UNVALIDATED wire-side entry a transport hands
|
|
34
|
+
* over (phase 2 grows `_meta` here — per-resource declared CSP, conformance
|
|
35
|
+
* map step 3), while the payload is the narrowed thing a host mounts.
|
|
36
|
+
*/
|
|
37
|
+
export interface McpResourceReadResult {
|
|
38
|
+
uri: string;
|
|
39
|
+
mimeType?: string;
|
|
40
|
+
text?: string;
|
|
41
|
+
/** Base64 payload arm — hosts decode via {@link resourceHtml}. */
|
|
42
|
+
blob?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Phase-1 sandbox-trust channel resolution (conformance map, retirement
|
|
47
|
+
* step 3): a `ui://ggui/…` locator mounts through the ggui-CSP host page
|
|
48
|
+
* (`"ggui"` — the shell boots ggui's runtime bundle and opens its WSS),
|
|
49
|
+
* anything else through the self-only page (`"inline"` — arbitrary tenant
|
|
50
|
+
* HTML). Retired when servers declare runtime/ws origins per resource
|
|
51
|
+
* (`_meta.ui.csp` on the read result); until then the uri prefix is the only
|
|
52
|
+
* signal a host has BEFORE fetching mount material.
|
|
53
|
+
*/
|
|
54
|
+
export function uiResourceChannel(resourceUri: string): Exclude<ViewMountChannel, "locator"> {
|
|
55
|
+
return resourceUri.startsWith("ui://ggui/") ? "ggui" : "inline";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The host-supplied transport {@link createMcpUiResourceReader} assembles over. */
|
|
59
|
+
export interface CreateMcpUiResourceReaderDeps {
|
|
60
|
+
/**
|
|
61
|
+
* One `resources/read` of `uri` on the host's own MCP connection (or an
|
|
62
|
+
* equivalent authenticated proxy), returning the first `contents[]` entry —
|
|
63
|
+
* or `undefined` when the read yields none. Throwing is treated as a miss.
|
|
64
|
+
*/
|
|
65
|
+
readResource: (uri: string) => Promise<McpResourceReadResult | undefined>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Assemble a `UiResourceReader` from a host-supplied `resources/read`
|
|
70
|
+
* transport. The returned reader resolves only the mountable arms — a read
|
|
71
|
+
* either yields mount material or the honest placeholder, never another
|
|
72
|
+
* locator.
|
|
73
|
+
*/
|
|
74
|
+
export function createMcpUiResourceReader(
|
|
75
|
+
deps: CreateMcpUiResourceReaderDeps,
|
|
76
|
+
): (resourceUri: string) => Promise<ResolvedViewMount | undefined> {
|
|
77
|
+
return async (resourceUri) => {
|
|
78
|
+
let entry: McpResourceReadResult | undefined;
|
|
79
|
+
try {
|
|
80
|
+
entry = await deps.readResource(resourceUri);
|
|
81
|
+
} catch {
|
|
82
|
+
return undefined; // transport failure == deny == miss → placeholder
|
|
83
|
+
}
|
|
84
|
+
if (!entry) return undefined;
|
|
85
|
+
// Runtime re-narrowing, not trust in the annotation: transports are
|
|
86
|
+
// host-supplied and may be plain JS. Same field rules as `asResourcePayload`,
|
|
87
|
+
// applied to the typed entry (which may carry extra wire fields — dropped).
|
|
88
|
+
if (typeof entry.uri !== "string") return undefined;
|
|
89
|
+
if (typeof entry.text !== "string" && typeof entry.blob !== "string") return undefined;
|
|
90
|
+
const resource: McpUiResourcePayload = {
|
|
91
|
+
uri: entry.uri,
|
|
92
|
+
...(typeof entry.mimeType === "string" ? { mimeType: entry.mimeType } : {}),
|
|
93
|
+
...(typeof entry.text === "string" ? { text: entry.text } : {}),
|
|
94
|
+
...(typeof entry.blob === "string" ? { blob: entry.blob } : {}),
|
|
95
|
+
};
|
|
96
|
+
return { channel: uiResourceChannel(resourceUri), resource };
|
|
97
|
+
};
|
|
98
|
+
}
|