@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.
@@ -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
- const loaded = await load();
54
- settled = true;
55
- value = loaded;
56
- return loaded;
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
- const response = await fetch(url, { signal: AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS), redirect: "error" });
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(() => fetchIssuerOAuthServerMetadata(configuredIssuer));
230
+ const oauthServerMetadata = cachedPromise(
231
+ () => fetchIssuerOAuthServerMetadata(configuredIssuer),
232
+ "oauth.discovery.metadata"
233
+ );
168
234
  return {
169
235
  resolveIssuer: async () => configuredIssuer,
170
- resolveJwksUri: async () => auth.jwksUri ?? (await oauthServerMetadata()).jwks_uri
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
 
@@ -230,6 +304,14 @@ var OAuthTokenError = class extends Error {
230
304
  function resolveAcceptedAudiences(auth, resource) {
231
305
  return auth.acceptedAudiences ?? [resource];
232
306
  }
307
+ function tokenHeaderFields(token) {
308
+ try {
309
+ const header = (0, import_jose.decodeProtectedHeader)(token);
310
+ return { jwtAlg: header.alg, jwtKid: header.kid, tokenLength: token.length };
311
+ } catch {
312
+ return { tokenLength: token.length };
313
+ }
314
+ }
233
315
  function isJwksFetchFailure(err) {
234
316
  if (err instanceof import_jose.errors.JWKSTimeout || err instanceof import_jose.errors.JWKSInvalid)
235
317
  return true;
@@ -251,19 +333,23 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
251
333
  return payload;
252
334
  } catch (err) {
253
335
  if (isJwksFetchFailure(err)) {
336
+ log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
254
337
  throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
255
338
  }
339
+ log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
256
340
  throw err;
257
341
  }
258
342
  }
259
343
  function assertNonEmptySubject(claims) {
260
344
  const sub = claims["sub"];
261
345
  if (typeof sub !== "string" || sub.trim() === "") {
346
+ log.debug("oauth.verify.bad_subject", { subType: typeof sub });
262
347
  throw new OAuthTokenError(401, "invalid_token", "token subject claim must be a non-empty string");
263
348
  }
264
349
  }
265
350
  function assertOAuthClientClaim(auth, clientId) {
266
351
  if (auth.requireOAuthClientClaim !== false && !clientId) {
352
+ log.debug("oauth.verify.missing_client_claim", { outcome: "401 invalid_token" });
267
353
  throw new OAuthTokenError(401, "invalid_token", "OAuth client claim is required");
268
354
  }
269
355
  }
@@ -288,17 +374,23 @@ function buildMcpAuthContext(args) {
288
374
  };
289
375
  }
290
376
  function createOAuthTokenVerifier(auth, discovery) {
291
- const loadRemoteJwksKeySet = cachedPromise(
292
- () => discovery.resolveJwksUri().then((jwksURI) => (0, import_jose.createRemoteJWKSet)(new URL(jwksURI)))
293
- );
294
377
  return async (token, request, options) => {
295
378
  const resource = resolveProtectedResource(auth, request, options);
296
379
  const issuer = await discovery.resolveIssuer();
297
380
  const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
298
- const claims = await verifyJwtClaims(token, await loadRemoteJwksKeySet(), issuer, acceptedAudiences, auth);
381
+ log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
382
+ const jwksUri = await discovery.resolveJwksUri();
383
+ log.debug("oauth.jwks.keyset_created", { jwksUri });
384
+ const keySet = (0, import_jose.createRemoteJWKSet)(new URL(jwksUri));
385
+ const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
299
386
  assertNonEmptySubject(claims);
300
387
  const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
301
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
+ });
302
394
  return context;
303
395
  };
304
396
  }
@@ -398,22 +490,27 @@ function createRequestAuthorizer(mcp, options = {}) {
398
490
  if (runtime.kind === "unconfigured")
399
491
  return { ok: true };
400
492
  const token = parseBearerToken(request);
401
- if (!token)
493
+ if (!token) {
494
+ log.info("auth.no_bearer_token", { outcome: "401" });
402
495
  return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
496
+ }
403
497
  try {
404
498
  const auth = await runtime.verify(token, request, runtime.options);
405
499
  assertRequiredScopes(runtime.auth, auth);
406
500
  return { ok: true, auth };
407
501
  } catch (err) {
408
502
  if (err instanceof OAuthConfigurationError) {
503
+ log.error("auth.config_error", { ...describeError(err), outcome: "500" });
409
504
  return { ok: false, response: oauthConfigurationErrorResponse() };
410
505
  }
411
506
  if (err instanceof OAuthTokenError) {
507
+ log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
412
508
  return {
413
509
  ok: false,
414
510
  response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
415
511
  };
416
512
  }
513
+ log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
417
514
  return {
418
515
  ok: false,
419
516
  response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
@@ -466,6 +563,26 @@ var ToolContext = class {
466
563
  }
467
564
  };
468
565
 
566
+ // src/core/cors.ts
567
+ var EXPOSE_HEADERS = "WWW-Authenticate, Mcp-Session-Id, Mcp-Protocol-Version";
568
+ var ALLOW_HEADERS = "Authorization, Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID";
569
+ function withCors(response) {
570
+ response.headers.set("Access-Control-Allow-Origin", "*");
571
+ response.headers.set("Access-Control-Expose-Headers", EXPOSE_HEADERS);
572
+ return response;
573
+ }
574
+ function corsPreflightResponse(allowMethods) {
575
+ return new Response(null, {
576
+ status: 204,
577
+ headers: {
578
+ "Access-Control-Allow-Origin": "*",
579
+ "Access-Control-Allow-Methods": allowMethods,
580
+ "Access-Control-Allow-Headers": ALLOW_HEADERS,
581
+ "Access-Control-Max-Age": "86400"
582
+ }
583
+ });
584
+ }
585
+
469
586
  // src/protocols/mcp/protocol.ts
470
587
  function adaptToolToSdkCallback(tool, auth) {
471
588
  return async (first) => {
@@ -484,7 +601,7 @@ function adaptToolToSdkCallback(tool, auth) {
484
601
  }
485
602
  function createMcpProtocolHandler(mcp, options = {}) {
486
603
  const authorizer = createRequestAuthorizer(mcp, options);
487
- return async (request) => {
604
+ const handle = async (request) => {
488
605
  const authResult = await authorizer.authorize(request);
489
606
  if (!authResult.ok)
490
607
  return authResult.response;
@@ -511,28 +628,31 @@ function createMcpProtocolHandler(mcp, options = {}) {
511
628
  });
512
629
  await server.connect(transport);
513
630
  return await transport.handleRequest(request);
514
- } catch {
631
+ } catch (err) {
632
+ log.error("mcp.transport_error", { ...describeError(err), outcome: "500 internal error" });
515
633
  return Response.json(
516
634
  { jsonrpc: "2.0", id: null, error: { code: -32603, message: "internal error" } },
517
635
  { status: 500 }
518
636
  );
519
637
  }
520
638
  };
639
+ return async (request) => {
640
+ if (request.method === "OPTIONS")
641
+ return corsPreflightResponse("GET, POST, DELETE, OPTIONS");
642
+ return withCors(await handle(request));
643
+ };
521
644
  }
522
645
 
523
646
  // src/protocols/oauth-metadata.ts
524
- var CORS_ORIGIN = { "Access-Control-Allow-Origin": "*" };
525
- function withCors(response) {
526
- response.headers.set("Access-Control-Allow-Origin", "*");
527
- return response;
528
- }
529
647
  function notFound() {
530
- return new Response(JSON.stringify({ error: "not found" }), {
531
- status: 404,
532
- // `no-store` so a 404 (OAuth unconfigured) isn't heuristically cached and
533
- // then served past a later deploy that enables OAuth and starts returning 200.
534
- headers: { ...JSON_HEADERS, ...CORS_ORIGIN, "Cache-Control": "no-store" }
535
- });
648
+ return withCors(
649
+ new Response(JSON.stringify({ error: "not found" }), {
650
+ status: 404,
651
+ // `no-store` so a 404 (OAuth unconfigured) isn't heuristically cached and
652
+ // then served past a later deploy that enables OAuth and starts returning 200.
653
+ headers: { ...JSON_HEADERS, "Cache-Control": "no-store" }
654
+ })
655
+ );
536
656
  }
537
657
  async function buildProtectedResourceMetadata(mcp, auth, request, options, discovery) {
538
658
  const issuer = await discovery.resolveIssuer();
@@ -557,17 +677,12 @@ function createOAuthProtectedResourceMetadataHandler(mcp, options = {}) {
557
677
  return async (request) => {
558
678
  if (runtime.kind !== "configured" || runtime.auth.protectedResourceMetadataUrl !== void 0)
559
679
  return notFound();
560
- if (request.method === "OPTIONS") {
561
- return new Response(null, {
562
- status: 204,
563
- headers: { ...CORS_ORIGIN, "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS" }
564
- });
565
- }
680
+ if (request.method === "OPTIONS")
681
+ return corsPreflightResponse("GET, HEAD, OPTIONS");
566
682
  if (request.method !== "GET" && request.method !== "HEAD")
567
683
  return withCors(methodNotAllowed("GET, HEAD, OPTIONS"));
568
684
  const headers = {
569
685
  ...JSON_HEADERS,
570
- ...CORS_ORIGIN,
571
686
  "Cache-Control": "public, max-age=300",
572
687
  Vary: "Host"
573
688
  };
@@ -579,9 +694,10 @@ function createOAuthProtectedResourceMetadataHandler(mcp, options = {}) {
579
694
  runtime.options,
580
695
  runtime.discovery
581
696
  );
582
- const response = Response.json(metadata, { headers });
697
+ const response = withCors(Response.json(metadata, { headers }));
583
698
  return request.method === "HEAD" ? headResponse(response) : response;
584
- } catch {
699
+ } catch (err) {
700
+ log.error("oauth.metadata.config_error", { ...describeError(err), outcome: "500 oauth configuration error" });
585
701
  const response = withCors(oauthConfigurationErrorResponse());
586
702
  return request.method === "HEAD" ? headResponse(response) : response;
587
703
  }
@@ -603,12 +719,12 @@ function shapeToJsonSchema(shape) {
603
719
  function createListToolsHandler(mcp, options = {}) {
604
720
  assertRestResourceBinding(mcp, options);
605
721
  const authorizer = createRequestAuthorizer(mcp, options);
606
- return async (request) => {
722
+ const handle = async (request) => {
607
723
  const authResult = await authorizer.authorize(request);
608
724
  if (!authResult.ok)
609
725
  return authResult.response;
610
726
  if (request.method !== "GET" && request.method !== "HEAD")
611
- return methodNotAllowed("GET, HEAD");
727
+ return methodNotAllowed("GET, HEAD, OPTIONS");
612
728
  const body = {
613
729
  server: { name: mcp.name, version: mcp.version, title: mcp.title },
614
730
  tools: mcp.tools.map((tool) => ({
@@ -623,6 +739,11 @@ function createListToolsHandler(mcp, options = {}) {
623
739
  const response = Response.json(body);
624
740
  return request.method === "HEAD" ? headResponse(response) : response;
625
741
  };
742
+ return async (request) => {
743
+ if (request.method === "OPTIONS")
744
+ return corsPreflightResponse("GET, HEAD, OPTIONS");
745
+ return withCors(await handle(request));
746
+ };
626
747
  }
627
748
 
628
749
  // src/protocols/rest/invoke-tool.ts
@@ -642,12 +763,12 @@ function isEmptyArgs(value) {
642
763
  function createInvokeToolHandler(mcp, options = {}) {
643
764
  assertRestResourceBinding(mcp, options);
644
765
  const authorizer = createRequestAuthorizer(mcp, options);
645
- return async (request, toolName) => {
766
+ const handle = async (request, toolName) => {
646
767
  const authResult = await authorizer.authorize(request);
647
768
  if (!authResult.ok)
648
769
  return authResult.response;
649
770
  if (request.method !== "POST")
650
- return methodNotAllowed("POST");
771
+ return methodNotAllowed("POST, OPTIONS");
651
772
  const tool = mcp.tools.find((t) => t.name === toolName);
652
773
  if (!tool) {
653
774
  return new Response(JSON.stringify({ error: `unknown tool: ${safeReflectName(toolName)}` }), {
@@ -715,6 +836,11 @@ function createInvokeToolHandler(mcp, options = {}) {
715
836
  isError: result.isError
716
837
  });
717
838
  };
839
+ return async (request, toolName) => {
840
+ if (request.method === "OPTIONS")
841
+ return corsPreflightResponse("POST, OPTIONS");
842
+ return withCors(await handle(request, toolName));
843
+ };
718
844
  }
719
845
 
720
846
  // src/stacks/tanstack/handlers.ts
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  createMcpProtocolHandler
3
- } from "../../chunk-GLG5RZGE.js";
3
+ } from "../../chunk-G5GWSV5D.js";
4
4
  import {
5
5
  createOAuthProtectedResourceMetadataHandler
6
- } from "../../chunk-DDF63QWG.js";
6
+ } from "../../chunk-722HKLIU.js";
7
7
  import {
8
8
  createInvokeToolHandler,
9
9
  createListToolsHandler
10
- } from "../../chunk-VD6CS7Y6.js";
10
+ } from "../../chunk-53M2X7FU.js";
11
11
  import "../../chunk-MA5H6PSF.js";
12
- import "../../chunk-XEDRJFAR.js";
13
- import "../../chunk-QA3FWDUV.js";
12
+ import "../../chunk-NTXHOUK6.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.5.1",
3
+ "version": "0.7.0",
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": {
@@ -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
- };