@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 CHANGED
@@ -182,6 +182,27 @@ The request-derived `resource` default trusts the incoming `Host` to name this s
182
182
 
183
183
  Use `auth.oauth.issuer(...)`, set `resource` or `acceptedAudiences` to anchor the accepted audience, and optionally set `jwksUri` and `requiredScopes`. For Supabase project auth, set `acceptedAudiences: "authenticated"`, keep app/business checks in app code, and forward `ctx.getToken()` to Supabase for RLS-backed data access.
184
184
 
185
+ ## Debug logging
186
+
187
+ The runtime is silent by default. Raise the log level to trace OAuth discovery, JWKS resolution, and token verification when a deployed MCP server returns `401`/`500` and you need to see why. Logs go to `console` (so they land in your platform's function logs) and **never include the bearer token, full claims, or PII** — only non-secret fields such as the JWT header `alg`/`kid`, `issuer`, `jwks_uri`, the token `sub`/`client_id`/`scopes`, and `jose` error codes.
188
+
189
+ Enable it either way:
190
+
191
+ ```ts
192
+ // Programmatic — works on every runtime, including Workers that pass env via bindings.
193
+ import { setLogLevel } from "@lovable.dev/mcp-js";
194
+ setLogLevel("debug"); // "silent" | "error" | "warn" | "info" | "debug"
195
+ ```
196
+
197
+ ```bash
198
+ # Or via env var, read once at startup. Node/Deno only: it reads `process.env`,
199
+ # which is absent on a deployed Cloudflare Worker (env arrives via bindings), so
200
+ # the var is silently ignored there — use `setLogLevel()` on Workers.
201
+ LOVABLE_MCP_LOG_LEVEL=debug
202
+ ```
203
+
204
+ A `500` on an OAuth-protected route is always one of two causes, logged at `error`: an `OAuthConfigurationError` from issuer-metadata discovery or a JWKS *fetch* failure (`oauth.discovery.config_error` / `oauth.jwks.fetch_failed` → `auth.config_error`), or a transport-level fault in the MCP handler (`mcp.transport_error`). Auth outcomes are logged at `info`: a granted request as `oauth.verify.ok`, and a `401`/`403` as `auth.token_rejected` (with the `jose` reason) or `auth.no_bearer_token` — so a request rejected for insufficient scope shows both `oauth.verify.ok` and the `auth.token_rejected` that follows it.
205
+
185
206
  ## Subpath exports
186
207
 
187
208
  | Subpath | Contents |
@@ -5,7 +5,11 @@ import {
5
5
  corsPreflightResponse,
6
6
  createRequestAuthorizer,
7
7
  withCors
8
- } from "./chunk-ZKKLOL2C.js";
8
+ } from "./chunk-HRMLGCXV.js";
9
+ import {
10
+ describeError,
11
+ log
12
+ } from "./chunk-QC3DXQTH.js";
9
13
 
10
14
  // src/protocols/mcp/protocol.ts
11
15
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -54,7 +58,8 @@ function createMcpProtocolHandler(mcp, options = {}) {
54
58
  });
55
59
  await server.connect(transport);
56
60
  return await transport.handleRequest(request);
57
- } catch {
61
+ } catch (err) {
62
+ log.error("mcp.transport_error", { ...describeError(err), outcome: "500 internal error" });
58
63
  return Response.json(
59
64
  { jsonrpc: "2.0", id: null, error: { code: -32603, message: "internal error" } },
60
65
  { status: 500 }
@@ -1,7 +1,9 @@
1
1
  import {
2
+ describeError,
3
+ log,
2
4
  parseSafeUrl,
3
5
  trimTrailingSlash
4
- } from "./chunk-QA3FWDUV.js";
6
+ } from "./chunk-QC3DXQTH.js";
5
7
  import {
6
8
  OAUTH_PROTECTED_RESOURCE_METADATA_PATH
7
9
  } from "./chunk-6DXGZZA4.js";
@@ -43,16 +45,29 @@ function resolveResourcePath(resourcePath, request) {
43
45
  }
44
46
 
45
47
  // src/core/promise.ts
46
- function cachedPromise(load) {
48
+ function cachedPromise(load, label) {
47
49
  let settled = false;
48
50
  let value;
49
51
  return async () => {
50
- if (settled)
52
+ if (settled) {
53
+ if (label)
54
+ log.debug(`${label}.cache_hit`);
51
55
  return value;
52
- const loaded = await load();
53
- settled = true;
54
- value = loaded;
55
- return loaded;
56
+ }
57
+ if (label)
58
+ log.debug(`${label}.load_start`);
59
+ try {
60
+ const loaded = await load();
61
+ settled = true;
62
+ value = loaded;
63
+ if (label)
64
+ log.debug(`${label}.settled`);
65
+ return loaded;
66
+ } catch (err) {
67
+ if (label)
68
+ log.debug(`${label}.load_failed`, describeError(err));
69
+ throw err;
70
+ }
56
71
  };
57
72
  }
58
73
 
@@ -92,13 +107,16 @@ async function fetchFirstValidOAuthServerMetadata(metadataUrls, expectedIssuer)
92
107
  try {
93
108
  return await fetchOAuthServerMetadata(url, expectedIssuer);
94
109
  } catch (err) {
110
+ log.debug("oauth.discovery.attempt_failed", { url, ...describeError(err) });
95
111
  errors.push(`${url}: ${err instanceof Error ? err.message : String(err)}`);
96
112
  }
97
113
  }
114
+ log.error("oauth.discovery.exhausted", { expectedIssuer, urlsTried: metadataUrls, errors });
98
115
  throw new Error(`failed to discover OAuth server metadata (${errors.join("; ")})`);
99
116
  }
100
117
  async function fetchOAuthServerMetadata(url, expectedIssuer) {
101
- const response = await fetch(url, { signal: AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS), redirect: "error" });
118
+ log.debug("oauth.discovery.fetch", { url });
119
+ const response = await fetch(url, { signal: AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS), redirect: "manual" });
102
120
  if (!response.ok) {
103
121
  throw new Error(String(response.status));
104
122
  }
@@ -108,6 +126,7 @@ async function fetchOAuthServerMetadata(url, expectedIssuer) {
108
126
  }
109
127
  parseSafeUrl("discovered issuer", json.issuer);
110
128
  if (trimTrailingSlash(json.issuer) !== expectedIssuer) {
129
+ log.warn("oauth.discovery.issuer_mismatch", { url, expectedIssuer, published: json.issuer });
111
130
  throw new Error("issuer mismatch");
112
131
  }
113
132
  if (typeof json.jwks_uri !== "string") {
@@ -120,6 +139,11 @@ async function fetchIssuerOAuthServerMetadata(issuer) {
120
139
  try {
121
140
  return await fetchFirstValidOAuthServerMetadata(oauthMetadataUrlsForIssuer(issuer), issuer);
122
141
  } catch (err) {
142
+ log.error("oauth.discovery.config_error", {
143
+ issuer,
144
+ ...describeError(err),
145
+ outcome: "500 oauth configuration error"
146
+ });
123
147
  throw new OAuthConfigurationError(
124
148
  `OAuth issuer discovery failed: ${err instanceof Error ? err.message : String(err)}`
125
149
  );
@@ -127,15 +151,26 @@ async function fetchIssuerOAuthServerMetadata(issuer) {
127
151
  }
128
152
  function createOAuthDiscoveryResolver(auth) {
129
153
  const configuredIssuer = trimTrailingSlash(auth.issuer);
130
- const oauthServerMetadata = cachedPromise(() => fetchIssuerOAuthServerMetadata(configuredIssuer));
154
+ const oauthServerMetadata = cachedPromise(
155
+ () => fetchIssuerOAuthServerMetadata(configuredIssuer),
156
+ "oauth.discovery.metadata"
157
+ );
131
158
  return {
132
159
  resolveIssuer: async () => configuredIssuer,
133
- resolveJwksUri: async () => auth.jwksUri ?? (await oauthServerMetadata()).jwks_uri
160
+ resolveJwksUri: async () => {
161
+ if (auth.jwksUri) {
162
+ log.debug("oauth.jwks.resolved", { jwksUri: auth.jwksUri, source: "configured" });
163
+ return auth.jwksUri;
164
+ }
165
+ const jwksUri = (await oauthServerMetadata()).jwks_uri;
166
+ log.debug("oauth.jwks.resolved", { jwksUri, source: "discovered" });
167
+ return jwksUri;
168
+ }
134
169
  };
135
170
  }
136
171
 
137
172
  // src/auth/verifier.ts
138
- import { createRemoteJWKSet, errors as joseErrors, jwtVerify } from "jose";
173
+ import { createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
139
174
 
140
175
  // src/auth/claims.ts
141
176
  function readString(value) {
@@ -155,6 +190,7 @@ function stringClaim(claims, name) {
155
190
  // src/auth/verifier.ts
156
191
  var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
157
192
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
193
+ var JWKS_FETCH_TIMEOUT_MS = 5e3;
158
194
  var OAuthTokenError = class extends Error {
159
195
  constructor(status, oauthError, message) {
160
196
  super(message);
@@ -166,12 +202,25 @@ var OAuthTokenError = class extends Error {
166
202
  function resolveAcceptedAudiences(auth, resource) {
167
203
  return auth.acceptedAudiences ?? [resource];
168
204
  }
169
- function isJwksFetchFailure(err) {
170
- if (err instanceof joseErrors.JWKSTimeout || err instanceof joseErrors.JWKSInvalid)
171
- return true;
172
- if (!(err instanceof joseErrors.JOSEError))
173
- return err instanceof Error;
174
- return err.code === "ERR_JOSE_GENERIC";
205
+ function tokenHeaderFields(token) {
206
+ try {
207
+ const header = decodeProtectedHeader(token);
208
+ return { jwtAlg: header.alg, jwtKid: header.kid, tokenLength: token.length };
209
+ } catch {
210
+ return { tokenLength: token.length };
211
+ }
212
+ }
213
+ async function fetchVerificationKeySet(jwksUri) {
214
+ try {
215
+ const response = await fetch(jwksUri, { signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS), redirect: "manual" });
216
+ if (!response.ok)
217
+ throw new Error(`JWKS endpoint returned ${response.status}`);
218
+ const json = await response.json();
219
+ return createLocalJWKSet(json);
220
+ } catch (err) {
221
+ log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
222
+ throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
223
+ }
175
224
  }
176
225
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
177
226
  try {
@@ -186,20 +235,20 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
186
235
  });
187
236
  return payload;
188
237
  } catch (err) {
189
- if (isJwksFetchFailure(err)) {
190
- throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
191
- }
238
+ log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
192
239
  throw err;
193
240
  }
194
241
  }
195
242
  function assertNonEmptySubject(claims) {
196
243
  const sub = claims["sub"];
197
244
  if (typeof sub !== "string" || sub.trim() === "") {
245
+ log.debug("oauth.verify.bad_subject", { subType: typeof sub });
198
246
  throw new OAuthTokenError(401, "invalid_token", "token subject claim must be a non-empty string");
199
247
  }
200
248
  }
201
249
  function assertOAuthClientClaim(auth, clientId) {
202
250
  if (auth.requireOAuthClientClaim !== false && !clientId) {
251
+ log.debug("oauth.verify.missing_client_claim", { outcome: "401 invalid_token" });
203
252
  throw new OAuthTokenError(401, "invalid_token", "OAuth client claim is required");
204
253
  }
205
254
  }
@@ -224,17 +273,23 @@ function buildMcpAuthContext(args) {
224
273
  };
225
274
  }
226
275
  function createOAuthTokenVerifier(auth, discovery) {
227
- const loadRemoteJwksKeySet = cachedPromise(
228
- () => discovery.resolveJwksUri().then((jwksURI) => createRemoteJWKSet(new URL(jwksURI)))
229
- );
230
276
  return async (token, request, options) => {
231
277
  const resource = resolveProtectedResource(auth, request, options);
232
278
  const issuer = await discovery.resolveIssuer();
233
279
  const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
234
- const claims = await verifyJwtClaims(token, await loadRemoteJwksKeySet(), issuer, acceptedAudiences, auth);
280
+ log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
281
+ const jwksUri = await discovery.resolveJwksUri();
282
+ log.debug("oauth.jwks.fetch", { jwksUri });
283
+ const keySet = await fetchVerificationKeySet(jwksUri);
284
+ const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
235
285
  assertNonEmptySubject(claims);
236
286
  const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
237
287
  assertOAuthClientClaim(auth, context.principal.clientId);
288
+ log.info("oauth.verify.ok", {
289
+ sub: context.principal.sub,
290
+ clientId: context.principal.clientId,
291
+ scopes: context.principal.scopes
292
+ });
238
293
  return context;
239
294
  };
240
295
  }
@@ -334,22 +389,27 @@ function createRequestAuthorizer(mcp, options = {}) {
334
389
  if (runtime.kind === "unconfigured")
335
390
  return { ok: true };
336
391
  const token = parseBearerToken(request);
337
- if (!token)
392
+ if (!token) {
393
+ log.info("auth.no_bearer_token", { outcome: "401" });
338
394
  return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
395
+ }
339
396
  try {
340
397
  const auth = await runtime.verify(token, request, runtime.options);
341
398
  assertRequiredScopes(runtime.auth, auth);
342
399
  return { ok: true, auth };
343
400
  } catch (err) {
344
401
  if (err instanceof OAuthConfigurationError) {
402
+ log.error("auth.config_error", { ...describeError(err), outcome: "500" });
345
403
  return { ok: false, response: oauthConfigurationErrorResponse() };
346
404
  }
347
405
  if (err instanceof OAuthTokenError) {
406
+ log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
348
407
  return {
349
408
  ok: false,
350
409
  response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
351
410
  };
352
411
  }
412
+ log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
353
413
  return {
354
414
  ok: false,
355
415
  response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
@@ -9,7 +9,7 @@ import {
9
9
  headResponse,
10
10
  methodNotAllowed,
11
11
  withCors
12
- } from "./chunk-ZKKLOL2C.js";
12
+ } from "./chunk-HRMLGCXV.js";
13
13
 
14
14
  // src/protocols/rest/list-tools.ts
15
15
  import { objectFromShape } from "@modelcontextprotocol/sdk/server/zod-compat.js";
@@ -0,0 +1,87 @@
1
+ // src/core/logger.ts
2
+ var LEVEL_RANK = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 };
3
+ function isLogLevel(value) {
4
+ return typeof value === "string" && value in LEVEL_RANK;
5
+ }
6
+ function readEnvLevel() {
7
+ try {
8
+ const raw = typeof process !== "undefined" ? process.env?.["LOVABLE_MCP_LOG_LEVEL"] : void 0;
9
+ const normalized = raw?.trim().toLowerCase();
10
+ return isLogLevel(normalized) ? normalized : void 0;
11
+ } catch {
12
+ return void 0;
13
+ }
14
+ }
15
+ var currentLevel = readEnvLevel() ?? "silent";
16
+ function setLogLevel(level) {
17
+ currentLevel = level;
18
+ }
19
+ function enabled(level) {
20
+ return LEVEL_RANK[level] <= LEVEL_RANK[currentLevel];
21
+ }
22
+ function emit(level, method, event, fields) {
23
+ if (!enabled(level))
24
+ return;
25
+ const message = `[mcp-js] ${event}`;
26
+ if (fields)
27
+ console[method](message, fields);
28
+ else
29
+ console[method](message);
30
+ }
31
+ var log = {
32
+ error: (event, fields) => emit("error", "error", event, fields),
33
+ warn: (event, fields) => emit("warn", "warn", event, fields),
34
+ info: (event, fields) => emit("info", "info", event, fields),
35
+ debug: (event, fields) => emit("debug", "debug", event, fields)
36
+ };
37
+ function describeError(err) {
38
+ if (err instanceof Error) {
39
+ const code = err.code;
40
+ return { name: err.name, message: err.message, ...typeof code === "string" ? { code } : {} };
41
+ }
42
+ return { value: String(err) };
43
+ }
44
+
45
+ // src/core/url.ts
46
+ function trimTrailingSlash(value) {
47
+ return value.replace(/\/+$/, "");
48
+ }
49
+ function isLocalHTTPHost(hostname) {
50
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
51
+ }
52
+ function urlSafetyProblem(url) {
53
+ const isAllowedHTTP = url.protocol === "http:" && isLocalHTTPHost(url.hostname);
54
+ if (url.protocol !== "https:" && !isAllowedHTTP) {
55
+ return "must use https://, except localhost development URLs";
56
+ }
57
+ if (url.username || url.password) {
58
+ return "must not include credentials";
59
+ }
60
+ if (url.search || url.hash) {
61
+ return "must not include query or fragment";
62
+ }
63
+ return void 0;
64
+ }
65
+
66
+ // src/core/validation.ts
67
+ function parseSafeUrl(subject, raw, ErrorClass = Error) {
68
+ let url;
69
+ try {
70
+ url = new URL(raw);
71
+ } catch {
72
+ throw new ErrorClass(`${subject} must be an absolute URL`);
73
+ }
74
+ const problem = urlSafetyProblem(url);
75
+ if (problem) {
76
+ throw new ErrorClass(`${subject} ${problem}`);
77
+ }
78
+ return url;
79
+ }
80
+
81
+ export {
82
+ trimTrailingSlash,
83
+ parseSafeUrl,
84
+ setLogLevel,
85
+ log,
86
+ describeError
87
+ };
@@ -7,7 +7,11 @@ import {
7
7
  oauthConfigurationErrorResponse,
8
8
  resolveProtectedResource,
9
9
  withCors
10
- } from "./chunk-ZKKLOL2C.js";
10
+ } from "./chunk-HRMLGCXV.js";
11
+ import {
12
+ describeError,
13
+ log
14
+ } from "./chunk-QC3DXQTH.js";
11
15
 
12
16
  // src/protocols/oauth-metadata.ts
13
17
  function notFound() {
@@ -62,7 +66,8 @@ function createOAuthProtectedResourceMetadataHandler(mcp, options = {}) {
62
66
  );
63
67
  const response = withCors(Response.json(metadata, { headers }));
64
68
  return request.method === "HEAD" ? headResponse(response) : response;
65
- } catch {
69
+ } catch (err) {
70
+ log.error("oauth.metadata.config_error", { ...describeError(err), outcome: "500 oauth configuration error" });
66
71
  const response = withCors(oauthConfigurationErrorResponse());
67
72
  return request.method === "HEAD" ? headResponse(response) : response;
68
73
  }
package/dist/index.cjs CHANGED
@@ -23,7 +23,8 @@ __export(src_exports, {
23
23
  ToolContext: () => ToolContext,
24
24
  auth: () => auth,
25
25
  defineMcp: () => defineMcp,
26
- defineTool: () => defineTool
26
+ defineTool: () => defineTool,
27
+ setLogLevel: () => setLogLevel
27
28
  });
28
29
  module.exports = __toCommonJS(src_exports);
29
30
 
@@ -262,10 +263,30 @@ var ToolContext = class {
262
263
  return this.#auth?.principal.claims;
263
264
  }
264
265
  };
266
+
267
+ // src/core/logger.ts
268
+ var LEVEL_RANK = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 };
269
+ function isLogLevel(value) {
270
+ return typeof value === "string" && value in LEVEL_RANK;
271
+ }
272
+ function readEnvLevel() {
273
+ try {
274
+ const raw = typeof process !== "undefined" ? process.env?.["LOVABLE_MCP_LOG_LEVEL"] : void 0;
275
+ const normalized = raw?.trim().toLowerCase();
276
+ return isLogLevel(normalized) ? normalized : void 0;
277
+ } catch {
278
+ return void 0;
279
+ }
280
+ }
281
+ var currentLevel = readEnvLevel() ?? "silent";
282
+ function setLogLevel(level) {
283
+ currentLevel = level;
284
+ }
265
285
  // Annotate the CommonJS export names for ESM import in node:
266
286
  0 && (module.exports = {
267
287
  ToolContext,
268
288
  auth,
269
289
  defineMcp,
270
- defineTool
290
+ defineTool,
291
+ setLogLevel
271
292
  });
package/dist/index.d.cts CHANGED
@@ -53,4 +53,12 @@ declare const auth: Readonly<{
53
53
  }>;
54
54
  }>;
55
55
 
56
- export { type IssuerOAuthOptions, McpDefinitionInput, ToolDefinition, ZodRawShape, auth, defineMcp, defineTool };
56
+ type LogLevel = "silent" | "error" | "warn" | "info" | "debug";
57
+ /**
58
+ * Set the runtime log level programmatically. Use this when the deployment can't
59
+ * surface `LOVABLE_MCP_LOG_LEVEL` to the package (e.g. a Cloudflare Worker that
60
+ * passes vars via bindings): call `setLogLevel("debug")` once at server startup.
61
+ */
62
+ declare function setLogLevel(level: LogLevel): void;
63
+
64
+ export { type IssuerOAuthOptions, type LogLevel, McpDefinitionInput, ToolDefinition, ZodRawShape, auth, defineMcp, defineTool, setLogLevel };
package/dist/index.d.ts CHANGED
@@ -53,4 +53,12 @@ declare const auth: Readonly<{
53
53
  }>;
54
54
  }>;
55
55
 
56
- export { type IssuerOAuthOptions, McpDefinitionInput, ToolDefinition, ZodRawShape, auth, defineMcp, defineTool };
56
+ type LogLevel = "silent" | "error" | "warn" | "info" | "debug";
57
+ /**
58
+ * Set the runtime log level programmatically. Use this when the deployment can't
59
+ * surface `LOVABLE_MCP_LOG_LEVEL` to the package (e.g. a Cloudflare Worker that
60
+ * passes vars via bindings): call `setLogLevel("debug")` once at server startup.
61
+ */
62
+ declare function setLogLevel(level: LogLevel): void;
63
+
64
+ export { type IssuerOAuthOptions, type LogLevel, McpDefinitionInput, ToolDefinition, ZodRawShape, auth, defineMcp, defineTool, setLogLevel };
package/dist/index.js CHANGED
@@ -2,8 +2,9 @@ import {
2
2
  ToolContext
3
3
  } from "./chunk-MA5H6PSF.js";
4
4
  import {
5
- parseSafeUrl
6
- } from "./chunk-QA3FWDUV.js";
5
+ parseSafeUrl,
6
+ setLogLevel
7
+ } from "./chunk-QC3DXQTH.js";
7
8
 
8
9
  // src/core/define.ts
9
10
  function assertUniqueNames(mcp) {
@@ -168,5 +169,6 @@ export {
168
169
  ToolContext,
169
170
  auth,
170
171
  defineMcp,
171
- defineTool
172
+ defineTool,
173
+ setLogLevel
172
174
  };