@guuey/mcp-apps-host 0.28.1 → 0.30.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/dist/action.d.ts +31 -2
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +126 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/action.ts +144 -3
- package/src/index.ts +5 -0
package/dist/action.d.ts
CHANGED
|
@@ -61,13 +61,42 @@ export type McpToolCallResult = {
|
|
|
61
61
|
* (the wsToken TTL is 180 s; without the relay a view on SSE/polling
|
|
62
62
|
* dies at that mark — guuey#220);
|
|
63
63
|
* - `ggui_runtime_pull` — the host-relayed polling rung where the
|
|
64
|
-
* sandbox's own transports are blocked (bridge-pull, terminal rung)
|
|
64
|
+
* sandbox's own transports are blocked (bridge-pull, terminal rung);
|
|
65
|
+
* - `ggui_runtime_telemetry` — the card's own health events, relayed only
|
|
66
|
+
* as the closed kind set {@link UI_TELEMETRY_KINDS}, filtered here before
|
|
67
|
+
* the relay calls the door.
|
|
65
68
|
*
|
|
66
|
-
*
|
|
69
|
+
* Three of these are runtime plumbing whose arguments are opaque runtime
|
|
67
70
|
* state — see {@link UI_SEMANTIC_ACTION_TOOLS} for the ONLY set a host may
|
|
68
71
|
* treat as "the user did something".
|
|
69
72
|
*/
|
|
70
73
|
export declare const UI_ACTION_TOOLS: ReadonlySet<string>;
|
|
74
|
+
/** The card-health telemetry tool. Never a user gesture. */
|
|
75
|
+
export declare const UI_TELEMETRY_TOOL = "ggui_runtime_telemetry";
|
|
76
|
+
/**
|
|
77
|
+
* What an admitted telemetry kind may carry as `detail`: nothing (`none`), or
|
|
78
|
+
* one exact, closed shape with no free text — three booleans (`boot-path`), a
|
|
79
|
+
* transport kind and a boolean (`subscribe`), or a render session id
|
|
80
|
+
* (`render-session-id`).
|
|
81
|
+
*/
|
|
82
|
+
export type UiTelemetryDetailRule = "none" | "boot-path" | "subscribe" | "render-session-id";
|
|
83
|
+
/**
|
|
84
|
+
* The closed set of health-event kinds a card may report through the relay:
|
|
85
|
+
* event names and ggui's own ids only, no free text and no user identity.
|
|
86
|
+
* Exact strings, never a prefix. The server doors admit exactly the same set
|
|
87
|
+
* and refuse a whole call that carries anything else; this twin filters
|
|
88
|
+
* events before the door, so a conforming host never sends a refusable call.
|
|
89
|
+
*/
|
|
90
|
+
export declare const UI_TELEMETRY_KINDS: ReadonlyMap<string, UiTelemetryDetailRule>;
|
|
91
|
+
/** The per-call event cap the doors enforce. */
|
|
92
|
+
export declare const MAX_UI_TELEMETRY_EVENTS = 40;
|
|
93
|
+
/**
|
|
94
|
+
* The telemetry call the host relays: the card's `sessionId` (when it is a
|
|
95
|
+
* string) and only the admitted events, oldest first, at most
|
|
96
|
+
* {@link MAX_UI_TELEMETRY_EVENTS}. Whole events are dropped, never edited.
|
|
97
|
+
* `undefined` when nothing is left to relay.
|
|
98
|
+
*/
|
|
99
|
+
export declare function admittedTelemetryArguments(args: McpToolStructuredContent | undefined): McpToolStructuredContent | undefined;
|
|
71
100
|
/**
|
|
72
101
|
* The strict subset of {@link UI_ACTION_TOOLS} that carries a USER gesture —
|
|
73
102
|
* the only calls a host may project into user-facing affordances (composer
|
package/dist/action.d.ts.map
CHANGED
|
@@ -1 +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
|
|
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;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAK9C,CAAC;AAEH,4DAA4D;AAC5D,eAAO,MAAM,iBAAiB,2BAA2B,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,GAAG,mBAAmB,CAAC;AAE7F;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,qBAAqB,CAYxE,CAAC;AAEH,gDAAgD;AAChD,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAmE1C;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,wBAAwB,GAAG,SAAS,GACzC,wBAAwB,GAAG,SAAS,CAatC;AAOD;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,EAAE,WAAW,CAAC,MAAM,CAEvD,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;IACtB;;;;;;;OAOG;IACH,qBAAqB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD;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;AAKD;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,uJACyG,CAAC;AAElJ;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,0BAA0B,GAC/B,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,iBAAiB,CAAC,CA4D1D"}
|
package/dist/action.js
CHANGED
|
@@ -23,9 +23,12 @@
|
|
|
23
23
|
* (the wsToken TTL is 180 s; without the relay a view on SSE/polling
|
|
24
24
|
* dies at that mark — guuey#220);
|
|
25
25
|
* - `ggui_runtime_pull` — the host-relayed polling rung where the
|
|
26
|
-
* sandbox's own transports are blocked (bridge-pull, terminal rung)
|
|
26
|
+
* sandbox's own transports are blocked (bridge-pull, terminal rung);
|
|
27
|
+
* - `ggui_runtime_telemetry` — the card's own health events, relayed only
|
|
28
|
+
* as the closed kind set {@link UI_TELEMETRY_KINDS}, filtered here before
|
|
29
|
+
* the relay calls the door.
|
|
27
30
|
*
|
|
28
|
-
*
|
|
31
|
+
* Three of these are runtime plumbing whose arguments are opaque runtime
|
|
29
32
|
* state — see {@link UI_SEMANTIC_ACTION_TOOLS} for the ONLY set a host may
|
|
30
33
|
* treat as "the user did something".
|
|
31
34
|
*/
|
|
@@ -33,7 +36,118 @@ export const UI_ACTION_TOOLS = new Set([
|
|
|
33
36
|
"ggui_runtime_submit_action",
|
|
34
37
|
"ggui_runtime_refresh_ws_token",
|
|
35
38
|
"ggui_runtime_pull",
|
|
39
|
+
"ggui_runtime_telemetry",
|
|
36
40
|
]);
|
|
41
|
+
/** The card-health telemetry tool. Never a user gesture. */
|
|
42
|
+
export const UI_TELEMETRY_TOOL = "ggui_runtime_telemetry";
|
|
43
|
+
/**
|
|
44
|
+
* The closed set of health-event kinds a card may report through the relay:
|
|
45
|
+
* event names and ggui's own ids only, no free text and no user identity.
|
|
46
|
+
* Exact strings, never a prefix. The server doors admit exactly the same set
|
|
47
|
+
* and refuse a whole call that carries anything else; this twin filters
|
|
48
|
+
* events before the door, so a conforming host never sends a refusable call.
|
|
49
|
+
*/
|
|
50
|
+
export const UI_TELEMETRY_KINDS = new Map([
|
|
51
|
+
["boot.path", "boot-path"],
|
|
52
|
+
["boot.static_only_no_bridge", "none"],
|
|
53
|
+
["status.connecting", "none"],
|
|
54
|
+
["status.reconnecting", "none"],
|
|
55
|
+
["status.connected", "none"],
|
|
56
|
+
["status.disconnected", "none"],
|
|
57
|
+
["subscribe.resolved", "subscribe"],
|
|
58
|
+
["doorbell.ring", "render-session-id"],
|
|
59
|
+
]);
|
|
60
|
+
/** The per-call event cap the doors enforce. */
|
|
61
|
+
export const MAX_UI_TELEMETRY_EVENTS = 40;
|
|
62
|
+
/** A render session id as ggui's hosted service mints it: `render_<uuid>`. */
|
|
63
|
+
const RENDER_SESSION_ID_RE = /^render_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
64
|
+
const SUBSCRIBE_TRANSPORT_KINDS = new Set(["ws", "sse", "polling"]);
|
|
65
|
+
function isPlainObject(value) {
|
|
66
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
function hasExactlyKeys(value, keys) {
|
|
69
|
+
const own = Object.keys(value);
|
|
70
|
+
return own.length === keys.length && keys.every((k) => Object.prototype.hasOwnProperty.call(value, k));
|
|
71
|
+
}
|
|
72
|
+
/** The detail parsed as JSON, or `undefined` when it is not JSON (the event is then dropped). */
|
|
73
|
+
function parsedJson(detail) {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(detail);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function telemetryDetailAdmitted(rule, detail) {
|
|
82
|
+
if (detail === undefined)
|
|
83
|
+
return true;
|
|
84
|
+
if (typeof detail !== "string")
|
|
85
|
+
return false;
|
|
86
|
+
switch (rule) {
|
|
87
|
+
case "none":
|
|
88
|
+
return false;
|
|
89
|
+
case "render-session-id":
|
|
90
|
+
return RENDER_SESSION_ID_RE.test(detail);
|
|
91
|
+
case "boot-path": {
|
|
92
|
+
const v = parsedJson(detail);
|
|
93
|
+
return (isPlainObject(v) &&
|
|
94
|
+
hasExactlyKeys(v, ["hasStaticContent", "hasLiveTrio", "bridgeCapable"]) &&
|
|
95
|
+
typeof v["hasStaticContent"] === "boolean" &&
|
|
96
|
+
typeof v["hasLiveTrio"] === "boolean" &&
|
|
97
|
+
typeof v["bridgeCapable"] === "boolean");
|
|
98
|
+
}
|
|
99
|
+
case "subscribe": {
|
|
100
|
+
const v = parsedJson(detail);
|
|
101
|
+
return (isPlainObject(v) &&
|
|
102
|
+
hasExactlyKeys(v, ["kind", "hasAck"]) &&
|
|
103
|
+
typeof v["kind"] === "string" &&
|
|
104
|
+
SUBSCRIBE_TRANSPORT_KINDS.has(v["kind"]) &&
|
|
105
|
+
typeof v["hasAck"] === "boolean");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** One event the host will relay: its keys are exactly `at`, `kind` and an admitted `detail`. */
|
|
110
|
+
function admittedTelemetryEvent(event) {
|
|
111
|
+
if (!isPlainObject(event))
|
|
112
|
+
return undefined;
|
|
113
|
+
if (Object.keys(event).some((k) => k !== "at" && k !== "kind" && k !== "detail"))
|
|
114
|
+
return undefined;
|
|
115
|
+
const at = event["at"];
|
|
116
|
+
if (typeof at !== "number" || !Number.isFinite(at) || at < 0)
|
|
117
|
+
return undefined;
|
|
118
|
+
const kind = event["kind"];
|
|
119
|
+
const rule = typeof kind === "string" ? UI_TELEMETRY_KINDS.get(kind) : undefined;
|
|
120
|
+
if (rule === undefined || !telemetryDetailAdmitted(rule, event["detail"]))
|
|
121
|
+
return undefined;
|
|
122
|
+
return event;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The telemetry call the host relays: the card's `sessionId` (when it is a
|
|
126
|
+
* string) and only the admitted events, oldest first, at most
|
|
127
|
+
* {@link MAX_UI_TELEMETRY_EVENTS}. Whole events are dropped, never edited.
|
|
128
|
+
* `undefined` when nothing is left to relay.
|
|
129
|
+
*/
|
|
130
|
+
export function admittedTelemetryArguments(args) {
|
|
131
|
+
if (args === undefined || !Array.isArray(args["events"]))
|
|
132
|
+
return undefined;
|
|
133
|
+
const events = [];
|
|
134
|
+
for (const event of args["events"]) {
|
|
135
|
+
const admitted = admittedTelemetryEvent(event);
|
|
136
|
+
if (admitted !== undefined)
|
|
137
|
+
events.push(admitted);
|
|
138
|
+
}
|
|
139
|
+
if (events.length === 0)
|
|
140
|
+
return undefined;
|
|
141
|
+
const sessionId = args["sessionId"];
|
|
142
|
+
return {
|
|
143
|
+
...(typeof sessionId === "string" ? { sessionId } : {}),
|
|
144
|
+
events: events.slice(-MAX_UI_TELEMETRY_EVENTS),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** The local answer when a telemetry call has nothing admitted to relay: ggui's own `{ok: true}` shape. */
|
|
148
|
+
function telemetryNothingToRelayResult() {
|
|
149
|
+
return { content: [], structuredContent: { ok: true } };
|
|
150
|
+
}
|
|
37
151
|
/**
|
|
38
152
|
* The strict subset of {@link UI_ACTION_TOOLS} that carries a USER gesture —
|
|
39
153
|
* the only calls a host may project into user-facing affordances (composer
|
|
@@ -187,6 +301,15 @@ export function createMcpUiActionRelay(deps) {
|
|
|
187
301
|
return unavailableToolCallResult();
|
|
188
302
|
if (!request.resourceUri.startsWith("ui://"))
|
|
189
303
|
return unavailableToolCallResult();
|
|
304
|
+
// The card's health events: only the admitted kinds reach the door, and a
|
|
305
|
+
// door that refuses the tool (an older server) answers in-band like any
|
|
306
|
+
// other failure. Telemetry never touches the pull circuit.
|
|
307
|
+
let callArguments = request.arguments;
|
|
308
|
+
if (request.name === UI_TELEMETRY_TOOL) {
|
|
309
|
+
callArguments = admittedTelemetryArguments(request.arguments);
|
|
310
|
+
if (callArguments === undefined)
|
|
311
|
+
return telemetryNothingToRelayResult();
|
|
312
|
+
}
|
|
190
313
|
const isPull = request.name === PULL_TOOL;
|
|
191
314
|
// Circuit OPEN for this locator's pull → fail-fast, never touch the door.
|
|
192
315
|
if (isPull && (pullFailures.get(request.resourceUri) ?? 0) >= PULL_CIRCUIT_THRESHOLD) {
|
|
@@ -194,7 +317,7 @@ export function createMcpUiActionRelay(deps) {
|
|
|
194
317
|
}
|
|
195
318
|
let raw;
|
|
196
319
|
try {
|
|
197
|
-
raw = await deps.callTool(request.resourceUri, request.name,
|
|
320
|
+
raw = await deps.callTool(request.resourceUri, request.name, callArguments);
|
|
198
321
|
}
|
|
199
322
|
catch {
|
|
200
323
|
if (isPull)
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { resolveViewMount, snapshotViewMount, toolResultViewMount, type LocatorV
|
|
|
8
8
|
export { ACTION_STAGED_MSG, isStageableAction, stagedActionText, withActionStaging, type ActionStaging, } from "./action-staging.js";
|
|
9
9
|
export { createMcpUiResourceReader, declaredResourceCsp, uiResourceChannel, type CreateMcpUiResourceReaderDeps, type McpResourceReadResult, } from "./reader.js";
|
|
10
10
|
export type { McpUiResourceCsp } from "@modelcontextprotocol/ext-apps";
|
|
11
|
-
export { asToolCallResult, createMcpUiActionRelay, unavailableToolCallResult, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, UI_SEMANTIC_ACTION_TOOLS, type CreateMcpUiActionRelayDeps, type McpToolCallContent, type McpToolCallResult, type McpToolStructuredContent, type UiActionRequest, } from "./action.js";
|
|
11
|
+
export { admittedTelemetryArguments, asToolCallResult, createMcpUiActionRelay, MAX_UI_TELEMETRY_EVENTS, unavailableToolCallResult, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, UI_SEMANTIC_ACTION_TOOLS, UI_TELEMETRY_KINDS, UI_TELEMETRY_TOOL, type CreateMcpUiActionRelayDeps, type UiTelemetryDetailRule, type McpToolCallContent, type McpToolCallResult, type McpToolStructuredContent, type UiActionRequest, } from "./action.js";
|
|
12
12
|
export { initializeResult, initialViewHostState, resourceReadResponse, RESOURCES_READ_METHOD, teardownMessage, toolCallResponse, TOOLS_CALL_METHOD, viewHostElapsed, viewHostReceive, diagnoseCspViolation, type CspViolationLike, type ViewCspDiagnosis, type ViewCspOrigins, type ViewHostBehavior, type ViewHostEffect, type ViewHostOutbound, type ViewHostPhase, type ViewHostInfo, type ViewHostState, type ViewHostTransition, type ViewRequestId, } from "./view-host-protocol.js";
|
|
13
13
|
export { attachViewHost, viewDocumentHtml, MESSAGE_ANSWER_CAP_MS, type AttachViewHostConfig, type UserMessageDelivery, type ViewCspEvents, type ViewFrameLike, type ViewHostEvents, } from "./view-host.js";
|
|
14
14
|
export { attachSandboxPageDelivery, isSandboxProxyReady, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, type SandboxPageDeliveryConfig, } from "./sandbox-page.js";
|
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,iBAAiB,EACjB,oBAAoB,EACpB,SAAS,EACT,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,iBAAiB,EACjB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAC;AAIrB,YAAY,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EACf,0BAA0B,EAC1B,wBAAwB,EACxB,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,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,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,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,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;AAI3B,OAAO,EAAE,uBAAuB,EAAE,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAIzF,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,EAC3B,wBAAwB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,WAAW,GACjB,MAAM,2BAA2B,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,iBAAiB,EACjB,oBAAoB,EACpB,SAAS,EACT,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,iBAAiB,EACjB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAC;AAIrB,YAAY,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,sBAAsB,EACtB,uBAAuB,EACvB,yBAAyB,EACzB,eAAe,EACf,0BAA0B,EAC1B,wBAAwB,EACxB,kBAAkB,EAClB,iBAAiB,EACjB,KAAK,0BAA0B,EAC/B,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,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,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,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;AAI3B,OAAO,EAAE,uBAAuB,EAAE,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAIzF,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,EAC3B,wBAAwB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,WAAW,GACjB,MAAM,2BAA2B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export { asResourcePayload, asUiResource, blockUiResource, isJsonObject, resourc
|
|
|
7
7
|
export { resolveViewMount, snapshotViewMount, toolResultViewMount, } from "./card-mount.js";
|
|
8
8
|
export { ACTION_STAGED_MSG, isStageableAction, stagedActionText, withActionStaging, } from "./action-staging.js";
|
|
9
9
|
export { createMcpUiResourceReader, declaredResourceCsp, uiResourceChannel, } from "./reader.js";
|
|
10
|
-
export { asToolCallResult, createMcpUiActionRelay, unavailableToolCallResult, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, UI_SEMANTIC_ACTION_TOOLS, } from "./action.js";
|
|
10
|
+
export { admittedTelemetryArguments, asToolCallResult, createMcpUiActionRelay, MAX_UI_TELEMETRY_EVENTS, unavailableToolCallResult, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, UI_SEMANTIC_ACTION_TOOLS, UI_TELEMETRY_KINDS, UI_TELEMETRY_TOOL, } from "./action.js";
|
|
11
11
|
export { initializeResult, initialViewHostState, resourceReadResponse, RESOURCES_READ_METHOD, teardownMessage, toolCallResponse, TOOLS_CALL_METHOD, viewHostElapsed, viewHostReceive, diagnoseCspViolation, } from "./view-host-protocol.js";
|
|
12
12
|
export { attachViewHost, viewDocumentHtml, MESSAGE_ANSWER_CAP_MS, } from "./view-host.js";
|
|
13
13
|
export { attachSandboxPageDelivery, isSandboxProxyReady, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, } from "./sandbox-page.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guuey/mcp-apps-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.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",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@ggui-ai/protocol": "0.
|
|
36
|
+
"@ggui-ai/protocol": "0.24.0",
|
|
37
37
|
"@modelcontextprotocol/ext-apps": "^1.7.5",
|
|
38
|
-
"@silverprotocol/core": "0.
|
|
38
|
+
"@silverprotocol/core": "0.11.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"react": ">=18"
|
package/src/action.ts
CHANGED
|
@@ -57,9 +57,12 @@ export type McpToolCallResult = {
|
|
|
57
57
|
* (the wsToken TTL is 180 s; without the relay a view on SSE/polling
|
|
58
58
|
* dies at that mark — guuey#220);
|
|
59
59
|
* - `ggui_runtime_pull` — the host-relayed polling rung where the
|
|
60
|
-
* sandbox's own transports are blocked (bridge-pull, terminal rung)
|
|
60
|
+
* sandbox's own transports are blocked (bridge-pull, terminal rung);
|
|
61
|
+
* - `ggui_runtime_telemetry` — the card's own health events, relayed only
|
|
62
|
+
* as the closed kind set {@link UI_TELEMETRY_KINDS}, filtered here before
|
|
63
|
+
* the relay calls the door.
|
|
61
64
|
*
|
|
62
|
-
*
|
|
65
|
+
* Three of these are runtime plumbing whose arguments are opaque runtime
|
|
63
66
|
* state — see {@link UI_SEMANTIC_ACTION_TOOLS} for the ONLY set a host may
|
|
64
67
|
* treat as "the user did something".
|
|
65
68
|
*/
|
|
@@ -67,8 +70,137 @@ export const UI_ACTION_TOOLS: ReadonlySet<string> = new Set([
|
|
|
67
70
|
"ggui_runtime_submit_action",
|
|
68
71
|
"ggui_runtime_refresh_ws_token",
|
|
69
72
|
"ggui_runtime_pull",
|
|
73
|
+
"ggui_runtime_telemetry",
|
|
70
74
|
]);
|
|
71
75
|
|
|
76
|
+
/** The card-health telemetry tool. Never a user gesture. */
|
|
77
|
+
export const UI_TELEMETRY_TOOL = "ggui_runtime_telemetry";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* What an admitted telemetry kind may carry as `detail`: nothing (`none`), or
|
|
81
|
+
* one exact, closed shape with no free text — three booleans (`boot-path`), a
|
|
82
|
+
* transport kind and a boolean (`subscribe`), or a render session id
|
|
83
|
+
* (`render-session-id`).
|
|
84
|
+
*/
|
|
85
|
+
export type UiTelemetryDetailRule = "none" | "boot-path" | "subscribe" | "render-session-id";
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The closed set of health-event kinds a card may report through the relay:
|
|
89
|
+
* event names and ggui's own ids only, no free text and no user identity.
|
|
90
|
+
* Exact strings, never a prefix. The server doors admit exactly the same set
|
|
91
|
+
* and refuse a whole call that carries anything else; this twin filters
|
|
92
|
+
* events before the door, so a conforming host never sends a refusable call.
|
|
93
|
+
*/
|
|
94
|
+
export const UI_TELEMETRY_KINDS: ReadonlyMap<string, UiTelemetryDetailRule> = new Map<
|
|
95
|
+
string,
|
|
96
|
+
UiTelemetryDetailRule
|
|
97
|
+
>([
|
|
98
|
+
["boot.path", "boot-path"],
|
|
99
|
+
["boot.static_only_no_bridge", "none"],
|
|
100
|
+
["status.connecting", "none"],
|
|
101
|
+
["status.reconnecting", "none"],
|
|
102
|
+
["status.connected", "none"],
|
|
103
|
+
["status.disconnected", "none"],
|
|
104
|
+
["subscribe.resolved", "subscribe"],
|
|
105
|
+
["doorbell.ring", "render-session-id"],
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
/** The per-call event cap the doors enforce. */
|
|
109
|
+
export const MAX_UI_TELEMETRY_EVENTS = 40;
|
|
110
|
+
|
|
111
|
+
/** A render session id as ggui's hosted service mints it: `render_<uuid>`. */
|
|
112
|
+
const RENDER_SESSION_ID_RE = /^render_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
113
|
+
const SUBSCRIBE_TRANSPORT_KINDS: ReadonlySet<string> = new Set(["ws", "sse", "polling"]);
|
|
114
|
+
|
|
115
|
+
function isPlainObject(value: unknown): value is { [key: string]: unknown } {
|
|
116
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function hasExactlyKeys(value: { [key: string]: unknown }, keys: readonly string[]): boolean {
|
|
120
|
+
const own = Object.keys(value);
|
|
121
|
+
return own.length === keys.length && keys.every((k) => Object.prototype.hasOwnProperty.call(value, k));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The detail parsed as JSON, or `undefined` when it is not JSON (the event is then dropped). */
|
|
125
|
+
function parsedJson(detail: string): unknown {
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(detail);
|
|
128
|
+
} catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function telemetryDetailAdmitted(rule: UiTelemetryDetailRule, detail: unknown): boolean {
|
|
134
|
+
if (detail === undefined) return true;
|
|
135
|
+
if (typeof detail !== "string") return false;
|
|
136
|
+
switch (rule) {
|
|
137
|
+
case "none":
|
|
138
|
+
return false;
|
|
139
|
+
case "render-session-id":
|
|
140
|
+
return RENDER_SESSION_ID_RE.test(detail);
|
|
141
|
+
case "boot-path": {
|
|
142
|
+
const v = parsedJson(detail);
|
|
143
|
+
return (
|
|
144
|
+
isPlainObject(v) &&
|
|
145
|
+
hasExactlyKeys(v, ["hasStaticContent", "hasLiveTrio", "bridgeCapable"]) &&
|
|
146
|
+
typeof v["hasStaticContent"] === "boolean" &&
|
|
147
|
+
typeof v["hasLiveTrio"] === "boolean" &&
|
|
148
|
+
typeof v["bridgeCapable"] === "boolean"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
case "subscribe": {
|
|
152
|
+
const v = parsedJson(detail);
|
|
153
|
+
return (
|
|
154
|
+
isPlainObject(v) &&
|
|
155
|
+
hasExactlyKeys(v, ["kind", "hasAck"]) &&
|
|
156
|
+
typeof v["kind"] === "string" &&
|
|
157
|
+
SUBSCRIBE_TRANSPORT_KINDS.has(v["kind"]) &&
|
|
158
|
+
typeof v["hasAck"] === "boolean"
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** One event the host will relay: its keys are exactly `at`, `kind` and an admitted `detail`. */
|
|
165
|
+
function admittedTelemetryEvent(event: unknown): McpToolStructuredContent | undefined {
|
|
166
|
+
if (!isPlainObject(event)) return undefined;
|
|
167
|
+
if (Object.keys(event).some((k) => k !== "at" && k !== "kind" && k !== "detail")) return undefined;
|
|
168
|
+
const at = event["at"];
|
|
169
|
+
if (typeof at !== "number" || !Number.isFinite(at) || at < 0) return undefined;
|
|
170
|
+
const kind = event["kind"];
|
|
171
|
+
const rule = typeof kind === "string" ? UI_TELEMETRY_KINDS.get(kind) : undefined;
|
|
172
|
+
if (rule === undefined || !telemetryDetailAdmitted(rule, event["detail"])) return undefined;
|
|
173
|
+
return event;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The telemetry call the host relays: the card's `sessionId` (when it is a
|
|
178
|
+
* string) and only the admitted events, oldest first, at most
|
|
179
|
+
* {@link MAX_UI_TELEMETRY_EVENTS}. Whole events are dropped, never edited.
|
|
180
|
+
* `undefined` when nothing is left to relay.
|
|
181
|
+
*/
|
|
182
|
+
export function admittedTelemetryArguments(
|
|
183
|
+
args: McpToolStructuredContent | undefined,
|
|
184
|
+
): McpToolStructuredContent | undefined {
|
|
185
|
+
if (args === undefined || !Array.isArray(args["events"])) return undefined;
|
|
186
|
+
const events: McpToolStructuredContent[] = [];
|
|
187
|
+
for (const event of args["events"]) {
|
|
188
|
+
const admitted = admittedTelemetryEvent(event);
|
|
189
|
+
if (admitted !== undefined) events.push(admitted);
|
|
190
|
+
}
|
|
191
|
+
if (events.length === 0) return undefined;
|
|
192
|
+
const sessionId = args["sessionId"];
|
|
193
|
+
return {
|
|
194
|
+
...(typeof sessionId === "string" ? { sessionId } : {}),
|
|
195
|
+
events: events.slice(-MAX_UI_TELEMETRY_EVENTS),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The local answer when a telemetry call has nothing admitted to relay: ggui's own `{ok: true}` shape. */
|
|
200
|
+
function telemetryNothingToRelayResult(): McpToolCallResult {
|
|
201
|
+
return { content: [], structuredContent: { ok: true } };
|
|
202
|
+
}
|
|
203
|
+
|
|
72
204
|
/**
|
|
73
205
|
* The strict subset of {@link UI_ACTION_TOOLS} that carries a USER gesture —
|
|
74
206
|
* the only calls a host may project into user-facing affordances (composer
|
|
@@ -265,6 +397,15 @@ export function createMcpUiActionRelay(
|
|
|
265
397
|
if (!UI_ACTION_TOOLS.has(request.name)) return unavailableToolCallResult();
|
|
266
398
|
if (!request.resourceUri.startsWith("ui://")) return unavailableToolCallResult();
|
|
267
399
|
|
|
400
|
+
// The card's health events: only the admitted kinds reach the door, and a
|
|
401
|
+
// door that refuses the tool (an older server) answers in-band like any
|
|
402
|
+
// other failure. Telemetry never touches the pull circuit.
|
|
403
|
+
let callArguments = request.arguments;
|
|
404
|
+
if (request.name === UI_TELEMETRY_TOOL) {
|
|
405
|
+
callArguments = admittedTelemetryArguments(request.arguments);
|
|
406
|
+
if (callArguments === undefined) return telemetryNothingToRelayResult();
|
|
407
|
+
}
|
|
408
|
+
|
|
268
409
|
const isPull = request.name === PULL_TOOL;
|
|
269
410
|
// Circuit OPEN for this locator's pull → fail-fast, never touch the door.
|
|
270
411
|
if (isPull && (pullFailures.get(request.resourceUri) ?? 0) >= PULL_CIRCUIT_THRESHOLD) {
|
|
@@ -273,7 +414,7 @@ export function createMcpUiActionRelay(
|
|
|
273
414
|
|
|
274
415
|
let raw: unknown;
|
|
275
416
|
try {
|
|
276
|
-
raw = await deps.callTool(request.resourceUri, request.name,
|
|
417
|
+
raw = await deps.callTool(request.resourceUri, request.name, callArguments);
|
|
277
418
|
} catch {
|
|
278
419
|
if (isPull) recordPull(request.resourceUri, true); // transport failure == unavailable
|
|
279
420
|
return unavailableToolCallResult(); // in-band
|
package/src/index.ts
CHANGED
|
@@ -46,13 +46,18 @@ export {
|
|
|
46
46
|
// `ViewCspOrigins`, re-exported under its spec name for mount consumers.
|
|
47
47
|
export type { McpUiResourceCsp } from "@modelcontextprotocol/ext-apps";
|
|
48
48
|
export {
|
|
49
|
+
admittedTelemetryArguments,
|
|
49
50
|
asToolCallResult,
|
|
50
51
|
createMcpUiActionRelay,
|
|
52
|
+
MAX_UI_TELEMETRY_EVENTS,
|
|
51
53
|
unavailableToolCallResult,
|
|
52
54
|
UI_ACTION_TOOLS,
|
|
53
55
|
UI_ACTION_UNAVAILABLE_TEXT,
|
|
54
56
|
UI_SEMANTIC_ACTION_TOOLS,
|
|
57
|
+
UI_TELEMETRY_KINDS,
|
|
58
|
+
UI_TELEMETRY_TOOL,
|
|
55
59
|
type CreateMcpUiActionRelayDeps,
|
|
60
|
+
type UiTelemetryDetailRule,
|
|
56
61
|
type McpToolCallContent,
|
|
57
62
|
type McpToolCallResult,
|
|
58
63
|
type McpToolStructuredContent,
|