@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.
- package/dist/api-renders-routes.d.ts +21 -0
- package/dist/api-renders-routes.d.ts.map +1 -1
- package/dist/api-renders-routes.js +22 -5
- package/dist/build-mcp.d.ts +38 -7
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +23 -2
- package/dist/code-module-variant.d.ts +150 -0
- package/dist/code-module-variant.d.ts.map +1 -0
- package/dist/code-module-variant.js +243 -0
- package/dist/code-routes.d.ts +12 -2
- package/dist/code-routes.d.ts.map +1 -1
- package/dist/code-routes.js +12 -2
- package/dist/control-service.d.ts +29 -3
- package/dist/control-service.d.ts.map +1 -1
- package/dist/control-service.js +26 -2
- package/dist/health-routes.d.ts +19 -3
- package/dist/health-routes.d.ts.map +1 -1
- package/dist/health-routes.js +26 -18
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/mcp-apps-outbound.d.ts +40 -8
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +132 -30
- package/dist/mcp-endpoint-routes.d.ts +6 -0
- package/dist/mcp-endpoint-routes.d.ts.map +1 -1
- package/dist/mcp-endpoint-routes.js +67 -1
- package/dist/oauth-as-routes.d.ts +11 -0
- package/dist/oauth-as-routes.d.ts.map +1 -1
- package/dist/oauth-as-routes.js +9 -1
- package/dist/runtime-bundle-hash.d.ts +12 -0
- package/dist/runtime-bundle-hash.d.ts.map +1 -1
- package/dist/runtime-bundle-hash.js +19 -0
- package/dist/server.d.ts +223 -56
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +263 -125
- 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
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
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
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
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
|
|
591
|
+
* Origins of server-stamped connect URLs (`wsUrl`, `sseUrl`,
|
|
534
592
|
* `pollingUrl`) — each parseable entry's origin is unioned into
|
|
535
|
-
* `connectDomains` (deduplicated)
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
* connect-src-
|
|
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
|
-
|
|
1698
|
-
|
|
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
|
|
1730
|
-
// URLs so their origins ride `connectDomains`
|
|
1731
|
-
// fetch are connect-src-governed).
|
|
1732
|
-
//
|
|
1733
|
-
//
|
|
1734
|
-
//
|
|
1735
|
-
|
|
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
|
-
|
|
1738
|
-
sessionApiUrls
|
|
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
|
-
|
|
1968
|
-
|
|
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;
|
|
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,
|
|
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"}
|
package/dist/oauth-as-routes.js
CHANGED
|
@@ -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
|
+
}
|