@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
|
@@ -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
|
|
|
@@ -228,6 +302,14 @@ var OAuthTokenError = class extends Error {
|
|
|
228
302
|
function resolveAcceptedAudiences(auth, resource) {
|
|
229
303
|
return auth.acceptedAudiences ?? [resource];
|
|
230
304
|
}
|
|
305
|
+
function tokenHeaderFields(token) {
|
|
306
|
+
try {
|
|
307
|
+
const header = (0, import_jose.decodeProtectedHeader)(token);
|
|
308
|
+
return { jwtAlg: header.alg, jwtKid: header.kid, tokenLength: token.length };
|
|
309
|
+
} catch {
|
|
310
|
+
return { tokenLength: token.length };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
231
313
|
function isJwksFetchFailure(err) {
|
|
232
314
|
if (err instanceof import_jose.errors.JWKSTimeout || err instanceof import_jose.errors.JWKSInvalid)
|
|
233
315
|
return true;
|
|
@@ -249,19 +331,23 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
|
249
331
|
return payload;
|
|
250
332
|
} catch (err) {
|
|
251
333
|
if (isJwksFetchFailure(err)) {
|
|
334
|
+
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
252
335
|
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
253
336
|
}
|
|
337
|
+
log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
|
|
254
338
|
throw err;
|
|
255
339
|
}
|
|
256
340
|
}
|
|
257
341
|
function assertNonEmptySubject(claims) {
|
|
258
342
|
const sub = claims["sub"];
|
|
259
343
|
if (typeof sub !== "string" || sub.trim() === "") {
|
|
344
|
+
log.debug("oauth.verify.bad_subject", { subType: typeof sub });
|
|
260
345
|
throw new OAuthTokenError(401, "invalid_token", "token subject claim must be a non-empty string");
|
|
261
346
|
}
|
|
262
347
|
}
|
|
263
348
|
function assertOAuthClientClaim(auth, clientId) {
|
|
264
349
|
if (auth.requireOAuthClientClaim !== false && !clientId) {
|
|
350
|
+
log.debug("oauth.verify.missing_client_claim", { outcome: "401 invalid_token" });
|
|
265
351
|
throw new OAuthTokenError(401, "invalid_token", "OAuth client claim is required");
|
|
266
352
|
}
|
|
267
353
|
}
|
|
@@ -286,17 +372,23 @@ function buildMcpAuthContext(args) {
|
|
|
286
372
|
};
|
|
287
373
|
}
|
|
288
374
|
function createOAuthTokenVerifier(auth, discovery) {
|
|
289
|
-
const loadRemoteJwksKeySet = cachedPromise(
|
|
290
|
-
() => discovery.resolveJwksUri().then((jwksURI) => (0, import_jose.createRemoteJWKSet)(new URL(jwksURI)))
|
|
291
|
-
);
|
|
292
375
|
return async (token, request, options) => {
|
|
293
376
|
const resource = resolveProtectedResource(auth, request, options);
|
|
294
377
|
const issuer = await discovery.resolveIssuer();
|
|
295
378
|
const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
|
|
296
|
-
|
|
379
|
+
log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
|
|
380
|
+
const jwksUri = await discovery.resolveJwksUri();
|
|
381
|
+
log.debug("oauth.jwks.keyset_created", { jwksUri });
|
|
382
|
+
const keySet = (0, import_jose.createRemoteJWKSet)(new URL(jwksUri));
|
|
383
|
+
const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
|
|
297
384
|
assertNonEmptySubject(claims);
|
|
298
385
|
const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
|
|
299
386
|
assertOAuthClientClaim(auth, context.principal.clientId);
|
|
387
|
+
log.info("oauth.verify.ok", {
|
|
388
|
+
sub: context.principal.sub,
|
|
389
|
+
clientId: context.principal.clientId,
|
|
390
|
+
scopes: context.principal.scopes
|
|
391
|
+
});
|
|
300
392
|
return context;
|
|
301
393
|
};
|
|
302
394
|
}
|
|
@@ -396,22 +488,27 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
396
488
|
if (runtime.kind === "unconfigured")
|
|
397
489
|
return { ok: true };
|
|
398
490
|
const token = parseBearerToken(request);
|
|
399
|
-
if (!token)
|
|
491
|
+
if (!token) {
|
|
492
|
+
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
400
493
|
return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
|
|
494
|
+
}
|
|
401
495
|
try {
|
|
402
496
|
const auth = await runtime.verify(token, request, runtime.options);
|
|
403
497
|
assertRequiredScopes(runtime.auth, auth);
|
|
404
498
|
return { ok: true, auth };
|
|
405
499
|
} catch (err) {
|
|
406
500
|
if (err instanceof OAuthConfigurationError) {
|
|
501
|
+
log.error("auth.config_error", { ...describeError(err), outcome: "500" });
|
|
407
502
|
return { ok: false, response: oauthConfigurationErrorResponse() };
|
|
408
503
|
}
|
|
409
504
|
if (err instanceof OAuthTokenError) {
|
|
505
|
+
log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
|
|
410
506
|
return {
|
|
411
507
|
ok: false,
|
|
412
508
|
response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
|
|
413
509
|
};
|
|
414
510
|
}
|
|
511
|
+
log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
|
|
415
512
|
return {
|
|
416
513
|
ok: false,
|
|
417
514
|
response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
|
|
@@ -421,6 +518,26 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
421
518
|
};
|
|
422
519
|
}
|
|
423
520
|
|
|
521
|
+
// src/core/cors.ts
|
|
522
|
+
var EXPOSE_HEADERS = "WWW-Authenticate, Mcp-Session-Id, Mcp-Protocol-Version";
|
|
523
|
+
var ALLOW_HEADERS = "Authorization, Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID";
|
|
524
|
+
function withCors(response) {
|
|
525
|
+
response.headers.set("Access-Control-Allow-Origin", "*");
|
|
526
|
+
response.headers.set("Access-Control-Expose-Headers", EXPOSE_HEADERS);
|
|
527
|
+
return response;
|
|
528
|
+
}
|
|
529
|
+
function corsPreflightResponse(allowMethods) {
|
|
530
|
+
return new Response(null, {
|
|
531
|
+
status: 204,
|
|
532
|
+
headers: {
|
|
533
|
+
"Access-Control-Allow-Origin": "*",
|
|
534
|
+
"Access-Control-Allow-Methods": allowMethods,
|
|
535
|
+
"Access-Control-Allow-Headers": ALLOW_HEADERS,
|
|
536
|
+
"Access-Control-Max-Age": "86400"
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
424
541
|
// src/protocols/rest/list-tools.ts
|
|
425
542
|
function shapeToJsonSchema(shape) {
|
|
426
543
|
if (!shape)
|
|
@@ -434,12 +551,12 @@ function shapeToJsonSchema(shape) {
|
|
|
434
551
|
function createListToolsHandler(mcp, options = {}) {
|
|
435
552
|
assertRestResourceBinding(mcp, options);
|
|
436
553
|
const authorizer = createRequestAuthorizer(mcp, options);
|
|
437
|
-
|
|
554
|
+
const handle = async (request) => {
|
|
438
555
|
const authResult = await authorizer.authorize(request);
|
|
439
556
|
if (!authResult.ok)
|
|
440
557
|
return authResult.response;
|
|
441
558
|
if (request.method !== "GET" && request.method !== "HEAD")
|
|
442
|
-
return methodNotAllowed("GET, HEAD");
|
|
559
|
+
return methodNotAllowed("GET, HEAD, OPTIONS");
|
|
443
560
|
const body = {
|
|
444
561
|
server: { name: mcp.name, version: mcp.version, title: mcp.title },
|
|
445
562
|
tools: mcp.tools.map((tool) => ({
|
|
@@ -454,6 +571,11 @@ function createListToolsHandler(mcp, options = {}) {
|
|
|
454
571
|
const response = Response.json(body);
|
|
455
572
|
return request.method === "HEAD" ? headResponse(response) : response;
|
|
456
573
|
};
|
|
574
|
+
return async (request) => {
|
|
575
|
+
if (request.method === "OPTIONS")
|
|
576
|
+
return corsPreflightResponse("GET, HEAD, OPTIONS");
|
|
577
|
+
return withCors(await handle(request));
|
|
578
|
+
};
|
|
457
579
|
}
|
|
458
580
|
|
|
459
581
|
// src/protocols/rest/invoke-tool.ts
|
|
@@ -518,12 +640,12 @@ function isEmptyArgs(value) {
|
|
|
518
640
|
function createInvokeToolHandler(mcp, options = {}) {
|
|
519
641
|
assertRestResourceBinding(mcp, options);
|
|
520
642
|
const authorizer = createRequestAuthorizer(mcp, options);
|
|
521
|
-
|
|
643
|
+
const handle = async (request, toolName) => {
|
|
522
644
|
const authResult = await authorizer.authorize(request);
|
|
523
645
|
if (!authResult.ok)
|
|
524
646
|
return authResult.response;
|
|
525
647
|
if (request.method !== "POST")
|
|
526
|
-
return methodNotAllowed("POST");
|
|
648
|
+
return methodNotAllowed("POST, OPTIONS");
|
|
527
649
|
const tool = mcp.tools.find((t) => t.name === toolName);
|
|
528
650
|
if (!tool) {
|
|
529
651
|
return new Response(JSON.stringify({ error: `unknown tool: ${safeReflectName(toolName)}` }), {
|
|
@@ -591,6 +713,11 @@ function createInvokeToolHandler(mcp, options = {}) {
|
|
|
591
713
|
isError: result.isError
|
|
592
714
|
});
|
|
593
715
|
};
|
|
716
|
+
return async (request, toolName) => {
|
|
717
|
+
if (request.method === "OPTIONS")
|
|
718
|
+
return corsPreflightResponse("POST, OPTIONS");
|
|
719
|
+
return withCors(await handle(request, toolName));
|
|
720
|
+
};
|
|
594
721
|
}
|
|
595
722
|
// Annotate the CommonJS export names for ESM import in node:
|
|
596
723
|
0 && (module.exports = {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createInvokeToolHandler,
|
|
3
3
|
createListToolsHandler
|
|
4
|
-
} from "../../chunk-
|
|
4
|
+
} from "../../chunk-53M2X7FU.js";
|
|
5
5
|
import "../../chunk-MA5H6PSF.js";
|
|
6
|
-
import "../../chunk-
|
|
7
|
-
import "../../chunk-
|
|
6
|
+
import "../../chunk-NTXHOUK6.js";
|
|
7
|
+
import "../../chunk-QC3DXQTH.js";
|
|
8
8
|
import "../../chunk-6DXGZZA4.js";
|
|
9
9
|
export {
|
|
10
10
|
createInvokeToolHandler,
|