@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 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
- * Two of these are transport plumbing whose arguments are opaque runtime
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
@@ -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;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAI9C,CAAC;AAEH;;;;;;;;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,CAmD1D"}
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
- * Two of these are transport plumbing whose arguments are opaque runtime
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, request.arguments);
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";
@@ -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.28.1",
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.23.0",
36
+ "@ggui-ai/protocol": "0.24.0",
37
37
  "@modelcontextprotocol/ext-apps": "^1.7.5",
38
- "@silverprotocol/core": "0.7.2"
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
- * Two of these are transport plumbing whose arguments are opaque runtime
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, request.arguments);
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,