@guuey/mcp-apps-host 0.22.0 → 0.22.1

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
@@ -101,6 +101,15 @@ export interface CreateMcpUiActionRelayDeps {
101
101
  * treated as unavailable.
102
102
  */
103
103
  callTool: (uri: string, name: string, args: McpToolStructuredContent | undefined) => Promise<unknown>;
104
+ /**
105
+ * Fired ONCE when a card locator's `ggui_runtime_pull` circuit OPENS
106
+ * (guuey#1249 item 4) — the live session is unrestorable (N consecutive
107
+ * both-doors-gone pulls). The complement to leg 1's storm-break: the host
108
+ * surfaces a visible "this session ended — start a new chat" state and drops
109
+ * the stale thread, so a tripped circuit is never a silent frozen card.
110
+ * Optional: a host that only wants the storm bounded omits it.
111
+ */
112
+ onSessionUnrestorable?: (resourceUri: string) => void;
104
113
  }
105
114
  /** The request shape a mounted card's `onCallTool` bridge produces. */
106
115
  export interface UiActionRequest {
@@ -109,10 +118,37 @@ export interface UiActionRequest {
109
118
  name: string;
110
119
  arguments?: McpToolStructuredContent;
111
120
  }
121
+ /**
122
+ * Consecutive `unavailable` pull results (per card locator) that OPEN the
123
+ * circuit (guuey#1235 leg 1). At the #1233 storm's ~12-18 pulls/min a 3-strike
124
+ * trip bounds the hammer in ~10-15s; a single good pull resets it, so a
125
+ * transient blip never trips.
126
+ */
127
+ export declare const PULL_CIRCUIT_THRESHOLD = 3;
128
+ /**
129
+ * Logged ONCE when a locator's pull circuit opens — the bounded-storm marker
130
+ * (sentry's readability ask; the #1233 next-incident is scopeable from it).
131
+ */
132
+ export declare const UI_ACTION_PULL_CIRCUIT_OPEN = "UI_ACTION_PULL_CIRCUIT_OPEN";
112
133
  /**
113
134
  * Assemble the sandbox-facing action relay from a host transport. The
114
135
  * returned function is shaped for an `onCallTool` bridge: it always
115
136
  * resolves (never rejects), answering in-band.
137
+ *
138
+ * guuey#1235 leg 1 — the `ggui_runtime_pull` CIRCUIT BREAK. A card whose live
139
+ * session died keeps auto-polling on its own interval; each pull `tools/call`
140
+ * 404s at the pod door → `unavailable` → the sandbox polls again, hammering the
141
+ * door indefinitely (the #1233 prod storm: 57+ over 15 min). After
142
+ * {@link PULL_CIRCUIT_THRESHOLD} CONSECUTIVE `unavailable` pull results for a
143
+ * locator, the circuit OPENS: the relay stops calling the transport for that
144
+ * locator's pull (fail-fast, no network) — the server storm is bounded. A
145
+ * single non-`unavailable` pull result CLOSES it (the session recovered). Only
146
+ * the pull rung is counted — a failed user gesture (`submit_action`) or token
147
+ * refresh must never trip the auto-poll break, and never opens another rung.
148
+ *
149
+ * The complement — telling the USER the session is unrestorable so a tripped
150
+ * circuit is not a silent freeze — is the `onSessionUnrestorable` signal
151
+ * (guuey#1249 item 4); this leg only bounds the hammer.
116
152
  */
117
153
  export declare function createMcpUiActionRelay(deps: CreateMcpUiActionRelayDeps): (request: UiActionRequest) => Promise<McpToolCallResult>;
118
154
  //# sourceMappingURL=action.d.ts.map
@@ -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;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"}
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;;;GAGG;AACH,eAAO,MAAM,2BAA2B,gCAAgC,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,0BAA0B,GAC/B,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAmD1D"}
package/dist/action.js CHANGED
@@ -113,26 +113,91 @@ export function asToolCallResult(value) {
113
113
  : {}),
114
114
  };
115
115
  }
116
+ /** The host-relayed auto-poll rung (guuey#1235). A dead session's pull is the storm. */
117
+ const PULL_TOOL = "ggui_runtime_pull";
118
+ /**
119
+ * Consecutive `unavailable` pull results (per card locator) that OPEN the
120
+ * circuit (guuey#1235 leg 1). At the #1233 storm's ~12-18 pulls/min a 3-strike
121
+ * trip bounds the hammer in ~10-15s; a single good pull resets it, so a
122
+ * transient blip never trips.
123
+ */
124
+ export const PULL_CIRCUIT_THRESHOLD = 3;
125
+ /**
126
+ * Logged ONCE when a locator's pull circuit opens — the bounded-storm marker
127
+ * (sentry's readability ask; the #1233 next-incident is scopeable from it).
128
+ */
129
+ export const UI_ACTION_PULL_CIRCUIT_OPEN = "UI_ACTION_PULL_CIRCUIT_OPEN";
116
130
  /**
117
131
  * Assemble the sandbox-facing action relay from a host transport. The
118
132
  * returned function is shaped for an `onCallTool` bridge: it always
119
133
  * resolves (never rejects), answering in-band.
134
+ *
135
+ * guuey#1235 leg 1 — the `ggui_runtime_pull` CIRCUIT BREAK. A card whose live
136
+ * session died keeps auto-polling on its own interval; each pull `tools/call`
137
+ * 404s at the pod door → `unavailable` → the sandbox polls again, hammering the
138
+ * door indefinitely (the #1233 prod storm: 57+ over 15 min). After
139
+ * {@link PULL_CIRCUIT_THRESHOLD} CONSECUTIVE `unavailable` pull results for a
140
+ * locator, the circuit OPENS: the relay stops calling the transport for that
141
+ * locator's pull (fail-fast, no network) — the server storm is bounded. A
142
+ * single non-`unavailable` pull result CLOSES it (the session recovered). Only
143
+ * the pull rung is counted — a failed user gesture (`submit_action`) or token
144
+ * refresh must never trip the auto-poll break, and never opens another rung.
145
+ *
146
+ * The complement — telling the USER the session is unrestorable so a tripped
147
+ * circuit is not a silent freeze — is the `onSessionUnrestorable` signal
148
+ * (guuey#1249 item 4); this leg only bounds the hammer.
120
149
  */
121
150
  export function createMcpUiActionRelay(deps) {
151
+ // Per-relay-instance (one card mount). A recovered/absent locator is deleted,
152
+ // so this stays as small as the mounted cards; the mount tears it down.
153
+ const pullFailures = new Map();
154
+ const recordPull = (uri, unavailable) => {
155
+ if (!unavailable) {
156
+ pullFailures.delete(uri); // the session answered → close the circuit
157
+ return;
158
+ }
159
+ const next = (pullFailures.get(uri) ?? 0) + 1;
160
+ pullFailures.set(uri, next);
161
+ if (next === PULL_CIRCUIT_THRESHOLD) {
162
+ // Once, at the trip — not per subsequent short-circuited poll.
163
+ console.warn(UI_ACTION_PULL_CIRCUIT_OPEN, {
164
+ resourceUri: uri,
165
+ consecutiveUnavailable: next,
166
+ });
167
+ // guuey#1249 item 4: tell the host the session is unrestorable so the
168
+ // bounded circuit isn't a silent freeze. A throw from the host callback
169
+ // must never break the relay's never-reject contract.
170
+ try {
171
+ deps.onSessionUnrestorable?.(uri);
172
+ }
173
+ catch {
174
+ // A host-supplied callback that throws is the host's bug, not the
175
+ // relay's — the storm is already bounded either way.
176
+ }
177
+ }
178
+ };
122
179
  return async (request) => {
123
180
  if (!UI_ACTION_TOOLS.has(request.name))
124
181
  return unavailableToolCallResult();
125
182
  if (!request.resourceUri.startsWith("ui://"))
126
183
  return unavailableToolCallResult();
184
+ const isPull = request.name === PULL_TOOL;
185
+ // Circuit OPEN for this locator's pull → fail-fast, never touch the door.
186
+ if (isPull && (pullFailures.get(request.resourceUri) ?? 0) >= PULL_CIRCUIT_THRESHOLD) {
187
+ return unavailableToolCallResult();
188
+ }
127
189
  let raw;
128
190
  try {
129
191
  raw = await deps.callTool(request.resourceUri, request.name, request.arguments);
130
192
  }
131
193
  catch {
132
- return unavailableToolCallResult(); // transport failure == unavailable, in-band
194
+ if (isPull)
195
+ recordPull(request.resourceUri, true); // transport failure == unavailable
196
+ return unavailableToolCallResult(); // in-band
133
197
  }
134
- if (raw === undefined)
135
- return unavailableToolCallResult();
136
- return asToolCallResult(raw) ?? unavailableToolCallResult();
198
+ const result = raw === undefined ? undefined : asToolCallResult(raw);
199
+ if (isPull)
200
+ recordPull(request.resourceUri, result === undefined);
201
+ return result ?? unavailableToolCallResult();
137
202
  };
138
203
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guuey/mcp-apps-host",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
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",
package/src/action.ts CHANGED
@@ -166,6 +166,15 @@ export interface CreateMcpUiActionRelayDeps {
166
166
  name: string,
167
167
  args: McpToolStructuredContent | undefined,
168
168
  ) => Promise<unknown>;
169
+ /**
170
+ * Fired ONCE when a card locator's `ggui_runtime_pull` circuit OPENS
171
+ * (guuey#1249 item 4) — the live session is unrestorable (N consecutive
172
+ * both-doors-gone pulls). The complement to leg 1's storm-break: the host
173
+ * surfaces a visible "this session ended — start a new chat" state and drops
174
+ * the stale thread, so a tripped circuit is never a silent frozen card.
175
+ * Optional: a host that only wants the storm bounded omits it.
176
+ */
177
+ onSessionUnrestorable?: (resourceUri: string) => void;
169
178
  }
170
179
 
171
180
  /** The request shape a mounted card's `onCallTool` bridge produces. */
@@ -176,24 +185,94 @@ export interface UiActionRequest {
176
185
  arguments?: McpToolStructuredContent;
177
186
  }
178
187
 
188
+ /** The host-relayed auto-poll rung (guuey#1235). A dead session's pull is the storm. */
189
+ const PULL_TOOL = "ggui_runtime_pull";
190
+
191
+ /**
192
+ * Consecutive `unavailable` pull results (per card locator) that OPEN the
193
+ * circuit (guuey#1235 leg 1). At the #1233 storm's ~12-18 pulls/min a 3-strike
194
+ * trip bounds the hammer in ~10-15s; a single good pull resets it, so a
195
+ * transient blip never trips.
196
+ */
197
+ export const PULL_CIRCUIT_THRESHOLD = 3;
198
+
199
+ /**
200
+ * Logged ONCE when a locator's pull circuit opens — the bounded-storm marker
201
+ * (sentry's readability ask; the #1233 next-incident is scopeable from it).
202
+ */
203
+ export const UI_ACTION_PULL_CIRCUIT_OPEN = "UI_ACTION_PULL_CIRCUIT_OPEN";
204
+
179
205
  /**
180
206
  * Assemble the sandbox-facing action relay from a host transport. The
181
207
  * returned function is shaped for an `onCallTool` bridge: it always
182
208
  * resolves (never rejects), answering in-band.
209
+ *
210
+ * guuey#1235 leg 1 — the `ggui_runtime_pull` CIRCUIT BREAK. A card whose live
211
+ * session died keeps auto-polling on its own interval; each pull `tools/call`
212
+ * 404s at the pod door → `unavailable` → the sandbox polls again, hammering the
213
+ * door indefinitely (the #1233 prod storm: 57+ over 15 min). After
214
+ * {@link PULL_CIRCUIT_THRESHOLD} CONSECUTIVE `unavailable` pull results for a
215
+ * locator, the circuit OPENS: the relay stops calling the transport for that
216
+ * locator's pull (fail-fast, no network) — the server storm is bounded. A
217
+ * single non-`unavailable` pull result CLOSES it (the session recovered). Only
218
+ * the pull rung is counted — a failed user gesture (`submit_action`) or token
219
+ * refresh must never trip the auto-poll break, and never opens another rung.
220
+ *
221
+ * The complement — telling the USER the session is unrestorable so a tripped
222
+ * circuit is not a silent freeze — is the `onSessionUnrestorable` signal
223
+ * (guuey#1249 item 4); this leg only bounds the hammer.
183
224
  */
184
225
  export function createMcpUiActionRelay(
185
226
  deps: CreateMcpUiActionRelayDeps,
186
227
  ): (request: UiActionRequest) => Promise<McpToolCallResult> {
228
+ // Per-relay-instance (one card mount). A recovered/absent locator is deleted,
229
+ // so this stays as small as the mounted cards; the mount tears it down.
230
+ const pullFailures = new Map<string, number>();
231
+
232
+ const recordPull = (uri: string, unavailable: boolean): void => {
233
+ if (!unavailable) {
234
+ pullFailures.delete(uri); // the session answered → close the circuit
235
+ return;
236
+ }
237
+ const next = (pullFailures.get(uri) ?? 0) + 1;
238
+ pullFailures.set(uri, next);
239
+ if (next === PULL_CIRCUIT_THRESHOLD) {
240
+ // Once, at the trip — not per subsequent short-circuited poll.
241
+ console.warn(UI_ACTION_PULL_CIRCUIT_OPEN, {
242
+ resourceUri: uri,
243
+ consecutiveUnavailable: next,
244
+ });
245
+ // guuey#1249 item 4: tell the host the session is unrestorable so the
246
+ // bounded circuit isn't a silent freeze. A throw from the host callback
247
+ // must never break the relay's never-reject contract.
248
+ try {
249
+ deps.onSessionUnrestorable?.(uri);
250
+ } catch {
251
+ // A host-supplied callback that throws is the host's bug, not the
252
+ // relay's — the storm is already bounded either way.
253
+ }
254
+ }
255
+ };
256
+
187
257
  return async (request) => {
188
258
  if (!UI_ACTION_TOOLS.has(request.name)) return unavailableToolCallResult();
189
259
  if (!request.resourceUri.startsWith("ui://")) return unavailableToolCallResult();
260
+
261
+ const isPull = request.name === PULL_TOOL;
262
+ // Circuit OPEN for this locator's pull → fail-fast, never touch the door.
263
+ if (isPull && (pullFailures.get(request.resourceUri) ?? 0) >= PULL_CIRCUIT_THRESHOLD) {
264
+ return unavailableToolCallResult();
265
+ }
266
+
190
267
  let raw: unknown;
191
268
  try {
192
269
  raw = await deps.callTool(request.resourceUri, request.name, request.arguments);
193
270
  } catch {
194
- return unavailableToolCallResult(); // transport failure == unavailable, in-band
271
+ if (isPull) recordPull(request.resourceUri, true); // transport failure == unavailable
272
+ return unavailableToolCallResult(); // in-band
195
273
  }
196
- if (raw === undefined) return unavailableToolCallResult();
197
- return asToolCallResult(raw) ?? unavailableToolCallResult();
274
+ const result = raw === undefined ? undefined : asToolCallResult(raw);
275
+ if (isPull) recordPull(request.resourceUri, result === undefined);
276
+ return result ?? unavailableToolCallResult();
198
277
  };
199
278
  }