@lovable.dev/mcp-js 0.7.0 → 0.8.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.
@@ -133,7 +133,11 @@ src/
133
133
  stacks/
134
134
  tanstack/
135
135
  handlers.ts # TanStack route-ctx adapters
136
- vite.ts # mcpPlugin (route emission + stale-file cleanup)
136
+ vite.ts # mcpPlugin (route emission + stale-file cleanup; no manifest)
137
+ ssr-loader.ts # produceViaSsr: load the entry via a throwaway Vite SSR server
138
+ extract-manifest.ts # project a defineMcp result to the manifest (types + manifestFromDefinition)
139
+ manifest-io.ts # runExtract/syncManifest: write/remove .lovable/mcp/manifest.json
140
+ cli/extract-manifest.ts # lovable-mcp-extract-manifest bin (runs runExtract on cwd)
137
141
  index.ts # barrel
138
142
  tests/
139
143
  core/{define,promise,url}.test.ts
@@ -143,7 +147,7 @@ tests/
143
147
  rest/{list-tools,invoke-tool}.test.ts
144
148
  parity.test.ts # REST↔MCP equivalence contract
145
149
  stacks/
146
- tanstack/{handlers,vite}.test.ts
150
+ tanstack/{handlers,vite,extract-manifest,manifest-io,ssr-loader}.test.ts
147
151
  integration/ # boots an example app, hits live HTTP
148
152
  global-setup.ts # spawns example/tanstack on :8080
149
153
  tools.test.ts # non-OAuth example
package/README.md CHANGED
@@ -53,6 +53,16 @@ MCP (`POST /mcp`) is the public wire format clients speak directly. REST (`/.mcp
53
53
 
54
54
  The OAuth metadata route is emitted by default and returns `404` until OAuth auth is configured. Disable it with `mcpPlugin({ protectedResourceMetadataRoute: false })` only if the app owns `/.well-known/oauth-protected-resource` itself.
55
55
 
56
+ ### `.lovable/mcp/manifest.json`
57
+
58
+ `.lovable/mcp/manifest.json` is a snapshot the Lovable platform reads to register the MCP server. Envelope fields: `version` (manifest schema version), `sdk_version` (the `@lovable.dev/mcp-js` release that wrote the snapshot), `path`, and `auth`. `auth` is the server's auth configuration, lifted into the envelope (not into `mcp`): `{ "type": "none" }`, or `{ "type": "oauth", ... }` mirroring the `defineMcp({ auth })` config (snake_case — `issuer`, `accepted_audiences`, `required_scopes`, `resource`, …). The `mcp` field is **exactly the `GET /.mcp/list-tools` body** — `server` plus the tool catalog (`name`/`title`/`description`/`annotations` and JSON-Schema `inputSchema`/`outputSchema`) — so the committed snapshot can't drift from what the live route serves. It's produced by loading the entry and reading the catalog off the `defineMcp` result, so the manifest reflects exactly what the server exposes, including tools built programmatically (spreads, computed names, tools from npm). **The `lovable-mcp-extract-manifest` CLI writes it** (loading the entry through Vite's SSR module loader, so it works under Node/Bun), and removes it when the entry is deleted. The Lovable platform runs the CLI in its commit pipeline; the Vite plugin only generates routes. Commit it: the platform reads the committed file.
59
+
60
+ Three caveats:
61
+
62
+ - **The entry must import cleanly.** Because extraction imports the entry, it must not throw at module load — read env vars and do I/O *inside* tool handlers, not at module top level. A top-level throw also breaks Worker cold-start, so this is correct authoring regardless. An entry that can't be imported (or doesn't `export default defineMcp(...)`) **makes the extract CLI exit non-zero with a clear error** rather than emitting a partial manifest.
63
+ - **Tool `title`/`description` and the `auth` config are serialized verbatim.** They land in a committed file. The `auth` config is whatever the entry resolves at build time — an issuer read from env resolves to its build-time value (e.g. a fallback like `https://supabase.invalid/auth/v1` when the env var is unset), so don't put secrets in it.
64
+ - **The `version` field is a wire-format version, not the package version.** The platform reader must accept a manifest `version` before this package starts emitting it, so bumping it is a coordinated deploy with the Lovable backend — not something an app author changes.
65
+
56
66
  ## OAuth resource-server auth
57
67
 
58
68
  `@lovable.dev/mcp-js` can protect an app-hosted MCP server as an OAuth 2.1 resource server. The package does not implement `/authorize`, `/token`, client registration, refresh tokens, or consent UI; those stay with the authorization server (for today's Supabase-backed Lovable Cloud apps, Supabase Auth). The MCP runtime validates bearer JWTs, publishes RFC 9728 protected-resource metadata, returns `WWW-Authenticate: Bearer ... resource_metadata="..."` challenges, and passes verified claims to tools.
@@ -5,7 +5,7 @@ import {
5
5
  corsPreflightResponse,
6
6
  createRequestAuthorizer,
7
7
  withCors
8
- } from "./chunk-NTXHOUK6.js";
8
+ } from "./chunk-HRMLGCXV.js";
9
9
  import {
10
10
  describeError,
11
11
  log
@@ -6,55 +6,12 @@ import {
6
6
  assertRestResourceBinding,
7
7
  corsPreflightResponse,
8
8
  createRequestAuthorizer,
9
- headResponse,
10
9
  methodNotAllowed,
11
10
  withCors
12
- } from "./chunk-NTXHOUK6.js";
13
-
14
- // src/protocols/rest/list-tools.ts
15
- import { objectFromShape } from "@modelcontextprotocol/sdk/server/zod-compat.js";
16
- import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
17
- function shapeToJsonSchema(shape) {
18
- if (!shape)
19
- return null;
20
- try {
21
- return toJsonSchemaCompat(objectFromShape(shape));
22
- } catch {
23
- return null;
24
- }
25
- }
26
- function createListToolsHandler(mcp, options = {}) {
27
- assertRestResourceBinding(mcp, options);
28
- const authorizer = createRequestAuthorizer(mcp, options);
29
- const handle = async (request) => {
30
- const authResult = await authorizer.authorize(request);
31
- if (!authResult.ok)
32
- return authResult.response;
33
- if (request.method !== "GET" && request.method !== "HEAD")
34
- return methodNotAllowed("GET, HEAD, OPTIONS");
35
- const body = {
36
- server: { name: mcp.name, version: mcp.version, title: mcp.title },
37
- tools: mcp.tools.map((tool) => ({
38
- name: tool.name,
39
- title: tool.title,
40
- description: tool.description,
41
- annotations: tool.annotations,
42
- inputSchema: shapeToJsonSchema(tool.inputSchema),
43
- outputSchema: shapeToJsonSchema(tool.outputSchema)
44
- }))
45
- };
46
- const response = Response.json(body);
47
- return request.method === "HEAD" ? headResponse(response) : response;
48
- };
49
- return async (request) => {
50
- if (request.method === "OPTIONS")
51
- return corsPreflightResponse("GET, HEAD, OPTIONS");
52
- return withCors(await handle(request));
53
- };
54
- }
11
+ } from "./chunk-HRMLGCXV.js";
55
12
 
56
13
  // src/protocols/rest/invoke-tool.ts
57
- import { getParseErrorMessage, objectFromShape as objectFromShape2, safeParseAsync } from "@modelcontextprotocol/sdk/server/zod-compat.js";
14
+ import { getParseErrorMessage, objectFromShape, safeParseAsync } from "@modelcontextprotocol/sdk/server/zod-compat.js";
58
15
  var MAX_REFLECTED_TOOL_NAME = 256;
59
16
  function safeReflectName(name) {
60
17
  const text = String(name);
@@ -98,7 +55,7 @@ function createInvokeToolHandler(mcp, options = {}) {
98
55
  let args = rawArgs;
99
56
  if (tool.inputSchema) {
100
57
  try {
101
- const schema = objectFromShape2(tool.inputSchema);
58
+ const schema = objectFromShape(tool.inputSchema);
102
59
  const parsed = await safeParseAsync(schema, rawArgs);
103
60
  if (!parsed.success) {
104
61
  return new Response(
@@ -151,6 +108,5 @@ function createInvokeToolHandler(mcp, options = {}) {
151
108
  }
152
109
 
153
110
  export {
154
- createListToolsHandler,
155
111
  createInvokeToolHandler
156
112
  };
@@ -0,0 +1,57 @@
1
+ import {
2
+ assertRestResourceBinding,
3
+ corsPreflightResponse,
4
+ createRequestAuthorizer,
5
+ headResponse,
6
+ methodNotAllowed,
7
+ withCors
8
+ } from "./chunk-HRMLGCXV.js";
9
+
10
+ // src/protocols/rest/list-tools.ts
11
+ import { objectFromShape } from "@modelcontextprotocol/sdk/server/zod-compat.js";
12
+ import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
13
+ function shapeToJsonSchema(shape) {
14
+ if (!shape)
15
+ return null;
16
+ try {
17
+ return toJsonSchemaCompat(objectFromShape(shape));
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+ function buildMcpListing(mcp) {
23
+ return {
24
+ server: { name: mcp.name, version: mcp.version, title: mcp.title },
25
+ tools: mcp.tools.map((tool) => ({
26
+ name: tool.name,
27
+ title: tool.title,
28
+ description: tool.description,
29
+ annotations: tool.annotations,
30
+ inputSchema: shapeToJsonSchema(tool.inputSchema),
31
+ outputSchema: shapeToJsonSchema(tool.outputSchema)
32
+ }))
33
+ };
34
+ }
35
+ function createListToolsHandler(mcp, options = {}) {
36
+ assertRestResourceBinding(mcp, options);
37
+ const authorizer = createRequestAuthorizer(mcp, options);
38
+ const handle = async (request) => {
39
+ const authResult = await authorizer.authorize(request);
40
+ if (!authResult.ok)
41
+ return authResult.response;
42
+ if (request.method !== "GET" && request.method !== "HEAD")
43
+ return methodNotAllowed("GET, HEAD, OPTIONS");
44
+ const response = Response.json(buildMcpListing(mcp));
45
+ return request.method === "HEAD" ? headResponse(response) : response;
46
+ };
47
+ return async (request) => {
48
+ if (request.method === "OPTIONS")
49
+ return corsPreflightResponse("GET, HEAD, OPTIONS");
50
+ return withCors(await handle(request));
51
+ };
52
+ }
53
+
54
+ export {
55
+ buildMcpListing,
56
+ createListToolsHandler
57
+ };
@@ -170,7 +170,7 @@ function createOAuthDiscoveryResolver(auth) {
170
170
  }
171
171
 
172
172
  // src/auth/verifier.ts
173
- import { createRemoteJWKSet, decodeProtectedHeader, errors as joseErrors, jwtVerify } from "jose";
173
+ import { createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
174
174
 
175
175
  // src/auth/claims.ts
176
176
  function readString(value) {
@@ -190,6 +190,7 @@ function stringClaim(claims, name) {
190
190
  // src/auth/verifier.ts
191
191
  var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
192
192
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
193
+ var JWKS_FETCH_TIMEOUT_MS = 5e3;
193
194
  var OAuthTokenError = class extends Error {
194
195
  constructor(status, oauthError, message) {
195
196
  super(message);
@@ -209,12 +210,17 @@ function tokenHeaderFields(token) {
209
210
  return { tokenLength: token.length };
210
211
  }
211
212
  }
212
- function isJwksFetchFailure(err) {
213
- if (err instanceof joseErrors.JWKSTimeout || err instanceof joseErrors.JWKSInvalid)
214
- return true;
215
- if (!(err instanceof joseErrors.JOSEError))
216
- return err instanceof Error;
217
- return err.code === "ERR_JOSE_GENERIC";
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
+ }
218
224
  }
219
225
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
220
226
  try {
@@ -229,10 +235,6 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
229
235
  });
230
236
  return payload;
231
237
  } catch (err) {
232
- if (isJwksFetchFailure(err)) {
233
- log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
234
- throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
235
- }
236
238
  log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
237
239
  throw err;
238
240
  }
@@ -277,8 +279,8 @@ function createOAuthTokenVerifier(auth, discovery) {
277
279
  const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
278
280
  log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
279
281
  const jwksUri = await discovery.resolveJwksUri();
280
- log.debug("oauth.jwks.keyset_created", { jwksUri });
281
- const keySet = createRemoteJWKSet(new URL(jwksUri));
282
+ log.debug("oauth.jwks.fetch", { jwksUri });
283
+ const keySet = await fetchVerificationKeySet(jwksUri);
282
284
  const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
283
285
  assertNonEmptySubject(claims);
284
286
  const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
@@ -0,0 +1,8 @@
1
+ // src/stacks/tanstack/fs-errors.ts
2
+ function isFileMissing(err) {
3
+ return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
4
+ }
5
+
6
+ export {
7
+ isFileMissing
8
+ };
@@ -7,7 +7,7 @@ import {
7
7
  oauthConfigurationErrorResponse,
8
8
  resolveProtectedResource,
9
9
  withCors
10
- } from "./chunk-NTXHOUK6.js";
10
+ } from "./chunk-HRMLGCXV.js";
11
11
  import {
12
12
  describeError,
13
13
  log
@@ -281,6 +281,7 @@ function stringClaim(claims, name) {
281
281
  // src/auth/verifier.ts
282
282
  var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
283
283
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
284
+ var JWKS_FETCH_TIMEOUT_MS = 5e3;
284
285
  var OAuthTokenError = class extends Error {
285
286
  constructor(status, oauthError, message) {
286
287
  super(message);
@@ -300,12 +301,17 @@ function tokenHeaderFields(token) {
300
301
  return { tokenLength: token.length };
301
302
  }
302
303
  }
303
- function isJwksFetchFailure(err) {
304
- if (err instanceof import_jose.errors.JWKSTimeout || err instanceof import_jose.errors.JWKSInvalid)
305
- return true;
306
- if (!(err instanceof import_jose.errors.JOSEError))
307
- return err instanceof Error;
308
- return err.code === "ERR_JOSE_GENERIC";
304
+ async function fetchVerificationKeySet(jwksUri) {
305
+ try {
306
+ const response = await fetch(jwksUri, { signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS), redirect: "manual" });
307
+ if (!response.ok)
308
+ throw new Error(`JWKS endpoint returned ${response.status}`);
309
+ const json = await response.json();
310
+ return (0, import_jose.createLocalJWKSet)(json);
311
+ } catch (err) {
312
+ log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
313
+ throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
314
+ }
309
315
  }
310
316
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
311
317
  try {
@@ -320,10 +326,6 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
320
326
  });
321
327
  return payload;
322
328
  } catch (err) {
323
- if (isJwksFetchFailure(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
- }
327
329
  log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
328
330
  throw err;
329
331
  }
@@ -368,8 +370,8 @@ function createOAuthTokenVerifier(auth, discovery) {
368
370
  const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
369
371
  log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
370
372
  const jwksUri = await discovery.resolveJwksUri();
371
- log.debug("oauth.jwks.keyset_created", { jwksUri });
372
- const keySet = (0, import_jose.createRemoteJWKSet)(new URL(jwksUri));
373
+ log.debug("oauth.jwks.fetch", { jwksUri });
374
+ const keySet = await fetchVerificationKeySet(jwksUri);
373
375
  const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
374
376
  assertNonEmptySubject(claims);
375
377
  const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createMcpProtocolHandler
3
- } from "../../chunk-G5GWSV5D.js";
3
+ } from "../../chunk-3XPKR4G6.js";
4
4
  import "../../chunk-MA5H6PSF.js";
5
- import "../../chunk-NTXHOUK6.js";
5
+ import "../../chunk-HRMLGCXV.js";
6
6
  import "../../chunk-QC3DXQTH.js";
7
7
  import "../../chunk-6DXGZZA4.js";
8
8
  export {
@@ -283,6 +283,7 @@ function stringClaim(claims, name) {
283
283
  // src/auth/verifier.ts
284
284
  var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
285
285
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
286
+ var JWKS_FETCH_TIMEOUT_MS = 5e3;
286
287
  var OAuthTokenError = class extends Error {
287
288
  constructor(status, oauthError, message) {
288
289
  super(message);
@@ -302,12 +303,17 @@ function tokenHeaderFields(token) {
302
303
  return { tokenLength: token.length };
303
304
  }
304
305
  }
305
- function isJwksFetchFailure(err) {
306
- if (err instanceof import_jose.errors.JWKSTimeout || err instanceof import_jose.errors.JWKSInvalid)
307
- return true;
308
- if (!(err instanceof import_jose.errors.JOSEError))
309
- return err instanceof Error;
310
- return err.code === "ERR_JOSE_GENERIC";
306
+ async function fetchVerificationKeySet(jwksUri) {
307
+ try {
308
+ const response = await fetch(jwksUri, { signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS), redirect: "manual" });
309
+ if (!response.ok)
310
+ throw new Error(`JWKS endpoint returned ${response.status}`);
311
+ const json = await response.json();
312
+ return (0, import_jose.createLocalJWKSet)(json);
313
+ } catch (err) {
314
+ log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
315
+ throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
316
+ }
311
317
  }
312
318
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
313
319
  try {
@@ -322,10 +328,6 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
322
328
  });
323
329
  return payload;
324
330
  } catch (err) {
325
- if (isJwksFetchFailure(err)) {
326
- log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
327
- throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
328
- }
329
331
  log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
330
332
  throw err;
331
333
  }
@@ -370,8 +372,8 @@ function createOAuthTokenVerifier(auth, discovery) {
370
372
  const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
371
373
  log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
372
374
  const jwksUri = await discovery.resolveJwksUri();
373
- log.debug("oauth.jwks.keyset_created", { jwksUri });
374
- const keySet = (0, import_jose.createRemoteJWKSet)(new URL(jwksUri));
375
+ log.debug("oauth.jwks.fetch", { jwksUri });
376
+ const keySet = await fetchVerificationKeySet(jwksUri);
375
377
  const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
376
378
  assertNonEmptySubject(claims);
377
379
  const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createOAuthProtectedResourceMetadataHandler
3
- } from "../chunk-722HKLIU.js";
4
- import "../chunk-NTXHOUK6.js";
3
+ } from "../chunk-W7F6JRDB.js";
4
+ import "../chunk-HRMLGCXV.js";
5
5
  import "../chunk-QC3DXQTH.js";
6
6
  import "../chunk-6DXGZZA4.js";
7
7
  export {
@@ -291,6 +291,7 @@ function stringClaim(claims, name) {
291
291
  // src/auth/verifier.ts
292
292
  var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
293
293
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
294
+ var JWKS_FETCH_TIMEOUT_MS = 5e3;
294
295
  var OAuthTokenError = class extends Error {
295
296
  constructor(status, oauthError, message) {
296
297
  super(message);
@@ -310,12 +311,17 @@ function tokenHeaderFields(token) {
310
311
  return { tokenLength: token.length };
311
312
  }
312
313
  }
313
- function isJwksFetchFailure(err) {
314
- if (err instanceof import_jose.errors.JWKSTimeout || err instanceof import_jose.errors.JWKSInvalid)
315
- return true;
316
- if (!(err instanceof import_jose.errors.JOSEError))
317
- return err instanceof Error;
318
- return err.code === "ERR_JOSE_GENERIC";
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
+ }
319
325
  }
320
326
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
321
327
  try {
@@ -330,10 +336,6 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
330
336
  });
331
337
  return payload;
332
338
  } catch (err) {
333
- if (isJwksFetchFailure(err)) {
334
- log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
335
- throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
336
- }
337
339
  log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
338
340
  throw err;
339
341
  }
@@ -378,8 +380,8 @@ function createOAuthTokenVerifier(auth, discovery) {
378
380
  const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
379
381
  log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
380
382
  const jwksUri = await discovery.resolveJwksUri();
381
- log.debug("oauth.jwks.keyset_created", { jwksUri });
382
- const keySet = (0, import_jose.createRemoteJWKSet)(new URL(jwksUri));
383
+ log.debug("oauth.jwks.fetch", { jwksUri });
384
+ const keySet = await fetchVerificationKeySet(jwksUri);
383
385
  const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
384
386
  assertNonEmptySubject(claims);
385
387
  const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
@@ -548,6 +550,19 @@ function shapeToJsonSchema(shape) {
548
550
  return null;
549
551
  }
550
552
  }
553
+ function buildMcpListing(mcp) {
554
+ return {
555
+ server: { name: mcp.name, version: mcp.version, title: mcp.title },
556
+ tools: mcp.tools.map((tool) => ({
557
+ name: tool.name,
558
+ title: tool.title,
559
+ description: tool.description,
560
+ annotations: tool.annotations,
561
+ inputSchema: shapeToJsonSchema(tool.inputSchema),
562
+ outputSchema: shapeToJsonSchema(tool.outputSchema)
563
+ }))
564
+ };
565
+ }
551
566
  function createListToolsHandler(mcp, options = {}) {
552
567
  assertRestResourceBinding(mcp, options);
553
568
  const authorizer = createRequestAuthorizer(mcp, options);
@@ -557,18 +572,7 @@ function createListToolsHandler(mcp, options = {}) {
557
572
  return authResult.response;
558
573
  if (request.method !== "GET" && request.method !== "HEAD")
559
574
  return methodNotAllowed("GET, HEAD, OPTIONS");
560
- const body = {
561
- server: { name: mcp.name, version: mcp.version, title: mcp.title },
562
- tools: mcp.tools.map((tool) => ({
563
- name: tool.name,
564
- title: tool.title,
565
- description: tool.description,
566
- annotations: tool.annotations,
567
- inputSchema: shapeToJsonSchema(tool.inputSchema),
568
- outputSchema: shapeToJsonSchema(tool.outputSchema)
569
- }))
570
- };
571
- const response = Response.json(body);
575
+ const response = Response.json(buildMcpListing(mcp));
572
576
  return request.method === "HEAD" ? headResponse(response) : response;
573
577
  };
574
578
  return async (request) => {
@@ -1,9 +1,11 @@
1
1
  import {
2
- createInvokeToolHandler,
3
- createListToolsHandler
4
- } from "../../chunk-53M2X7FU.js";
2
+ createInvokeToolHandler
3
+ } from "../../chunk-4Y72OCPS.js";
5
4
  import "../../chunk-MA5H6PSF.js";
6
- import "../../chunk-NTXHOUK6.js";
5
+ import {
6
+ createListToolsHandler
7
+ } from "../../chunk-E335BBVM.js";
8
+ import "../../chunk-HRMLGCXV.js";
7
9
  import "../../chunk-QC3DXQTH.js";
8
10
  import "../../chunk-6DXGZZA4.js";
9
11
  export {
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/stacks/tanstack/manifest-io.ts
5
+ var import_node_crypto = require("crypto");
6
+ var import_node_fs = require("fs");
7
+ var import_node_path = require("path");
8
+ var import_vite2 = require("vite");
9
+
10
+ // package.json
11
+ var version = "0.8.0";
12
+
13
+ // src/protocols/rest/list-tools.ts
14
+ var import_zod_compat = require("@modelcontextprotocol/sdk/server/zod-compat.js");
15
+ var import_zod_json_schema_compat = require("@modelcontextprotocol/sdk/server/zod-json-schema-compat.js");
16
+
17
+ // src/core/logger.ts
18
+ var LEVEL_RANK = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 };
19
+ function isLogLevel(value) {
20
+ return typeof value === "string" && value in LEVEL_RANK;
21
+ }
22
+ function readEnvLevel() {
23
+ try {
24
+ const raw = typeof process !== "undefined" ? process.env?.["LOVABLE_MCP_LOG_LEVEL"] : void 0;
25
+ const normalized = raw?.trim().toLowerCase();
26
+ return isLogLevel(normalized) ? normalized : void 0;
27
+ } catch {
28
+ return void 0;
29
+ }
30
+ }
31
+ var currentLevel = readEnvLevel() ?? "silent";
32
+
33
+ // src/auth/verifier.ts
34
+ var import_jose = require("jose");
35
+
36
+ // src/protocols/rest/list-tools.ts
37
+ function shapeToJsonSchema(shape) {
38
+ if (!shape)
39
+ return null;
40
+ try {
41
+ return (0, import_zod_json_schema_compat.toJsonSchemaCompat)((0, import_zod_compat.objectFromShape)(shape));
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ function buildMcpListing(mcp) {
47
+ return {
48
+ server: { name: mcp.name, version: mcp.version, title: mcp.title },
49
+ tools: mcp.tools.map((tool) => ({
50
+ name: tool.name,
51
+ title: tool.title,
52
+ description: tool.description,
53
+ annotations: tool.annotations,
54
+ inputSchema: shapeToJsonSchema(tool.inputSchema),
55
+ outputSchema: shapeToJsonSchema(tool.outputSchema)
56
+ }))
57
+ };
58
+ }
59
+
60
+ // src/stacks/tanstack/extract-manifest.ts
61
+ var MANIFEST_VERSION = 1;
62
+ var MANIFEST_RELATIVE_PATH = ".lovable/mcp/manifest.json";
63
+ function manifestAuth(auth) {
64
+ if (!auth)
65
+ return { type: "none" };
66
+ const oauth = { type: "oauth", issuer: auth.issuer };
67
+ if (auth.resource !== void 0)
68
+ oauth.resource = auth.resource;
69
+ if (auth.resourceName !== void 0)
70
+ oauth.resource_name = auth.resourceName;
71
+ if (auth.resourceDocumentation !== void 0)
72
+ oauth.resource_documentation = auth.resourceDocumentation;
73
+ if (auth.protectedResourceMetadataUrl !== void 0)
74
+ oauth.protected_resource_metadata_url = auth.protectedResourceMetadataUrl;
75
+ if (auth.requireOAuthClientClaim !== void 0)
76
+ oauth.require_oauth_client_claim = auth.requireOAuthClientClaim;
77
+ if (auth.acceptedAudiences !== void 0)
78
+ oauth.accepted_audiences = auth.acceptedAudiences;
79
+ if (auth.requiredScopes !== void 0)
80
+ oauth.required_scopes = auth.requiredScopes;
81
+ if (auth.jwksUri !== void 0)
82
+ oauth.jwks_uri = auth.jwksUri;
83
+ if (auth.algorithms !== void 0)
84
+ oauth.algorithms = auth.algorithms;
85
+ if (auth.clockToleranceSeconds !== void 0)
86
+ oauth.clock_tolerance_seconds = auth.clockToleranceSeconds;
87
+ return oauth;
88
+ }
89
+ function isToolShape(tool) {
90
+ if (typeof tool !== "object" || tool === null)
91
+ return false;
92
+ const t = tool;
93
+ return typeof t.name === "string" && typeof t.title === "string" && typeof t.description === "string";
94
+ }
95
+ function isAuthShape(auth) {
96
+ if (auth === void 0)
97
+ return true;
98
+ if (typeof auth !== "object" || auth === null)
99
+ return false;
100
+ const a = auth;
101
+ return a.type === "oauth" && typeof a.issuer === "string" && a.issuer.length > 0;
102
+ }
103
+ function manifestFromDefinition(definition, urlPath, source = "the MCP entry") {
104
+ const def = definition;
105
+ const validShape = !!def && typeof def === "object" && typeof def.name === "string" && typeof def.title === "string" && typeof def.version === "string" && Array.isArray(def.tools) && def.tools.every(isToolShape) && isAuthShape(def.auth);
106
+ if (!validShape) {
107
+ const got = definition == null ? "no default export" : typeof definition;
108
+ throw new Error(`@lovable.dev/mcp-js: ${source} must \`export default defineMcp(...)\` (got ${got}).`);
109
+ }
110
+ return {
111
+ version: MANIFEST_VERSION,
112
+ sdk_version: version,
113
+ path: urlPath,
114
+ auth: manifestAuth(def.auth),
115
+ mcp: buildMcpListing(def)
116
+ };
117
+ }
118
+
119
+ // src/stacks/tanstack/fs-errors.ts
120
+ function isFileMissing(err) {
121
+ return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
122
+ }
123
+
124
+ // src/stacks/tanstack/ssr-loader.ts
125
+ var import_vite = require("vite");
126
+ async function produceViaSsr(projectRoot2, alias, entryAbs, urlPath) {
127
+ const server = await (0, import_vite.createServer)({
128
+ configFile: false,
129
+ root: projectRoot2,
130
+ logLevel: "silent",
131
+ server: { middlewareMode: true, hmr: false, watch: null },
132
+ optimizeDeps: { noDiscovery: true },
133
+ resolve: { alias }
134
+ });
135
+ try {
136
+ const mod = await server.ssrLoadModule(entryAbs);
137
+ return manifestFromDefinition(mod.default, urlPath, entryAbs);
138
+ } finally {
139
+ await server.close();
140
+ }
141
+ }
142
+
143
+ // src/stacks/tanstack/manifest-io.ts
144
+ var MCP_PLUGIN_NAME = "@lovable.dev/mcp-js";
145
+ var DEFAULT_MCP_ENTRY = "src/lib/mcp/index.ts";
146
+ var DEFAULT_MCP_PATH = "/mcp";
147
+ function writeManifestIfChanged(file, manifest) {
148
+ const content = JSON.stringify(manifest, null, 2) + "\n";
149
+ let existing;
150
+ try {
151
+ existing = (0, import_node_fs.readFileSync)(file, "utf8");
152
+ } catch (err) {
153
+ if (!isFileMissing(err))
154
+ throw err;
155
+ }
156
+ if (existing === content)
157
+ return;
158
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(file), { recursive: true });
159
+ const tmp = `${file}.${(0, import_node_crypto.randomUUID)()}.tmp`;
160
+ try {
161
+ (0, import_node_fs.writeFileSync)(tmp, content, "utf8");
162
+ (0, import_node_fs.renameSync)(tmp, file);
163
+ } catch (err) {
164
+ try {
165
+ (0, import_node_fs.unlinkSync)(tmp);
166
+ } catch {
167
+ }
168
+ throw err;
169
+ }
170
+ }
171
+ function removeManifest(file) {
172
+ try {
173
+ (0, import_node_fs.unlinkSync)(file);
174
+ } catch (err) {
175
+ if (!isFileMissing(err))
176
+ throw err;
177
+ }
178
+ }
179
+ async function syncManifest(manifestFile, entryAbs, urlPath, produce) {
180
+ let entryExists = true;
181
+ try {
182
+ (0, import_node_fs.lstatSync)(entryAbs);
183
+ } catch (err) {
184
+ if (!isFileMissing(err))
185
+ throw err;
186
+ entryExists = false;
187
+ }
188
+ if (!entryExists) {
189
+ removeManifest(manifestFile);
190
+ return;
191
+ }
192
+ const manifest = await produce(entryAbs, urlPath);
193
+ writeManifestIfChanged(manifestFile, manifest);
194
+ }
195
+ async function runExtract(projectRoot2) {
196
+ const resolved = await (0, import_vite2.resolveConfig)({ root: projectRoot2, logLevel: "silent" }, "serve");
197
+ const alias = resolved.resolve?.alias ?? [];
198
+ const pluginApi = resolved.plugins.find((p) => p.name === MCP_PLUGIN_NAME)?.api;
199
+ const entryAbs = pluginApi?.mcpEntry ?? (0, import_node_path.resolve)(projectRoot2, DEFAULT_MCP_ENTRY);
200
+ const urlPath = pluginApi?.urlPath ?? DEFAULT_MCP_PATH;
201
+ await syncManifest(
202
+ (0, import_node_path.resolve)(projectRoot2, MANIFEST_RELATIVE_PATH),
203
+ entryAbs,
204
+ urlPath,
205
+ (entry, path) => produceViaSsr(projectRoot2, alias, entry, path)
206
+ );
207
+ }
208
+
209
+ // src/stacks/tanstack/cli/extract-manifest.ts
210
+ var projectRoot = process.argv[2] ?? process.cwd();
211
+ runExtract(projectRoot).catch((err) => {
212
+ console.error(err instanceof Error ? err.message : String(err));
213
+ process.exit(1);
214
+ });
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildMcpListing
4
+ } from "../../../chunk-E335BBVM.js";
5
+ import "../../../chunk-HRMLGCXV.js";
6
+ import "../../../chunk-QC3DXQTH.js";
7
+ import {
8
+ isFileMissing
9
+ } from "../../../chunk-QJ7XEKT3.js";
10
+ import "../../../chunk-6DXGZZA4.js";
11
+
12
+ // src/stacks/tanstack/manifest-io.ts
13
+ import { randomUUID } from "crypto";
14
+ import { lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
15
+ import { dirname, resolve } from "path";
16
+ import { resolveConfig } from "vite";
17
+
18
+ // package.json
19
+ var version = "0.8.0";
20
+
21
+ // src/stacks/tanstack/extract-manifest.ts
22
+ var MANIFEST_VERSION = 1;
23
+ var MANIFEST_RELATIVE_PATH = ".lovable/mcp/manifest.json";
24
+ function manifestAuth(auth) {
25
+ if (!auth)
26
+ return { type: "none" };
27
+ const oauth = { type: "oauth", issuer: auth.issuer };
28
+ if (auth.resource !== void 0)
29
+ oauth.resource = auth.resource;
30
+ if (auth.resourceName !== void 0)
31
+ oauth.resource_name = auth.resourceName;
32
+ if (auth.resourceDocumentation !== void 0)
33
+ oauth.resource_documentation = auth.resourceDocumentation;
34
+ if (auth.protectedResourceMetadataUrl !== void 0)
35
+ oauth.protected_resource_metadata_url = auth.protectedResourceMetadataUrl;
36
+ if (auth.requireOAuthClientClaim !== void 0)
37
+ oauth.require_oauth_client_claim = auth.requireOAuthClientClaim;
38
+ if (auth.acceptedAudiences !== void 0)
39
+ oauth.accepted_audiences = auth.acceptedAudiences;
40
+ if (auth.requiredScopes !== void 0)
41
+ oauth.required_scopes = auth.requiredScopes;
42
+ if (auth.jwksUri !== void 0)
43
+ oauth.jwks_uri = auth.jwksUri;
44
+ if (auth.algorithms !== void 0)
45
+ oauth.algorithms = auth.algorithms;
46
+ if (auth.clockToleranceSeconds !== void 0)
47
+ oauth.clock_tolerance_seconds = auth.clockToleranceSeconds;
48
+ return oauth;
49
+ }
50
+ function isToolShape(tool) {
51
+ if (typeof tool !== "object" || tool === null)
52
+ return false;
53
+ const t = tool;
54
+ return typeof t.name === "string" && typeof t.title === "string" && typeof t.description === "string";
55
+ }
56
+ function isAuthShape(auth) {
57
+ if (auth === void 0)
58
+ return true;
59
+ if (typeof auth !== "object" || auth === null)
60
+ return false;
61
+ const a = auth;
62
+ return a.type === "oauth" && typeof a.issuer === "string" && a.issuer.length > 0;
63
+ }
64
+ function manifestFromDefinition(definition, urlPath, source = "the MCP entry") {
65
+ const def = definition;
66
+ const validShape = !!def && typeof def === "object" && typeof def.name === "string" && typeof def.title === "string" && typeof def.version === "string" && Array.isArray(def.tools) && def.tools.every(isToolShape) && isAuthShape(def.auth);
67
+ if (!validShape) {
68
+ const got = definition == null ? "no default export" : typeof definition;
69
+ throw new Error(`@lovable.dev/mcp-js: ${source} must \`export default defineMcp(...)\` (got ${got}).`);
70
+ }
71
+ return {
72
+ version: MANIFEST_VERSION,
73
+ sdk_version: version,
74
+ path: urlPath,
75
+ auth: manifestAuth(def.auth),
76
+ mcp: buildMcpListing(def)
77
+ };
78
+ }
79
+
80
+ // src/stacks/tanstack/ssr-loader.ts
81
+ import { createServer } from "vite";
82
+ async function produceViaSsr(projectRoot2, alias, entryAbs, urlPath) {
83
+ const server = await createServer({
84
+ configFile: false,
85
+ root: projectRoot2,
86
+ logLevel: "silent",
87
+ server: { middlewareMode: true, hmr: false, watch: null },
88
+ optimizeDeps: { noDiscovery: true },
89
+ resolve: { alias }
90
+ });
91
+ try {
92
+ const mod = await server.ssrLoadModule(entryAbs);
93
+ return manifestFromDefinition(mod.default, urlPath, entryAbs);
94
+ } finally {
95
+ await server.close();
96
+ }
97
+ }
98
+
99
+ // src/stacks/tanstack/manifest-io.ts
100
+ var MCP_PLUGIN_NAME = "@lovable.dev/mcp-js";
101
+ var DEFAULT_MCP_ENTRY = "src/lib/mcp/index.ts";
102
+ var DEFAULT_MCP_PATH = "/mcp";
103
+ function writeManifestIfChanged(file, manifest) {
104
+ const content = JSON.stringify(manifest, null, 2) + "\n";
105
+ let existing;
106
+ try {
107
+ existing = readFileSync(file, "utf8");
108
+ } catch (err) {
109
+ if (!isFileMissing(err))
110
+ throw err;
111
+ }
112
+ if (existing === content)
113
+ return;
114
+ mkdirSync(dirname(file), { recursive: true });
115
+ const tmp = `${file}.${randomUUID()}.tmp`;
116
+ try {
117
+ writeFileSync(tmp, content, "utf8");
118
+ renameSync(tmp, file);
119
+ } catch (err) {
120
+ try {
121
+ unlinkSync(tmp);
122
+ } catch {
123
+ }
124
+ throw err;
125
+ }
126
+ }
127
+ function removeManifest(file) {
128
+ try {
129
+ unlinkSync(file);
130
+ } catch (err) {
131
+ if (!isFileMissing(err))
132
+ throw err;
133
+ }
134
+ }
135
+ async function syncManifest(manifestFile, entryAbs, urlPath, produce) {
136
+ let entryExists = true;
137
+ try {
138
+ lstatSync(entryAbs);
139
+ } catch (err) {
140
+ if (!isFileMissing(err))
141
+ throw err;
142
+ entryExists = false;
143
+ }
144
+ if (!entryExists) {
145
+ removeManifest(manifestFile);
146
+ return;
147
+ }
148
+ const manifest = await produce(entryAbs, urlPath);
149
+ writeManifestIfChanged(manifestFile, manifest);
150
+ }
151
+ async function runExtract(projectRoot2) {
152
+ const resolved = await resolveConfig({ root: projectRoot2, logLevel: "silent" }, "serve");
153
+ const alias = resolved.resolve?.alias ?? [];
154
+ const pluginApi = resolved.plugins.find((p) => p.name === MCP_PLUGIN_NAME)?.api;
155
+ const entryAbs = pluginApi?.mcpEntry ?? resolve(projectRoot2, DEFAULT_MCP_ENTRY);
156
+ const urlPath = pluginApi?.urlPath ?? DEFAULT_MCP_PATH;
157
+ await syncManifest(
158
+ resolve(projectRoot2, MANIFEST_RELATIVE_PATH),
159
+ entryAbs,
160
+ urlPath,
161
+ (entry, path) => produceViaSsr(projectRoot2, alias, entry, path)
162
+ );
163
+ }
164
+
165
+ // src/stacks/tanstack/cli/extract-manifest.ts
166
+ var projectRoot = process.argv[2] ?? process.cwd();
167
+ runExtract(projectRoot).catch((err) => {
168
+ console.error(err instanceof Error ? err.message : String(err));
169
+ process.exit(1);
170
+ });
@@ -293,6 +293,7 @@ function stringClaim(claims, name) {
293
293
  // src/auth/verifier.ts
294
294
  var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
295
295
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
296
+ var JWKS_FETCH_TIMEOUT_MS = 5e3;
296
297
  var OAuthTokenError = class extends Error {
297
298
  constructor(status, oauthError, message) {
298
299
  super(message);
@@ -312,12 +313,17 @@ function tokenHeaderFields(token) {
312
313
  return { tokenLength: token.length };
313
314
  }
314
315
  }
315
- function isJwksFetchFailure(err) {
316
- if (err instanceof import_jose.errors.JWKSTimeout || err instanceof import_jose.errors.JWKSInvalid)
317
- return true;
318
- if (!(err instanceof import_jose.errors.JOSEError))
319
- return err instanceof Error;
320
- return err.code === "ERR_JOSE_GENERIC";
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
+ }
321
327
  }
322
328
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
323
329
  try {
@@ -332,10 +338,6 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
332
338
  });
333
339
  return payload;
334
340
  } catch (err) {
335
- if (isJwksFetchFailure(err)) {
336
- log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
337
- throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
338
- }
339
341
  log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
340
342
  throw err;
341
343
  }
@@ -380,8 +382,8 @@ function createOAuthTokenVerifier(auth, discovery) {
380
382
  const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
381
383
  log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
382
384
  const jwksUri = await discovery.resolveJwksUri();
383
- log.debug("oauth.jwks.keyset_created", { jwksUri });
384
- const keySet = (0, import_jose.createRemoteJWKSet)(new URL(jwksUri));
385
+ log.debug("oauth.jwks.fetch", { jwksUri });
386
+ const keySet = await fetchVerificationKeySet(jwksUri);
385
387
  const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
386
388
  assertNonEmptySubject(claims);
387
389
  const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
@@ -716,6 +718,19 @@ function shapeToJsonSchema(shape) {
716
718
  return null;
717
719
  }
718
720
  }
721
+ function buildMcpListing(mcp) {
722
+ return {
723
+ server: { name: mcp.name, version: mcp.version, title: mcp.title },
724
+ tools: mcp.tools.map((tool) => ({
725
+ name: tool.name,
726
+ title: tool.title,
727
+ description: tool.description,
728
+ annotations: tool.annotations,
729
+ inputSchema: shapeToJsonSchema(tool.inputSchema),
730
+ outputSchema: shapeToJsonSchema(tool.outputSchema)
731
+ }))
732
+ };
733
+ }
719
734
  function createListToolsHandler(mcp, options = {}) {
720
735
  assertRestResourceBinding(mcp, options);
721
736
  const authorizer = createRequestAuthorizer(mcp, options);
@@ -725,18 +740,7 @@ function createListToolsHandler(mcp, options = {}) {
725
740
  return authResult.response;
726
741
  if (request.method !== "GET" && request.method !== "HEAD")
727
742
  return methodNotAllowed("GET, HEAD, OPTIONS");
728
- const body = {
729
- server: { name: mcp.name, version: mcp.version, title: mcp.title },
730
- tools: mcp.tools.map((tool) => ({
731
- name: tool.name,
732
- title: tool.title,
733
- description: tool.description,
734
- annotations: tool.annotations,
735
- inputSchema: shapeToJsonSchema(tool.inputSchema),
736
- outputSchema: shapeToJsonSchema(tool.outputSchema)
737
- }))
738
- };
739
- const response = Response.json(body);
743
+ const response = Response.json(buildMcpListing(mcp));
740
744
  return request.method === "HEAD" ? headResponse(response) : response;
741
745
  };
742
746
  return async (request) => {
@@ -1,15 +1,17 @@
1
1
  import {
2
2
  createMcpProtocolHandler
3
- } from "../../chunk-G5GWSV5D.js";
3
+ } from "../../chunk-3XPKR4G6.js";
4
4
  import {
5
5
  createOAuthProtectedResourceMetadataHandler
6
- } from "../../chunk-722HKLIU.js";
6
+ } from "../../chunk-W7F6JRDB.js";
7
7
  import {
8
- createInvokeToolHandler,
9
- createListToolsHandler
10
- } from "../../chunk-53M2X7FU.js";
8
+ createInvokeToolHandler
9
+ } from "../../chunk-4Y72OCPS.js";
11
10
  import "../../chunk-MA5H6PSF.js";
12
- import "../../chunk-NTXHOUK6.js";
11
+ import {
12
+ createListToolsHandler
13
+ } from "../../chunk-E335BBVM.js";
14
+ import "../../chunk-HRMLGCXV.js";
13
15
  import "../../chunk-QC3DXQTH.js";
14
16
  import "../../chunk-6DXGZZA4.js";
15
17
 
@@ -32,11 +32,13 @@ var import_node_path = require("path");
32
32
  // src/auth/metadata-path.ts
33
33
  var OAUTH_PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
34
34
 
35
- // src/stacks/tanstack/vite.ts
36
- var GENERATED_BANNER = "// AUTO-GENERATED by @lovable.dev/mcp-js \u2014 do not edit. Regenerated by the Vite plugin.\n// To take ownership, delete this banner line; the plugin then leaves the file alone.";
35
+ // src/stacks/tanstack/fs-errors.ts
37
36
  function isFileMissing(err) {
38
37
  return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
39
38
  }
39
+
40
+ // src/stacks/tanstack/vite.ts
41
+ var GENERATED_BANNER = "// AUTO-GENERATED by @lovable.dev/mcp-js \u2014 do not edit. Regenerated by the Vite plugin.\n// To take ownership, delete this banner line; the plugin then leaves the file alone.";
40
42
  function normalizePath(p) {
41
43
  return p.split(import_node_path.sep).join("/");
42
44
  }
@@ -251,6 +253,18 @@ function mcpPlugin(options = {}) {
251
253
  };
252
254
  return {
253
255
  name: "@lovable.dev/mcp-js",
256
+ // The extract CLI reads the resolved entry + URL path back through this
257
+ // (via `resolveConfig().plugins`) so the manifest honors custom
258
+ // `mcpEntry`/`path` instead of assuming the defaults. Getters because
259
+ // `mcpEntry` is finalized in `configResolved`.
260
+ api: {
261
+ get mcpEntry() {
262
+ return mcpEntry;
263
+ },
264
+ get urlPath() {
265
+ return urlPath;
266
+ }
267
+ },
254
268
  configResolved(config) {
255
269
  projectRoot = config.root;
256
270
  mcpEntry = (0, import_node_path.resolve)(projectRoot, mcpEntryOption);
@@ -262,19 +276,18 @@ function mcpPlugin(options = {}) {
262
276
  regenerate();
263
277
  },
264
278
  configureServer(server) {
265
- const watchedEntry = mcpEntry;
266
- const onEntryChange = (file) => {
267
- if (normalizePath(file) === normalizePath(watchedEntry)) {
279
+ const onChange = (file) => {
280
+ if (normalizePath(file) === normalizePath(mcpEntry)) {
268
281
  regenerate();
269
282
  }
270
283
  };
271
- server.watcher.on("add", onEntryChange);
272
- server.watcher.on("change", onEntryChange);
273
- server.watcher.on("unlink", onEntryChange);
284
+ server.watcher.on("add", onChange);
285
+ server.watcher.on("change", onChange);
286
+ server.watcher.on("unlink", onChange);
274
287
  server.watcher.once("close", () => {
275
- server.watcher.off("add", onEntryChange);
276
- server.watcher.off("change", onEntryChange);
277
- server.watcher.off("unlink", onEntryChange);
288
+ server.watcher.off("add", onChange);
289
+ server.watcher.off("change", onChange);
290
+ server.watcher.off("unlink", onChange);
278
291
  });
279
292
  },
280
293
  buildStart() {
@@ -49,6 +49,10 @@ interface McpPluginOptions {
49
49
  */
50
50
  protectedResourceMetadataRoute?: boolean;
51
51
  }
52
+ interface McpPluginApi {
53
+ readonly mcpEntry: string;
54
+ readonly urlPath: string;
55
+ }
52
56
  declare function assertUrlPathShape(urlPath: string): string;
53
57
  declare function deriveRouteFileName(urlPath: string): string;
54
58
  /**
@@ -57,8 +61,12 @@ declare function deriveRouteFileName(urlPath: string): string;
57
61
  * Place this in your `vite.config.ts` plugins array. Emits during
58
62
  * `configResolved` (so dev-server boot and the TanStack file-router both
59
63
  * see the routes on the first request) and re-emits when the MCP entry
60
- * file changes during dev.
64
+ * file appears or disappears during dev.
65
+ *
66
+ * The `.lovable/mcp/manifest.json` snapshot is produced separately by the
67
+ * `lovable-mcp-extract-manifest` CLI (run by a host platform at build/commit
68
+ * time) — not by this plugin.
61
69
  */
62
70
  declare function mcpPlugin(options?: McpPluginOptions): Plugin;
63
71
 
64
- export { type McpPluginOptions, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
72
+ export { type McpPluginApi, type McpPluginOptions, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
@@ -49,6 +49,10 @@ interface McpPluginOptions {
49
49
  */
50
50
  protectedResourceMetadataRoute?: boolean;
51
51
  }
52
+ interface McpPluginApi {
53
+ readonly mcpEntry: string;
54
+ readonly urlPath: string;
55
+ }
52
56
  declare function assertUrlPathShape(urlPath: string): string;
53
57
  declare function deriveRouteFileName(urlPath: string): string;
54
58
  /**
@@ -57,8 +61,12 @@ declare function deriveRouteFileName(urlPath: string): string;
57
61
  * Place this in your `vite.config.ts` plugins array. Emits during
58
62
  * `configResolved` (so dev-server boot and the TanStack file-router both
59
63
  * see the routes on the first request) and re-emits when the MCP entry
60
- * file changes during dev.
64
+ * file appears or disappears during dev.
65
+ *
66
+ * The `.lovable/mcp/manifest.json` snapshot is produced separately by the
67
+ * `lovable-mcp-extract-manifest` CLI (run by a host platform at build/commit
68
+ * time) — not by this plugin.
61
69
  */
62
70
  declare function mcpPlugin(options?: McpPluginOptions): Plugin;
63
71
 
64
- export { type McpPluginOptions, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
72
+ export { type McpPluginApi, type McpPluginOptions, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
@@ -1,3 +1,6 @@
1
+ import {
2
+ isFileMissing
3
+ } from "../../chunk-QJ7XEKT3.js";
1
4
  import {
2
5
  OAUTH_PROTECTED_RESOURCE_METADATA_PATH
3
6
  } from "../../chunk-6DXGZZA4.js";
@@ -6,9 +9,6 @@ import {
6
9
  import { lstatSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
7
10
  import { dirname, join, relative, resolve, sep } from "path";
8
11
  var GENERATED_BANNER = "// AUTO-GENERATED by @lovable.dev/mcp-js \u2014 do not edit. Regenerated by the Vite plugin.\n// To take ownership, delete this banner line; the plugin then leaves the file alone.";
9
- function isFileMissing(err) {
10
- return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
11
- }
12
12
  function normalizePath(p) {
13
13
  return p.split(sep).join("/");
14
14
  }
@@ -223,6 +223,18 @@ function mcpPlugin(options = {}) {
223
223
  };
224
224
  return {
225
225
  name: "@lovable.dev/mcp-js",
226
+ // The extract CLI reads the resolved entry + URL path back through this
227
+ // (via `resolveConfig().plugins`) so the manifest honors custom
228
+ // `mcpEntry`/`path` instead of assuming the defaults. Getters because
229
+ // `mcpEntry` is finalized in `configResolved`.
230
+ api: {
231
+ get mcpEntry() {
232
+ return mcpEntry;
233
+ },
234
+ get urlPath() {
235
+ return urlPath;
236
+ }
237
+ },
226
238
  configResolved(config) {
227
239
  projectRoot = config.root;
228
240
  mcpEntry = resolve(projectRoot, mcpEntryOption);
@@ -234,19 +246,18 @@ function mcpPlugin(options = {}) {
234
246
  regenerate();
235
247
  },
236
248
  configureServer(server) {
237
- const watchedEntry = mcpEntry;
238
- const onEntryChange = (file) => {
239
- if (normalizePath(file) === normalizePath(watchedEntry)) {
249
+ const onChange = (file) => {
250
+ if (normalizePath(file) === normalizePath(mcpEntry)) {
240
251
  regenerate();
241
252
  }
242
253
  };
243
- server.watcher.on("add", onEntryChange);
244
- server.watcher.on("change", onEntryChange);
245
- server.watcher.on("unlink", onEntryChange);
254
+ server.watcher.on("add", onChange);
255
+ server.watcher.on("change", onChange);
256
+ server.watcher.on("unlink", onChange);
246
257
  server.watcher.once("close", () => {
247
- server.watcher.off("add", onEntryChange);
248
- server.watcher.off("change", onEntryChange);
249
- server.watcher.off("unlink", onEntryChange);
258
+ server.watcher.off("add", onChange);
259
+ server.watcher.off("change", onChange);
260
+ server.watcher.off("unlink", onChange);
250
261
  });
251
262
  },
252
263
  buildStart() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovable.dev/mcp-js",
3
- "version": "0.7.0",
3
+ "version": "0.8.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": {
@@ -50,6 +50,9 @@
50
50
  "require": "./dist/stacks/tanstack/vite.cjs"
51
51
  }
52
52
  },
53
+ "bin": {
54
+ "lovable-mcp-extract-manifest": "./dist/stacks/tanstack/cli/extract-manifest.cjs"
55
+ },
53
56
  "files": [
54
57
  "dist"
55
58
  ],
@@ -79,8 +82,8 @@
79
82
  "zod": "^4.1.13"
80
83
  },
81
84
  "scripts": {
82
- "build": "tsup src/index.ts src/protocols/mcp/index.ts src/protocols/oauth-metadata.ts src/protocols/rest/index.ts src/stacks/tanstack/index.ts src/stacks/tanstack/vite.ts --format cjs,esm --dts --outDir dist",
83
- "dev": "tsup src/index.ts src/protocols/mcp/index.ts src/protocols/oauth-metadata.ts src/protocols/rest/index.ts src/stacks/tanstack/index.ts src/stacks/tanstack/vite.ts --format cjs,esm --dts --watch",
85
+ "build": "tsup src/index.ts src/protocols/mcp/index.ts src/protocols/oauth-metadata.ts src/protocols/rest/index.ts src/stacks/tanstack/index.ts src/stacks/tanstack/vite.ts src/stacks/tanstack/cli/extract-manifest.ts --format cjs,esm --dts --outDir dist",
86
+ "dev": "tsup src/index.ts src/protocols/mcp/index.ts src/protocols/oauth-metadata.ts src/protocols/rest/index.ts src/stacks/tanstack/index.ts src/stacks/tanstack/vite.ts src/stacks/tanstack/cli/extract-manifest.ts --format cjs,esm --dts --watch",
84
87
  "typecheck": "tsgo --noEmit",
85
88
  "format": "oxfmt --write src/ tests/",
86
89
  "format:check": "oxfmt --check src/ tests/",