@ggui-ai/mcp-server 0.8.0 → 0.10.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 (67) 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 +54 -28
  4. package/dist/api-renders-stream-route.d.ts +80 -0
  5. package/dist/api-renders-stream-route.d.ts.map +1 -0
  6. package/dist/api-renders-stream-route.js +311 -0
  7. package/dist/build-mcp.d.ts +48 -7
  8. package/dist/build-mcp.d.ts.map +1 -1
  9. package/dist/build-mcp.js +87 -6
  10. package/dist/code-module-variant.d.ts +150 -0
  11. package/dist/code-module-variant.d.ts.map +1 -0
  12. package/dist/code-module-variant.js +243 -0
  13. package/dist/code-routes.d.ts +12 -2
  14. package/dist/code-routes.d.ts.map +1 -1
  15. package/dist/code-routes.js +12 -2
  16. package/dist/console-session-routes.d.ts.map +1 -1
  17. package/dist/console-session-routes.js +11 -0
  18. package/dist/control-service.d.ts +29 -3
  19. package/dist/control-service.d.ts.map +1 -1
  20. package/dist/control-service.js +26 -2
  21. package/dist/ggui-session-channel/action-ingress.d.ts +2 -2
  22. package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -1
  23. package/dist/ggui-session-channel/channel-subscriptions.d.ts +4 -4
  24. package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -1
  25. package/dist/ggui-session-channel/internal-types.d.ts +63 -9
  26. package/dist/ggui-session-channel/internal-types.d.ts.map +1 -1
  27. package/dist/ggui-session-channel/outbound.d.ts +15 -5
  28. package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
  29. package/dist/ggui-session-channel/outbound.js +64 -24
  30. package/dist/ggui-session-channel/socket-router.d.ts +8 -3
  31. package/dist/ggui-session-channel/socket-router.d.ts.map +1 -1
  32. package/dist/ggui-session-channel/socket-router.js +6 -1
  33. package/dist/ggui-session-channel/subscribe.d.ts +48 -3
  34. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
  35. package/dist/ggui-session-channel/subscribe.js +97 -36
  36. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +23 -13
  37. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -1
  38. package/dist/ggui-session-channel/subscriber-lifecycle.js +24 -11
  39. package/dist/ggui-session-channel.d.ts +58 -11
  40. package/dist/ggui-session-channel.d.ts.map +1 -1
  41. package/dist/ggui-session-channel.js +55 -19
  42. package/dist/health-routes.d.ts +19 -3
  43. package/dist/health-routes.d.ts.map +1 -1
  44. package/dist/health-routes.js +26 -18
  45. package/dist/index.d.ts +6 -3
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +13 -1
  48. package/dist/instructions-presets.js +10 -10
  49. package/dist/mcp-apps-outbound.d.ts +88 -11
  50. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  51. package/dist/mcp-apps-outbound.js +470 -77
  52. package/dist/mcp-endpoint-routes.d.ts +23 -5
  53. package/dist/mcp-endpoint-routes.d.ts.map +1 -1
  54. package/dist/mcp-endpoint-routes.js +69 -1
  55. package/dist/oauth-as-routes.d.ts +11 -0
  56. package/dist/oauth-as-routes.d.ts.map +1 -1
  57. package/dist/oauth-as-routes.js +45 -1
  58. package/dist/oauth.d.ts.map +1 -1
  59. package/dist/oauth.js +8 -1
  60. package/dist/runtime-bundle-hash.d.ts +55 -0
  61. package/dist/runtime-bundle-hash.d.ts.map +1 -0
  62. package/dist/runtime-bundle-hash.js +85 -0
  63. package/dist/runtime-bundle-route.js +1 -1
  64. package/dist/server.d.ts +239 -61
  65. package/dist/server.d.ts.map +1 -1
  66. package/dist/server.js +355 -143
  67. package/package.json +13 -12
@@ -46,6 +46,22 @@ interface PerAppRouting {
46
46
  readonly pathPrefix?: string;
47
47
  readonly authorize?: (urlAppId: string, identity: AuthResult) => Promise<void>;
48
48
  }
49
+ /**
50
+ * Return shape for an `errorMapper` hook — a domain error mapped onto
51
+ * an HTTP/JSON-RPC response triple, with optional response headers.
52
+ *
53
+ * `headers` is deliberately generic HTTP plumbing (e.g. `Retry-After`
54
+ * on a `503`), not a deployment-specific concept — any operator
55
+ * mapping a domain error to a status code that conventionally carries
56
+ * a header can use it.
57
+ */
58
+ export interface ErrorMapperResult {
59
+ readonly status: number;
60
+ readonly code: number;
61
+ readonly message: string;
62
+ /** Response headers to set before the JSON body is written. */
63
+ readonly headers?: Readonly<Record<string, string>>;
64
+ }
49
65
  interface MountOptions {
50
66
  /** Express app to mount onto. */
51
67
  readonly app: Express;
@@ -64,6 +80,12 @@ interface MountOptions {
64
80
  * transport.
65
81
  */
66
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>;
67
89
  /** Validated isolated-service list (`validateMcpServices` output). */
68
90
  readonly mcpServices: ReadonlyArray<McpService>;
69
91
  /** Request-scoped HandlerContext storage shared with the handlers. */
@@ -79,11 +101,7 @@ interface MountOptions {
79
101
  /** Operator-configured issuer URL override (OAuth). */
80
102
  readonly oauthIssuerUrl?: string;
81
103
  /** Operator-supplied error → HTTP/JSON-RPC mapping hook. */
82
- readonly errorMapper?: (err: unknown) => {
83
- readonly status: number;
84
- readonly code: number;
85
- readonly message: string;
86
- } | undefined;
104
+ readonly errorMapper?: (err: unknown) => ErrorMapperResult | undefined;
87
105
  /**
88
106
  * Per-boot `buildMcpServer` options. Assembled once by the composer
89
107
  * (every input is fixed at composition time); the handler spreads a
@@ -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,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,CACrB,GAAG,EAAE,OAAO,KACT;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAC9F;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC;CACjD;AAyBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CA4U1D"}
1
+ {"version":3,"file":"mcp-endpoint-routes.d.ts","sourceRoot":"","sources":["../src/mcp-endpoint-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAGH,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;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClD,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,CA+Z1D"}
@@ -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, {
@@ -235,6 +297,8 @@ export function mountMcpEndpoints(opts) {
235
297
  }
236
298
  }
237
299
  if (mapped) {
300
+ if (mapped.headers)
301
+ res.set(mapped.headers);
238
302
  res.status(mapped.status).json({
239
303
  jsonrpc: "2.0",
240
304
  error: { code: mapped.code, message: mapped.message },
@@ -271,6 +335,10 @@ export function mountMcpEndpoints(opts) {
271
335
  const controlMcpHandler = makeMcpHandler(controlHandlers, {
272
336
  anonymous: true,
273
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,
274
342
  });
275
343
  // Universal endpoint — `appId` resolved from the auth identity via
276
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,CAwE5E"}
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
@@ -66,6 +74,42 @@ export function mountOAuthAuthorizationServerRoutes(opts) {
66
74
  handleProtectedResourceMetadata(req, res, oauthConfig, `${pathPrefix}/${appId}`);
67
75
  });
68
76
  }
77
+ // ── RFC 9728 §3.1 path-INSERTED discovery ──────────────────────────
78
+ // The RFC forms the metadata URL by inserting the well-known
79
+ // component BETWEEN origin and resource path:
80
+ // resource https://host/apps/<id>
81
+ // → https://host/.well-known/oauth-protected-resource/apps/<id>
82
+ // The suffix-form routes above (`<path>/.well-known/...`) predate the
83
+ // final RFC and STAY for grandfathered clients. claude.ai's connector
84
+ // connect flow (observed 2026-08-12) fetches ONLY the inserted form —
85
+ // its absence 404'd the discovery chain, which surfaced as "Couldn't
86
+ // register with GGUI's sign-in service" on every NEW connect: a
87
+ // launch-day onboarding blocker. Both forms serve the identical
88
+ // document.
89
+ if (universalMcpPath !== "/") {
90
+ app.get(`/.well-known/oauth-protected-resource${universalMcpPath}`, (req, res) => handleProtectedResourceMetadata(req, res, oauthConfig, universalMcpPath));
91
+ }
92
+ if (perAppRouting !== undefined) {
93
+ const { paramName, pathPrefix = "" } = perAppRouting;
94
+ app.get(`/.well-known/oauth-protected-resource${pathPrefix}/:${paramName}`, (req, res) => {
95
+ const appId = req.params[paramName];
96
+ if (typeof appId !== "string" || appId.length === 0) {
97
+ res.status(404).json({ error: "not_found" });
98
+ return;
99
+ }
100
+ handleProtectedResourceMetadata(req, res, oauthConfig, `${pathPrefix}/${appId}`);
101
+ });
102
+ // Path-inserted AS-metadata twin (RFC 8414 §3.1). Our issuer is
103
+ // origin-only, so the ROOT document is the normative one — but
104
+ // several MCP clients derive an AS-metadata URL by inserting into
105
+ // the RESOURCE path when the protected-resource fetch fails or
106
+ // races. Serving the identical document here is spec-tolerant and
107
+ // keeps their fallback chain alive instead of dead-ending DCR.
108
+ app.get(`/.well-known/oauth-authorization-server${pathPrefix}/:${paramName}`, (req, res) => handleAuthorizationServerMetadata(req, res, oauthConfig));
109
+ }
110
+ if (universalMcpPath !== "/") {
111
+ app.get(`/.well-known/oauth-authorization-server${universalMcpPath}`, (req, res) => handleAuthorizationServerMetadata(req, res, oauthConfig));
112
+ }
69
113
  app.get("/.well-known/oauth-authorization-server", (req, res) => handleAuthorizationServerMetadata(req, res, oauthConfig));
70
114
  app.post("/oauth/register", (req, res) => {
71
115
  void handleRegister(req, res, oauthConfig, oauthStorage);
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
package/dist/oauth.js CHANGED
@@ -143,7 +143,14 @@ export function resolveIssuerUrl(req, configured) {
143
143
  */
144
144
  export function buildWwwAuthenticate(issuerUrl, resourcePath = '') {
145
145
  const normalized = resourcePath.replace(/\/$/, '');
146
- const resourceMetadataUrl = `${issuerUrl}${normalized}/.well-known/oauth-protected-resource`;
146
+ // RFC 9728 §3.1 path-INSERTED form — the well-known component goes
147
+ // BETWEEN origin and resource path (`/.well-known/oauth-protected-
148
+ // resource/apps/<id>`), NOT appended after it. The pre-RFC suffix
149
+ // form this header used to advertise is still SERVED (grandfathered
150
+ // clients), but the advert must be the spec shape: claude.ai's
151
+ // connect flow follows this header verbatim and 404'd on the suffix
152
+ // URL, dead-ending Dynamic Client Registration for new connects.
153
+ const resourceMetadataUrl = `${issuerUrl}/.well-known/oauth-protected-resource${normalized}`;
147
154
  return `Bearer realm="mcp", resource_metadata="${resourceMetadataUrl}"`;
148
155
  }
149
156
  // =============================================================================
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Content-hash naming for the iframe-runtime bundle URL (#472).
3
+ *
4
+ * ONE derivation, three consumers:
5
+ *
6
+ * - `createGguiServer` (server.ts) hashes the captured bundle bytes,
7
+ * mounts the immutable twin route, and rewrites the stamped
8
+ * `runtimeUrl` — see the "Content-hashed runtime URL" block there.
9
+ * - `mountRuntimeBundleRoute` serves the hashed name it is handed.
10
+ * - Deployments that compose their own render handler (bypassing the
11
+ * factory's default handler set) mint an absolute `runtimeUrl`
12
+ * themselves; {@link resolveHashedRuntimeBundleUrl} lets them stamp
13
+ * the SAME hashed name the co-resident factory mounts, instead of
14
+ * re-deriving the scheme by hand and drifting.
15
+ *
16
+ * The scheme: `sha256(bytes)` truncated to 12 hex chars, inserted
17
+ * before the filename's extension — `iframe-runtime.js` →
18
+ * `iframe-runtime.<hash>.js`. 12 chars (48 bits) is plenty for
19
+ * cache-busting across deploys; this is not an integrity check (SRI
20
+ * would be a separate, full-length hash).
21
+ */
22
+ /** Truncated content hash used in the bundle's immutable URL name. */
23
+ export declare function computeRuntimeBundleHash(bytes: Buffer): string;
24
+ /**
25
+ * Insert `hash` into `urlOrPath`'s filename, but ONLY when that
26
+ * filename is exactly `plainName` — a URL pointing at a foreign copy
27
+ * of the bundle (different name) is returned untouched, because the
28
+ * foreign host serves only the name the operator configured. Applies
29
+ * to both bare paths (`/_ggui/iframe-runtime.js`) and absolute URLs
30
+ * (`https://cdn.example.com/_ggui/iframe-runtime.js`).
31
+ */
32
+ export declare function insertRuntimeBundleHash(urlOrPath: string, hash: string, plainName: string): string;
33
+ /**
34
+ * Rewrite `plainUrl` (an absolute URL or path ending in the default
35
+ * bundle filename) to its content-hashed twin, hashing the bundle at
36
+ * `bundleFile` (default: the workspace's built bundle — the same file
37
+ * `createGguiServer` serves, so both stamp the same name). Falls back
38
+ * to `plainUrl` unchanged when the bundle is unreadable — the plain
39
+ * `no-cache` route always exists, so the fallback stays correct, just
40
+ * revalidated.
41
+ */
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;
55
+ //# sourceMappingURL=runtime-bundle-hash.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Content-hash naming for the iframe-runtime bundle URL (#472).
3
+ *
4
+ * ONE derivation, three consumers:
5
+ *
6
+ * - `createGguiServer` (server.ts) hashes the captured bundle bytes,
7
+ * mounts the immutable twin route, and rewrites the stamped
8
+ * `runtimeUrl` — see the "Content-hashed runtime URL" block there.
9
+ * - `mountRuntimeBundleRoute` serves the hashed name it is handed.
10
+ * - Deployments that compose their own render handler (bypassing the
11
+ * factory's default handler set) mint an absolute `runtimeUrl`
12
+ * themselves; {@link resolveHashedRuntimeBundleUrl} lets them stamp
13
+ * the SAME hashed name the co-resident factory mounts, instead of
14
+ * re-deriving the scheme by hand and drifting.
15
+ *
16
+ * The scheme: `sha256(bytes)` truncated to 12 hex chars, inserted
17
+ * before the filename's extension — `iframe-runtime.js` →
18
+ * `iframe-runtime.<hash>.js`. 12 chars (48 bits) is plenty for
19
+ * cache-busting across deploys; this is not an integrity check (SRI
20
+ * would be a separate, full-length hash).
21
+ */
22
+ import { createHash } from "node:crypto";
23
+ import fs from "node:fs";
24
+ import { RUNTIME_BUNDLE_FILE, RUNTIME_BUNDLE_URL_PATH } from "@ggui-ai/iframe-runtime/server";
25
+ /** Truncated content hash used in the bundle's immutable URL name. */
26
+ export function computeRuntimeBundleHash(bytes) {
27
+ return createHash("sha256").update(bytes).digest("hex").slice(0, 12);
28
+ }
29
+ /**
30
+ * Insert `hash` into `urlOrPath`'s filename, but ONLY when that
31
+ * filename is exactly `plainName` — a URL pointing at a foreign copy
32
+ * of the bundle (different name) is returned untouched, because the
33
+ * foreign host serves only the name the operator configured. Applies
34
+ * to both bare paths (`/_ggui/iframe-runtime.js`) and absolute URLs
35
+ * (`https://cdn.example.com/_ggui/iframe-runtime.js`).
36
+ */
37
+ export function insertRuntimeBundleHash(urlOrPath, hash, plainName) {
38
+ const dot = plainName.lastIndexOf(".");
39
+ const hashedName = dot === -1
40
+ ? `${plainName}.${hash}`
41
+ : `${plainName.slice(0, dot)}.${hash}${plainName.slice(dot)}`;
42
+ if (!urlOrPath.endsWith(`/${plainName}`) && urlOrPath !== plainName)
43
+ return urlOrPath;
44
+ return `${urlOrPath.slice(0, urlOrPath.length - plainName.length)}${hashedName}`;
45
+ }
46
+ /**
47
+ * Rewrite `plainUrl` (an absolute URL or path ending in the default
48
+ * bundle filename) to its content-hashed twin, hashing the bundle at
49
+ * `bundleFile` (default: the workspace's built bundle — the same file
50
+ * `createGguiServer` serves, so both stamp the same name). Falls back
51
+ * to `plainUrl` unchanged when the bundle is unreadable — the plain
52
+ * `no-cache` route always exists, so the fallback stays correct, just
53
+ * revalidated.
54
+ */
55
+ export function resolveHashedRuntimeBundleUrl(plainUrl, bundleFile = RUNTIME_BUNDLE_FILE) {
56
+ let bytes;
57
+ try {
58
+ bytes = fs.readFileSync(bundleFile);
59
+ }
60
+ catch {
61
+ // Bundle not built / not shipped — mint the plain revalidated name.
62
+ return plainUrl;
63
+ }
64
+ const plainName = RUNTIME_BUNDLE_URL_PATH.slice(RUNTIME_BUNDLE_URL_PATH.lastIndexOf("/") + 1);
65
+ return insertRuntimeBundleHash(plainUrl, computeRuntimeBundleHash(bytes), plainName);
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
+ }
@@ -55,7 +55,7 @@ export function mountRuntimeBundleRoute(opts) {
55
55
  res.setHeader("Cache-Control", "no-cache");
56
56
  // CORS: the bundle MUST be loadable from `<script type="module"
57
57
  // src=...>` inside a sandboxed `srcdoc` iframe (the
58
- // `<McpAppIframe>` mount path — see `packages/ggui-react/src/
58
+ // `<McpAppIframe>` mount path — see `packages/mcp-apps-react/src/
59
59
  // McpAppIframe/dispatch.ts::deriveResourceMountSource`). Such an
60
60
  // iframe has the `null` origin and module-script fetches always
61
61
  // run in CORS mode; without a permissive header browsers reject