@ggui-ai/mcp-server 0.9.0 → 0.11.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 (37) hide show
  1. package/dist/api-renders-routes.d.ts +21 -0
  2. package/dist/api-renders-routes.d.ts.map +1 -1
  3. package/dist/api-renders-routes.js +22 -5
  4. package/dist/build-mcp.d.ts +38 -7
  5. package/dist/build-mcp.d.ts.map +1 -1
  6. package/dist/build-mcp.js +23 -2
  7. package/dist/code-module-variant.d.ts +150 -0
  8. package/dist/code-module-variant.d.ts.map +1 -0
  9. package/dist/code-module-variant.js +243 -0
  10. package/dist/code-routes.d.ts +12 -2
  11. package/dist/code-routes.d.ts.map +1 -1
  12. package/dist/code-routes.js +12 -2
  13. package/dist/control-service.d.ts +29 -3
  14. package/dist/control-service.d.ts.map +1 -1
  15. package/dist/control-service.js +26 -2
  16. package/dist/health-routes.d.ts +19 -3
  17. package/dist/health-routes.d.ts.map +1 -1
  18. package/dist/health-routes.js +26 -18
  19. package/dist/index.d.ts +3 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +7 -1
  22. package/dist/mcp-apps-outbound.d.ts +40 -8
  23. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  24. package/dist/mcp-apps-outbound.js +132 -30
  25. package/dist/mcp-endpoint-routes.d.ts +6 -0
  26. package/dist/mcp-endpoint-routes.d.ts.map +1 -1
  27. package/dist/mcp-endpoint-routes.js +67 -1
  28. package/dist/oauth-as-routes.d.ts +11 -0
  29. package/dist/oauth-as-routes.d.ts.map +1 -1
  30. package/dist/oauth-as-routes.js +9 -1
  31. package/dist/runtime-bundle-hash.d.ts +12 -0
  32. package/dist/runtime-bundle-hash.d.ts.map +1 -1
  33. package/dist/runtime-bundle-hash.js +19 -0
  34. package/dist/server.d.ts +223 -56
  35. package/dist/server.d.ts.map +1 -1
  36. package/dist/server.js +263 -125
  37. package/package.json +13 -12
@@ -31,7 +31,7 @@
31
31
  * build target — per the design lock, the shell is served by the
32
32
  * same `@ggui-ai/mcp-server` instance that mints the bootstrap.
33
33
  */
34
- import { deriveBundleOrigins, deriveContractBundle, derivePublicEnvProjection, deriveRenderMeta, filterDescriptorsToContract, findBlueprintExact, spreadRenderMetaViewOntoSlice, wsOriginToHttpOrigin, } from "@ggui-ai/mcp-server-handlers/renders";
34
+ import { deriveBundleOrigins, deriveContractBundle, derivePublicEnvProjection, deriveRenderMeta, filterDescriptorsToContract, findBlueprintExact, resolveSliceTheme, spreadRenderMetaViewOntoSlice, wsOriginToHttpOrigin, } from "@ggui-ai/mcp-server-handlers/renders";
35
35
  import { RESOURCE_NOT_FOUND_MESSAGE, deriveContextDefault, isRecord, resolveAppGadgets, resourceReadErrorToJsonRpc, } from "@ggui-ai/protocol";
36
36
  import { GGUI_RENDER_RESOURCE_MIME, GGUI_RENDER_RESOURCE_URI, GGUI_RENDER_SHELL_SURFACE, MCP_APPS_UI_CAPABILITY, MCP_APP_BOOTSTRAP_FAILED_TYPE, asGguiRenderBootstrap, composeSessionApiUrls, deriveContextName, escapeInlineScript, gguiShellHtml, parseEpochUri, toMcpAppEnvelope, } from "@ggui-ai/protocol/integrations/mcp-apps";
37
37
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -287,13 +287,66 @@ window.addEventListener('message',function(ev){
287
287
  // at the top level.
288
288
  var specMeta=readMetaFromCallToolResult(m.params);
289
289
  if(specMeta){mountFromMeta(specMeta);return;}
290
- // R5 (2026-05-26) -- the /r/<shortCode> HTTP fallback was removed
291
- // along with the bearer-by-obscurity model. Hosts that strip
292
- // _meta on the tool-result wire have no fallback path here;
293
- // spec-canonical hosts deliver meta inline and land in the branch
294
- // above.
290
+ // Read-plane door (ggui#537). A server running the read-plane-only
291
+ // posture publishes only the view's IDENTITY on the result -- the
292
+ // ui:// locator on structuredContent.resourceUri and the spec
293
+ // pointer _meta.ui.resourceUri -- and no bootstrap material. The
294
+ // per-render resource that locator names is the self-contained
295
+ // shell, whose document inlines the very envelope this shell needs;
296
+ // ask the HOST to read it (resources/read, proxied by the host's
297
+ // own MCP client -- no fetch from this sandbox, so a host CSP with
298
+ // no connect-src to the server is not in the way). R5 (2026-05-26)
299
+ // removed the /r/<shortCode> HTTP fallback with the bearer-by-
300
+ // obscurity model; this door is the spec-canonical replacement.
301
+ var locator=readLocatorFromCallToolResult(m.params);
302
+ if(locator){mountFromLocator(locator);return;}
295
303
  }
296
304
  });
305
+ function readLocatorFromCallToolResult(params){
306
+ if(!params||typeof params!=='object')return null;
307
+ var sc=params.structuredContent, meta=params._meta, uri=null;
308
+ if(sc&&typeof sc==='object'&&typeof sc.resourceUri==='string')uri=sc.resourceUri;
309
+ else if(meta&&typeof meta==='object'&&meta.ui&&typeof meta.ui==='object'&&typeof meta.ui.resourceUri==='string')uri=meta.ui.resourceUri;
310
+ if(!uri||uri.indexOf('ui://ggui/render/')!==0)return null;
311
+ return uri;
312
+ }
313
+ function envelopeFromResourceDoc(text){
314
+ // The per-render self-contained shell inlines its envelope on ONE
315
+ // line inside its meta script tag: globalThis.__GGUI_META__ = {...};
316
+ // (protocol gguiShellHtml). The JSON is angle-bracket-escaped by the
317
+ // assembler, so the first ';' followed by the closing script tag
318
+ // after the marker is the terminator.
319
+ if(typeof text!=='string')return null;
320
+ var marker='globalThis.__GGUI_META__ = ';
321
+ var start=text.indexOf(marker);
322
+ if(start<0)return null;
323
+ start+=marker.length;
324
+ var end=text.indexOf(';<'+'/script>',start);
325
+ if(end<0)return null;
326
+ try{var env=JSON.parse(text.slice(start,end));}catch(e){return null;}
327
+ return (env&&typeof env==='object'&&env['ai.ggui/render']&&typeof env['ai.ggui/render'].runtimeUrl==='string')?env:null;
328
+ }
329
+ async function mountFromLocator(uri){
330
+ if(mounted)return;
331
+ setOverlay('Resolving view…');
332
+ var res;
333
+ try{res=await postRpc('resources/read',{uri:uri});}
334
+ catch(e){
335
+ var rmsg='READ_DOOR_FAILED: the host could not read '+uri+' -- '+(e&&e.message||JSON.stringify(e));
336
+ showFailure('This view could not be resolved',rmsg,function(){mountFromLocator(uri);});
337
+ postBootstrapFailed('MALFORMED_BOOTSTRAP',rmsg);
338
+ return;
339
+ }
340
+ var c=res&&res.contents&&res.contents[0];
341
+ var env=envelopeFromResourceDoc(c&&c.text);
342
+ if(!env){
343
+ var emsg='MALFORMED_BOOTSTRAP: resource '+uri+' carried no ai.ggui/render envelope with a runtimeUrl.';
344
+ showFailure('This view could not start',emsg,function(){mountFromLocator(uri);});
345
+ postBootstrapFailed('MALFORMED_BOOTSTRAP',emsg);
346
+ return;
347
+ }
348
+ mountFromMeta(env);
349
+ }
297
350
  // Named so the failure card's Retry can re-run the whole handshake —
298
351
  // a host that missed or rejected the first ui/initialize may answer a
299
352
  // second (observed with hosts that attach their listener late).
@@ -351,10 +404,15 @@ startInit();
351
404
  // diverged. Painting the served document's own surface here removes the
352
405
  // dependency on a browser honoring iframe transparency.
353
406
  //
354
- // Value-resolution only — no `--ggui-*` token added or renamed. The
355
- // constant itself lives with the protocol host-helper (the shared
356
- // self-contained-shell assembler paints the same surface); imported
357
- // above and reused here for the thin postMessage shell.
407
+ // The paint is inline, so no stylesheet `background` rule can undo
408
+ // it — which is why the constant's `var()` chain opens with the
409
+ // `--ggui-shell-background` override point: content that KNOWS its
410
+ // host composites behind the document (the runtime's system-card
411
+ // layer inside Claude) sets that property from its stylesheet and
412
+ // the inline value re-resolves to transparent in place. See
413
+ // `GGUI_RENDER_SHELL_SURFACE` in the protocol host-helper (the
414
+ // shared self-contained-shell assembler paints the same surface);
415
+ // imported above and reused here for the thin postMessage shell.
358
416
  // `#ggui-root` here is LOAD-BEARING for the shell script (NOT a React
359
417
  // mount target): the inline script grabs it as `rootEl` for the
360
418
  // pre-mount overlays ("Initializing…", "Waiting for tool result…",
@@ -530,13 +588,15 @@ function buildCspMeta(publicBaseUrl,
530
588
  */
531
589
  runtimeUrl,
532
590
  /**
533
- * Origins of server-stamped fetch/stream URLs (`sseUrl`,
591
+ * Origins of server-stamped connect URLs (`wsUrl`, `sseUrl`,
534
592
  * `pollingUrl`) — each parseable entry's origin is unioned into
535
- * `connectDomains` (deduplicated). Same-origin deployments add
536
- * nothing new; split-origin session APIs (dedicated streaming host)
537
- * would otherwise be silently blocked by `connect-src` on
538
- * spec-compliant hosts — EventSource and fetch are both
539
- * connect-src-governed.
593
+ * `connectDomains` (deduplicated), scheme preserved (`ws`/`wss` are
594
+ * WHATWG special schemes, so a `wss://` entry contributes its
595
+ * `wss://` origin). Same-origin deployments add nothing new;
596
+ * split-origin live channels or session APIs would otherwise be
597
+ * silently blocked by `connect-src` on spec-compliant hosts —
598
+ * WebSocket, EventSource, and fetch are all connect-src-governed,
599
+ * and the base's ws-twin flip only covers the base's own host.
540
600
  */
541
601
  extraConnectUrls) {
542
602
  const source = publicBaseUrl ?? runtimeUrl;
@@ -805,6 +865,11 @@ export function buildSelfContainedShell(opts) {
805
865
  ...(opts.codeHash !== undefined ? { codeHash: opts.codeHash } : {}),
806
866
  }
807
867
  : {}),
868
+ // Strict-CSP module-variant twin — only WITH a raw static carrier
869
+ // (the protocol parser drops it otherwise).
870
+ ...(!isSystem && (hasCodeUrl || hasCodeB64) && opts.codeModuleUrl !== undefined
871
+ ? { codeModuleUrl: opts.codeModuleUrl }
872
+ : {}),
808
873
  ...(!isSystem && hasCodeB64 ? { codeB64: opts.codeB64 } : {}),
809
874
  ...(opts.propsJson !== undefined ? { propsJson: opts.propsJson } : {}),
810
875
  ...(opts.contextSlots !== undefined && opts.contextSlots.length > 0
@@ -836,7 +901,12 @@ export function buildSelfContainedShell(opts) {
836
901
  // standalone served iframe (claude.ai per-render resource shells,
837
902
  // `/r/<shortCode>`), never inlined into a host page — so it paints
838
903
  // its own theme-surface backdrop. See `GguiShellHtmlOptions` for the
839
- // Safari white-canvas rationale.
904
+ // Safari white-canvas rationale. System-kind renders keep this
905
+ // posture too: the surface paint resolves through the
906
+ // `--ggui-shell-background` override point, so the card layer —
907
+ // the only layer that can detect a compositing host at runtime —
908
+ // drops the backdrop to transparent itself where that is right,
909
+ // and every other context keeps the per-browser-consistent paint.
840
910
  return gguiShellHtml(bootstrap, { background: "surface" });
841
911
  }
842
912
  /**
@@ -1539,6 +1609,7 @@ export function registerGguiRenderResourceTemplate(server, opts) {
1539
1609
  // the mount.
1540
1610
  let codeUrl;
1541
1611
  let codeHash;
1612
+ let codeModuleUrl;
1542
1613
  let contractHash;
1543
1614
  let validatorsUrl;
1544
1615
  if (!isSystem && opts.codeStore && opts.codeBaseUrl) {
@@ -1548,6 +1619,13 @@ export function registerGguiRenderResourceTemplate(server, opts) {
1548
1619
  codeHash = hash;
1549
1620
  const base = opts.codeBaseUrl.replace(/\/$/, "");
1550
1621
  codeUrl = `${base}/code/${hash}.js`;
1622
+ // Strict-CSP module-variant twin (ggui#522 slice 2) — a
1623
+ // decline (`undefined`) just means the blob ladder carries it.
1624
+ codeModuleUrl = opts.mintCodeModuleUrl?.({
1625
+ code: picked.componentCode,
1626
+ hash,
1627
+ base,
1628
+ });
1551
1629
  }
1552
1630
  catch (cause) {
1553
1631
  channelFault ??= { cause };
@@ -1677,6 +1755,7 @@ export function registerGguiRenderResourceTemplate(server, opts) {
1677
1755
  ? {
1678
1756
  codeUrl,
1679
1757
  ...(codeHash !== undefined ? { codeHash } : {}),
1758
+ ...(codeModuleUrl !== undefined ? { codeModuleUrl } : {}),
1680
1759
  }
1681
1760
  : {}),
1682
1761
  // Inline fetch-free channel (size-capped, projected by
@@ -1694,8 +1773,21 @@ export function registerGguiRenderResourceTemplate(server, opts) {
1694
1773
  ...(wsExpiresAt !== undefined ? { expiresAt: wsExpiresAt } : {}),
1695
1774
  }
1696
1775
  : {}),
1697
- ...(opts.themeId !== undefined ? { themeId: opts.themeId } : {}),
1698
- ...(opts.themeMode !== undefined ? { themeMode: opts.themeMode } : {}),
1776
+ // Layered theme — the SAME resolver the tool-result slice uses
1777
+ // (live pick > per-render override on this render > static
1778
+ // ggui.json), so a `ggui_render({ themeId })` override survives a
1779
+ // mount-by-read (ggui#539). The picked source IS the committed
1780
+ // render, so its `themeId` is the override the emitter read at
1781
+ // commit time — component variant only, exactly the emitter's
1782
+ // own narrowing (render.ts resultMeta): mcpApps / system renders
1783
+ // carry no user-facing theme. Keys stay absent when unresolved.
1784
+ ...resolveSliceTheme(opts, picked.source.type !== "mcpApps" && picked.source.type !== "system"
1785
+ ? picked.source.themeId
1786
+ : undefined,
1787
+ // ggui#589 — the session theme's own mode, from the SAME
1788
+ // projection (`view`, derived above) that emits the `theme`
1789
+ // object on this shell's envelope.
1790
+ view.theme?.mode),
1699
1791
  // State + policy view fields (theme overlay, propsJson,
1700
1792
  // contextSlots, permissionsPolicy, gadgets, #483 epoch) — ONE
1701
1793
  // shared spread so the resource-served shell cannot drift from
@@ -1726,16 +1818,23 @@ export function registerGguiRenderResourceTemplate(server, opts) {
1726
1818
  // derives these via deriveBundleOrigins; this is the per-call
1727
1819
  // resource mirror.
1728
1820
  const gadgetOrigins = deriveBundleOrigins(picked.source);
1729
- // Per-render CSP base: recompute with the stamped session-API
1730
- // URLs so their origins ride `connectDomains` (EventSource +
1731
- // fetch are connect-src-governed). Same-origin stamps dedupe to
1732
- // the registration-time declaration; a base derived from the
1733
- // wsUrl origin flip (publicBaseUrl absent) adds the flip origin
1734
- // that would otherwise be silently blocked.
1735
- const renderCspBase = sessionApiUrls !== undefined
1821
+ // Per-render CSP base: recompute with the stamped live-channel +
1822
+ // session-API URLs so their origins ride `connectDomains`
1823
+ // (WebSocket, EventSource, and fetch are all connect-src-governed).
1824
+ // The stamped `wsUrl` MUST be its own entry: CSP never
1825
+ // cross-translates `https://` ↔ `wss://`, and the base's ws-twin
1826
+ // flip only covers deployments whose base origin IS the ws host —
1827
+ // when `publicBaseUrl` is absent and the runtime bundle lives on
1828
+ // an assets CDN origin, the flip declares the CDN's wss twin while
1829
+ // the actual socket host goes undeclared and the live channel dies
1830
+ // in the mounted iframe (#479, observed as the cloud-render
1831
+ // capstone's CSP block). Same-origin stamps dedupe to the
1832
+ // registration-time declaration.
1833
+ const renderCspBase = sessionApiUrls !== undefined || wsUrl !== undefined
1736
1834
  ? buildCspMeta(opts.publicBaseUrl, opts.runtimeUrl, [
1737
- sessionApiUrls.sseUrl,
1738
- sessionApiUrls.pollingUrl,
1835
+ wsUrl,
1836
+ sessionApiUrls?.sseUrl,
1837
+ sessionApiUrls?.pollingUrl,
1739
1838
  ])
1740
1839
  : templateCspMeta;
1741
1840
  return shellContents(uri, html, augmentCspMeta(gadgetOrigins, renderCspBase));
@@ -1964,8 +2063,11 @@ export function registerGguiRenderResourceTemplate(server, opts) {
1964
2063
  appId: opts.defaultAppIdFallback,
1965
2064
  blueprint,
1966
2065
  runtimeUrl: opts.runtimeUrl,
1967
- ...(opts.themeId !== undefined ? { themeId: opts.themeId } : {}),
1968
- ...(opts.themeMode !== undefined ? { themeMode: opts.themeMode } : {}),
2066
+ // No render row exists on this branch, so there is no
2067
+ // per-render override to honor (and no session theme — the
2068
+ // ggui#589 layer is honestly absent) — but the live pick still
2069
+ // beats the static preset, same resolver as every other shell.
2070
+ ...resolveSliceTheme(opts, undefined, undefined),
1969
2071
  ...(opts.codeStore !== undefined ? { codeStore: opts.codeStore } : {}),
1970
2072
  ...(opts.codeBaseUrl !== undefined ? { codeBaseUrl: opts.codeBaseUrl } : {}),
1971
2073
  });
@@ -80,6 +80,12 @@ interface MountOptions {
80
80
  * transport.
81
81
  */
82
82
  readonly controlHandlers: ReadonlyArray<SharedHandler<ZodRawShape, ZodRawShape>>;
83
+ /**
84
+ * The control plane's ops-tool name set (captured by
85
+ * `buildControlService` before audience-stripping). Drives the
86
+ * transport-level anonymous-ops OAuth challenge (ggui#505).
87
+ */
88
+ readonly controlOpsToolNames: ReadonlySet<string>;
83
89
  /** Validated isolated-service list (`validateMcpServices` output). */
84
90
  readonly mcpServices: ReadonlyArray<McpService>;
85
91
  /** Request-scoped HandlerContext storage shared with the handlers. */
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-endpoint-routes.d.ts","sourceRoot":"","sources":["../src/mcp-endpoint-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAElF,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAC1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAEvC,OAAO,EAAkB,KAAK,qBAAqB,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAM7F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGlD,sFAAsF;AACtF,UAAU,aAAa;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChF;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;AAED,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,iEAAiE;IACjE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;IAC1E;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;IACjF,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAChD,sEAAsE;IACtE,QAAQ,CAAC,GAAG,EAAE,iBAAiB,CAAC,cAAc,CAAC,CAAC;IAChD,qDAAqD;IACrD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,CAAC;IAC3D,wEAAwE;IACxE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC,iEAAiE;IACjE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,uDAAuD;IACvD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,iBAAiB,GAAG,SAAS,CAAC;IACvE;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC;CACjD;AAyBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CA2U1D"}
1
+ {"version":3,"file":"mcp-endpoint-routes.d.ts","sourceRoot":"","sources":["../src/mcp-endpoint-routes.ts"],"names":[],"mappings":"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"}
@@ -31,6 +31,7 @@
31
31
  * (`agent` / `runtime` / `protocol` / `ops`) and the wire-name prefix
32
32
  * rules.
33
33
  */
34
+ import { isRecord } from "@ggui-ai/protocol";
34
35
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
35
36
  import { randomUUID } from "node:crypto";
36
37
  import { resolveIdentity, UnauthenticatedError } from "./auth.js";
@@ -64,7 +65,28 @@ function resolveWwwAuthResourcePath(req, perAppRouting) {
64
65
  * routes self-register.
65
66
  */
66
67
  export function mountMcpEndpoints(opts) {
67
- const { app, logger, auth, info, handlers, controlHandlers, mcpServices, als, appIdFromIdentity, universalMcpPath, perAppRouting, oauthEnabled, oauthIssuerUrl, errorMapper, buildMcpOptions, } = opts;
68
+ const { app, logger, auth, info, handlers, controlHandlers, controlOpsToolNames, mcpServices, als, appIdFromIdentity, universalMcpPath, perAppRouting, oauthEnabled, oauthIssuerUrl, errorMapper, buildMcpOptions, } = opts;
69
+ /**
70
+ * OAuth auto-negotiation for the control plane (ggui#505): the first
71
+ * tool name in the request body that is an ops tool, or `null` when
72
+ * the request contains none. JSON-RPC bodies may be a single message
73
+ * or a batch; a batch containing ANY ops call challenges as a whole
74
+ * (mixed anonymous batches are not a supported shape).
75
+ *
76
+ * External-boundary narrowing via `isRecord` — the body is unvalidated
77
+ * wire input here; the MCP transport re-validates after dispatch.
78
+ */
79
+ const findOpsToolCall = (body, opsToolNames) => {
80
+ const messages = Array.isArray(body) ? body : [body];
81
+ for (const m of messages) {
82
+ if (!isRecord(m) || m.method !== "tools/call" || !isRecord(m.params))
83
+ continue;
84
+ const name = m.params.name;
85
+ if (typeof name === "string" && opsToolNames.has(name))
86
+ return name;
87
+ }
88
+ return null;
89
+ };
68
90
  const makeMcpHandler = (routeHandlers, handlerOpts) => async (req, res) => {
69
91
  const requestId = typeof req.headers["x-request-id"] === "string"
70
92
  ? req.headers["x-request-id"]
@@ -154,6 +176,37 @@ export function mountMcpEndpoints(opts) {
154
176
  });
155
177
  return;
156
178
  }
179
+ // OAuth auto-negotiation (ggui#505) — anonymous ops calls get a
180
+ // transport 401 BEFORE dispatch, with the standards trigger in
181
+ // the header AND the actionable guidance agents read in the
182
+ // body. Runs only when the route opted in (the control plane)
183
+ // and only for resolved-anonymous callers naming an ops tool —
184
+ // every other message on the route keeps the anonymous-capable
185
+ // design-time posture. This makes the documented
186
+ // AuthRequiredError→401 mapping observable at the transport; the
187
+ // per-tool auth gate stays as defense-in-depth for any path that
188
+ // reaches dispatch.
189
+ if (handlerOpts?.anonymousOpsChallenge !== undefined && identity.source === "anonymous") {
190
+ const opsToolName = findOpsToolCall(req.body, handlerOpts.anonymousOpsChallenge);
191
+ if (opsToolName !== null) {
192
+ reqLogger.info("anonymous_ops_call_challenged", { tool: opsToolName });
193
+ if (oauthEnabled) {
194
+ res.setHeader("WWW-Authenticate", buildWwwAuthenticate(resolveIssuerUrl(req, oauthIssuerUrl), CONTROL_PATH));
195
+ }
196
+ res.status(401).json({
197
+ jsonrpc: "2.0",
198
+ error: {
199
+ code: -32000,
200
+ message: `${opsToolName} is an operator tool and needs an authenticated caller. ` +
201
+ `Present a bearer token this deployment accepts, or complete the OAuth flow ` +
202
+ `advertised in WWW-Authenticate (universal connector keys come from the ` +
203
+ `console: Connector keys → New key, leave the app unset).`,
204
+ },
205
+ id: null,
206
+ });
207
+ return;
208
+ }
209
+ }
157
210
  // Per-tenant URL routing. When `perAppRouting`
158
211
  // is configured AND the request matched the per-app path
159
212
  // `/:${paramName}/mcp`, Express populates `req.params[paramName]`
@@ -202,6 +255,15 @@ export function mountMcpEndpoints(opts) {
202
255
  // context shape; OSS handlers continue to ignore both fields.
203
256
  ...(identity.identity.kind === "app" ? { apiKeyHash: identity.identity.apiKeyHash } : {}),
204
257
  ...(identity.identity.kind === "user" ? { userId: identity.identity.userId } : {}),
258
+ // What the credential itself may act on, when the adapter
259
+ // distinguishes credential scopes. Identity-independent by
260
+ // design: one account can present a key bound to a single app
261
+ // on one request and an account-wide key on the next, and the
262
+ // resolved userId is the same string both times — so the scope
263
+ // has to ride the request, not be re-derived from the identity.
264
+ ...(identity.credentialScope !== undefined
265
+ ? { credentialScope: identity.credentialScope }
266
+ : {}),
205
267
  };
206
268
  reqLogger.debug?.("mcp_request", { appId: ctx.appId });
207
269
  const mcp = buildMcpServer(info, routeHandlers, () => als.getStore() ?? ctx, reqLogger, {
@@ -273,6 +335,10 @@ export function mountMcpEndpoints(opts) {
273
335
  const controlMcpHandler = makeMcpHandler(controlHandlers, {
274
336
  anonymous: true,
275
337
  rejectFederated: true,
338
+ // The control service captures this set BEFORE stripAudience
339
+ // erases the tags — filtering `controlHandlers` here would yield
340
+ // an empty set and silently disable the challenge (ggui#505).
341
+ anonymousOpsChallenge: controlOpsToolNames,
276
342
  });
277
343
  // Universal endpoint — `appId` resolved from the auth identity via
278
344
  // `appIdFromIdentity`. Cloud `mcp.ggui.ai` deployments resolve this
@@ -37,6 +37,17 @@ interface MountOptions {
37
37
  readonly paramName: string;
38
38
  readonly pathPrefix?: string;
39
39
  };
40
+ /**
41
+ * Control-plane mount path (`/control`). When set, the control
42
+ * plane becomes an RFC 9728/8707-nameable resource: both PRM
43
+ * discovery forms mount for it, and hosts can complete the OAuth
44
+ * ceremony naming it directly instead of consenting against the
45
+ * data plane and reusing the bearer (ggui#505). The minted key is
46
+ * universal — the consent page extracts an appId only from the
47
+ * per-app resource shape, and control-plane ops are account-level
48
+ * by design.
49
+ */
50
+ readonly controlPath?: string;
40
51
  /** Auth adapter the consent-submit handler resolves bearers against. */
41
52
  readonly auth: AuthAdapter;
42
53
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-as-routes.d.ts","sourceRoot":"","sources":["../src/oauth-as-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC5E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAOL,KAAK,WAAW,EAChB,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;AAEpB,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,+DAA+D;IAC/D,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,mEAAmE;IACnE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE;QACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;KAC9B,CAAC;IACF,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,cAAc,GAAG,IAAI,CAAC;CACzD;AAED;;;;;;GAMG;AACH,wBAAgB,mCAAmC,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAyH5E"}
1
+ {"version":3,"file":"oauth-as-routes.d.ts","sourceRoot":"","sources":["../src/oauth-as-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC5E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAOL,KAAK,WAAW,EAChB,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;AAEpB,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,+DAA+D;IAC/D,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,mEAAmE;IACnE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE;QACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;KAC9B,CAAC;IACF;;;;;;;;;OASG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,cAAc,GAAG,IAAI,CAAC;CACzD;AAED;;;;;;GAMG;AACH,wBAAgB,mCAAmC,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAsI5E"}
@@ -25,11 +25,19 @@ import { handleAuthorizationServerMetadata, handleAuthorizeGet, handleAuthorizeP
25
25
  * insecure flows). Returns nothing — the routes self-register.
26
26
  */
27
27
  export function mountOAuthAuthorizationServerRoutes(opts) {
28
- const { app, oauthConfig, oauthStorage, universalMcpPath, perAppRouting, auth, getPairingService, } = opts;
28
+ const { app, oauthConfig, oauthStorage, universalMcpPath, perAppRouting, controlPath, auth, getPairingService, } = opts;
29
29
  // `trust proxy` so req.protocol + req.host honor X-Forwarded-Proto +
30
30
  // X-Forwarded-Host.
31
31
  app.set("trust proxy", true);
32
32
  app.get("/.well-known/oauth-protected-resource", (req, res) => handleProtectedResourceMetadata(req, res, oauthConfig, universalMcpPath));
33
+ // Control-plane PRM (ggui#505) — both discovery forms, mirroring the
34
+ // universal endpoint's pair below: the suffix form for grandfathered
35
+ // clients and the RFC 9728 §3.1 path-inserted form claude.ai's
36
+ // connect flow actually fetches. Same document either way.
37
+ if (controlPath !== undefined) {
38
+ app.get(`${controlPath}/.well-known/oauth-protected-resource`, (req, res) => handleProtectedResourceMetadata(req, res, oauthConfig, controlPath));
39
+ app.get(`/.well-known/oauth-protected-resource${controlPath}`, (req, res) => handleProtectedResourceMetadata(req, res, oauthConfig, controlPath));
40
+ }
33
41
  // Per-app protected-resource metadata (RFC 9728 per-resource
34
42
  // discovery). When `perAppRouting` is configured,
35
43
  // mount a second well-known endpoint under the same path prefix
@@ -40,4 +40,16 @@ export declare function insertRuntimeBundleHash(urlOrPath: string, hash: string,
40
40
  * revalidated.
41
41
  */
42
42
  export declare function resolveHashedRuntimeBundleUrl(plainUrl: string, bundleFile?: string): string;
43
+ /**
44
+ * The 12-hex runtime-bundle content hash for the bundle at
45
+ * `bundleFile` (default: the workspace's built bundle — the same file
46
+ * `createGguiServer` hashes, so both derive the same value), or
47
+ * `undefined` when the bundle is unreadable. Deployments that compose
48
+ * their own render handler (the cloud pod's `handlers: tools` shape)
49
+ * use this to build a `createCodeModuleUrlMinter` that stamps the SAME
50
+ * `<rt>` the co-resident factory's variant route serves (ggui#522
51
+ * slice 2) — re-deriving the scheme by hand is how the codeUrl binding
52
+ * drifted in slice 1.
53
+ */
54
+ export declare function resolveRuntimeBundleHash(bundleFile?: string): string | undefined;
43
55
  //# sourceMappingURL=runtime-bundle-hash.d.ts.map
@@ -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"}
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"}
@@ -64,3 +64,22 @@ export function resolveHashedRuntimeBundleUrl(plainUrl, bundleFile = RUNTIME_BUN
64
64
  const plainName = RUNTIME_BUNDLE_URL_PATH.slice(RUNTIME_BUNDLE_URL_PATH.lastIndexOf("/") + 1);
65
65
  return insertRuntimeBundleHash(plainUrl, computeRuntimeBundleHash(bytes), plainName);
66
66
  }
67
+ /**
68
+ * The 12-hex runtime-bundle content hash for the bundle at
69
+ * `bundleFile` (default: the workspace's built bundle — the same file
70
+ * `createGguiServer` hashes, so both derive the same value), or
71
+ * `undefined` when the bundle is unreadable. Deployments that compose
72
+ * their own render handler (the cloud pod's `handlers: tools` shape)
73
+ * use this to build a `createCodeModuleUrlMinter` that stamps the SAME
74
+ * `<rt>` the co-resident factory's variant route serves (ggui#522
75
+ * slice 2) — re-deriving the scheme by hand is how the codeUrl binding
76
+ * drifted in slice 1.
77
+ */
78
+ export function resolveRuntimeBundleHash(bundleFile = RUNTIME_BUNDLE_FILE) {
79
+ try {
80
+ return computeRuntimeBundleHash(fs.readFileSync(bundleFile));
81
+ }
82
+ catch {
83
+ return undefined;
84
+ }
85
+ }