@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
|
@@ -41,17 +41,71 @@ function methodNotAllowed(allow) {
|
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// src/core/logger.ts
|
|
45
|
+
var LEVEL_RANK = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 };
|
|
46
|
+
function isLogLevel(value) {
|
|
47
|
+
return typeof value === "string" && value in LEVEL_RANK;
|
|
48
|
+
}
|
|
49
|
+
function readEnvLevel() {
|
|
50
|
+
try {
|
|
51
|
+
const raw = typeof process !== "undefined" ? process.env?.["LOVABLE_MCP_LOG_LEVEL"] : void 0;
|
|
52
|
+
const normalized = raw?.trim().toLowerCase();
|
|
53
|
+
return isLogLevel(normalized) ? normalized : void 0;
|
|
54
|
+
} catch {
|
|
55
|
+
return void 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
var currentLevel = readEnvLevel() ?? "silent";
|
|
59
|
+
function enabled(level) {
|
|
60
|
+
return LEVEL_RANK[level] <= LEVEL_RANK[currentLevel];
|
|
61
|
+
}
|
|
62
|
+
function emit(level, method, event, fields) {
|
|
63
|
+
if (!enabled(level))
|
|
64
|
+
return;
|
|
65
|
+
const message = `[mcp-js] ${event}`;
|
|
66
|
+
if (fields)
|
|
67
|
+
console[method](message, fields);
|
|
68
|
+
else
|
|
69
|
+
console[method](message);
|
|
70
|
+
}
|
|
71
|
+
var log = {
|
|
72
|
+
error: (event, fields) => emit("error", "error", event, fields),
|
|
73
|
+
warn: (event, fields) => emit("warn", "warn", event, fields),
|
|
74
|
+
info: (event, fields) => emit("info", "info", event, fields),
|
|
75
|
+
debug: (event, fields) => emit("debug", "debug", event, fields)
|
|
76
|
+
};
|
|
77
|
+
function describeError(err) {
|
|
78
|
+
if (err instanceof Error) {
|
|
79
|
+
const code = err.code;
|
|
80
|
+
return { name: err.name, message: err.message, ...typeof code === "string" ? { code } : {} };
|
|
81
|
+
}
|
|
82
|
+
return { value: String(err) };
|
|
83
|
+
}
|
|
84
|
+
|
|
44
85
|
// src/core/promise.ts
|
|
45
|
-
function cachedPromise(load) {
|
|
86
|
+
function cachedPromise(load, label) {
|
|
46
87
|
let settled = false;
|
|
47
88
|
let value;
|
|
48
89
|
return async () => {
|
|
49
|
-
if (settled)
|
|
90
|
+
if (settled) {
|
|
91
|
+
if (label)
|
|
92
|
+
log.debug(`${label}.cache_hit`);
|
|
50
93
|
return value;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
94
|
+
}
|
|
95
|
+
if (label)
|
|
96
|
+
log.debug(`${label}.load_start`);
|
|
97
|
+
try {
|
|
98
|
+
const loaded = await load();
|
|
99
|
+
settled = true;
|
|
100
|
+
value = loaded;
|
|
101
|
+
if (label)
|
|
102
|
+
log.debug(`${label}.settled`);
|
|
103
|
+
return loaded;
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (label)
|
|
106
|
+
log.debug(`${label}.load_failed`, describeError(err));
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
55
109
|
};
|
|
56
110
|
}
|
|
57
111
|
|
|
@@ -127,13 +181,16 @@ async function fetchFirstValidOAuthServerMetadata(metadataUrls, expectedIssuer)
|
|
|
127
181
|
try {
|
|
128
182
|
return await fetchOAuthServerMetadata(url, expectedIssuer);
|
|
129
183
|
} catch (err) {
|
|
184
|
+
log.debug("oauth.discovery.attempt_failed", { url, ...describeError(err) });
|
|
130
185
|
errors.push(`${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
131
186
|
}
|
|
132
187
|
}
|
|
188
|
+
log.error("oauth.discovery.exhausted", { expectedIssuer, urlsTried: metadataUrls, errors });
|
|
133
189
|
throw new Error(`failed to discover OAuth server metadata (${errors.join("; ")})`);
|
|
134
190
|
}
|
|
135
191
|
async function fetchOAuthServerMetadata(url, expectedIssuer) {
|
|
136
|
-
|
|
192
|
+
log.debug("oauth.discovery.fetch", { url });
|
|
193
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS), redirect: "manual" });
|
|
137
194
|
if (!response.ok) {
|
|
138
195
|
throw new Error(String(response.status));
|
|
139
196
|
}
|
|
@@ -143,6 +200,7 @@ async function fetchOAuthServerMetadata(url, expectedIssuer) {
|
|
|
143
200
|
}
|
|
144
201
|
parseSafeUrl("discovered issuer", json.issuer);
|
|
145
202
|
if (trimTrailingSlash(json.issuer) !== expectedIssuer) {
|
|
203
|
+
log.warn("oauth.discovery.issuer_mismatch", { url, expectedIssuer, published: json.issuer });
|
|
146
204
|
throw new Error("issuer mismatch");
|
|
147
205
|
}
|
|
148
206
|
if (typeof json.jwks_uri !== "string") {
|
|
@@ -155,6 +213,11 @@ async function fetchIssuerOAuthServerMetadata(issuer) {
|
|
|
155
213
|
try {
|
|
156
214
|
return await fetchFirstValidOAuthServerMetadata(oauthMetadataUrlsForIssuer(issuer), issuer);
|
|
157
215
|
} catch (err) {
|
|
216
|
+
log.error("oauth.discovery.config_error", {
|
|
217
|
+
issuer,
|
|
218
|
+
...describeError(err),
|
|
219
|
+
outcome: "500 oauth configuration error"
|
|
220
|
+
});
|
|
158
221
|
throw new OAuthConfigurationError(
|
|
159
222
|
`OAuth issuer discovery failed: ${err instanceof Error ? err.message : String(err)}`
|
|
160
223
|
);
|
|
@@ -162,10 +225,21 @@ async function fetchIssuerOAuthServerMetadata(issuer) {
|
|
|
162
225
|
}
|
|
163
226
|
function createOAuthDiscoveryResolver(auth) {
|
|
164
227
|
const configuredIssuer = trimTrailingSlash(auth.issuer);
|
|
165
|
-
const oauthServerMetadata = cachedPromise(
|
|
228
|
+
const oauthServerMetadata = cachedPromise(
|
|
229
|
+
() => fetchIssuerOAuthServerMetadata(configuredIssuer),
|
|
230
|
+
"oauth.discovery.metadata"
|
|
231
|
+
);
|
|
166
232
|
return {
|
|
167
233
|
resolveIssuer: async () => configuredIssuer,
|
|
168
|
-
resolveJwksUri: async () =>
|
|
234
|
+
resolveJwksUri: async () => {
|
|
235
|
+
if (auth.jwksUri) {
|
|
236
|
+
log.debug("oauth.jwks.resolved", { jwksUri: auth.jwksUri, source: "configured" });
|
|
237
|
+
return auth.jwksUri;
|
|
238
|
+
}
|
|
239
|
+
const jwksUri = (await oauthServerMetadata()).jwks_uri;
|
|
240
|
+
log.debug("oauth.jwks.resolved", { jwksUri, source: "discovered" });
|
|
241
|
+
return jwksUri;
|
|
242
|
+
}
|
|
169
243
|
};
|
|
170
244
|
}
|
|
171
245
|
|
|
@@ -217,6 +291,7 @@ function stringClaim(claims, name) {
|
|
|
217
291
|
// src/auth/verifier.ts
|
|
218
292
|
var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
|
|
219
293
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
294
|
+
var JWKS_FETCH_TIMEOUT_MS = 5e3;
|
|
220
295
|
var OAuthTokenError = class extends Error {
|
|
221
296
|
constructor(status, oauthError, message) {
|
|
222
297
|
super(message);
|
|
@@ -228,12 +303,25 @@ var OAuthTokenError = class extends Error {
|
|
|
228
303
|
function resolveAcceptedAudiences(auth, resource) {
|
|
229
304
|
return auth.acceptedAudiences ?? [resource];
|
|
230
305
|
}
|
|
231
|
-
function
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
306
|
+
function tokenHeaderFields(token) {
|
|
307
|
+
try {
|
|
308
|
+
const header = (0, import_jose.decodeProtectedHeader)(token);
|
|
309
|
+
return { jwtAlg: header.alg, jwtKid: header.kid, tokenLength: token.length };
|
|
310
|
+
} catch {
|
|
311
|
+
return { tokenLength: token.length };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function fetchVerificationKeySet(jwksUri) {
|
|
315
|
+
try {
|
|
316
|
+
const response = await fetch(jwksUri, { signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS), redirect: "manual" });
|
|
317
|
+
if (!response.ok)
|
|
318
|
+
throw new Error(`JWKS endpoint returned ${response.status}`);
|
|
319
|
+
const json = await response.json();
|
|
320
|
+
return (0, import_jose.createLocalJWKSet)(json);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
323
|
+
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
324
|
+
}
|
|
237
325
|
}
|
|
238
326
|
async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
239
327
|
try {
|
|
@@ -248,20 +336,20 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
|
248
336
|
});
|
|
249
337
|
return payload;
|
|
250
338
|
} catch (err) {
|
|
251
|
-
|
|
252
|
-
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
253
|
-
}
|
|
339
|
+
log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
|
|
254
340
|
throw err;
|
|
255
341
|
}
|
|
256
342
|
}
|
|
257
343
|
function assertNonEmptySubject(claims) {
|
|
258
344
|
const sub = claims["sub"];
|
|
259
345
|
if (typeof sub !== "string" || sub.trim() === "") {
|
|
346
|
+
log.debug("oauth.verify.bad_subject", { subType: typeof sub });
|
|
260
347
|
throw new OAuthTokenError(401, "invalid_token", "token subject claim must be a non-empty string");
|
|
261
348
|
}
|
|
262
349
|
}
|
|
263
350
|
function assertOAuthClientClaim(auth, clientId) {
|
|
264
351
|
if (auth.requireOAuthClientClaim !== false && !clientId) {
|
|
352
|
+
log.debug("oauth.verify.missing_client_claim", { outcome: "401 invalid_token" });
|
|
265
353
|
throw new OAuthTokenError(401, "invalid_token", "OAuth client claim is required");
|
|
266
354
|
}
|
|
267
355
|
}
|
|
@@ -286,17 +374,23 @@ function buildMcpAuthContext(args) {
|
|
|
286
374
|
};
|
|
287
375
|
}
|
|
288
376
|
function createOAuthTokenVerifier(auth, discovery) {
|
|
289
|
-
const loadRemoteJwksKeySet = cachedPromise(
|
|
290
|
-
() => discovery.resolveJwksUri().then((jwksURI) => (0, import_jose.createRemoteJWKSet)(new URL(jwksURI)))
|
|
291
|
-
);
|
|
292
377
|
return async (token, request, options) => {
|
|
293
378
|
const resource = resolveProtectedResource(auth, request, options);
|
|
294
379
|
const issuer = await discovery.resolveIssuer();
|
|
295
380
|
const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
|
|
296
|
-
|
|
381
|
+
log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
|
|
382
|
+
const jwksUri = await discovery.resolveJwksUri();
|
|
383
|
+
log.debug("oauth.jwks.fetch", { jwksUri });
|
|
384
|
+
const keySet = await fetchVerificationKeySet(jwksUri);
|
|
385
|
+
const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
|
|
297
386
|
assertNonEmptySubject(claims);
|
|
298
387
|
const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
|
|
299
388
|
assertOAuthClientClaim(auth, context.principal.clientId);
|
|
389
|
+
log.info("oauth.verify.ok", {
|
|
390
|
+
sub: context.principal.sub,
|
|
391
|
+
clientId: context.principal.clientId,
|
|
392
|
+
scopes: context.principal.scopes
|
|
393
|
+
});
|
|
300
394
|
return context;
|
|
301
395
|
};
|
|
302
396
|
}
|
|
@@ -396,22 +490,27 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
396
490
|
if (runtime.kind === "unconfigured")
|
|
397
491
|
return { ok: true };
|
|
398
492
|
const token = parseBearerToken(request);
|
|
399
|
-
if (!token)
|
|
493
|
+
if (!token) {
|
|
494
|
+
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
400
495
|
return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
|
|
496
|
+
}
|
|
401
497
|
try {
|
|
402
498
|
const auth = await runtime.verify(token, request, runtime.options);
|
|
403
499
|
assertRequiredScopes(runtime.auth, auth);
|
|
404
500
|
return { ok: true, auth };
|
|
405
501
|
} catch (err) {
|
|
406
502
|
if (err instanceof OAuthConfigurationError) {
|
|
503
|
+
log.error("auth.config_error", { ...describeError(err), outcome: "500" });
|
|
407
504
|
return { ok: false, response: oauthConfigurationErrorResponse() };
|
|
408
505
|
}
|
|
409
506
|
if (err instanceof OAuthTokenError) {
|
|
507
|
+
log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
|
|
410
508
|
return {
|
|
411
509
|
ok: false,
|
|
412
510
|
response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
|
|
413
511
|
};
|
|
414
512
|
}
|
|
513
|
+
log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
|
|
415
514
|
return {
|
|
416
515
|
ok: false,
|
|
417
516
|
response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createInvokeToolHandler,
|
|
3
3
|
createListToolsHandler
|
|
4
|
-
} from "../../chunk-
|
|
4
|
+
} from "../../chunk-LMT6RXUF.js";
|
|
5
5
|
import "../../chunk-MA5H6PSF.js";
|
|
6
|
-
import "../../chunk-
|
|
7
|
-
import "../../chunk-
|
|
6
|
+
import "../../chunk-HRMLGCXV.js";
|
|
7
|
+
import "../../chunk-QC3DXQTH.js";
|
|
8
8
|
import "../../chunk-6DXGZZA4.js";
|
|
9
9
|
export {
|
|
10
10
|
createInvokeToolHandler,
|
|
@@ -43,17 +43,71 @@ function methodNotAllowed(allow) {
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// src/core/logger.ts
|
|
47
|
+
var LEVEL_RANK = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 };
|
|
48
|
+
function isLogLevel(value) {
|
|
49
|
+
return typeof value === "string" && value in LEVEL_RANK;
|
|
50
|
+
}
|
|
51
|
+
function readEnvLevel() {
|
|
52
|
+
try {
|
|
53
|
+
const raw = typeof process !== "undefined" ? process.env?.["LOVABLE_MCP_LOG_LEVEL"] : void 0;
|
|
54
|
+
const normalized = raw?.trim().toLowerCase();
|
|
55
|
+
return isLogLevel(normalized) ? normalized : void 0;
|
|
56
|
+
} catch {
|
|
57
|
+
return void 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
var currentLevel = readEnvLevel() ?? "silent";
|
|
61
|
+
function enabled(level) {
|
|
62
|
+
return LEVEL_RANK[level] <= LEVEL_RANK[currentLevel];
|
|
63
|
+
}
|
|
64
|
+
function emit(level, method, event, fields) {
|
|
65
|
+
if (!enabled(level))
|
|
66
|
+
return;
|
|
67
|
+
const message = `[mcp-js] ${event}`;
|
|
68
|
+
if (fields)
|
|
69
|
+
console[method](message, fields);
|
|
70
|
+
else
|
|
71
|
+
console[method](message);
|
|
72
|
+
}
|
|
73
|
+
var log = {
|
|
74
|
+
error: (event, fields) => emit("error", "error", event, fields),
|
|
75
|
+
warn: (event, fields) => emit("warn", "warn", event, fields),
|
|
76
|
+
info: (event, fields) => emit("info", "info", event, fields),
|
|
77
|
+
debug: (event, fields) => emit("debug", "debug", event, fields)
|
|
78
|
+
};
|
|
79
|
+
function describeError(err) {
|
|
80
|
+
if (err instanceof Error) {
|
|
81
|
+
const code = err.code;
|
|
82
|
+
return { name: err.name, message: err.message, ...typeof code === "string" ? { code } : {} };
|
|
83
|
+
}
|
|
84
|
+
return { value: String(err) };
|
|
85
|
+
}
|
|
86
|
+
|
|
46
87
|
// src/core/promise.ts
|
|
47
|
-
function cachedPromise(load) {
|
|
88
|
+
function cachedPromise(load, label) {
|
|
48
89
|
let settled = false;
|
|
49
90
|
let value;
|
|
50
91
|
return async () => {
|
|
51
|
-
if (settled)
|
|
92
|
+
if (settled) {
|
|
93
|
+
if (label)
|
|
94
|
+
log.debug(`${label}.cache_hit`);
|
|
52
95
|
return value;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
96
|
+
}
|
|
97
|
+
if (label)
|
|
98
|
+
log.debug(`${label}.load_start`);
|
|
99
|
+
try {
|
|
100
|
+
const loaded = await load();
|
|
101
|
+
settled = true;
|
|
102
|
+
value = loaded;
|
|
103
|
+
if (label)
|
|
104
|
+
log.debug(`${label}.settled`);
|
|
105
|
+
return loaded;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (label)
|
|
108
|
+
log.debug(`${label}.load_failed`, describeError(err));
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
57
111
|
};
|
|
58
112
|
}
|
|
59
113
|
|
|
@@ -129,13 +183,16 @@ async function fetchFirstValidOAuthServerMetadata(metadataUrls, expectedIssuer)
|
|
|
129
183
|
try {
|
|
130
184
|
return await fetchOAuthServerMetadata(url, expectedIssuer);
|
|
131
185
|
} catch (err) {
|
|
186
|
+
log.debug("oauth.discovery.attempt_failed", { url, ...describeError(err) });
|
|
132
187
|
errors.push(`${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
133
188
|
}
|
|
134
189
|
}
|
|
190
|
+
log.error("oauth.discovery.exhausted", { expectedIssuer, urlsTried: metadataUrls, errors });
|
|
135
191
|
throw new Error(`failed to discover OAuth server metadata (${errors.join("; ")})`);
|
|
136
192
|
}
|
|
137
193
|
async function fetchOAuthServerMetadata(url, expectedIssuer) {
|
|
138
|
-
|
|
194
|
+
log.debug("oauth.discovery.fetch", { url });
|
|
195
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS), redirect: "manual" });
|
|
139
196
|
if (!response.ok) {
|
|
140
197
|
throw new Error(String(response.status));
|
|
141
198
|
}
|
|
@@ -145,6 +202,7 @@ async function fetchOAuthServerMetadata(url, expectedIssuer) {
|
|
|
145
202
|
}
|
|
146
203
|
parseSafeUrl("discovered issuer", json.issuer);
|
|
147
204
|
if (trimTrailingSlash(json.issuer) !== expectedIssuer) {
|
|
205
|
+
log.warn("oauth.discovery.issuer_mismatch", { url, expectedIssuer, published: json.issuer });
|
|
148
206
|
throw new Error("issuer mismatch");
|
|
149
207
|
}
|
|
150
208
|
if (typeof json.jwks_uri !== "string") {
|
|
@@ -157,6 +215,11 @@ async function fetchIssuerOAuthServerMetadata(issuer) {
|
|
|
157
215
|
try {
|
|
158
216
|
return await fetchFirstValidOAuthServerMetadata(oauthMetadataUrlsForIssuer(issuer), issuer);
|
|
159
217
|
} catch (err) {
|
|
218
|
+
log.error("oauth.discovery.config_error", {
|
|
219
|
+
issuer,
|
|
220
|
+
...describeError(err),
|
|
221
|
+
outcome: "500 oauth configuration error"
|
|
222
|
+
});
|
|
160
223
|
throw new OAuthConfigurationError(
|
|
161
224
|
`OAuth issuer discovery failed: ${err instanceof Error ? err.message : String(err)}`
|
|
162
225
|
);
|
|
@@ -164,10 +227,21 @@ async function fetchIssuerOAuthServerMetadata(issuer) {
|
|
|
164
227
|
}
|
|
165
228
|
function createOAuthDiscoveryResolver(auth) {
|
|
166
229
|
const configuredIssuer = trimTrailingSlash(auth.issuer);
|
|
167
|
-
const oauthServerMetadata = cachedPromise(
|
|
230
|
+
const oauthServerMetadata = cachedPromise(
|
|
231
|
+
() => fetchIssuerOAuthServerMetadata(configuredIssuer),
|
|
232
|
+
"oauth.discovery.metadata"
|
|
233
|
+
);
|
|
168
234
|
return {
|
|
169
235
|
resolveIssuer: async () => configuredIssuer,
|
|
170
|
-
resolveJwksUri: async () =>
|
|
236
|
+
resolveJwksUri: async () => {
|
|
237
|
+
if (auth.jwksUri) {
|
|
238
|
+
log.debug("oauth.jwks.resolved", { jwksUri: auth.jwksUri, source: "configured" });
|
|
239
|
+
return auth.jwksUri;
|
|
240
|
+
}
|
|
241
|
+
const jwksUri = (await oauthServerMetadata()).jwks_uri;
|
|
242
|
+
log.debug("oauth.jwks.resolved", { jwksUri, source: "discovered" });
|
|
243
|
+
return jwksUri;
|
|
244
|
+
}
|
|
171
245
|
};
|
|
172
246
|
}
|
|
173
247
|
|
|
@@ -219,6 +293,7 @@ function stringClaim(claims, name) {
|
|
|
219
293
|
// src/auth/verifier.ts
|
|
220
294
|
var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
|
|
221
295
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
296
|
+
var JWKS_FETCH_TIMEOUT_MS = 5e3;
|
|
222
297
|
var OAuthTokenError = class extends Error {
|
|
223
298
|
constructor(status, oauthError, message) {
|
|
224
299
|
super(message);
|
|
@@ -230,12 +305,25 @@ var OAuthTokenError = class extends Error {
|
|
|
230
305
|
function resolveAcceptedAudiences(auth, resource) {
|
|
231
306
|
return auth.acceptedAudiences ?? [resource];
|
|
232
307
|
}
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
308
|
+
function tokenHeaderFields(token) {
|
|
309
|
+
try {
|
|
310
|
+
const header = (0, import_jose.decodeProtectedHeader)(token);
|
|
311
|
+
return { jwtAlg: header.alg, jwtKid: header.kid, tokenLength: token.length };
|
|
312
|
+
} catch {
|
|
313
|
+
return { tokenLength: token.length };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function fetchVerificationKeySet(jwksUri) {
|
|
317
|
+
try {
|
|
318
|
+
const response = await fetch(jwksUri, { signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS), redirect: "manual" });
|
|
319
|
+
if (!response.ok)
|
|
320
|
+
throw new Error(`JWKS endpoint returned ${response.status}`);
|
|
321
|
+
const json = await response.json();
|
|
322
|
+
return (0, import_jose.createLocalJWKSet)(json);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
325
|
+
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
326
|
+
}
|
|
239
327
|
}
|
|
240
328
|
async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
241
329
|
try {
|
|
@@ -250,20 +338,20 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
|
250
338
|
});
|
|
251
339
|
return payload;
|
|
252
340
|
} catch (err) {
|
|
253
|
-
|
|
254
|
-
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
255
|
-
}
|
|
341
|
+
log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
|
|
256
342
|
throw err;
|
|
257
343
|
}
|
|
258
344
|
}
|
|
259
345
|
function assertNonEmptySubject(claims) {
|
|
260
346
|
const sub = claims["sub"];
|
|
261
347
|
if (typeof sub !== "string" || sub.trim() === "") {
|
|
348
|
+
log.debug("oauth.verify.bad_subject", { subType: typeof sub });
|
|
262
349
|
throw new OAuthTokenError(401, "invalid_token", "token subject claim must be a non-empty string");
|
|
263
350
|
}
|
|
264
351
|
}
|
|
265
352
|
function assertOAuthClientClaim(auth, clientId) {
|
|
266
353
|
if (auth.requireOAuthClientClaim !== false && !clientId) {
|
|
354
|
+
log.debug("oauth.verify.missing_client_claim", { outcome: "401 invalid_token" });
|
|
267
355
|
throw new OAuthTokenError(401, "invalid_token", "OAuth client claim is required");
|
|
268
356
|
}
|
|
269
357
|
}
|
|
@@ -288,17 +376,23 @@ function buildMcpAuthContext(args) {
|
|
|
288
376
|
};
|
|
289
377
|
}
|
|
290
378
|
function createOAuthTokenVerifier(auth, discovery) {
|
|
291
|
-
const loadRemoteJwksKeySet = cachedPromise(
|
|
292
|
-
() => discovery.resolveJwksUri().then((jwksURI) => (0, import_jose.createRemoteJWKSet)(new URL(jwksURI)))
|
|
293
|
-
);
|
|
294
379
|
return async (token, request, options) => {
|
|
295
380
|
const resource = resolveProtectedResource(auth, request, options);
|
|
296
381
|
const issuer = await discovery.resolveIssuer();
|
|
297
382
|
const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
|
|
298
|
-
|
|
383
|
+
log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
|
|
384
|
+
const jwksUri = await discovery.resolveJwksUri();
|
|
385
|
+
log.debug("oauth.jwks.fetch", { jwksUri });
|
|
386
|
+
const keySet = await fetchVerificationKeySet(jwksUri);
|
|
387
|
+
const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
|
|
299
388
|
assertNonEmptySubject(claims);
|
|
300
389
|
const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
|
|
301
390
|
assertOAuthClientClaim(auth, context.principal.clientId);
|
|
391
|
+
log.info("oauth.verify.ok", {
|
|
392
|
+
sub: context.principal.sub,
|
|
393
|
+
clientId: context.principal.clientId,
|
|
394
|
+
scopes: context.principal.scopes
|
|
395
|
+
});
|
|
302
396
|
return context;
|
|
303
397
|
};
|
|
304
398
|
}
|
|
@@ -398,22 +492,27 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
398
492
|
if (runtime.kind === "unconfigured")
|
|
399
493
|
return { ok: true };
|
|
400
494
|
const token = parseBearerToken(request);
|
|
401
|
-
if (!token)
|
|
495
|
+
if (!token) {
|
|
496
|
+
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
402
497
|
return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
|
|
498
|
+
}
|
|
403
499
|
try {
|
|
404
500
|
const auth = await runtime.verify(token, request, runtime.options);
|
|
405
501
|
assertRequiredScopes(runtime.auth, auth);
|
|
406
502
|
return { ok: true, auth };
|
|
407
503
|
} catch (err) {
|
|
408
504
|
if (err instanceof OAuthConfigurationError) {
|
|
505
|
+
log.error("auth.config_error", { ...describeError(err), outcome: "500" });
|
|
409
506
|
return { ok: false, response: oauthConfigurationErrorResponse() };
|
|
410
507
|
}
|
|
411
508
|
if (err instanceof OAuthTokenError) {
|
|
509
|
+
log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
|
|
412
510
|
return {
|
|
413
511
|
ok: false,
|
|
414
512
|
response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
|
|
415
513
|
};
|
|
416
514
|
}
|
|
515
|
+
log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
|
|
417
516
|
return {
|
|
418
517
|
ok: false,
|
|
419
518
|
response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
|
|
@@ -531,7 +630,8 @@ function createMcpProtocolHandler(mcp, options = {}) {
|
|
|
531
630
|
});
|
|
532
631
|
await server.connect(transport);
|
|
533
632
|
return await transport.handleRequest(request);
|
|
534
|
-
} catch {
|
|
633
|
+
} catch (err) {
|
|
634
|
+
log.error("mcp.transport_error", { ...describeError(err), outcome: "500 internal error" });
|
|
535
635
|
return Response.json(
|
|
536
636
|
{ jsonrpc: "2.0", id: null, error: { code: -32603, message: "internal error" } },
|
|
537
637
|
{ status: 500 }
|
|
@@ -598,7 +698,8 @@ function createOAuthProtectedResourceMetadataHandler(mcp, options = {}) {
|
|
|
598
698
|
);
|
|
599
699
|
const response = withCors(Response.json(metadata, { headers }));
|
|
600
700
|
return request.method === "HEAD" ? headResponse(response) : response;
|
|
601
|
-
} catch {
|
|
701
|
+
} catch (err) {
|
|
702
|
+
log.error("oauth.metadata.config_error", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
602
703
|
const response = withCors(oauthConfigurationErrorResponse());
|
|
603
704
|
return request.method === "HEAD" ? headResponse(response) : response;
|
|
604
705
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpProtocolHandler
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-3XPKR4G6.js";
|
|
4
4
|
import {
|
|
5
5
|
createOAuthProtectedResourceMetadataHandler
|
|
6
|
-
} from "../../chunk-
|
|
6
|
+
} from "../../chunk-W7F6JRDB.js";
|
|
7
7
|
import {
|
|
8
8
|
createInvokeToolHandler,
|
|
9
9
|
createListToolsHandler
|
|
10
|
-
} from "../../chunk-
|
|
10
|
+
} from "../../chunk-LMT6RXUF.js";
|
|
11
11
|
import "../../chunk-MA5H6PSF.js";
|
|
12
|
-
import "../../chunk-
|
|
13
|
-
import "../../chunk-
|
|
12
|
+
import "../../chunk-HRMLGCXV.js";
|
|
13
|
+
import "../../chunk-QC3DXQTH.js";
|
|
14
14
|
import "../../chunk-6DXGZZA4.js";
|
|
15
15
|
|
|
16
16
|
// src/stacks/tanstack/handlers.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovable.dev/mcp-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Author MCP servers for Lovable apps. Declare tools with defineTool, register them in defineMcp, and the framework adapter (TanStack today, Supabase Edge Functions next) emits the route(s) at build time.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/dist/chunk-QA3FWDUV.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// src/core/url.ts
|
|
2
|
-
function trimTrailingSlash(value) {
|
|
3
|
-
return value.replace(/\/+$/, "");
|
|
4
|
-
}
|
|
5
|
-
function isLocalHTTPHost(hostname) {
|
|
6
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
7
|
-
}
|
|
8
|
-
function urlSafetyProblem(url) {
|
|
9
|
-
const isAllowedHTTP = url.protocol === "http:" && isLocalHTTPHost(url.hostname);
|
|
10
|
-
if (url.protocol !== "https:" && !isAllowedHTTP) {
|
|
11
|
-
return "must use https://, except localhost development URLs";
|
|
12
|
-
}
|
|
13
|
-
if (url.username || url.password) {
|
|
14
|
-
return "must not include credentials";
|
|
15
|
-
}
|
|
16
|
-
if (url.search || url.hash) {
|
|
17
|
-
return "must not include query or fragment";
|
|
18
|
-
}
|
|
19
|
-
return void 0;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// src/core/validation.ts
|
|
23
|
-
function parseSafeUrl(subject, raw, ErrorClass = Error) {
|
|
24
|
-
let url;
|
|
25
|
-
try {
|
|
26
|
-
url = new URL(raw);
|
|
27
|
-
} catch {
|
|
28
|
-
throw new ErrorClass(`${subject} must be an absolute URL`);
|
|
29
|
-
}
|
|
30
|
-
const problem = urlSafetyProblem(url);
|
|
31
|
-
if (problem) {
|
|
32
|
-
throw new ErrorClass(`${subject} ${problem}`);
|
|
33
|
-
}
|
|
34
|
-
return url;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export {
|
|
38
|
-
trimTrailingSlash,
|
|
39
|
-
parseSafeUrl
|
|
40
|
-
};
|