@lovable.dev/mcp-js 0.6.0 → 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 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 |
@@ -9,7 +9,7 @@ import {
9
9
  headResponse,
10
10
  methodNotAllowed,
11
11
  withCors
12
- } from "./chunk-ZKKLOL2C.js";
12
+ } from "./chunk-NTXHOUK6.js";
13
13
 
14
14
  // src/protocols/rest/list-tools.ts
15
15
  import { objectFromShape } from "@modelcontextprotocol/sdk/server/zod-compat.js";
@@ -7,7 +7,11 @@ import {
7
7
  oauthConfigurationErrorResponse,
8
8
  resolveProtectedResource,
9
9
  withCors
10
- } from "./chunk-ZKKLOL2C.js";
10
+ } from "./chunk-NTXHOUK6.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
  }
@@ -5,7 +5,11 @@ import {
5
5
  corsPreflightResponse,
6
6
  createRequestAuthorizer,
7
7
  withCors
8
- } from "./chunk-ZKKLOL2C.js";
8
+ } from "./chunk-NTXHOUK6.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 { createRemoteJWKSet, decodeProtectedHeader, errors as joseErrors, jwtVerify } from "jose";
139
174
 
140
175
  // src/auth/claims.ts
141
176
  function readString(value) {
@@ -166,6 +201,14 @@ var OAuthTokenError = class extends Error {
166
201
  function resolveAcceptedAudiences(auth, resource) {
167
202
  return auth.acceptedAudiences ?? [resource];
168
203
  }
204
+ function tokenHeaderFields(token) {
205
+ try {
206
+ const header = decodeProtectedHeader(token);
207
+ return { jwtAlg: header.alg, jwtKid: header.kid, tokenLength: token.length };
208
+ } catch {
209
+ return { tokenLength: token.length };
210
+ }
211
+ }
169
212
  function isJwksFetchFailure(err) {
170
213
  if (err instanceof joseErrors.JWKSTimeout || err instanceof joseErrors.JWKSInvalid)
171
214
  return true;
@@ -187,19 +230,23 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
187
230
  return payload;
188
231
  } catch (err) {
189
232
  if (isJwksFetchFailure(err)) {
233
+ log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
190
234
  throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
191
235
  }
236
+ log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
192
237
  throw err;
193
238
  }
194
239
  }
195
240
  function assertNonEmptySubject(claims) {
196
241
  const sub = claims["sub"];
197
242
  if (typeof sub !== "string" || sub.trim() === "") {
243
+ log.debug("oauth.verify.bad_subject", { subType: typeof sub });
198
244
  throw new OAuthTokenError(401, "invalid_token", "token subject claim must be a non-empty string");
199
245
  }
200
246
  }
201
247
  function assertOAuthClientClaim(auth, clientId) {
202
248
  if (auth.requireOAuthClientClaim !== false && !clientId) {
249
+ log.debug("oauth.verify.missing_client_claim", { outcome: "401 invalid_token" });
203
250
  throw new OAuthTokenError(401, "invalid_token", "OAuth client claim is required");
204
251
  }
205
252
  }
@@ -224,17 +271,23 @@ function buildMcpAuthContext(args) {
224
271
  };
225
272
  }
226
273
  function createOAuthTokenVerifier(auth, discovery) {
227
- const loadRemoteJwksKeySet = cachedPromise(
228
- () => discovery.resolveJwksUri().then((jwksURI) => createRemoteJWKSet(new URL(jwksURI)))
229
- );
230
274
  return async (token, request, options) => {
231
275
  const resource = resolveProtectedResource(auth, request, options);
232
276
  const issuer = await discovery.resolveIssuer();
233
277
  const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
234
- const claims = await verifyJwtClaims(token, await loadRemoteJwksKeySet(), issuer, acceptedAudiences, auth);
278
+ log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
279
+ const jwksUri = await discovery.resolveJwksUri();
280
+ log.debug("oauth.jwks.keyset_created", { jwksUri });
281
+ const keySet = createRemoteJWKSet(new URL(jwksUri));
282
+ const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
235
283
  assertNonEmptySubject(claims);
236
284
  const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
237
285
  assertOAuthClientClaim(auth, context.principal.clientId);
286
+ log.info("oauth.verify.ok", {
287
+ sub: context.principal.sub,
288
+ clientId: context.principal.clientId,
289
+ scopes: context.principal.scopes
290
+ });
238
291
  return context;
239
292
  };
240
293
  }
@@ -334,22 +387,27 @@ function createRequestAuthorizer(mcp, options = {}) {
334
387
  if (runtime.kind === "unconfigured")
335
388
  return { ok: true };
336
389
  const token = parseBearerToken(request);
337
- if (!token)
390
+ if (!token) {
391
+ log.info("auth.no_bearer_token", { outcome: "401" });
338
392
  return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
393
+ }
339
394
  try {
340
395
  const auth = await runtime.verify(token, request, runtime.options);
341
396
  assertRequiredScopes(runtime.auth, auth);
342
397
  return { ok: true, auth };
343
398
  } catch (err) {
344
399
  if (err instanceof OAuthConfigurationError) {
400
+ log.error("auth.config_error", { ...describeError(err), outcome: "500" });
345
401
  return { ok: false, response: oauthConfigurationErrorResponse() };
346
402
  }
347
403
  if (err instanceof OAuthTokenError) {
404
+ log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
348
405
  return {
349
406
  ok: false,
350
407
  response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
351
408
  };
352
409
  }
410
+ log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
353
411
  return {
354
412
  ok: false,
355
413
  response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
@@ -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
+ };
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
  };