@lovable.dev/mcp-js 0.5.1 → 0.7.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/README.md +21 -0
- package/dist/{chunk-VD6CS7Y6.js → chunk-53M2X7FU.js} +18 -6
- package/dist/{chunk-DDF63QWG.js → chunk-722HKLIU.js} +21 -22
- package/dist/{chunk-GLG5RZGE.js → chunk-G5GWSV5D.js} +16 -4
- package/dist/{chunk-XEDRJFAR.js → chunk-NTXHOUK6.js} +97 -17
- package/dist/chunk-QC3DXQTH.js +87 -0
- package/dist/index.cjs +23 -2
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +5 -3
- package/dist/protocols/mcp/index.cjs +139 -16
- package/dist/protocols/mcp/index.js +3 -3
- package/dist/protocols/oauth-metadata.cjs +135 -30
- package/dist/protocols/oauth-metadata.js +3 -3
- package/dist/protocols/rest/index.cjs +145 -18
- package/dist/protocols/rest/index.js +3 -3
- package/dist/stacks/tanstack/index.cjs +166 -40
- package/dist/stacks/tanstack/index.js +5 -5
- package/package.json +1 -1
- package/dist/chunk-QA3FWDUV.js +0 -40
|
@@ -31,17 +31,71 @@ var import_webStandardStreamableHttp = require("@modelcontextprotocol/sdk/server
|
|
|
31
31
|
// src/core/http.ts
|
|
32
32
|
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
33
33
|
|
|
34
|
+
// src/core/logger.ts
|
|
35
|
+
var LEVEL_RANK = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 };
|
|
36
|
+
function isLogLevel(value) {
|
|
37
|
+
return typeof value === "string" && value in LEVEL_RANK;
|
|
38
|
+
}
|
|
39
|
+
function readEnvLevel() {
|
|
40
|
+
try {
|
|
41
|
+
const raw = typeof process !== "undefined" ? process.env?.["LOVABLE_MCP_LOG_LEVEL"] : void 0;
|
|
42
|
+
const normalized = raw?.trim().toLowerCase();
|
|
43
|
+
return isLogLevel(normalized) ? normalized : void 0;
|
|
44
|
+
} catch {
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
var currentLevel = readEnvLevel() ?? "silent";
|
|
49
|
+
function enabled(level) {
|
|
50
|
+
return LEVEL_RANK[level] <= LEVEL_RANK[currentLevel];
|
|
51
|
+
}
|
|
52
|
+
function emit(level, method, event, fields) {
|
|
53
|
+
if (!enabled(level))
|
|
54
|
+
return;
|
|
55
|
+
const message = `[mcp-js] ${event}`;
|
|
56
|
+
if (fields)
|
|
57
|
+
console[method](message, fields);
|
|
58
|
+
else
|
|
59
|
+
console[method](message);
|
|
60
|
+
}
|
|
61
|
+
var log = {
|
|
62
|
+
error: (event, fields) => emit("error", "error", event, fields),
|
|
63
|
+
warn: (event, fields) => emit("warn", "warn", event, fields),
|
|
64
|
+
info: (event, fields) => emit("info", "info", event, fields),
|
|
65
|
+
debug: (event, fields) => emit("debug", "debug", event, fields)
|
|
66
|
+
};
|
|
67
|
+
function describeError(err) {
|
|
68
|
+
if (err instanceof Error) {
|
|
69
|
+
const code = err.code;
|
|
70
|
+
return { name: err.name, message: err.message, ...typeof code === "string" ? { code } : {} };
|
|
71
|
+
}
|
|
72
|
+
return { value: String(err) };
|
|
73
|
+
}
|
|
74
|
+
|
|
34
75
|
// src/core/promise.ts
|
|
35
|
-
function cachedPromise(load) {
|
|
76
|
+
function cachedPromise(load, label) {
|
|
36
77
|
let settled = false;
|
|
37
78
|
let value;
|
|
38
79
|
return async () => {
|
|
39
|
-
if (settled)
|
|
80
|
+
if (settled) {
|
|
81
|
+
if (label)
|
|
82
|
+
log.debug(`${label}.cache_hit`);
|
|
40
83
|
return value;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
84
|
+
}
|
|
85
|
+
if (label)
|
|
86
|
+
log.debug(`${label}.load_start`);
|
|
87
|
+
try {
|
|
88
|
+
const loaded = await load();
|
|
89
|
+
settled = true;
|
|
90
|
+
value = loaded;
|
|
91
|
+
if (label)
|
|
92
|
+
log.debug(`${label}.settled`);
|
|
93
|
+
return loaded;
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (label)
|
|
96
|
+
log.debug(`${label}.load_failed`, describeError(err));
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
45
99
|
};
|
|
46
100
|
}
|
|
47
101
|
|
|
@@ -117,13 +171,16 @@ async function fetchFirstValidOAuthServerMetadata(metadataUrls, expectedIssuer)
|
|
|
117
171
|
try {
|
|
118
172
|
return await fetchOAuthServerMetadata(url, expectedIssuer);
|
|
119
173
|
} catch (err) {
|
|
174
|
+
log.debug("oauth.discovery.attempt_failed", { url, ...describeError(err) });
|
|
120
175
|
errors.push(`${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
121
176
|
}
|
|
122
177
|
}
|
|
178
|
+
log.error("oauth.discovery.exhausted", { expectedIssuer, urlsTried: metadataUrls, errors });
|
|
123
179
|
throw new Error(`failed to discover OAuth server metadata (${errors.join("; ")})`);
|
|
124
180
|
}
|
|
125
181
|
async function fetchOAuthServerMetadata(url, expectedIssuer) {
|
|
126
|
-
|
|
182
|
+
log.debug("oauth.discovery.fetch", { url });
|
|
183
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS), redirect: "manual" });
|
|
127
184
|
if (!response.ok) {
|
|
128
185
|
throw new Error(String(response.status));
|
|
129
186
|
}
|
|
@@ -133,6 +190,7 @@ async function fetchOAuthServerMetadata(url, expectedIssuer) {
|
|
|
133
190
|
}
|
|
134
191
|
parseSafeUrl("discovered issuer", json.issuer);
|
|
135
192
|
if (trimTrailingSlash(json.issuer) !== expectedIssuer) {
|
|
193
|
+
log.warn("oauth.discovery.issuer_mismatch", { url, expectedIssuer, published: json.issuer });
|
|
136
194
|
throw new Error("issuer mismatch");
|
|
137
195
|
}
|
|
138
196
|
if (typeof json.jwks_uri !== "string") {
|
|
@@ -145,6 +203,11 @@ async function fetchIssuerOAuthServerMetadata(issuer) {
|
|
|
145
203
|
try {
|
|
146
204
|
return await fetchFirstValidOAuthServerMetadata(oauthMetadataUrlsForIssuer(issuer), issuer);
|
|
147
205
|
} catch (err) {
|
|
206
|
+
log.error("oauth.discovery.config_error", {
|
|
207
|
+
issuer,
|
|
208
|
+
...describeError(err),
|
|
209
|
+
outcome: "500 oauth configuration error"
|
|
210
|
+
});
|
|
148
211
|
throw new OAuthConfigurationError(
|
|
149
212
|
`OAuth issuer discovery failed: ${err instanceof Error ? err.message : String(err)}`
|
|
150
213
|
);
|
|
@@ -152,10 +215,21 @@ async function fetchIssuerOAuthServerMetadata(issuer) {
|
|
|
152
215
|
}
|
|
153
216
|
function createOAuthDiscoveryResolver(auth) {
|
|
154
217
|
const configuredIssuer = trimTrailingSlash(auth.issuer);
|
|
155
|
-
const oauthServerMetadata = cachedPromise(
|
|
218
|
+
const oauthServerMetadata = cachedPromise(
|
|
219
|
+
() => fetchIssuerOAuthServerMetadata(configuredIssuer),
|
|
220
|
+
"oauth.discovery.metadata"
|
|
221
|
+
);
|
|
156
222
|
return {
|
|
157
223
|
resolveIssuer: async () => configuredIssuer,
|
|
158
|
-
resolveJwksUri: async () =>
|
|
224
|
+
resolveJwksUri: async () => {
|
|
225
|
+
if (auth.jwksUri) {
|
|
226
|
+
log.debug("oauth.jwks.resolved", { jwksUri: auth.jwksUri, source: "configured" });
|
|
227
|
+
return auth.jwksUri;
|
|
228
|
+
}
|
|
229
|
+
const jwksUri = (await oauthServerMetadata()).jwks_uri;
|
|
230
|
+
log.debug("oauth.jwks.resolved", { jwksUri, source: "discovered" });
|
|
231
|
+
return jwksUri;
|
|
232
|
+
}
|
|
159
233
|
};
|
|
160
234
|
}
|
|
161
235
|
|
|
@@ -218,6 +292,14 @@ var OAuthTokenError = class extends Error {
|
|
|
218
292
|
function resolveAcceptedAudiences(auth, resource) {
|
|
219
293
|
return auth.acceptedAudiences ?? [resource];
|
|
220
294
|
}
|
|
295
|
+
function tokenHeaderFields(token) {
|
|
296
|
+
try {
|
|
297
|
+
const header = (0, import_jose.decodeProtectedHeader)(token);
|
|
298
|
+
return { jwtAlg: header.alg, jwtKid: header.kid, tokenLength: token.length };
|
|
299
|
+
} catch {
|
|
300
|
+
return { tokenLength: token.length };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
221
303
|
function isJwksFetchFailure(err) {
|
|
222
304
|
if (err instanceof import_jose.errors.JWKSTimeout || err instanceof import_jose.errors.JWKSInvalid)
|
|
223
305
|
return true;
|
|
@@ -239,19 +321,23 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
|
239
321
|
return payload;
|
|
240
322
|
} catch (err) {
|
|
241
323
|
if (isJwksFetchFailure(err)) {
|
|
324
|
+
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
242
325
|
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
243
326
|
}
|
|
327
|
+
log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
|
|
244
328
|
throw err;
|
|
245
329
|
}
|
|
246
330
|
}
|
|
247
331
|
function assertNonEmptySubject(claims) {
|
|
248
332
|
const sub = claims["sub"];
|
|
249
333
|
if (typeof sub !== "string" || sub.trim() === "") {
|
|
334
|
+
log.debug("oauth.verify.bad_subject", { subType: typeof sub });
|
|
250
335
|
throw new OAuthTokenError(401, "invalid_token", "token subject claim must be a non-empty string");
|
|
251
336
|
}
|
|
252
337
|
}
|
|
253
338
|
function assertOAuthClientClaim(auth, clientId) {
|
|
254
339
|
if (auth.requireOAuthClientClaim !== false && !clientId) {
|
|
340
|
+
log.debug("oauth.verify.missing_client_claim", { outcome: "401 invalid_token" });
|
|
255
341
|
throw new OAuthTokenError(401, "invalid_token", "OAuth client claim is required");
|
|
256
342
|
}
|
|
257
343
|
}
|
|
@@ -276,17 +362,23 @@ function buildMcpAuthContext(args) {
|
|
|
276
362
|
};
|
|
277
363
|
}
|
|
278
364
|
function createOAuthTokenVerifier(auth, discovery) {
|
|
279
|
-
const loadRemoteJwksKeySet = cachedPromise(
|
|
280
|
-
() => discovery.resolveJwksUri().then((jwksURI) => (0, import_jose.createRemoteJWKSet)(new URL(jwksURI)))
|
|
281
|
-
);
|
|
282
365
|
return async (token, request, options) => {
|
|
283
366
|
const resource = resolveProtectedResource(auth, request, options);
|
|
284
367
|
const issuer = await discovery.resolveIssuer();
|
|
285
368
|
const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
|
|
286
|
-
|
|
369
|
+
log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
|
|
370
|
+
const jwksUri = await discovery.resolveJwksUri();
|
|
371
|
+
log.debug("oauth.jwks.keyset_created", { jwksUri });
|
|
372
|
+
const keySet = (0, import_jose.createRemoteJWKSet)(new URL(jwksUri));
|
|
373
|
+
const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
|
|
287
374
|
assertNonEmptySubject(claims);
|
|
288
375
|
const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
|
|
289
376
|
assertOAuthClientClaim(auth, context.principal.clientId);
|
|
377
|
+
log.info("oauth.verify.ok", {
|
|
378
|
+
sub: context.principal.sub,
|
|
379
|
+
clientId: context.principal.clientId,
|
|
380
|
+
scopes: context.principal.scopes
|
|
381
|
+
});
|
|
290
382
|
return context;
|
|
291
383
|
};
|
|
292
384
|
}
|
|
@@ -378,22 +470,27 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
378
470
|
if (runtime.kind === "unconfigured")
|
|
379
471
|
return { ok: true };
|
|
380
472
|
const token = parseBearerToken(request);
|
|
381
|
-
if (!token)
|
|
473
|
+
if (!token) {
|
|
474
|
+
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
382
475
|
return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
|
|
476
|
+
}
|
|
383
477
|
try {
|
|
384
478
|
const auth = await runtime.verify(token, request, runtime.options);
|
|
385
479
|
assertRequiredScopes(runtime.auth, auth);
|
|
386
480
|
return { ok: true, auth };
|
|
387
481
|
} catch (err) {
|
|
388
482
|
if (err instanceof OAuthConfigurationError) {
|
|
483
|
+
log.error("auth.config_error", { ...describeError(err), outcome: "500" });
|
|
389
484
|
return { ok: false, response: oauthConfigurationErrorResponse() };
|
|
390
485
|
}
|
|
391
486
|
if (err instanceof OAuthTokenError) {
|
|
487
|
+
log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
|
|
392
488
|
return {
|
|
393
489
|
ok: false,
|
|
394
490
|
response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
|
|
395
491
|
};
|
|
396
492
|
}
|
|
493
|
+
log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
|
|
397
494
|
return {
|
|
398
495
|
ok: false,
|
|
399
496
|
response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
|
|
@@ -446,6 +543,26 @@ var ToolContext = class {
|
|
|
446
543
|
}
|
|
447
544
|
};
|
|
448
545
|
|
|
546
|
+
// src/core/cors.ts
|
|
547
|
+
var EXPOSE_HEADERS = "WWW-Authenticate, Mcp-Session-Id, Mcp-Protocol-Version";
|
|
548
|
+
var ALLOW_HEADERS = "Authorization, Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID";
|
|
549
|
+
function withCors(response) {
|
|
550
|
+
response.headers.set("Access-Control-Allow-Origin", "*");
|
|
551
|
+
response.headers.set("Access-Control-Expose-Headers", EXPOSE_HEADERS);
|
|
552
|
+
return response;
|
|
553
|
+
}
|
|
554
|
+
function corsPreflightResponse(allowMethods) {
|
|
555
|
+
return new Response(null, {
|
|
556
|
+
status: 204,
|
|
557
|
+
headers: {
|
|
558
|
+
"Access-Control-Allow-Origin": "*",
|
|
559
|
+
"Access-Control-Allow-Methods": allowMethods,
|
|
560
|
+
"Access-Control-Allow-Headers": ALLOW_HEADERS,
|
|
561
|
+
"Access-Control-Max-Age": "86400"
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
449
566
|
// src/protocols/mcp/protocol.ts
|
|
450
567
|
function adaptToolToSdkCallback(tool, auth) {
|
|
451
568
|
return async (first) => {
|
|
@@ -464,7 +581,7 @@ function adaptToolToSdkCallback(tool, auth) {
|
|
|
464
581
|
}
|
|
465
582
|
function createMcpProtocolHandler(mcp, options = {}) {
|
|
466
583
|
const authorizer = createRequestAuthorizer(mcp, options);
|
|
467
|
-
|
|
584
|
+
const handle = async (request) => {
|
|
468
585
|
const authResult = await authorizer.authorize(request);
|
|
469
586
|
if (!authResult.ok)
|
|
470
587
|
return authResult.response;
|
|
@@ -491,13 +608,19 @@ function createMcpProtocolHandler(mcp, options = {}) {
|
|
|
491
608
|
});
|
|
492
609
|
await server.connect(transport);
|
|
493
610
|
return await transport.handleRequest(request);
|
|
494
|
-
} catch {
|
|
611
|
+
} catch (err) {
|
|
612
|
+
log.error("mcp.transport_error", { ...describeError(err), outcome: "500 internal error" });
|
|
495
613
|
return Response.json(
|
|
496
614
|
{ jsonrpc: "2.0", id: null, error: { code: -32603, message: "internal error" } },
|
|
497
615
|
{ status: 500 }
|
|
498
616
|
);
|
|
499
617
|
}
|
|
500
618
|
};
|
|
619
|
+
return async (request) => {
|
|
620
|
+
if (request.method === "OPTIONS")
|
|
621
|
+
return corsPreflightResponse("GET, POST, DELETE, OPTIONS");
|
|
622
|
+
return withCors(await handle(request));
|
|
623
|
+
};
|
|
501
624
|
}
|
|
502
625
|
// Annotate the CommonJS export names for ESM import in node:
|
|
503
626
|
0 && (module.exports = {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpProtocolHandler
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-G5GWSV5D.js";
|
|
4
4
|
import "../../chunk-MA5H6PSF.js";
|
|
5
|
-
import "../../chunk-
|
|
6
|
-
import "../../chunk-
|
|
5
|
+
import "../../chunk-NTXHOUK6.js";
|
|
6
|
+
import "../../chunk-QC3DXQTH.js";
|
|
7
7
|
import "../../chunk-6DXGZZA4.js";
|
|
8
8
|
export {
|
|
9
9
|
createMcpProtocolHandler
|
|
@@ -36,17 +36,71 @@ function methodNotAllowed(allow) {
|
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// src/core/logger.ts
|
|
40
|
+
var LEVEL_RANK = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 };
|
|
41
|
+
function isLogLevel(value) {
|
|
42
|
+
return typeof value === "string" && value in LEVEL_RANK;
|
|
43
|
+
}
|
|
44
|
+
function readEnvLevel() {
|
|
45
|
+
try {
|
|
46
|
+
const raw = typeof process !== "undefined" ? process.env?.["LOVABLE_MCP_LOG_LEVEL"] : void 0;
|
|
47
|
+
const normalized = raw?.trim().toLowerCase();
|
|
48
|
+
return isLogLevel(normalized) ? normalized : void 0;
|
|
49
|
+
} catch {
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
var currentLevel = readEnvLevel() ?? "silent";
|
|
54
|
+
function enabled(level) {
|
|
55
|
+
return LEVEL_RANK[level] <= LEVEL_RANK[currentLevel];
|
|
56
|
+
}
|
|
57
|
+
function emit(level, method, event, fields) {
|
|
58
|
+
if (!enabled(level))
|
|
59
|
+
return;
|
|
60
|
+
const message = `[mcp-js] ${event}`;
|
|
61
|
+
if (fields)
|
|
62
|
+
console[method](message, fields);
|
|
63
|
+
else
|
|
64
|
+
console[method](message);
|
|
65
|
+
}
|
|
66
|
+
var log = {
|
|
67
|
+
error: (event, fields) => emit("error", "error", event, fields),
|
|
68
|
+
warn: (event, fields) => emit("warn", "warn", event, fields),
|
|
69
|
+
info: (event, fields) => emit("info", "info", event, fields),
|
|
70
|
+
debug: (event, fields) => emit("debug", "debug", event, fields)
|
|
71
|
+
};
|
|
72
|
+
function describeError(err) {
|
|
73
|
+
if (err instanceof Error) {
|
|
74
|
+
const code = err.code;
|
|
75
|
+
return { name: err.name, message: err.message, ...typeof code === "string" ? { code } : {} };
|
|
76
|
+
}
|
|
77
|
+
return { value: String(err) };
|
|
78
|
+
}
|
|
79
|
+
|
|
39
80
|
// src/core/promise.ts
|
|
40
|
-
function cachedPromise(load) {
|
|
81
|
+
function cachedPromise(load, label) {
|
|
41
82
|
let settled = false;
|
|
42
83
|
let value;
|
|
43
84
|
return async () => {
|
|
44
|
-
if (settled)
|
|
85
|
+
if (settled) {
|
|
86
|
+
if (label)
|
|
87
|
+
log.debug(`${label}.cache_hit`);
|
|
45
88
|
return value;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
89
|
+
}
|
|
90
|
+
if (label)
|
|
91
|
+
log.debug(`${label}.load_start`);
|
|
92
|
+
try {
|
|
93
|
+
const loaded = await load();
|
|
94
|
+
settled = true;
|
|
95
|
+
value = loaded;
|
|
96
|
+
if (label)
|
|
97
|
+
log.debug(`${label}.settled`);
|
|
98
|
+
return loaded;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (label)
|
|
101
|
+
log.debug(`${label}.load_failed`, describeError(err));
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
50
104
|
};
|
|
51
105
|
}
|
|
52
106
|
|
|
@@ -122,13 +176,16 @@ async function fetchFirstValidOAuthServerMetadata(metadataUrls, expectedIssuer)
|
|
|
122
176
|
try {
|
|
123
177
|
return await fetchOAuthServerMetadata(url, expectedIssuer);
|
|
124
178
|
} catch (err) {
|
|
179
|
+
log.debug("oauth.discovery.attempt_failed", { url, ...describeError(err) });
|
|
125
180
|
errors.push(`${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
126
181
|
}
|
|
127
182
|
}
|
|
183
|
+
log.error("oauth.discovery.exhausted", { expectedIssuer, urlsTried: metadataUrls, errors });
|
|
128
184
|
throw new Error(`failed to discover OAuth server metadata (${errors.join("; ")})`);
|
|
129
185
|
}
|
|
130
186
|
async function fetchOAuthServerMetadata(url, expectedIssuer) {
|
|
131
|
-
|
|
187
|
+
log.debug("oauth.discovery.fetch", { url });
|
|
188
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS), redirect: "manual" });
|
|
132
189
|
if (!response.ok) {
|
|
133
190
|
throw new Error(String(response.status));
|
|
134
191
|
}
|
|
@@ -138,6 +195,7 @@ async function fetchOAuthServerMetadata(url, expectedIssuer) {
|
|
|
138
195
|
}
|
|
139
196
|
parseSafeUrl("discovered issuer", json.issuer);
|
|
140
197
|
if (trimTrailingSlash(json.issuer) !== expectedIssuer) {
|
|
198
|
+
log.warn("oauth.discovery.issuer_mismatch", { url, expectedIssuer, published: json.issuer });
|
|
141
199
|
throw new Error("issuer mismatch");
|
|
142
200
|
}
|
|
143
201
|
if (typeof json.jwks_uri !== "string") {
|
|
@@ -150,6 +208,11 @@ async function fetchIssuerOAuthServerMetadata(issuer) {
|
|
|
150
208
|
try {
|
|
151
209
|
return await fetchFirstValidOAuthServerMetadata(oauthMetadataUrlsForIssuer(issuer), issuer);
|
|
152
210
|
} catch (err) {
|
|
211
|
+
log.error("oauth.discovery.config_error", {
|
|
212
|
+
issuer,
|
|
213
|
+
...describeError(err),
|
|
214
|
+
outcome: "500 oauth configuration error"
|
|
215
|
+
});
|
|
153
216
|
throw new OAuthConfigurationError(
|
|
154
217
|
`OAuth issuer discovery failed: ${err instanceof Error ? err.message : String(err)}`
|
|
155
218
|
);
|
|
@@ -157,10 +220,21 @@ async function fetchIssuerOAuthServerMetadata(issuer) {
|
|
|
157
220
|
}
|
|
158
221
|
function createOAuthDiscoveryResolver(auth) {
|
|
159
222
|
const configuredIssuer = trimTrailingSlash(auth.issuer);
|
|
160
|
-
const oauthServerMetadata = cachedPromise(
|
|
223
|
+
const oauthServerMetadata = cachedPromise(
|
|
224
|
+
() => fetchIssuerOAuthServerMetadata(configuredIssuer),
|
|
225
|
+
"oauth.discovery.metadata"
|
|
226
|
+
);
|
|
161
227
|
return {
|
|
162
228
|
resolveIssuer: async () => configuredIssuer,
|
|
163
|
-
resolveJwksUri: async () =>
|
|
229
|
+
resolveJwksUri: async () => {
|
|
230
|
+
if (auth.jwksUri) {
|
|
231
|
+
log.debug("oauth.jwks.resolved", { jwksUri: auth.jwksUri, source: "configured" });
|
|
232
|
+
return auth.jwksUri;
|
|
233
|
+
}
|
|
234
|
+
const jwksUri = (await oauthServerMetadata()).jwks_uri;
|
|
235
|
+
log.debug("oauth.jwks.resolved", { jwksUri, source: "discovered" });
|
|
236
|
+
return jwksUri;
|
|
237
|
+
}
|
|
164
238
|
};
|
|
165
239
|
}
|
|
166
240
|
|
|
@@ -220,6 +294,14 @@ var OAuthTokenError = class extends Error {
|
|
|
220
294
|
function resolveAcceptedAudiences(auth, resource) {
|
|
221
295
|
return auth.acceptedAudiences ?? [resource];
|
|
222
296
|
}
|
|
297
|
+
function tokenHeaderFields(token) {
|
|
298
|
+
try {
|
|
299
|
+
const header = (0, import_jose.decodeProtectedHeader)(token);
|
|
300
|
+
return { jwtAlg: header.alg, jwtKid: header.kid, tokenLength: token.length };
|
|
301
|
+
} catch {
|
|
302
|
+
return { tokenLength: token.length };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
223
305
|
function isJwksFetchFailure(err) {
|
|
224
306
|
if (err instanceof import_jose.errors.JWKSTimeout || err instanceof import_jose.errors.JWKSInvalid)
|
|
225
307
|
return true;
|
|
@@ -241,19 +323,23 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
|
241
323
|
return payload;
|
|
242
324
|
} catch (err) {
|
|
243
325
|
if (isJwksFetchFailure(err)) {
|
|
326
|
+
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
244
327
|
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
245
328
|
}
|
|
329
|
+
log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
|
|
246
330
|
throw err;
|
|
247
331
|
}
|
|
248
332
|
}
|
|
249
333
|
function assertNonEmptySubject(claims) {
|
|
250
334
|
const sub = claims["sub"];
|
|
251
335
|
if (typeof sub !== "string" || sub.trim() === "") {
|
|
336
|
+
log.debug("oauth.verify.bad_subject", { subType: typeof sub });
|
|
252
337
|
throw new OAuthTokenError(401, "invalid_token", "token subject claim must be a non-empty string");
|
|
253
338
|
}
|
|
254
339
|
}
|
|
255
340
|
function assertOAuthClientClaim(auth, clientId) {
|
|
256
341
|
if (auth.requireOAuthClientClaim !== false && !clientId) {
|
|
342
|
+
log.debug("oauth.verify.missing_client_claim", { outcome: "401 invalid_token" });
|
|
257
343
|
throw new OAuthTokenError(401, "invalid_token", "OAuth client claim is required");
|
|
258
344
|
}
|
|
259
345
|
}
|
|
@@ -278,17 +364,23 @@ function buildMcpAuthContext(args) {
|
|
|
278
364
|
};
|
|
279
365
|
}
|
|
280
366
|
function createOAuthTokenVerifier(auth, discovery) {
|
|
281
|
-
const loadRemoteJwksKeySet = cachedPromise(
|
|
282
|
-
() => discovery.resolveJwksUri().then((jwksURI) => (0, import_jose.createRemoteJWKSet)(new URL(jwksURI)))
|
|
283
|
-
);
|
|
284
367
|
return async (token, request, options) => {
|
|
285
368
|
const resource = resolveProtectedResource(auth, request, options);
|
|
286
369
|
const issuer = await discovery.resolveIssuer();
|
|
287
370
|
const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
|
|
288
|
-
|
|
371
|
+
log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
|
|
372
|
+
const jwksUri = await discovery.resolveJwksUri();
|
|
373
|
+
log.debug("oauth.jwks.keyset_created", { jwksUri });
|
|
374
|
+
const keySet = (0, import_jose.createRemoteJWKSet)(new URL(jwksUri));
|
|
375
|
+
const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
|
|
289
376
|
assertNonEmptySubject(claims);
|
|
290
377
|
const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
|
|
291
378
|
assertOAuthClientClaim(auth, context.principal.clientId);
|
|
379
|
+
log.info("oauth.verify.ok", {
|
|
380
|
+
sub: context.principal.sub,
|
|
381
|
+
clientId: context.principal.clientId,
|
|
382
|
+
scopes: context.principal.scopes
|
|
383
|
+
});
|
|
292
384
|
return context;
|
|
293
385
|
};
|
|
294
386
|
}
|
|
@@ -317,20 +409,37 @@ function getOAuthRuntime(mcp, options = {}) {
|
|
|
317
409
|
};
|
|
318
410
|
}
|
|
319
411
|
|
|
320
|
-
// src/
|
|
321
|
-
var
|
|
412
|
+
// src/core/cors.ts
|
|
413
|
+
var EXPOSE_HEADERS = "WWW-Authenticate, Mcp-Session-Id, Mcp-Protocol-Version";
|
|
414
|
+
var ALLOW_HEADERS = "Authorization, Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID";
|
|
322
415
|
function withCors(response) {
|
|
323
416
|
response.headers.set("Access-Control-Allow-Origin", "*");
|
|
417
|
+
response.headers.set("Access-Control-Expose-Headers", EXPOSE_HEADERS);
|
|
324
418
|
return response;
|
|
325
419
|
}
|
|
326
|
-
function
|
|
327
|
-
return new Response(
|
|
328
|
-
status:
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
420
|
+
function corsPreflightResponse(allowMethods) {
|
|
421
|
+
return new Response(null, {
|
|
422
|
+
status: 204,
|
|
423
|
+
headers: {
|
|
424
|
+
"Access-Control-Allow-Origin": "*",
|
|
425
|
+
"Access-Control-Allow-Methods": allowMethods,
|
|
426
|
+
"Access-Control-Allow-Headers": ALLOW_HEADERS,
|
|
427
|
+
"Access-Control-Max-Age": "86400"
|
|
428
|
+
}
|
|
332
429
|
});
|
|
333
430
|
}
|
|
431
|
+
|
|
432
|
+
// src/protocols/oauth-metadata.ts
|
|
433
|
+
function notFound() {
|
|
434
|
+
return withCors(
|
|
435
|
+
new Response(JSON.stringify({ error: "not found" }), {
|
|
436
|
+
status: 404,
|
|
437
|
+
// `no-store` so a 404 (OAuth unconfigured) isn't heuristically cached and
|
|
438
|
+
// then served past a later deploy that enables OAuth and starts returning 200.
|
|
439
|
+
headers: { ...JSON_HEADERS, "Cache-Control": "no-store" }
|
|
440
|
+
})
|
|
441
|
+
);
|
|
442
|
+
}
|
|
334
443
|
async function buildProtectedResourceMetadata(mcp, auth, request, options, discovery) {
|
|
335
444
|
const issuer = await discovery.resolveIssuer();
|
|
336
445
|
const body = {
|
|
@@ -354,17 +463,12 @@ function createOAuthProtectedResourceMetadataHandler(mcp, options = {}) {
|
|
|
354
463
|
return async (request) => {
|
|
355
464
|
if (runtime.kind !== "configured" || runtime.auth.protectedResourceMetadataUrl !== void 0)
|
|
356
465
|
return notFound();
|
|
357
|
-
if (request.method === "OPTIONS")
|
|
358
|
-
return
|
|
359
|
-
status: 204,
|
|
360
|
-
headers: { ...CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS" }
|
|
361
|
-
});
|
|
362
|
-
}
|
|
466
|
+
if (request.method === "OPTIONS")
|
|
467
|
+
return corsPreflightResponse("GET, HEAD, OPTIONS");
|
|
363
468
|
if (request.method !== "GET" && request.method !== "HEAD")
|
|
364
469
|
return withCors(methodNotAllowed("GET, HEAD, OPTIONS"));
|
|
365
470
|
const headers = {
|
|
366
471
|
...JSON_HEADERS,
|
|
367
|
-
...CORS_ORIGIN,
|
|
368
472
|
"Cache-Control": "public, max-age=300",
|
|
369
473
|
Vary: "Host"
|
|
370
474
|
};
|
|
@@ -376,9 +480,10 @@ function createOAuthProtectedResourceMetadataHandler(mcp, options = {}) {
|
|
|
376
480
|
runtime.options,
|
|
377
481
|
runtime.discovery
|
|
378
482
|
);
|
|
379
|
-
const response = Response.json(metadata, { headers });
|
|
483
|
+
const response = withCors(Response.json(metadata, { headers }));
|
|
380
484
|
return request.method === "HEAD" ? headResponse(response) : response;
|
|
381
|
-
} catch {
|
|
485
|
+
} catch (err) {
|
|
486
|
+
log.error("oauth.metadata.config_error", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
382
487
|
const response = withCors(oauthConfigurationErrorResponse());
|
|
383
488
|
return request.method === "HEAD" ? headResponse(response) : response;
|
|
384
489
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createOAuthProtectedResourceMetadataHandler
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
5
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-722HKLIU.js";
|
|
4
|
+
import "../chunk-NTXHOUK6.js";
|
|
5
|
+
import "../chunk-QC3DXQTH.js";
|
|
6
6
|
import "../chunk-6DXGZZA4.js";
|
|
7
7
|
export {
|
|
8
8
|
createOAuthProtectedResourceMetadataHandler
|