@lovable.dev/mcp-js 0.6.0 → 0.7.1
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-NJ5WBRYI.js → chunk-3XPKR4G6.js} +7 -2
- package/dist/{chunk-ZKKLOL2C.js → chunk-HRMLGCXV.js} +85 -25
- package/dist/{chunk-SYI32IRK.js → chunk-LMT6RXUF.js} +1 -1
- package/dist/chunk-QC3DXQTH.js +87 -0
- package/dist/{chunk-K2T4WTKX.js → chunk-W7F6JRDB.js} +7 -2
- 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 +124 -24
- package/dist/protocols/mcp/index.js +3 -3
- package/dist/protocols/oauth-metadata.cjs +118 -23
- package/dist/protocols/oauth-metadata.js +3 -3
- package/dist/protocols/rest/index.cjs +122 -23
- package/dist/protocols/rest/index.js +3 -3
- package/dist/stacks/tanstack/index.cjs +126 -25
- 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
|
|
|
@@ -207,6 +281,7 @@ function stringClaim(claims, name) {
|
|
|
207
281
|
// src/auth/verifier.ts
|
|
208
282
|
var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
|
|
209
283
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
284
|
+
var JWKS_FETCH_TIMEOUT_MS = 5e3;
|
|
210
285
|
var OAuthTokenError = class extends Error {
|
|
211
286
|
constructor(status, oauthError, message) {
|
|
212
287
|
super(message);
|
|
@@ -218,12 +293,25 @@ var OAuthTokenError = class extends Error {
|
|
|
218
293
|
function resolveAcceptedAudiences(auth, resource) {
|
|
219
294
|
return auth.acceptedAudiences ?? [resource];
|
|
220
295
|
}
|
|
221
|
-
function
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
296
|
+
function tokenHeaderFields(token) {
|
|
297
|
+
try {
|
|
298
|
+
const header = (0, import_jose.decodeProtectedHeader)(token);
|
|
299
|
+
return { jwtAlg: header.alg, jwtKid: header.kid, tokenLength: token.length };
|
|
300
|
+
} catch {
|
|
301
|
+
return { tokenLength: token.length };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function fetchVerificationKeySet(jwksUri) {
|
|
305
|
+
try {
|
|
306
|
+
const response = await fetch(jwksUri, { signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS), redirect: "manual" });
|
|
307
|
+
if (!response.ok)
|
|
308
|
+
throw new Error(`JWKS endpoint returned ${response.status}`);
|
|
309
|
+
const json = await response.json();
|
|
310
|
+
return (0, import_jose.createLocalJWKSet)(json);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
313
|
+
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
314
|
+
}
|
|
227
315
|
}
|
|
228
316
|
async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
229
317
|
try {
|
|
@@ -238,20 +326,20 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
|
238
326
|
});
|
|
239
327
|
return payload;
|
|
240
328
|
} catch (err) {
|
|
241
|
-
|
|
242
|
-
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
243
|
-
}
|
|
329
|
+
log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
|
|
244
330
|
throw err;
|
|
245
331
|
}
|
|
246
332
|
}
|
|
247
333
|
function assertNonEmptySubject(claims) {
|
|
248
334
|
const sub = claims["sub"];
|
|
249
335
|
if (typeof sub !== "string" || sub.trim() === "") {
|
|
336
|
+
log.debug("oauth.verify.bad_subject", { subType: typeof sub });
|
|
250
337
|
throw new OAuthTokenError(401, "invalid_token", "token subject claim must be a non-empty string");
|
|
251
338
|
}
|
|
252
339
|
}
|
|
253
340
|
function assertOAuthClientClaim(auth, clientId) {
|
|
254
341
|
if (auth.requireOAuthClientClaim !== false && !clientId) {
|
|
342
|
+
log.debug("oauth.verify.missing_client_claim", { outcome: "401 invalid_token" });
|
|
255
343
|
throw new OAuthTokenError(401, "invalid_token", "OAuth client claim is required");
|
|
256
344
|
}
|
|
257
345
|
}
|
|
@@ -276,17 +364,23 @@ function buildMcpAuthContext(args) {
|
|
|
276
364
|
};
|
|
277
365
|
}
|
|
278
366
|
function createOAuthTokenVerifier(auth, discovery) {
|
|
279
|
-
const loadRemoteJwksKeySet = cachedPromise(
|
|
280
|
-
() => discovery.resolveJwksUri().then((jwksURI) => (0, import_jose.createRemoteJWKSet)(new URL(jwksURI)))
|
|
281
|
-
);
|
|
282
367
|
return async (token, request, options) => {
|
|
283
368
|
const resource = resolveProtectedResource(auth, request, options);
|
|
284
369
|
const issuer = await discovery.resolveIssuer();
|
|
285
370
|
const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
|
|
286
|
-
|
|
371
|
+
log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
|
|
372
|
+
const jwksUri = await discovery.resolveJwksUri();
|
|
373
|
+
log.debug("oauth.jwks.fetch", { jwksUri });
|
|
374
|
+
const keySet = await fetchVerificationKeySet(jwksUri);
|
|
375
|
+
const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
|
|
287
376
|
assertNonEmptySubject(claims);
|
|
288
377
|
const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
|
|
289
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
|
+
});
|
|
290
384
|
return context;
|
|
291
385
|
};
|
|
292
386
|
}
|
|
@@ -378,22 +472,27 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
378
472
|
if (runtime.kind === "unconfigured")
|
|
379
473
|
return { ok: true };
|
|
380
474
|
const token = parseBearerToken(request);
|
|
381
|
-
if (!token)
|
|
475
|
+
if (!token) {
|
|
476
|
+
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
382
477
|
return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
|
|
478
|
+
}
|
|
383
479
|
try {
|
|
384
480
|
const auth = await runtime.verify(token, request, runtime.options);
|
|
385
481
|
assertRequiredScopes(runtime.auth, auth);
|
|
386
482
|
return { ok: true, auth };
|
|
387
483
|
} catch (err) {
|
|
388
484
|
if (err instanceof OAuthConfigurationError) {
|
|
485
|
+
log.error("auth.config_error", { ...describeError(err), outcome: "500" });
|
|
389
486
|
return { ok: false, response: oauthConfigurationErrorResponse() };
|
|
390
487
|
}
|
|
391
488
|
if (err instanceof OAuthTokenError) {
|
|
489
|
+
log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
|
|
392
490
|
return {
|
|
393
491
|
ok: false,
|
|
394
492
|
response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
|
|
395
493
|
};
|
|
396
494
|
}
|
|
495
|
+
log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
|
|
397
496
|
return {
|
|
398
497
|
ok: false,
|
|
399
498
|
response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
|
|
@@ -511,7 +610,8 @@ function createMcpProtocolHandler(mcp, options = {}) {
|
|
|
511
610
|
});
|
|
512
611
|
await server.connect(transport);
|
|
513
612
|
return await transport.handleRequest(request);
|
|
514
|
-
} catch {
|
|
613
|
+
} catch (err) {
|
|
614
|
+
log.error("mcp.transport_error", { ...describeError(err), outcome: "500 internal error" });
|
|
515
615
|
return Response.json(
|
|
516
616
|
{ jsonrpc: "2.0", id: null, error: { code: -32603, message: "internal error" } },
|
|
517
617
|
{ status: 500 }
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpProtocolHandler
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-3XPKR4G6.js";
|
|
4
4
|
import "../../chunk-MA5H6PSF.js";
|
|
5
|
-
import "../../chunk-
|
|
6
|
-
import "../../chunk-
|
|
5
|
+
import "../../chunk-HRMLGCXV.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
|
|
|
@@ -209,6 +283,7 @@ function stringClaim(claims, name) {
|
|
|
209
283
|
// src/auth/verifier.ts
|
|
210
284
|
var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
|
|
211
285
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
286
|
+
var JWKS_FETCH_TIMEOUT_MS = 5e3;
|
|
212
287
|
var OAuthTokenError = class extends Error {
|
|
213
288
|
constructor(status, oauthError, message) {
|
|
214
289
|
super(message);
|
|
@@ -220,12 +295,25 @@ var OAuthTokenError = class extends Error {
|
|
|
220
295
|
function resolveAcceptedAudiences(auth, resource) {
|
|
221
296
|
return auth.acceptedAudiences ?? [resource];
|
|
222
297
|
}
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
298
|
+
function tokenHeaderFields(token) {
|
|
299
|
+
try {
|
|
300
|
+
const header = (0, import_jose.decodeProtectedHeader)(token);
|
|
301
|
+
return { jwtAlg: header.alg, jwtKid: header.kid, tokenLength: token.length };
|
|
302
|
+
} catch {
|
|
303
|
+
return { tokenLength: token.length };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function fetchVerificationKeySet(jwksUri) {
|
|
307
|
+
try {
|
|
308
|
+
const response = await fetch(jwksUri, { signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS), redirect: "manual" });
|
|
309
|
+
if (!response.ok)
|
|
310
|
+
throw new Error(`JWKS endpoint returned ${response.status}`);
|
|
311
|
+
const json = await response.json();
|
|
312
|
+
return (0, import_jose.createLocalJWKSet)(json);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
315
|
+
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
316
|
+
}
|
|
229
317
|
}
|
|
230
318
|
async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
231
319
|
try {
|
|
@@ -240,20 +328,20 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
|
240
328
|
});
|
|
241
329
|
return payload;
|
|
242
330
|
} catch (err) {
|
|
243
|
-
|
|
244
|
-
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
245
|
-
}
|
|
331
|
+
log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
|
|
246
332
|
throw err;
|
|
247
333
|
}
|
|
248
334
|
}
|
|
249
335
|
function assertNonEmptySubject(claims) {
|
|
250
336
|
const sub = claims["sub"];
|
|
251
337
|
if (typeof sub !== "string" || sub.trim() === "") {
|
|
338
|
+
log.debug("oauth.verify.bad_subject", { subType: typeof sub });
|
|
252
339
|
throw new OAuthTokenError(401, "invalid_token", "token subject claim must be a non-empty string");
|
|
253
340
|
}
|
|
254
341
|
}
|
|
255
342
|
function assertOAuthClientClaim(auth, clientId) {
|
|
256
343
|
if (auth.requireOAuthClientClaim !== false && !clientId) {
|
|
344
|
+
log.debug("oauth.verify.missing_client_claim", { outcome: "401 invalid_token" });
|
|
257
345
|
throw new OAuthTokenError(401, "invalid_token", "OAuth client claim is required");
|
|
258
346
|
}
|
|
259
347
|
}
|
|
@@ -278,17 +366,23 @@ function buildMcpAuthContext(args) {
|
|
|
278
366
|
};
|
|
279
367
|
}
|
|
280
368
|
function createOAuthTokenVerifier(auth, discovery) {
|
|
281
|
-
const loadRemoteJwksKeySet = cachedPromise(
|
|
282
|
-
() => discovery.resolveJwksUri().then((jwksURI) => (0, import_jose.createRemoteJWKSet)(new URL(jwksURI)))
|
|
283
|
-
);
|
|
284
369
|
return async (token, request, options) => {
|
|
285
370
|
const resource = resolveProtectedResource(auth, request, options);
|
|
286
371
|
const issuer = await discovery.resolveIssuer();
|
|
287
372
|
const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
|
|
288
|
-
|
|
373
|
+
log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
|
|
374
|
+
const jwksUri = await discovery.resolveJwksUri();
|
|
375
|
+
log.debug("oauth.jwks.fetch", { jwksUri });
|
|
376
|
+
const keySet = await fetchVerificationKeySet(jwksUri);
|
|
377
|
+
const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
|
|
289
378
|
assertNonEmptySubject(claims);
|
|
290
379
|
const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
|
|
291
380
|
assertOAuthClientClaim(auth, context.principal.clientId);
|
|
381
|
+
log.info("oauth.verify.ok", {
|
|
382
|
+
sub: context.principal.sub,
|
|
383
|
+
clientId: context.principal.clientId,
|
|
384
|
+
scopes: context.principal.scopes
|
|
385
|
+
});
|
|
292
386
|
return context;
|
|
293
387
|
};
|
|
294
388
|
}
|
|
@@ -390,7 +484,8 @@ function createOAuthProtectedResourceMetadataHandler(mcp, options = {}) {
|
|
|
390
484
|
);
|
|
391
485
|
const response = withCors(Response.json(metadata, { headers }));
|
|
392
486
|
return request.method === "HEAD" ? headResponse(response) : response;
|
|
393
|
-
} catch {
|
|
487
|
+
} catch (err) {
|
|
488
|
+
log.error("oauth.metadata.config_error", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
394
489
|
const response = withCors(oauthConfigurationErrorResponse());
|
|
395
490
|
return request.method === "HEAD" ? headResponse(response) : response;
|
|
396
491
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createOAuthProtectedResourceMetadataHandler
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
5
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-W7F6JRDB.js";
|
|
4
|
+
import "../chunk-HRMLGCXV.js";
|
|
5
|
+
import "../chunk-QC3DXQTH.js";
|
|
6
6
|
import "../chunk-6DXGZZA4.js";
|
|
7
7
|
export {
|
|
8
8
|
createOAuthProtectedResourceMetadataHandler
|