@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.
- 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 +54 -28
- package/dist/api-renders-stream-route.d.ts +80 -0
- package/dist/api-renders-stream-route.d.ts.map +1 -0
- package/dist/api-renders-stream-route.js +311 -0
- package/dist/build-mcp.d.ts +48 -7
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +87 -6
- 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/console-session-routes.d.ts.map +1 -1
- package/dist/console-session-routes.js +11 -0
- 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/ggui-session-channel/action-ingress.d.ts +2 -2
- package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -1
- package/dist/ggui-session-channel/channel-subscriptions.d.ts +4 -4
- package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -1
- package/dist/ggui-session-channel/internal-types.d.ts +63 -9
- package/dist/ggui-session-channel/internal-types.d.ts.map +1 -1
- package/dist/ggui-session-channel/outbound.d.ts +15 -5
- package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
- package/dist/ggui-session-channel/outbound.js +64 -24
- package/dist/ggui-session-channel/socket-router.d.ts +8 -3
- package/dist/ggui-session-channel/socket-router.d.ts.map +1 -1
- package/dist/ggui-session-channel/socket-router.js +6 -1
- package/dist/ggui-session-channel/subscribe.d.ts +48 -3
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscribe.js +97 -36
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +23 -13
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscriber-lifecycle.js +24 -11
- package/dist/ggui-session-channel.d.ts +58 -11
- package/dist/ggui-session-channel.d.ts.map +1 -1
- package/dist/ggui-session-channel.js +55 -19
- 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 +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/instructions-presets.js +10 -10
- package/dist/mcp-apps-outbound.d.ts +88 -11
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +470 -77
- package/dist/mcp-endpoint-routes.d.ts +23 -5
- package/dist/mcp-endpoint-routes.d.ts.map +1 -1
- package/dist/mcp-endpoint-routes.js +69 -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 +45 -1
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +8 -1
- package/dist/runtime-bundle-hash.d.ts +55 -0
- package/dist/runtime-bundle-hash.d.ts.map +1 -0
- package/dist/runtime-bundle-hash.js +85 -0
- package/dist/runtime-bundle-route.js +1 -1
- package/dist/server.d.ts +239 -61
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +355 -143
- 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;
|
|
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, {
|
|
@@ -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,
|
|
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
|
|
@@ -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);
|
package/dist/oauth.d.ts.map
CHANGED
|
@@ -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
|
-
|
|
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/
|
|
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
|