@ggui-ai/mcp-server 0.24.0 → 0.26.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.
Files changed (70) hide show
  1. package/README.md +2 -2
  2. package/dist/api-renders-routes.d.ts +46 -5
  3. package/dist/api-renders-routes.d.ts.map +1 -1
  4. package/dist/api-renders-routes.js +98 -39
  5. package/dist/api-renders-stream-route.d.ts +2 -1
  6. package/dist/api-renders-stream-route.d.ts.map +1 -1
  7. package/dist/api-renders-stream-route.js +14 -2
  8. package/dist/build-mcp.d.ts +16 -0
  9. package/dist/build-mcp.d.ts.map +1 -1
  10. package/dist/build-mcp.js +82 -12
  11. package/dist/console-auth.d.ts +10 -10
  12. package/dist/console-auth.d.ts.map +1 -1
  13. package/dist/control-service.d.ts.map +1 -1
  14. package/dist/control-service.js +0 -3
  15. package/dist/first-seen.d.ts +14 -0
  16. package/dist/first-seen.d.ts.map +1 -0
  17. package/dist/first-seen.js +19 -0
  18. package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -1
  19. package/dist/ggui-session-channel/action-ingress.js +96 -8
  20. package/dist/ggui-session-channel/channel-subscriptions.d.ts +0 -1
  21. package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -1
  22. package/dist/ggui-session-channel/channel-subscriptions.js +4 -1
  23. package/dist/ggui-session-channel/internal-types.d.ts +20 -12
  24. package/dist/ggui-session-channel/internal-types.d.ts.map +1 -1
  25. package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
  26. package/dist/ggui-session-channel/outbound.js +18 -6
  27. package/dist/ggui-session-channel/stream-epoch.d.ts +61 -0
  28. package/dist/ggui-session-channel/stream-epoch.d.ts.map +1 -0
  29. package/dist/ggui-session-channel/stream-epoch.js +47 -0
  30. package/dist/ggui-session-channel/subscribe.d.ts +40 -53
  31. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
  32. package/dist/ggui-session-channel/subscribe.js +288 -124
  33. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -1
  34. package/dist/ggui-session-channel/subscriber-lifecycle.js +17 -7
  35. package/dist/ggui-session-channel.d.ts +43 -7
  36. package/dist/ggui-session-channel.d.ts.map +1 -1
  37. package/dist/ggui-session-channel.js +7 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -1
  41. package/dist/instructions-presets.d.ts +22 -21
  42. package/dist/instructions-presets.d.ts.map +1 -1
  43. package/dist/instructions-presets.js +38 -38
  44. package/dist/llm-backed-negotiator.d.ts +4 -37
  45. package/dist/llm-backed-negotiator.d.ts.map +1 -1
  46. package/dist/llm-backed-negotiator.js +28 -210
  47. package/dist/mcp-apps-outbound.d.ts +40 -91
  48. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  49. package/dist/mcp-apps-outbound.js +100 -15
  50. package/dist/mcp-endpoint-routes.d.ts +1 -0
  51. package/dist/mcp-endpoint-routes.d.ts.map +1 -1
  52. package/dist/mcp-endpoint-routes.js +38 -4
  53. package/dist/runtime-bundle-hash.d.ts +7 -0
  54. package/dist/runtime-bundle-hash.d.ts.map +1 -1
  55. package/dist/runtime-bundle-hash.js +8 -2
  56. package/dist/runtime-bundle-route.d.ts.map +1 -1
  57. package/dist/runtime-bundle-route.js +15 -0
  58. package/dist/server.d.ts +143 -85
  59. package/dist/server.d.ts.map +1 -1
  60. package/dist/server.js +224 -110
  61. package/dist/storage.d.ts +9 -1
  62. package/dist/storage.d.ts.map +1 -1
  63. package/dist/storage.js +4 -3
  64. package/dist/view-proof-gate.d.ts +96 -0
  65. package/dist/view-proof-gate.d.ts.map +1 -0
  66. package/dist/view-proof-gate.js +167 -0
  67. package/package.json +14 -15
  68. package/dist/render-read-gate.d.ts +0 -50
  69. package/dist/render-read-gate.d.ts.map +0 -1
  70. package/dist/render-read-gate.js +0 -36
@@ -34,12 +34,13 @@
34
34
  import { deriveBundleOrigins, deriveContractBundle, derivePublicEnvProjection, deriveRenderMeta, filterDescriptorsToContract, findBlueprintExact, resolveSliceTheme, spreadRenderMetaViewOntoSlice, wsOriginToHttpOrigin, } from "@ggui-ai/mcp-server-handlers/renders";
35
35
  import { fontFaceRules } from "@ggui-ai/design/themes";
36
36
  import { GGUI_WAVE_VERSION, RESOURCE_NOT_FOUND_MESSAGE, deriveContextDefault, isRecord, resolveAppGadgets, resourceReadErrorToJsonRpc, } from "@ggui-ai/protocol";
37
- import { GGUI_RENDER_RESOURCE_MIME, GGUI_RENDER_RESOURCE_URI, GGUI_RENDER_SHELL_SCHEME_STYLE, GGUI_RENDER_SHELL_SURFACE, GGUI_SHELL_LOADING_INDICATOR_HTML, MCP_APPS_UI_CAPABILITY, MCP_APP_BOOTSTRAP_FAILED_TYPE, asGguiRenderBootstrap, composeSessionApiUrls, deriveContextName, escapeInlineScript, gguiShellHtml, parseEpochUri, toMcpAppEnvelope, } from "@ggui-ai/protocol/integrations/mcp-apps";
37
+ import { GGUI_RENDER_RESOURCE_MIME, GGUI_RENDER_RESOURCE_URI, GGUI_RENDER_SHELL_SCHEME_STYLE, GGUI_RENDER_SHELL_SURFACE, GGUI_SHELL_LOADING_INDICATOR_HTML, MCP_APPS_UI_CAPABILITY, MCP_APP_BOOTSTRAP_FAILED_TYPE, asGguiRenderBootstrap, composeSessionApiUrls, deriveContextName, escapeInlineScript, gguiShellHtml, parseEpochUri, runtimeBundleHashedNameSource, toMcpAppEnvelope, } from "@ggui-ai/protocol/integrations/mcp-apps";
38
38
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
39
39
  import { registerAppResource } from "@modelcontextprotocol/ext-apps/server";
40
40
  import { createHash } from "node:crypto";
41
- import { renderReadAllowed } from "./render-read-gate.js";
41
+ import { RENDER_READ_APP_TRUST_OVER_SUBJECT, renderReadVerdict } from "@ggui-ai/mcp-server-handlers/renders";
42
42
  import { DEFAULT_BUILDER_APP_ID } from "./auth.js";
43
+ import { RUNTIME_BUNDLE_PLAIN_NAME } from "./runtime-bundle-hash.js";
43
44
  /**
44
45
  * Thin-shell body served from `ui://ggui/render` (C8 pivot).
45
46
  *
@@ -152,6 +153,12 @@ var rootEl=document.getElementById('ggui-root');
152
153
  rootEl.style.cssText='display:flex;flex-direction:column;height:100%;min-height:300px;margin:0';
153
154
  var mounted=false;
154
155
  var lastEnvelope=null;
156
+ // ggui#1501: a content-hashed runtime URL -> its unhashed twin (same origin,
157
+ // same path, only the hash removed). Every server replica serves the twin no-cache, so
158
+ // a hash no serving replica has (rolling deploy, rollback, replay) retries it once.
159
+ var RT_HASHED=new RegExp(${JSON.stringify(runtimeBundleHashedNameSource(RUNTIME_BUNDLE_PLAIN_NAME))});
160
+ var RT_BASE=${JSON.stringify(RUNTIME_BUNDLE_PLAIN_NAME.slice(0, RUNTIME_BUNDLE_PLAIN_NAME.lastIndexOf(".")))};
161
+ function plainRuntimeTwin(u){var m=RT_HASHED.exec(u);return m?m[1]+RT_BASE+m[2]+m[3]:null;}
155
162
  // Text color pairs with the shell surface: themed var when the runtime
156
163
  // injected theme CSS, else the scheme-scoped pre-theme ink the shell's
157
164
  // own scheme <style> defines (#662) — legible on whichever neutral
@@ -261,6 +268,10 @@ async function mountFromMeta(envelope){
261
268
  // bundle responds with the right CORS headers (the iframe-runtime
262
269
  // mount sets them). The self-contained shell already uses this
263
270
  // pattern; legacy postMessage shell now matches it.
271
+ rootEl.innerHTML='';
272
+ loadRuntime(runtimeUrl,false);
273
+ }
274
+ function loadRuntime(url,isTwin){
264
275
  try{
265
276
  var s=document.createElement('script');
266
277
  s.type='module';
@@ -272,14 +283,21 @@ async function mountFromMeta(envelope){
272
283
  // console -- the bundle ships ACAO=* so credentialed mode is
273
284
  // unnecessary.
274
285
  s.crossOrigin='anonymous';
275
- s.src=runtimeUrl;
286
+ s.src=url;
276
287
  s.onload=function(){mounted=true;};
277
288
  s.onerror=function(e){
289
+ // ggui#1501: a hashed URL gets ONE retry through its unhashed twin;
290
+ // the twin, or any other URL, fails as before.
291
+ var twin=isTwin?null:plainRuntimeTwin(url);
292
+ if(twin){
293
+ try{console.warn('[ggui] runtime bundle failed to load; retrying its unhashed twin once');}catch(_){}
294
+ loadRuntime(twin,true);
295
+ return;
296
+ }
278
297
  var msg='Runtime bundle failed to load: '+(e&&e.message||'script error');
279
298
  showFailure('This view could not load','BUNDLE_FETCH_FAILED: '+msg,function(){mountFromMeta(lastEnvelope);});
280
299
  postBootstrapFailed('BUNDLE_FETCH_FAILED',msg);
281
300
  };
282
- rootEl.innerHTML='';
283
301
  document.body.appendChild(s);
284
302
  }catch(e){
285
303
  var msg='Runtime bundle failed to load: '+(e&&e.message||e);
@@ -432,14 +450,20 @@ startInit();
432
450
  // The design scope root (`.ggui-rcr-*`) and the sandbox-proxy outer
433
451
  // document stay `background-color: transparent` by design (so a host
434
452
  // themeing the chat surface shows through AROUND the rendered content).
435
- // But nothing painted the rendered-content document's OWN backdrop:
436
- // Chrome composited the transparent iframe document over the dark host
437
- // app behind it (looked dark), while Safari renders a transparent
438
- // iframe document's backdrop as the opaque UA `Canvas` color (white) —
439
- // the per-browser divergence the bug reported. The component itself
440
- // themed correctly (its scoped vars resolve); only the page behind it
441
- // diverged. Painting the served document's own surface here removes the
442
- // dependency on a browser honoring iframe transparency.
453
+ // But nothing painted the rendered-content document's OWN backdrop,
454
+ // and an unpainted iframe document is only see-through while its used
455
+ // `color-scheme` matches its embedder's: on a mismatch the browser
456
+ // paints the iframe's canvas opaque in the document's own scheme
457
+ // colour (white for light, near-black for dark). This shell declares
458
+ // `light dark`, so its used scheme follows the viewer's preference,
459
+ // not the host's — a dark host under a light-preferring viewer got a
460
+ // white canvas. That is what the 2026-05-30 bug saw as
461
+ // Chrome-dark-versus-Safari-white (`8b52c42ed`); measured 2026-09-26
462
+ // (ggui#1125) Safari 26.3 and Chrome 154 behave alike, per scheme,
463
+ // not per engine. The component itself themed correctly (its scoped
464
+ // vars resolve); only the page behind it diverged. Painting the served
465
+ // document's own surface here removes the dependency on the schemes
466
+ // matching.
443
467
  //
444
468
  // The paint is inline, so no stylesheet `background` rule can undo
445
469
  // it — which is why the constant's `var()` chain opens with the
@@ -963,6 +987,7 @@ export function buildSelfContainedShell(opts) {
963
987
  // input is renamed to `wsToken` on the slice for wire-field parity.
964
988
  ...(opts.wsUrl !== undefined ? { wsUrl: opts.wsUrl } : {}),
965
989
  ...(opts.token !== undefined ? { wsToken: opts.token } : {}),
990
+ ...(opts.token !== undefined && opts.viewKey !== undefined ? { viewKey: opts.viewKey } : {}),
966
991
  ...(opts.expiresAt !== undefined ? { expiresAt: opts.expiresAt } : {}),
967
992
  // Polling fallback URL — lights up `@ggui-ai/live-channel`'s
968
993
  // events-polling transport when WS is unavailable. Absent ⇒
@@ -1030,7 +1055,11 @@ export function buildSelfContainedShell(opts) {
1030
1055
  // the only layer that can detect a compositing host at runtime —
1031
1056
  // drops the backdrop to transparent itself where that is right,
1032
1057
  // and every other context keeps the per-browser-consistent paint.
1033
- return gguiShellHtml(bootstrap, { background: "surface" });
1058
+ // ggui#1501: a hashed runtime URL that 404s (a hash no serving replica has,
1059
+ // during a rolling deploy, after a rollback, or on a replay) retries its
1060
+ // unhashed twin once. A custom `runtime.path` with another filename, or a
1061
+ // foreign URL, is not the hashed default name and gets no retry.
1062
+ return gguiShellHtml(bootstrap, { background: "surface", runtimeBundlePlainName: RUNTIME_BUNDLE_PLAIN_NAME });
1034
1063
  }
1035
1064
  /**
1036
1065
  * A `resources/read` on a render locator that cannot return a mount.
@@ -1250,6 +1279,19 @@ function pickComponentFromGguiSession(render) {
1250
1279
  *
1251
1280
  * @public
1252
1281
  */
1282
+ /**
1283
+ * Why the read door issues no view key on this read (ggui#1415), or
1284
+ * `undefined` when it issues one: the mount does not opt in, or the caller
1285
+ * is anonymous (or carries no auth source).
1286
+ */
1287
+ function viewKeyWithheldAtReadDoor(opts) {
1288
+ if (opts.issueViewKeys !== true)
1289
+ return "not_a_view_mount";
1290
+ const authSource = opts.getContext?.()?.authSource;
1291
+ if (authSource === undefined || authSource === "anonymous")
1292
+ return "anonymous_mount";
1293
+ return undefined;
1294
+ }
1253
1295
  export function registerGguiRenderResourceTemplate(server, opts) {
1254
1296
  // TWO templates registered against the same handler core:
1255
1297
  //
@@ -1488,8 +1530,19 @@ export function registerGguiRenderResourceTemplate(server, opts) {
1488
1530
  requestId: "resource-read",
1489
1531
  }
1490
1532
  : callerCtx;
1491
- if (renderReadAllowed(candidate, fallbackCtx))
1533
+ const verdict = renderReadVerdict(candidate, fallbackCtx);
1534
+ if (verdict.allowed) {
1535
+ // ggui#1553: rung 4 admitted an app credential to a subject-bound
1536
+ // session. Door, app, source; never the session or the subject.
1537
+ if (verdict.appTrustOverSubject) {
1538
+ opts.logger?.info(RENDER_READ_APP_TRUST_OVER_SUBJECT, {
1539
+ door: "resources_read",
1540
+ appId: candidate.appId,
1541
+ source: fallbackCtx?.authSource ?? null,
1542
+ });
1543
+ }
1492
1544
  return true;
1545
+ }
1493
1546
  opts.logger?.warn("render_resource_read_denied", {
1494
1547
  sessionId,
1495
1548
  rowAppId: candidate.appId,
@@ -1552,6 +1605,16 @@ export function registerGguiRenderResourceTemplate(server, opts) {
1552
1605
  if (record.blueprintId === null) {
1553
1606
  return { ok: false, failure: blueprintUnresolvable("the record names no blueprint") };
1554
1607
  }
1608
+ // ggui#1405 — an ephemeral id was never registered: the render served
1609
+ // fresh code at a key bound to other code. Say that, rather than
1610
+ // calling a row that never existed "gone".
1611
+ if (record.blueprintIdentity === "ephemeral") {
1612
+ return {
1613
+ ok: false,
1614
+ failure: blueprintUnresolvable("the render served code no blueprint stores (an ephemeral identity)"),
1615
+ terminal: true,
1616
+ };
1617
+ }
1555
1618
  const blueprint = await blueprintStore.get(record.blueprintId);
1556
1619
  if (blueprint === null) {
1557
1620
  return {
@@ -1816,9 +1879,25 @@ export function registerGguiRenderResourceTemplate(server, opts) {
1816
1879
  let wsUrl;
1817
1880
  let wsToken;
1818
1881
  let wsExpiresAt;
1882
+ let wsViewKey;
1819
1883
  if (opts.mintWsToken) {
1820
1884
  try {
1821
- const minted = opts.mintWsToken(sessionId, accessibleStored.appId);
1885
+ // ggui#1415: a view root, and its key, only on a key-issuing mount
1886
+ // and for a caller that is not anonymous; any other read mints the
1887
+ // live credential alone.
1888
+ const withheld = viewKeyWithheldAtReadDoor(opts);
1889
+ let minted;
1890
+ if (opts.mintViewRoot !== undefined && withheld === undefined) {
1891
+ const root = opts.mintViewRoot(sessionId, accessibleStored.appId);
1892
+ wsViewKey = root.viewKey;
1893
+ minted = root;
1894
+ }
1895
+ else {
1896
+ minted = opts.mintWsToken(sessionId, accessibleStored.appId);
1897
+ if (opts.mintViewRoot !== undefined && withheld !== undefined) {
1898
+ opts.logger?.info("view_key_not_issued", { sessionId, src: "read", reason: withheld });
1899
+ }
1900
+ }
1822
1901
  wsUrl = minted.wsUrl;
1823
1902
  wsToken = minted.token;
1824
1903
  // Forward the token TTL so the iframe-runtime can degrade to
@@ -1898,6 +1977,7 @@ export function registerGguiRenderResourceTemplate(server, opts) {
1898
1977
  wsUrl,
1899
1978
  token: wsToken,
1900
1979
  ...(wsExpiresAt !== undefined ? { expiresAt: wsExpiresAt } : {}),
1980
+ ...(wsViewKey !== undefined ? { viewKey: wsViewKey } : {}),
1901
1981
  }
1902
1982
  : {}),
1903
1983
  // Layered theme — the SAME resolver the tool-result slice uses
@@ -2165,6 +2245,11 @@ export function registerGguiRenderResourceTemplate(server, opts) {
2165
2245
  }
2166
2246
  else {
2167
2247
  failure = reminted.failure;
2248
+ // An entitled read of an ephemeral render stops here: the registry
2249
+ // holds OTHER code at this key (ggui#1405), so the fallback below
2250
+ // would mount the wrong card under this render's locator.
2251
+ if (reminted.terminal === true)
2252
+ throw new ResourceReadFailure(failure);
2168
2253
  }
2169
2254
  }
2170
2255
  // Registry-only fallback: render is gone (TTL / restart) but the
@@ -46,6 +46,7 @@ interface PerAppRouting {
46
46
  readonly paramPattern: string;
47
47
  readonly pathPrefix?: string;
48
48
  readonly authorize?: (urlAppId: string, identity: AuthResult) => Promise<void>;
49
+ readonly perAppOnlySources?: readonly AuthResult["source"][];
49
50
  }
50
51
  /**
51
52
  * Return shape for an `errorMapper` hook — a domain error mapped onto
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-endpoint-routes.d.ts","sourceRoot":"","sources":["../src/mcp-endpoint-routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mcp-endpoint-routes.d.ts","sourceRoot":"","sources":["../src/mcp-endpoint-routes.ts"],"names":[],"mappings":"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"}
@@ -284,6 +284,34 @@ export function mountMcpEndpoints(opts) {
284
284
  // may give the refusal a structured JSON-RPC `data` (bounded to
285
285
  // 401 / 403, see `mapAuthorizationRefusal`); otherwise — and for
286
286
  // every mapping outside those bounds — the default-deny 403 stands.
287
+ // ggui#1482 — a credential source the deployment lists as
288
+ // per-app-only is valid only at its app's endpoint. Every mount
289
+ // this handler serves without an app in its URL (the universal
290
+ // route, any isolated service) never runs `authorize`, so it
291
+ // refuses such a credential before dispatch rather than serve it
292
+ // without the checks that live there. (The control plane's own
293
+ // federated refusal above answers first on `/control`.) Named in
294
+ // the message so a caller corrects its URL instead of retrying.
295
+ // Untyped by contract, like every auth-class refusal (SPEC §7.1):
296
+ // it is about the caller's credential, not the app's state, so it
297
+ // carries no `data` and no registry code names it.
298
+ if (!hasUrlAppId && perAppRouting?.perAppOnlySources?.includes(identity.source)) {
299
+ reqLogger.warn("per_app_only_source_refused", {
300
+ route: handlerOpts?.mountPath ?? universalMcpPath,
301
+ path: req.path,
302
+ source: identity.source,
303
+ appId: appIdFromIdentity(identity),
304
+ });
305
+ res.status(403).json({
306
+ jsonrpc: "2.0",
307
+ error: {
308
+ code: MCP_ERROR_CODES.UNAUTHORIZED,
309
+ message: "this credential is valid only at its app's endpoint",
310
+ },
311
+ id: null,
312
+ });
313
+ return;
314
+ }
287
315
  if (hasUrlAppId && perAppRouting?.authorize) {
288
316
  try {
289
317
  await perAppRouting.authorize(urlAppId, identity);
@@ -342,6 +370,7 @@ export function mountMcpEndpoints(opts) {
342
370
  reqLogger.debug?.("mcp_request", { appId: ctx.appId });
343
371
  const mcp = buildMcpServer(info, routeHandlers, () => als.getStore() ?? ctx, reqLogger, {
344
372
  ...buildMcpOptions,
373
+ ...(handlerOpts?.issueViewKeys === true ? { issueViewKeys: true } : {}),
345
374
  });
346
375
  const transport = new StreamableHTTPServerTransport({
347
376
  sessionIdGenerator: undefined,
@@ -405,10 +434,13 @@ export function mountMcpEndpoints(opts) {
405
434
  // intersects ['agent','runtime']. Handlers without an explicit tag
406
435
  // default to ['agent'], so every untagged handler is agent-callable.
407
436
  const agentRouteHandlers = filterHandlersByAudience(handlers, DATA_PLANE_AUDIENCES);
408
- const agentMcpHandler = makeMcpHandler(agentRouteHandlers);
437
+ // The data plane delivers views to app-credentialed callers, so its read
438
+ // door is the one that issues view keys (ggui#1415).
439
+ const agentMcpHandler = makeMcpHandler(agentRouteHandlers, { issueViewKeys: true });
409
440
  // Control plane — anonymous-capable (design-time tools answer
410
441
  // bearer-less) with each ops tool re-imposing auth for itself.
411
442
  const controlMcpHandler = makeMcpHandler(controlHandlers, {
443
+ mountPath: CONTROL_PATH,
412
444
  anonymous: true,
413
445
  rejectFederated: true,
414
446
  // The control service captures this set BEFORE stripAudience
@@ -425,8 +457,10 @@ export function mountMcpEndpoints(opts) {
425
457
  // whose hostname already says "mcp" may serve it at `/` so the
426
458
  // bare-root URL is the universal endpoint — no path repeat.
427
459
  // Exposes audience tags ['agent', 'runtime'] — runtime tools stay
428
- // routable on the same endpoint but invisible to the agent's
429
- // `tools/list` via the `_meta.ui.visibility: ['app']` filter.
460
+ // routable on the same endpoint and ARE listed on its `tools/list`;
461
+ // their `_meta.ui.visibility: ['app']` marker asks a conforming HOST to
462
+ // withhold them from the model (SPEC §4.7 host obligation, ggui#1414) —
463
+ // the server validates the marker's shape and enforces nothing.
430
464
  app.post(universalMcpPath, agentMcpHandler);
431
465
  // Per-app endpoint — only mounted when the deployment opts in
432
466
  // via `perAppRouting`. The same handler reads `req.params[paramName]`
@@ -496,7 +530,7 @@ export function mountMcpEndpoints(opts) {
496
530
  // builder-kind identity with `source: 'anonymous'`. Default
497
531
  // (undefined / false) preserves the auth-required posture of
498
532
  // every canonical route.
499
- const svcMcpHandler = makeMcpHandler(svc.handlers, svc.anonymous ? { anonymous: true } : undefined);
533
+ const svcMcpHandler = makeMcpHandler(svc.handlers, svc.anonymous ? { anonymous: true, mountPath: svc.path } : { mountPath: svc.path });
500
534
  app.post(svc.path, svcMcpHandler);
501
535
  app.get(svc.path, methodNotAllowed);
502
536
  app.delete(svc.path, methodNotAllowed);
@@ -19,6 +19,13 @@
19
19
  * cache-busting across deploys; this is not an integrity check (SRI
20
20
  * would be a separate, full-length hash).
21
21
  */
22
+ /**
23
+ * The runtime bundle's PLAIN filename (`iframe-runtime.js`): the name every
24
+ * server mounts `no-cache`, and the name the content hash is inserted into.
25
+ * The shells' one-shot fallback (ggui#1501) retries this name when a hashed
26
+ * URL fails.
27
+ */
28
+ export declare const RUNTIME_BUNDLE_PLAIN_NAME: string;
22
29
  /** Truncated content hash used in the bundle's immutable URL name. */
23
30
  export declare function computeRuntimeBundleHash(bytes: Buffer): string;
24
31
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-bundle-hash.d.ts","sourceRoot":"","sources":["../src/runtime-bundle-hash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,sEAAsE;AACtE,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,MAAM,CAQR;AAED;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,MAAM,EAChB,UAAU,GAAE,MAA4B,GACvC,MAAM,CAYR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,GAAE,MAA4B,GACvC,MAAM,GAAG,SAAS,CAMpB"}
1
+ {"version":3,"file":"runtime-bundle-hash.d.ts","sourceRoot":"","sources":["../src/runtime-bundle-hash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,EAAE,MAEvC,CAAC;AAEF,sEAAsE;AACtE,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,MAAM,CAQR;AAED;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,MAAM,EAChB,UAAU,GAAE,MAA4B,GACvC,MAAM,CASR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,GAAE,MAA4B,GACvC,MAAM,GAAG,SAAS,CAMpB"}
@@ -22,6 +22,13 @@
22
22
  import { createHash } from "node:crypto";
23
23
  import fs from "node:fs";
24
24
  import { RUNTIME_BUNDLE_FILE, RUNTIME_BUNDLE_URL_PATH } from "@ggui-ai/iframe-runtime/server";
25
+ /**
26
+ * The runtime bundle's PLAIN filename (`iframe-runtime.js`): the name every
27
+ * server mounts `no-cache`, and the name the content hash is inserted into.
28
+ * The shells' one-shot fallback (ggui#1501) retries this name when a hashed
29
+ * URL fails.
30
+ */
31
+ export const RUNTIME_BUNDLE_PLAIN_NAME = RUNTIME_BUNDLE_URL_PATH.slice(RUNTIME_BUNDLE_URL_PATH.lastIndexOf("/") + 1);
25
32
  /** Truncated content hash used in the bundle's immutable URL name. */
26
33
  export function computeRuntimeBundleHash(bytes) {
27
34
  return createHash("sha256").update(bytes).digest("hex").slice(0, 12);
@@ -61,8 +68,7 @@ export function resolveHashedRuntimeBundleUrl(plainUrl, bundleFile = RUNTIME_BUN
61
68
  // Bundle not built / not shipped — mint the plain revalidated name.
62
69
  return plainUrl;
63
70
  }
64
- const plainName = RUNTIME_BUNDLE_URL_PATH.slice(RUNTIME_BUNDLE_URL_PATH.lastIndexOf("/") + 1);
65
- return insertRuntimeBundleHash(plainUrl, computeRuntimeBundleHash(bytes), plainName);
71
+ return insertRuntimeBundleHash(plainUrl, computeRuntimeBundleHash(bytes), RUNTIME_BUNDLE_PLAIN_NAME);
66
72
  }
67
73
  /**
68
74
  * The 12-hex runtime-bundle content hash for the bundle at
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-bundle-route.d.ts","sourceRoot":"","sources":["../src/runtime-bundle-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,sDAAsD;IACtD,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE;QAChB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAwEhE"}
1
+ {"version":3,"file":"runtime-bundle-route.d.ts","sourceRoot":"","sources":["../src/runtime-bundle-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,sDAAsD;IACtD,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE;QAChB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAuFhE"}
@@ -55,6 +55,21 @@ export function mountRuntimeBundleRoute(opts) {
55
55
  }
56
56
  if (existsSync(runtimeBundleFile)) {
57
57
  app.get(runtimePath, (_req, res) => {
58
+ // ggui#1501: with the hashed twin mounted, every STAMPED runtime URL
59
+ // is hashed, so a load of the plain name is a shell's one-shot
60
+ // fallback (a hash no serving replica has: rolling deploy, rollback,
61
+ // replay), or a probe; a probe reads the same, and that is the
62
+ // count's stated gap. Counted on the response's finish, whatever its
63
+ // status: the name is `no-cache`, so a CDN revalidates it with a
64
+ // conditional GET on viewer loads, which keeps the count per load
65
+ // (a CDN's minimum TTL can coalesce a burst within it), and a 304 is
66
+ // a fallback as much as a 200 is. No user-agent field: behind a CDN
67
+ // the origin sees the CDN's own.
68
+ if (opts.hashed !== undefined) {
69
+ res.on("finish", () => {
70
+ logger.info("runtime_bundle_plain_served", { status: res.statusCode });
71
+ });
72
+ }
58
73
  res.setHeader("Content-Type", "application/javascript; charset=utf-8");
59
74
  // Short cache — operators iterating on the renderer want fresh
60
75
  // copies after rebuild. Production long-term caching lives on