@lovable.dev/mcp-js 0.8.0 → 0.9.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.
Files changed (34) hide show
  1. package/README.md +3 -1
  2. package/dist/authorize-HTd0GKmB.d.ts +18 -0
  3. package/dist/{chunk-E335BBVM.js → chunk-2RGXCDBU.js} +1 -1
  4. package/dist/{chunk-W7F6JRDB.js → chunk-4JO3BZQG.js} +1 -1
  5. package/dist/{chunk-4Y72OCPS.js → chunk-6N7B7NZY.js} +1 -1
  6. package/dist/{chunk-3XPKR4G6.js → chunk-OXSSMKQE.js} +1 -1
  7. package/dist/{chunk-HRMLGCXV.js → chunk-YQBOGY7D.js} +37 -12
  8. package/dist/index.d.cts +2 -2
  9. package/dist/index.d.ts +2 -2
  10. package/dist/protocols/mcp/index.cjs +37 -12
  11. package/dist/protocols/mcp/index.d.cts +2 -2
  12. package/dist/protocols/mcp/index.d.ts +2 -2
  13. package/dist/protocols/mcp/index.js +2 -2
  14. package/dist/protocols/oauth-metadata.cjs +11 -3
  15. package/dist/protocols/oauth-metadata.d.cts +2 -2
  16. package/dist/protocols/oauth-metadata.d.ts +2 -2
  17. package/dist/protocols/oauth-metadata.js +2 -2
  18. package/dist/protocols/rest/index.cjs +37 -12
  19. package/dist/protocols/rest/index.d.cts +2 -2
  20. package/dist/protocols/rest/index.d.ts +2 -2
  21. package/dist/protocols/rest/index.js +3 -3
  22. package/dist/stacks/tanstack/cli/extract-manifest.cjs +1 -1
  23. package/dist/stacks/tanstack/cli/extract-manifest.js +3 -3
  24. package/dist/stacks/tanstack/index.cjs +37 -12
  25. package/dist/stacks/tanstack/index.d.cts +2 -2
  26. package/dist/stacks/tanstack/index.d.ts +2 -2
  27. package/dist/stacks/tanstack/index.js +5 -5
  28. package/dist/stacks/tanstack/vite.cjs +43 -8
  29. package/dist/stacks/tanstack/vite.d.cts +17 -1
  30. package/dist/stacks/tanstack/vite.d.ts +17 -1
  31. package/dist/stacks/tanstack/vite.js +42 -8
  32. package/dist/{types-zMlr1mOK.d.ts → types-C5SSORxe.d.ts} +4 -0
  33. package/package.json +1 -1
  34. package/dist/authorize-DpTEIFvs.d.ts +0 -9
package/README.md CHANGED
@@ -53,6 +53,8 @@ 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
+ To serve the metadata from a different path — e.g. a workspace-private project where only `/api/public/*` is anonymously reachable and `/.well-known/...` is gated — pass `mcpPlugin({ metadataPath: "/api/public/.well-known/oauth-protected-resource" })`. The plugin emits the route there and injects the same path into every handler, so the 401 `WWW-Authenticate` challenge advertises exactly what it serves. `metadataPath` is mutually exclusive with `auth.protectedResourceMetadataUrl` (and with `protectedResourceMetadataRoute: false`).
57
+
56
58
  ### `.lovable/mcp/manifest.json`
57
59
 
58
60
  `.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.
@@ -182,7 +184,7 @@ Use an OAuth access token from the configured authorization server for MCP calls
182
184
  | `algorithms` | Accepted JWT signing algorithms. Defaults to asymmetric algorithms (`RS*`, `ES*`, `EdDSA`); set explicitly only for a narrower policy. |
183
185
  | `clockToleranceSeconds` | Allowed JWT clock skew for `exp`/`nbf` checks, in seconds. Defaults to `30`; max `300`. |
184
186
  | `resourceName`, `resourceDocumentation` | Optional metadata fields for MCP clients and humans. `resourceName` defaults to the MCP server `title`. |
185
- | `protectedResourceMetadataUrl` | Absolute HTTPS URL of an externally hosted RFC 9728 protected-resource metadata document. When set, the SDK advertises this URL in the 401 `WWW-Authenticate: resource_metadata` challenge and stops generating its own metadata — the `/.well-known/oauth-protected-resource` handler returns 404. Use it when the resource server already publishes its own PRM; you'll typically also pass `protectedResourceMetadataRoute: false` to the Vite plugin so the now-unused route isn't emitted. |
187
+ | `protectedResourceMetadataUrl` | Absolute HTTPS URL of an externally hosted RFC 9728 protected-resource metadata document. When set, the SDK advertises this URL in the 401 `WWW-Authenticate: resource_metadata` challenge and stops generating its own metadata — the `/.well-known/oauth-protected-resource` handler returns 404. Use it when the resource server already publishes its own PRM; you'll typically also pass `protectedResourceMetadataRoute: false` to the Vite plugin so the now-unused route isn't emitted. Mutually exclusive with the plugin's `metadataPath` (which has the SDK serve its own document at a custom path); configuring both throws at handler construction. |
186
188
 
187
189
  Protected-resource metadata is derived from the same config. When `resource` is omitted, the published resource is the incoming request origin plus the MCP route path the stack adapter supplies as `resourcePath` (for example, `https://my-app.lovable.app/mcp`); `authorization_servers` is the configured issuer, and `resource_name` is `defineMcp({ title })` unless `resourceName` overrides it. Because the metadata endpoint is served from `/.well-known/...`, it can't infer the resource from its own path — the handler throws at construction when neither `resource` nor `resourcePath` is set (the generated TanStack routes always pass `resourcePath`). For Supabase auth, this metadata still names the MCP resource even though Supabase JWTs use the project audience `"authenticated"`.
188
190
 
@@ -0,0 +1,18 @@
1
+ interface McpRuntimeOptions {
2
+ /**
3
+ * Public MCP protocol path. REST companion handlers pass this so OAuth
4
+ * resource/audience checks bind to `/mcp`, not internal `/.mcp/*` RPC URLs.
5
+ */
6
+ resourcePath?: string;
7
+ /**
8
+ * Host-relative path of the SDK-served protected-resource metadata document.
9
+ * The Vite plugin injects it into every emitted handler when it generates the
10
+ * PRM route, so the 401 challenge advertises the same path the route serves.
11
+ * Its presence is the signal that the SDK owns the PRM document, so it is
12
+ * mutually exclusive with `auth.protectedResourceMetadataUrl` (which points at
13
+ * an externally hosted document). Defaults to the well-known path when absent.
14
+ */
15
+ metadataPath?: string;
16
+ }
17
+
18
+ export type { McpRuntimeOptions as M };
@@ -5,7 +5,7 @@ import {
5
5
  headResponse,
6
6
  methodNotAllowed,
7
7
  withCors
8
- } from "./chunk-HRMLGCXV.js";
8
+ } from "./chunk-YQBOGY7D.js";
9
9
 
10
10
  // src/protocols/rest/list-tools.ts
11
11
  import { objectFromShape } from "@modelcontextprotocol/sdk/server/zod-compat.js";
@@ -7,7 +7,7 @@ import {
7
7
  oauthConfigurationErrorResponse,
8
8
  resolveProtectedResource,
9
9
  withCors
10
- } from "./chunk-HRMLGCXV.js";
10
+ } from "./chunk-YQBOGY7D.js";
11
11
  import {
12
12
  describeError,
13
13
  log
@@ -8,7 +8,7 @@ import {
8
8
  createRequestAuthorizer,
9
9
  methodNotAllowed,
10
10
  withCors
11
- } from "./chunk-HRMLGCXV.js";
11
+ } from "./chunk-YQBOGY7D.js";
12
12
 
13
13
  // src/protocols/rest/invoke-tool.ts
14
14
  import { getParseErrorMessage, objectFromShape, safeParseAsync } from "@modelcontextprotocol/sdk/server/zod-compat.js";
@@ -5,7 +5,7 @@ import {
5
5
  corsPreflightResponse,
6
6
  createRequestAuthorizer,
7
7
  withCors
8
- } from "./chunk-HRMLGCXV.js";
8
+ } from "./chunk-YQBOGY7D.js";
9
9
  import {
10
10
  describeError,
11
11
  log
@@ -30,10 +30,10 @@ function resolveProtectedResource(auth, request, options) {
30
30
  resourceURL.hash = "";
31
31
  return trimTrailingSlash(resourceURL.toString());
32
32
  }
33
- function assertResourcePathShape(resourcePath) {
33
+ function assertResourcePathShape(resourcePath, label = "resourcePath") {
34
34
  if (!resourcePath.startsWith("/") || resourcePath.startsWith("//") || resourcePath.includes("\\") || /(^|\/)\.\.(\/|$)/.test(resourcePath)) {
35
35
  throw new Error(
36
- `@lovable.dev/mcp-js: resourcePath must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`
36
+ `@lovable.dev/mcp-js: ${label} must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`
37
37
  );
38
38
  }
39
39
  }
@@ -225,6 +225,7 @@ async function fetchVerificationKeySet(jwksUri) {
225
225
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
226
226
  try {
227
227
  const { payload } = await jwtVerify(token, keySet, {
228
+ typ: "at+jwt",
228
229
  // `issuer` is trimmed of any trailing slash; accept both forms so a token whose
229
230
  // `iss` carries the slash the AS publishes still verifies.
230
231
  issuer: [issuer, `${issuer}/`],
@@ -299,16 +300,16 @@ function quoteAuthenticateParam(value) {
299
300
  const sanitized = value.replace(/[\u0000-\u001F\u007F]/g, "");
300
301
  return `"${sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
301
302
  }
302
- function resolveProtectedResourceMetadataUrl(auth, request) {
303
+ function resolveProtectedResourceMetadataUrl(auth, request, options) {
303
304
  if (auth.protectedResourceMetadataUrl !== void 0)
304
305
  return auth.protectedResourceMetadataUrl;
305
306
  const base = auth.resource ?? request.url;
306
- return new URL(OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
307
+ return new URL(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
307
308
  }
308
- function wwwAuthenticateHeader(auth, request, params = {}) {
309
+ function wwwAuthenticateHeader(auth, request, options, params = {}) {
309
310
  const values = [
310
311
  `realm=${quoteAuthenticateParam("mcp")}`,
311
- `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request))}`
312
+ `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request, options))}`
312
313
  ];
313
314
  if (auth.requiredScopes && auth.requiredScopes.length > 0) {
314
315
  values.push(`scope=${quoteAuthenticateParam(auth.requiredScopes.join(" "))}`);
@@ -319,9 +320,12 @@ function wwwAuthenticateHeader(auth, request, params = {}) {
319
320
  values.push(`error_description=${quoteAuthenticateParam(params.errorDescription)}`);
320
321
  return `Bearer ${values.join(", ")}`;
321
322
  }
322
- function challengeResponse(auth, request, status, oauthError, errorDescription) {
323
+ function challengeResponse(auth, request, options, status, oauthError, errorDescription) {
323
324
  const headers = new Headers(JSON_HEADERS);
324
- headers.set("WWW-Authenticate", wwwAuthenticateHeader(auth, request, { error: oauthError, errorDescription }));
325
+ headers.set(
326
+ "WWW-Authenticate",
327
+ wwwAuthenticateHeader(auth, request, options, { error: oauthError, errorDescription })
328
+ );
325
329
  headers.set("Cache-Control", "no-store");
326
330
  return new Response(JSON.stringify({ error: status === 403 ? "forbidden" : "unauthorized" }), {
327
331
  status,
@@ -349,10 +353,17 @@ function parseBearerToken(request) {
349
353
  function getOAuthRuntime(mcp, options = {}) {
350
354
  if (options.resourcePath !== void 0)
351
355
  assertResourcePathShape(options.resourcePath);
352
- const stableOptions = { resourcePath: options.resourcePath };
356
+ if (options.metadataPath !== void 0)
357
+ assertResourcePathShape(options.metadataPath, "metadataPath");
358
+ const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
353
359
  const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
354
360
  if (!auth)
355
361
  return { kind: "unconfigured", options: stableOptions };
362
+ if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
363
+ throw new Error(
364
+ `@lovable.dev/mcp-js: the Vite plugin generates protected-resource metadata (metadataPath set), so auth.protectedResourceMetadataUrl must not also be set. Drop protectedResourceMetadataUrl, or set protectedResourceMetadataRoute: false in mcpPlugin(...) to host the document yourself.`
365
+ );
366
+ }
356
367
  const discovery = createOAuthDiscoveryResolver(auth);
357
368
  return {
358
369
  kind: "configured",
@@ -391,7 +402,7 @@ function createRequestAuthorizer(mcp, options = {}) {
391
402
  const token = parseBearerToken(request);
392
403
  if (!token) {
393
404
  log.info("auth.no_bearer_token", { outcome: "401" });
394
- return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
405
+ return { ok: false, response: challengeResponse(runtime.auth, request, runtime.options, 401) };
395
406
  }
396
407
  try {
397
408
  const auth = await runtime.verify(token, request, runtime.options);
@@ -406,13 +417,27 @@ function createRequestAuthorizer(mcp, options = {}) {
406
417
  log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
407
418
  return {
408
419
  ok: false,
409
- response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
420
+ response: challengeResponse(
421
+ runtime.auth,
422
+ request,
423
+ runtime.options,
424
+ err.status,
425
+ err.oauthError,
426
+ err.message
427
+ )
410
428
  };
411
429
  }
412
430
  log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
413
431
  return {
414
432
  ok: false,
415
- response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
433
+ response: challengeResponse(
434
+ runtime.auth,
435
+ request,
436
+ runtime.options,
437
+ 401,
438
+ "invalid_token",
439
+ "Invalid access token"
440
+ )
416
441
  };
417
442
  }
418
443
  }
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, j as ToolDefinition, M as McpAuthConfig } from './types-zMlr1mOK.js';
2
- export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink, f as ResourceLinkIcon, T as TextContent, g as ToolAnnotations, h as ToolContent, i as ToolContext, k as ToolHandlerResult, l as ZodSchema } from './types-zMlr1mOK.js';
1
+ import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, j as ToolDefinition, M as McpAuthConfig } from './types-C5SSORxe.js';
2
+ export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink, f as ResourceLinkIcon, T as TextContent, g as ToolAnnotations, h as ToolContent, i as ToolContext, k as ToolHandlerResult, l as ZodSchema } from './types-C5SSORxe.js';
3
3
  import 'zod';
4
4
 
5
5
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, j as ToolDefinition, M as McpAuthConfig } from './types-zMlr1mOK.js';
2
- export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink, f as ResourceLinkIcon, T as TextContent, g as ToolAnnotations, h as ToolContent, i as ToolContext, k as ToolHandlerResult, l as ZodSchema } from './types-zMlr1mOK.js';
1
+ import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, j as ToolDefinition, M as McpAuthConfig } from './types-C5SSORxe.js';
2
+ export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink, f as ResourceLinkIcon, T as TextContent, g as ToolAnnotations, h as ToolContent, i as ToolContext, k as ToolHandlerResult, l as ZodSchema } from './types-C5SSORxe.js';
3
3
  import 'zod';
4
4
 
5
5
  /**
@@ -246,10 +246,10 @@ function resolveProtectedResource(auth, request, options) {
246
246
  resourceURL.hash = "";
247
247
  return trimTrailingSlash(resourceURL.toString());
248
248
  }
249
- function assertResourcePathShape(resourcePath) {
249
+ function assertResourcePathShape(resourcePath, label = "resourcePath") {
250
250
  if (!resourcePath.startsWith("/") || resourcePath.startsWith("//") || resourcePath.includes("\\") || /(^|\/)\.\.(\/|$)/.test(resourcePath)) {
251
251
  throw new Error(
252
- `@lovable.dev/mcp-js: resourcePath must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`
252
+ `@lovable.dev/mcp-js: ${label} must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`
253
253
  );
254
254
  }
255
255
  }
@@ -316,6 +316,7 @@ async function fetchVerificationKeySet(jwksUri) {
316
316
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
317
317
  try {
318
318
  const { payload } = await (0, import_jose.jwtVerify)(token, keySet, {
319
+ typ: "at+jwt",
319
320
  // `issuer` is trimmed of any trailing slash; accept both forms so a token whose
320
321
  // `iss` carries the slash the AS publishes still verifies.
321
322
  issuer: [issuer, `${issuer}/`],
@@ -390,16 +391,16 @@ function quoteAuthenticateParam(value) {
390
391
  const sanitized = value.replace(/[\u0000-\u001F\u007F]/g, "");
391
392
  return `"${sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
392
393
  }
393
- function resolveProtectedResourceMetadataUrl(auth, request) {
394
+ function resolveProtectedResourceMetadataUrl(auth, request, options) {
394
395
  if (auth.protectedResourceMetadataUrl !== void 0)
395
396
  return auth.protectedResourceMetadataUrl;
396
397
  const base = auth.resource ?? request.url;
397
- return new URL(OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
398
+ return new URL(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
398
399
  }
399
- function wwwAuthenticateHeader(auth, request, params = {}) {
400
+ function wwwAuthenticateHeader(auth, request, options, params = {}) {
400
401
  const values = [
401
402
  `realm=${quoteAuthenticateParam("mcp")}`,
402
- `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request))}`
403
+ `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request, options))}`
403
404
  ];
404
405
  if (auth.requiredScopes && auth.requiredScopes.length > 0) {
405
406
  values.push(`scope=${quoteAuthenticateParam(auth.requiredScopes.join(" "))}`);
@@ -410,9 +411,12 @@ function wwwAuthenticateHeader(auth, request, params = {}) {
410
411
  values.push(`error_description=${quoteAuthenticateParam(params.errorDescription)}`);
411
412
  return `Bearer ${values.join(", ")}`;
412
413
  }
413
- function challengeResponse(auth, request, status, oauthError, errorDescription) {
414
+ function challengeResponse(auth, request, options, status, oauthError, errorDescription) {
414
415
  const headers = new Headers(JSON_HEADERS);
415
- headers.set("WWW-Authenticate", wwwAuthenticateHeader(auth, request, { error: oauthError, errorDescription }));
416
+ headers.set(
417
+ "WWW-Authenticate",
418
+ wwwAuthenticateHeader(auth, request, options, { error: oauthError, errorDescription })
419
+ );
416
420
  headers.set("Cache-Control", "no-store");
417
421
  return new Response(JSON.stringify({ error: status === 403 ? "forbidden" : "unauthorized" }), {
418
422
  status,
@@ -440,10 +444,17 @@ function parseBearerToken(request) {
440
444
  function getOAuthRuntime(mcp, options = {}) {
441
445
  if (options.resourcePath !== void 0)
442
446
  assertResourcePathShape(options.resourcePath);
443
- const stableOptions = { resourcePath: options.resourcePath };
447
+ if (options.metadataPath !== void 0)
448
+ assertResourcePathShape(options.metadataPath, "metadataPath");
449
+ const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
444
450
  const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
445
451
  if (!auth)
446
452
  return { kind: "unconfigured", options: stableOptions };
453
+ if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
454
+ throw new Error(
455
+ `@lovable.dev/mcp-js: the Vite plugin generates protected-resource metadata (metadataPath set), so auth.protectedResourceMetadataUrl must not also be set. Drop protectedResourceMetadataUrl, or set protectedResourceMetadataRoute: false in mcpPlugin(...) to host the document yourself.`
456
+ );
457
+ }
447
458
  const discovery = createOAuthDiscoveryResolver(auth);
448
459
  return {
449
460
  kind: "configured",
@@ -474,7 +485,7 @@ function createRequestAuthorizer(mcp, options = {}) {
474
485
  const token = parseBearerToken(request);
475
486
  if (!token) {
476
487
  log.info("auth.no_bearer_token", { outcome: "401" });
477
- return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
488
+ return { ok: false, response: challengeResponse(runtime.auth, request, runtime.options, 401) };
478
489
  }
479
490
  try {
480
491
  const auth = await runtime.verify(token, request, runtime.options);
@@ -489,13 +500,27 @@ function createRequestAuthorizer(mcp, options = {}) {
489
500
  log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
490
501
  return {
491
502
  ok: false,
492
- response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
503
+ response: challengeResponse(
504
+ runtime.auth,
505
+ request,
506
+ runtime.options,
507
+ err.status,
508
+ err.oauthError,
509
+ err.message
510
+ )
493
511
  };
494
512
  }
495
513
  log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
496
514
  return {
497
515
  ok: false,
498
- response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
516
+ response: challengeResponse(
517
+ runtime.auth,
518
+ request,
519
+ runtime.options,
520
+ 401,
521
+ "invalid_token",
522
+ "Invalid access token"
523
+ )
499
524
  };
500
525
  }
501
526
  }
@@ -1,5 +1,5 @@
1
- import { M as McpRuntimeOptions } from '../../authorize-DpTEIFvs.js';
2
- import { d as McpDefinition } from '../../types-zMlr1mOK.js';
1
+ import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
2
+ import { d as McpDefinition } from '../../types-C5SSORxe.js';
3
3
  import 'zod';
4
4
 
5
5
  type McpProtocolHandler = (request: Request) => Promise<Response>;
@@ -1,5 +1,5 @@
1
- import { M as McpRuntimeOptions } from '../../authorize-DpTEIFvs.js';
2
- import { d as McpDefinition } from '../../types-zMlr1mOK.js';
1
+ import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
2
+ import { d as McpDefinition } from '../../types-C5SSORxe.js';
3
3
  import 'zod';
4
4
 
5
5
  type McpProtocolHandler = (request: Request) => Promise<Response>;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createMcpProtocolHandler
3
- } from "../../chunk-3XPKR4G6.js";
3
+ } from "../../chunk-OXSSMKQE.js";
4
4
  import "../../chunk-MA5H6PSF.js";
5
- import "../../chunk-HRMLGCXV.js";
5
+ import "../../chunk-YQBOGY7D.js";
6
6
  import "../../chunk-QC3DXQTH.js";
7
7
  import "../../chunk-6DXGZZA4.js";
8
8
  export {
@@ -248,10 +248,10 @@ function resolveProtectedResource(auth, request, options) {
248
248
  resourceURL.hash = "";
249
249
  return trimTrailingSlash(resourceURL.toString());
250
250
  }
251
- function assertResourcePathShape(resourcePath) {
251
+ function assertResourcePathShape(resourcePath, label = "resourcePath") {
252
252
  if (!resourcePath.startsWith("/") || resourcePath.startsWith("//") || resourcePath.includes("\\") || /(^|\/)\.\.(\/|$)/.test(resourcePath)) {
253
253
  throw new Error(
254
- `@lovable.dev/mcp-js: resourcePath must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`
254
+ `@lovable.dev/mcp-js: ${label} must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`
255
255
  );
256
256
  }
257
257
  }
@@ -318,6 +318,7 @@ async function fetchVerificationKeySet(jwksUri) {
318
318
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
319
319
  try {
320
320
  const { payload } = await (0, import_jose.jwtVerify)(token, keySet, {
321
+ typ: "at+jwt",
321
322
  // `issuer` is trimmed of any trailing slash; accept both forms so a token whose
322
323
  // `iss` carries the slash the AS publishes still verifies.
323
324
  issuer: [issuer, `${issuer}/`],
@@ -397,10 +398,17 @@ function oauthConfigurationErrorResponse() {
397
398
  function getOAuthRuntime(mcp, options = {}) {
398
399
  if (options.resourcePath !== void 0)
399
400
  assertResourcePathShape(options.resourcePath);
400
- const stableOptions = { resourcePath: options.resourcePath };
401
+ if (options.metadataPath !== void 0)
402
+ assertResourcePathShape(options.metadataPath, "metadataPath");
403
+ const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
401
404
  const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
402
405
  if (!auth)
403
406
  return { kind: "unconfigured", options: stableOptions };
407
+ if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
408
+ throw new Error(
409
+ `@lovable.dev/mcp-js: the Vite plugin generates protected-resource metadata (metadataPath set), so auth.protectedResourceMetadataUrl must not also be set. Drop protectedResourceMetadataUrl, or set protectedResourceMetadataRoute: false in mcpPlugin(...) to host the document yourself.`
410
+ );
411
+ }
404
412
  const discovery = createOAuthDiscoveryResolver(auth);
405
413
  return {
406
414
  kind: "configured",
@@ -1,5 +1,5 @@
1
- import { M as McpRuntimeOptions } from '../authorize-DpTEIFvs.js';
2
- import { d as McpDefinition } from '../types-zMlr1mOK.js';
1
+ import { M as McpRuntimeOptions } from '../authorize-HTd0GKmB.js';
2
+ import { d as McpDefinition } from '../types-C5SSORxe.js';
3
3
  import 'zod';
4
4
 
5
5
  type OAuthProtectedResourceMetadataHandler = (request: Request) => Promise<Response>;
@@ -1,5 +1,5 @@
1
- import { M as McpRuntimeOptions } from '../authorize-DpTEIFvs.js';
2
- import { d as McpDefinition } from '../types-zMlr1mOK.js';
1
+ import { M as McpRuntimeOptions } from '../authorize-HTd0GKmB.js';
2
+ import { d as McpDefinition } from '../types-C5SSORxe.js';
3
3
  import 'zod';
4
4
 
5
5
  type OAuthProtectedResourceMetadataHandler = (request: Request) => Promise<Response>;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createOAuthProtectedResourceMetadataHandler
3
- } from "../chunk-W7F6JRDB.js";
4
- import "../chunk-HRMLGCXV.js";
3
+ } from "../chunk-4JO3BZQG.js";
4
+ import "../chunk-YQBOGY7D.js";
5
5
  import "../chunk-QC3DXQTH.js";
6
6
  import "../chunk-6DXGZZA4.js";
7
7
  export {
@@ -256,10 +256,10 @@ function resolveProtectedResource(auth, request, options) {
256
256
  resourceURL.hash = "";
257
257
  return trimTrailingSlash(resourceURL.toString());
258
258
  }
259
- function assertResourcePathShape(resourcePath) {
259
+ function assertResourcePathShape(resourcePath, label = "resourcePath") {
260
260
  if (!resourcePath.startsWith("/") || resourcePath.startsWith("//") || resourcePath.includes("\\") || /(^|\/)\.\.(\/|$)/.test(resourcePath)) {
261
261
  throw new Error(
262
- `@lovable.dev/mcp-js: resourcePath must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`
262
+ `@lovable.dev/mcp-js: ${label} must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`
263
263
  );
264
264
  }
265
265
  }
@@ -326,6 +326,7 @@ async function fetchVerificationKeySet(jwksUri) {
326
326
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
327
327
  try {
328
328
  const { payload } = await (0, import_jose.jwtVerify)(token, keySet, {
329
+ typ: "at+jwt",
329
330
  // `issuer` is trimmed of any trailing slash; accept both forms so a token whose
330
331
  // `iss` carries the slash the AS publishes still verifies.
331
332
  issuer: [issuer, `${issuer}/`],
@@ -400,16 +401,16 @@ function quoteAuthenticateParam(value) {
400
401
  const sanitized = value.replace(/[\u0000-\u001F\u007F]/g, "");
401
402
  return `"${sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
402
403
  }
403
- function resolveProtectedResourceMetadataUrl(auth, request) {
404
+ function resolveProtectedResourceMetadataUrl(auth, request, options) {
404
405
  if (auth.protectedResourceMetadataUrl !== void 0)
405
406
  return auth.protectedResourceMetadataUrl;
406
407
  const base = auth.resource ?? request.url;
407
- return new URL(OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
408
+ return new URL(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
408
409
  }
409
- function wwwAuthenticateHeader(auth, request, params = {}) {
410
+ function wwwAuthenticateHeader(auth, request, options, params = {}) {
410
411
  const values = [
411
412
  `realm=${quoteAuthenticateParam("mcp")}`,
412
- `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request))}`
413
+ `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request, options))}`
413
414
  ];
414
415
  if (auth.requiredScopes && auth.requiredScopes.length > 0) {
415
416
  values.push(`scope=${quoteAuthenticateParam(auth.requiredScopes.join(" "))}`);
@@ -420,9 +421,12 @@ function wwwAuthenticateHeader(auth, request, params = {}) {
420
421
  values.push(`error_description=${quoteAuthenticateParam(params.errorDescription)}`);
421
422
  return `Bearer ${values.join(", ")}`;
422
423
  }
423
- function challengeResponse(auth, request, status, oauthError, errorDescription) {
424
+ function challengeResponse(auth, request, options, status, oauthError, errorDescription) {
424
425
  const headers = new Headers(JSON_HEADERS);
425
- headers.set("WWW-Authenticate", wwwAuthenticateHeader(auth, request, { error: oauthError, errorDescription }));
426
+ headers.set(
427
+ "WWW-Authenticate",
428
+ wwwAuthenticateHeader(auth, request, options, { error: oauthError, errorDescription })
429
+ );
426
430
  headers.set("Cache-Control", "no-store");
427
431
  return new Response(JSON.stringify({ error: status === 403 ? "forbidden" : "unauthorized" }), {
428
432
  status,
@@ -450,10 +454,17 @@ function parseBearerToken(request) {
450
454
  function getOAuthRuntime(mcp, options = {}) {
451
455
  if (options.resourcePath !== void 0)
452
456
  assertResourcePathShape(options.resourcePath);
453
- const stableOptions = { resourcePath: options.resourcePath };
457
+ if (options.metadataPath !== void 0)
458
+ assertResourcePathShape(options.metadataPath, "metadataPath");
459
+ const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
454
460
  const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
455
461
  if (!auth)
456
462
  return { kind: "unconfigured", options: stableOptions };
463
+ if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
464
+ throw new Error(
465
+ `@lovable.dev/mcp-js: the Vite plugin generates protected-resource metadata (metadataPath set), so auth.protectedResourceMetadataUrl must not also be set. Drop protectedResourceMetadataUrl, or set protectedResourceMetadataRoute: false in mcpPlugin(...) to host the document yourself.`
466
+ );
467
+ }
457
468
  const discovery = createOAuthDiscoveryResolver(auth);
458
469
  return {
459
470
  kind: "configured",
@@ -492,7 +503,7 @@ function createRequestAuthorizer(mcp, options = {}) {
492
503
  const token = parseBearerToken(request);
493
504
  if (!token) {
494
505
  log.info("auth.no_bearer_token", { outcome: "401" });
495
- return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
506
+ return { ok: false, response: challengeResponse(runtime.auth, request, runtime.options, 401) };
496
507
  }
497
508
  try {
498
509
  const auth = await runtime.verify(token, request, runtime.options);
@@ -507,13 +518,27 @@ function createRequestAuthorizer(mcp, options = {}) {
507
518
  log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
508
519
  return {
509
520
  ok: false,
510
- response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
521
+ response: challengeResponse(
522
+ runtime.auth,
523
+ request,
524
+ runtime.options,
525
+ err.status,
526
+ err.oauthError,
527
+ err.message
528
+ )
511
529
  };
512
530
  }
513
531
  log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
514
532
  return {
515
533
  ok: false,
516
- response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
534
+ response: challengeResponse(
535
+ runtime.auth,
536
+ request,
537
+ runtime.options,
538
+ 401,
539
+ "invalid_token",
540
+ "Invalid access token"
541
+ )
517
542
  };
518
543
  }
519
544
  }
@@ -1,5 +1,5 @@
1
- import { M as McpRuntimeOptions } from '../../authorize-DpTEIFvs.js';
2
- import { d as McpDefinition } from '../../types-zMlr1mOK.js';
1
+ import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
2
+ import { d as McpDefinition } from '../../types-C5SSORxe.js';
3
3
  import 'zod';
4
4
 
5
5
  type RestListToolsHandler = (request: Request) => Promise<Response>;
@@ -1,5 +1,5 @@
1
- import { M as McpRuntimeOptions } from '../../authorize-DpTEIFvs.js';
2
- import { d as McpDefinition } from '../../types-zMlr1mOK.js';
1
+ import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
2
+ import { d as McpDefinition } from '../../types-C5SSORxe.js';
3
3
  import 'zod';
4
4
 
5
5
  type RestListToolsHandler = (request: Request) => Promise<Response>;
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createInvokeToolHandler
3
- } from "../../chunk-4Y72OCPS.js";
3
+ } from "../../chunk-6N7B7NZY.js";
4
4
  import "../../chunk-MA5H6PSF.js";
5
5
  import {
6
6
  createListToolsHandler
7
- } from "../../chunk-E335BBVM.js";
8
- import "../../chunk-HRMLGCXV.js";
7
+ } from "../../chunk-2RGXCDBU.js";
8
+ import "../../chunk-YQBOGY7D.js";
9
9
  import "../../chunk-QC3DXQTH.js";
10
10
  import "../../chunk-6DXGZZA4.js";
11
11
  export {
@@ -8,7 +8,7 @@ var import_node_path = require("path");
8
8
  var import_vite2 = require("vite");
9
9
 
10
10
  // package.json
11
- var version = "0.8.0";
11
+ var version = "0.9.1";
12
12
 
13
13
  // src/protocols/rest/list-tools.ts
14
14
  var import_zod_compat = require("@modelcontextprotocol/sdk/server/zod-compat.js");
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  buildMcpListing
4
- } from "../../../chunk-E335BBVM.js";
5
- import "../../../chunk-HRMLGCXV.js";
4
+ } from "../../../chunk-2RGXCDBU.js";
5
+ import "../../../chunk-YQBOGY7D.js";
6
6
  import "../../../chunk-QC3DXQTH.js";
7
7
  import {
8
8
  isFileMissing
@@ -16,7 +16,7 @@ import { dirname, resolve } from "path";
16
16
  import { resolveConfig } from "vite";
17
17
 
18
18
  // package.json
19
- var version = "0.8.0";
19
+ var version = "0.9.1";
20
20
 
21
21
  // src/stacks/tanstack/extract-manifest.ts
22
22
  var MANIFEST_VERSION = 1;
@@ -258,10 +258,10 @@ function resolveProtectedResource(auth, request, options) {
258
258
  resourceURL.hash = "";
259
259
  return trimTrailingSlash(resourceURL.toString());
260
260
  }
261
- function assertResourcePathShape(resourcePath) {
261
+ function assertResourcePathShape(resourcePath, label = "resourcePath") {
262
262
  if (!resourcePath.startsWith("/") || resourcePath.startsWith("//") || resourcePath.includes("\\") || /(^|\/)\.\.(\/|$)/.test(resourcePath)) {
263
263
  throw new Error(
264
- `@lovable.dev/mcp-js: resourcePath must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`
264
+ `@lovable.dev/mcp-js: ${label} must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`
265
265
  );
266
266
  }
267
267
  }
@@ -328,6 +328,7 @@ async function fetchVerificationKeySet(jwksUri) {
328
328
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
329
329
  try {
330
330
  const { payload } = await (0, import_jose.jwtVerify)(token, keySet, {
331
+ typ: "at+jwt",
331
332
  // `issuer` is trimmed of any trailing slash; accept both forms so a token whose
332
333
  // `iss` carries the slash the AS publishes still verifies.
333
334
  issuer: [issuer, `${issuer}/`],
@@ -402,16 +403,16 @@ function quoteAuthenticateParam(value) {
402
403
  const sanitized = value.replace(/[\u0000-\u001F\u007F]/g, "");
403
404
  return `"${sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
404
405
  }
405
- function resolveProtectedResourceMetadataUrl(auth, request) {
406
+ function resolveProtectedResourceMetadataUrl(auth, request, options) {
406
407
  if (auth.protectedResourceMetadataUrl !== void 0)
407
408
  return auth.protectedResourceMetadataUrl;
408
409
  const base = auth.resource ?? request.url;
409
- return new URL(OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
410
+ return new URL(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
410
411
  }
411
- function wwwAuthenticateHeader(auth, request, params = {}) {
412
+ function wwwAuthenticateHeader(auth, request, options, params = {}) {
412
413
  const values = [
413
414
  `realm=${quoteAuthenticateParam("mcp")}`,
414
- `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request))}`
415
+ `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request, options))}`
415
416
  ];
416
417
  if (auth.requiredScopes && auth.requiredScopes.length > 0) {
417
418
  values.push(`scope=${quoteAuthenticateParam(auth.requiredScopes.join(" "))}`);
@@ -422,9 +423,12 @@ function wwwAuthenticateHeader(auth, request, params = {}) {
422
423
  values.push(`error_description=${quoteAuthenticateParam(params.errorDescription)}`);
423
424
  return `Bearer ${values.join(", ")}`;
424
425
  }
425
- function challengeResponse(auth, request, status, oauthError, errorDescription) {
426
+ function challengeResponse(auth, request, options, status, oauthError, errorDescription) {
426
427
  const headers = new Headers(JSON_HEADERS);
427
- headers.set("WWW-Authenticate", wwwAuthenticateHeader(auth, request, { error: oauthError, errorDescription }));
428
+ headers.set(
429
+ "WWW-Authenticate",
430
+ wwwAuthenticateHeader(auth, request, options, { error: oauthError, errorDescription })
431
+ );
428
432
  headers.set("Cache-Control", "no-store");
429
433
  return new Response(JSON.stringify({ error: status === 403 ? "forbidden" : "unauthorized" }), {
430
434
  status,
@@ -452,10 +456,17 @@ function parseBearerToken(request) {
452
456
  function getOAuthRuntime(mcp, options = {}) {
453
457
  if (options.resourcePath !== void 0)
454
458
  assertResourcePathShape(options.resourcePath);
455
- const stableOptions = { resourcePath: options.resourcePath };
459
+ if (options.metadataPath !== void 0)
460
+ assertResourcePathShape(options.metadataPath, "metadataPath");
461
+ const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
456
462
  const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
457
463
  if (!auth)
458
464
  return { kind: "unconfigured", options: stableOptions };
465
+ if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
466
+ throw new Error(
467
+ `@lovable.dev/mcp-js: the Vite plugin generates protected-resource metadata (metadataPath set), so auth.protectedResourceMetadataUrl must not also be set. Drop protectedResourceMetadataUrl, or set protectedResourceMetadataRoute: false in mcpPlugin(...) to host the document yourself.`
468
+ );
469
+ }
459
470
  const discovery = createOAuthDiscoveryResolver(auth);
460
471
  return {
461
472
  kind: "configured",
@@ -494,7 +505,7 @@ function createRequestAuthorizer(mcp, options = {}) {
494
505
  const token = parseBearerToken(request);
495
506
  if (!token) {
496
507
  log.info("auth.no_bearer_token", { outcome: "401" });
497
- return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
508
+ return { ok: false, response: challengeResponse(runtime.auth, request, runtime.options, 401) };
498
509
  }
499
510
  try {
500
511
  const auth = await runtime.verify(token, request, runtime.options);
@@ -509,13 +520,27 @@ function createRequestAuthorizer(mcp, options = {}) {
509
520
  log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
510
521
  return {
511
522
  ok: false,
512
- response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
523
+ response: challengeResponse(
524
+ runtime.auth,
525
+ request,
526
+ runtime.options,
527
+ err.status,
528
+ err.oauthError,
529
+ err.message
530
+ )
513
531
  };
514
532
  }
515
533
  log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
516
534
  return {
517
535
  ok: false,
518
- response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
536
+ response: challengeResponse(
537
+ runtime.auth,
538
+ request,
539
+ runtime.options,
540
+ 401,
541
+ "invalid_token",
542
+ "Invalid access token"
543
+ )
519
544
  };
520
545
  }
521
546
  }
@@ -1,5 +1,5 @@
1
- import { M as McpRuntimeOptions } from '../../authorize-DpTEIFvs.js';
2
- import { d as McpDefinition } from '../../types-zMlr1mOK.js';
1
+ import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
2
+ import { d as McpDefinition } from '../../types-C5SSORxe.js';
3
3
  import 'zod';
4
4
 
5
5
  interface TanStackRouteCtx {
@@ -1,5 +1,5 @@
1
- import { M as McpRuntimeOptions } from '../../authorize-DpTEIFvs.js';
2
- import { d as McpDefinition } from '../../types-zMlr1mOK.js';
1
+ import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
2
+ import { d as McpDefinition } from '../../types-C5SSORxe.js';
3
3
  import 'zod';
4
4
 
5
5
  interface TanStackRouteCtx {
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  createMcpProtocolHandler
3
- } from "../../chunk-3XPKR4G6.js";
3
+ } from "../../chunk-OXSSMKQE.js";
4
4
  import {
5
5
  createOAuthProtectedResourceMetadataHandler
6
- } from "../../chunk-W7F6JRDB.js";
6
+ } from "../../chunk-4JO3BZQG.js";
7
7
  import {
8
8
  createInvokeToolHandler
9
- } from "../../chunk-4Y72OCPS.js";
9
+ } from "../../chunk-6N7B7NZY.js";
10
10
  import "../../chunk-MA5H6PSF.js";
11
11
  import {
12
12
  createListToolsHandler
13
- } from "../../chunk-E335BBVM.js";
14
- import "../../chunk-HRMLGCXV.js";
13
+ } from "../../chunk-2RGXCDBU.js";
14
+ import "../../chunk-YQBOGY7D.js";
15
15
  import "../../chunk-QC3DXQTH.js";
16
16
  import "../../chunk-6DXGZZA4.js";
17
17
 
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/stacks/tanstack/vite.ts
21
21
  var vite_exports = {};
22
22
  __export(vite_exports, {
23
+ assertMetadataPathShape: () => assertMetadataPathShape,
23
24
  assertUrlPathShape: () => assertUrlPathShape,
24
25
  default: () => vite_default,
25
26
  deriveRouteFileName: () => deriveRouteFileName,
@@ -55,15 +56,17 @@ function wellKnownRouteFile(routesDir, urlPath) {
55
56
  });
56
57
  return (0, import_node_path.resolve)(routesDir, ...fileSegments);
57
58
  }
58
- function resolveAllRoutes(projectRoot, routesDirOption, routeFileName) {
59
+ function resolveAllRoutes(projectRoot, routesDirOption, routeFileName, metadataUrlPath) {
59
60
  const routesDir = (0, import_node_path.resolve)(projectRoot, routesDirOption);
60
61
  assertContains(projectRoot, routesDir, `routesDir "${routesDirOption}"`);
61
62
  const mcpRouteFile = (0, import_node_path.resolve)(routesDir, routeFileName);
62
63
  assertContains(routesDir, mcpRouteFile, `routeFileName "${routeFileName}"`);
64
+ const metadataRouteFile = wellKnownRouteFile(routesDir, metadataUrlPath);
65
+ assertContains(routesDir, metadataRouteFile, `metadataPath "${metadataUrlPath}"`);
63
66
  return {
64
67
  routesDir,
65
68
  mcpRouteFile,
66
- metadataRouteFile: wellKnownRouteFile(routesDir, OAUTH_PROTECTED_RESOURCE_METADATA_PATH),
69
+ metadataRouteFile,
67
70
  listToolsRouteFile: (0, import_node_path.resolve)(routesDir, "[.mcp]", "list-tools.ts"),
68
71
  invokeToolRouteFile: (0, import_node_path.resolve)(routesDir, "[.mcp]", "invoke-tool", "$tool.ts")
69
72
  };
@@ -87,6 +90,24 @@ function assertUrlPathShape(urlPath) {
87
90
  function deriveRouteFileName(urlPath) {
88
91
  return `${assertUrlPathShape(urlPath)}.ts`;
89
92
  }
93
+ function assertMetadataPathShape(metadataPath) {
94
+ if (!metadataPath.startsWith("/")) {
95
+ throw new Error(`@lovable.dev/mcp-js: metadataPath must start with "/", got "${metadataPath}"`);
96
+ }
97
+ if (/\/{2,}/.test(metadataPath)) {
98
+ throw new Error(`@lovable.dev/mcp-js: metadataPath "${metadataPath}" contains empty segments ("//")`);
99
+ }
100
+ if (/(^|\/)\.\.(\/|$)/.test(metadataPath)) {
101
+ throw new Error(`@lovable.dev/mcp-js: metadataPath "${metadataPath}" must not contain ".." segments`);
102
+ }
103
+ const trimmed = metadataPath.replace(/^\/+/, "").replace(/\/+$/, "");
104
+ if (!trimmed) {
105
+ throw new Error(`@lovable.dev/mcp-js: metadataPath "${metadataPath}" has no route segment`);
106
+ }
107
+ if (!/^[a-zA-Z0-9_\-./]+$/.test(trimmed)) {
108
+ throw new Error(`@lovable.dev/mcp-js: metadataPath "${metadataPath}" contains unsupported characters`);
109
+ }
110
+ }
90
111
  function canonicalUrlPath(urlPath) {
91
112
  return `/${urlPath.replace(/^\/+/, "").replace(/\/+$/, "")}`;
92
113
  }
@@ -95,10 +116,11 @@ function relativeImportSpecifier(routeFile, target) {
95
116
  const withoutExt = rel.replace(/\.ts$/, "");
96
117
  return withoutExt.startsWith(".") ? withoutExt : `./${withoutExt}`;
97
118
  }
98
- function buildRouteSource(route, resourcePath, mcpEntry, projectRoot) {
119
+ function buildRouteSource(route, resourcePath, metadataPath, mcpEntry, projectRoot) {
99
120
  const mcpImport = relativeImportSpecifier(route.file, mcpEntry);
100
121
  const routeRel = normalizePath((0, import_node_path.relative)(projectRoot, route.file));
101
122
  const note = route.spaFallbackNote ? " // ANY: TanStack returns SPA HTML for methods not in `handlers`; the SDK 405s instead.\n" : "";
123
+ const runtimeOptions = metadataPath === void 0 ? `{ resourcePath: "${resourcePath}" }` : `{ resourcePath: "${resourcePath}", metadataPath: "${metadataPath}" }`;
102
124
  return `${GENERATED_BANNER}
103
125
  // route: ${route.routeLiteral}
104
126
  // emitted to: ${routeRel}
@@ -112,7 +134,7 @@ import mcp from "${mcpImport}";
112
134
  export const Route = createFileRoute("${route.routeLiteral}")({
113
135
  server: {
114
136
  handlers: {
115
- ${note} ANY: ${route.handlerFactory}(mcp, { resourcePath: "${resourcePath}" }),
137
+ ${note} ANY: ${route.handlerFactory}(mcp, ${runtimeOptions}),
116
138
  },
117
139
  },
118
140
  });
@@ -191,12 +213,23 @@ function mcpPlugin(options = {}) {
191
213
  const urlPath = canonicalUrlPath(rawUrlPath);
192
214
  const emitRestRoutes = options.restRoutes !== false;
193
215
  const emitProtectedResourceMetadataRoute = options.protectedResourceMetadataRoute !== false;
216
+ if (options.metadataPath !== void 0) {
217
+ if (!emitProtectedResourceMetadataRoute) {
218
+ throw new Error(
219
+ `@lovable.dev/mcp-js: metadataPath sets where the protected-resource metadata route is served, so it can't be combined with protectedResourceMetadataRoute: false.`
220
+ );
221
+ }
222
+ assertMetadataPathShape(options.metadataPath);
223
+ }
224
+ const metadataUrlPath = canonicalUrlPath(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH);
225
+ const injectedMetadataPath = emitProtectedResourceMetadataRoute ? metadataUrlPath : void 0;
194
226
  let projectRoot = process.cwd();
195
227
  let mcpEntry = (0, import_node_path.resolve)(projectRoot, mcpEntryOption);
196
228
  let { routesDir, mcpRouteFile, metadataRouteFile, listToolsRouteFile, invokeToolRouteFile } = resolveAllRoutes(
197
229
  projectRoot,
198
230
  routesDirOption,
199
- routeFileName
231
+ routeFileName,
232
+ metadataUrlPath
200
233
  );
201
234
  const regenerate = () => {
202
235
  let mcpEntryExists = true;
@@ -215,7 +248,7 @@ function mcpPlugin(options = {}) {
215
248
  if (emitProtectedResourceMetadataRoute) {
216
249
  routes.push({
217
250
  file: metadataRouteFile,
218
- routeLiteral: OAUTH_PROTECTED_RESOURCE_METADATA_PATH,
251
+ routeLiteral: metadataUrlPath,
219
252
  handlerFactory: "createTanStackOAuthProtectedResourceMetadataHandler"
220
253
  });
221
254
  }
@@ -237,7 +270,7 @@ function mcpPlugin(options = {}) {
237
270
  }
238
271
  for (const route of routes) {
239
272
  expected.add(route.file);
240
- writeIfChanged(route.file, buildRouteSource(route, urlPath, mcpEntry, projectRoot));
273
+ writeIfChanged(route.file, buildRouteSource(route, urlPath, injectedMetadataPath, mcpEntry, projectRoot));
241
274
  }
242
275
  }
243
276
  for (const file of findAdoptedFiles(routesDir)) {
@@ -271,7 +304,8 @@ function mcpPlugin(options = {}) {
271
304
  ({ routesDir, mcpRouteFile, metadataRouteFile, listToolsRouteFile, invokeToolRouteFile } = resolveAllRoutes(
272
305
  projectRoot,
273
306
  routesDirOption,
274
- routeFileName
307
+ routeFileName,
308
+ metadataUrlPath
275
309
  ));
276
310
  regenerate();
277
311
  },
@@ -298,6 +332,7 @@ function mcpPlugin(options = {}) {
298
332
  var vite_default = mcpPlugin;
299
333
  // Annotate the CommonJS export names for ESM import in node:
300
334
  0 && (module.exports = {
335
+ assertMetadataPathShape,
301
336
  assertUrlPathShape,
302
337
  deriveRouteFileName,
303
338
  mcpPlugin
@@ -48,6 +48,21 @@ interface McpPluginOptions {
48
48
  * @default true
49
49
  */
50
50
  protectedResourceMetadataRoute?: boolean;
51
+ /**
52
+ * Public URL path for the OAuth protected-resource metadata route (RFC 9728).
53
+ * Both the emitted route file and the runtime `WWW-Authenticate` challenge use
54
+ * this path, so they can't drift. Use it when a host gate exposes only a
55
+ * sub-prefix (e.g. workspace-private projects where only `/api/public/*` is
56
+ * anonymously reachable) and the default well-known path is blocked.
57
+ *
58
+ * Leading-dot segments are bracket-escaped for TanStack file routing
59
+ * (`.well-known` → `[.well-known]`); the final segment becomes the route file.
60
+ * Mutually exclusive with `auth.protectedResourceMetadataUrl` (that option
61
+ * advertises an externally hosted document instead of serving one), and
62
+ * cannot be combined with `protectedResourceMetadataRoute: false`.
63
+ * @default "/.well-known/oauth-protected-resource"
64
+ */
65
+ metadataPath?: string;
51
66
  }
52
67
  interface McpPluginApi {
53
68
  readonly mcpEntry: string;
@@ -55,6 +70,7 @@ interface McpPluginApi {
55
70
  }
56
71
  declare function assertUrlPathShape(urlPath: string): string;
57
72
  declare function deriveRouteFileName(urlPath: string): string;
73
+ declare function assertMetadataPathShape(metadataPath: string): void;
58
74
  /**
59
75
  * Generate the TanStack route(s) that mount the MCP server.
60
76
  *
@@ -69,4 +85,4 @@ declare function deriveRouteFileName(urlPath: string): string;
69
85
  */
70
86
  declare function mcpPlugin(options?: McpPluginOptions): Plugin;
71
87
 
72
- export { type McpPluginApi, type McpPluginOptions, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
88
+ export { type McpPluginApi, type McpPluginOptions, assertMetadataPathShape, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
@@ -48,6 +48,21 @@ interface McpPluginOptions {
48
48
  * @default true
49
49
  */
50
50
  protectedResourceMetadataRoute?: boolean;
51
+ /**
52
+ * Public URL path for the OAuth protected-resource metadata route (RFC 9728).
53
+ * Both the emitted route file and the runtime `WWW-Authenticate` challenge use
54
+ * this path, so they can't drift. Use it when a host gate exposes only a
55
+ * sub-prefix (e.g. workspace-private projects where only `/api/public/*` is
56
+ * anonymously reachable) and the default well-known path is blocked.
57
+ *
58
+ * Leading-dot segments are bracket-escaped for TanStack file routing
59
+ * (`.well-known` → `[.well-known]`); the final segment becomes the route file.
60
+ * Mutually exclusive with `auth.protectedResourceMetadataUrl` (that option
61
+ * advertises an externally hosted document instead of serving one), and
62
+ * cannot be combined with `protectedResourceMetadataRoute: false`.
63
+ * @default "/.well-known/oauth-protected-resource"
64
+ */
65
+ metadataPath?: string;
51
66
  }
52
67
  interface McpPluginApi {
53
68
  readonly mcpEntry: string;
@@ -55,6 +70,7 @@ interface McpPluginApi {
55
70
  }
56
71
  declare function assertUrlPathShape(urlPath: string): string;
57
72
  declare function deriveRouteFileName(urlPath: string): string;
73
+ declare function assertMetadataPathShape(metadataPath: string): void;
58
74
  /**
59
75
  * Generate the TanStack route(s) that mount the MCP server.
60
76
  *
@@ -69,4 +85,4 @@ declare function deriveRouteFileName(urlPath: string): string;
69
85
  */
70
86
  declare function mcpPlugin(options?: McpPluginOptions): Plugin;
71
87
 
72
- export { type McpPluginApi, type McpPluginOptions, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
88
+ export { type McpPluginApi, type McpPluginOptions, assertMetadataPathShape, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
@@ -25,15 +25,17 @@ function wellKnownRouteFile(routesDir, urlPath) {
25
25
  });
26
26
  return resolve(routesDir, ...fileSegments);
27
27
  }
28
- function resolveAllRoutes(projectRoot, routesDirOption, routeFileName) {
28
+ function resolveAllRoutes(projectRoot, routesDirOption, routeFileName, metadataUrlPath) {
29
29
  const routesDir = resolve(projectRoot, routesDirOption);
30
30
  assertContains(projectRoot, routesDir, `routesDir "${routesDirOption}"`);
31
31
  const mcpRouteFile = resolve(routesDir, routeFileName);
32
32
  assertContains(routesDir, mcpRouteFile, `routeFileName "${routeFileName}"`);
33
+ const metadataRouteFile = wellKnownRouteFile(routesDir, metadataUrlPath);
34
+ assertContains(routesDir, metadataRouteFile, `metadataPath "${metadataUrlPath}"`);
33
35
  return {
34
36
  routesDir,
35
37
  mcpRouteFile,
36
- metadataRouteFile: wellKnownRouteFile(routesDir, OAUTH_PROTECTED_RESOURCE_METADATA_PATH),
38
+ metadataRouteFile,
37
39
  listToolsRouteFile: resolve(routesDir, "[.mcp]", "list-tools.ts"),
38
40
  invokeToolRouteFile: resolve(routesDir, "[.mcp]", "invoke-tool", "$tool.ts")
39
41
  };
@@ -57,6 +59,24 @@ function assertUrlPathShape(urlPath) {
57
59
  function deriveRouteFileName(urlPath) {
58
60
  return `${assertUrlPathShape(urlPath)}.ts`;
59
61
  }
62
+ function assertMetadataPathShape(metadataPath) {
63
+ if (!metadataPath.startsWith("/")) {
64
+ throw new Error(`@lovable.dev/mcp-js: metadataPath must start with "/", got "${metadataPath}"`);
65
+ }
66
+ if (/\/{2,}/.test(metadataPath)) {
67
+ throw new Error(`@lovable.dev/mcp-js: metadataPath "${metadataPath}" contains empty segments ("//")`);
68
+ }
69
+ if (/(^|\/)\.\.(\/|$)/.test(metadataPath)) {
70
+ throw new Error(`@lovable.dev/mcp-js: metadataPath "${metadataPath}" must not contain ".." segments`);
71
+ }
72
+ const trimmed = metadataPath.replace(/^\/+/, "").replace(/\/+$/, "");
73
+ if (!trimmed) {
74
+ throw new Error(`@lovable.dev/mcp-js: metadataPath "${metadataPath}" has no route segment`);
75
+ }
76
+ if (!/^[a-zA-Z0-9_\-./]+$/.test(trimmed)) {
77
+ throw new Error(`@lovable.dev/mcp-js: metadataPath "${metadataPath}" contains unsupported characters`);
78
+ }
79
+ }
60
80
  function canonicalUrlPath(urlPath) {
61
81
  return `/${urlPath.replace(/^\/+/, "").replace(/\/+$/, "")}`;
62
82
  }
@@ -65,10 +85,11 @@ function relativeImportSpecifier(routeFile, target) {
65
85
  const withoutExt = rel.replace(/\.ts$/, "");
66
86
  return withoutExt.startsWith(".") ? withoutExt : `./${withoutExt}`;
67
87
  }
68
- function buildRouteSource(route, resourcePath, mcpEntry, projectRoot) {
88
+ function buildRouteSource(route, resourcePath, metadataPath, mcpEntry, projectRoot) {
69
89
  const mcpImport = relativeImportSpecifier(route.file, mcpEntry);
70
90
  const routeRel = normalizePath(relative(projectRoot, route.file));
71
91
  const note = route.spaFallbackNote ? " // ANY: TanStack returns SPA HTML for methods not in `handlers`; the SDK 405s instead.\n" : "";
92
+ const runtimeOptions = metadataPath === void 0 ? `{ resourcePath: "${resourcePath}" }` : `{ resourcePath: "${resourcePath}", metadataPath: "${metadataPath}" }`;
72
93
  return `${GENERATED_BANNER}
73
94
  // route: ${route.routeLiteral}
74
95
  // emitted to: ${routeRel}
@@ -82,7 +103,7 @@ import mcp from "${mcpImport}";
82
103
  export const Route = createFileRoute("${route.routeLiteral}")({
83
104
  server: {
84
105
  handlers: {
85
- ${note} ANY: ${route.handlerFactory}(mcp, { resourcePath: "${resourcePath}" }),
106
+ ${note} ANY: ${route.handlerFactory}(mcp, ${runtimeOptions}),
86
107
  },
87
108
  },
88
109
  });
@@ -161,12 +182,23 @@ function mcpPlugin(options = {}) {
161
182
  const urlPath = canonicalUrlPath(rawUrlPath);
162
183
  const emitRestRoutes = options.restRoutes !== false;
163
184
  const emitProtectedResourceMetadataRoute = options.protectedResourceMetadataRoute !== false;
185
+ if (options.metadataPath !== void 0) {
186
+ if (!emitProtectedResourceMetadataRoute) {
187
+ throw new Error(
188
+ `@lovable.dev/mcp-js: metadataPath sets where the protected-resource metadata route is served, so it can't be combined with protectedResourceMetadataRoute: false.`
189
+ );
190
+ }
191
+ assertMetadataPathShape(options.metadataPath);
192
+ }
193
+ const metadataUrlPath = canonicalUrlPath(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH);
194
+ const injectedMetadataPath = emitProtectedResourceMetadataRoute ? metadataUrlPath : void 0;
164
195
  let projectRoot = process.cwd();
165
196
  let mcpEntry = resolve(projectRoot, mcpEntryOption);
166
197
  let { routesDir, mcpRouteFile, metadataRouteFile, listToolsRouteFile, invokeToolRouteFile } = resolveAllRoutes(
167
198
  projectRoot,
168
199
  routesDirOption,
169
- routeFileName
200
+ routeFileName,
201
+ metadataUrlPath
170
202
  );
171
203
  const regenerate = () => {
172
204
  let mcpEntryExists = true;
@@ -185,7 +217,7 @@ function mcpPlugin(options = {}) {
185
217
  if (emitProtectedResourceMetadataRoute) {
186
218
  routes.push({
187
219
  file: metadataRouteFile,
188
- routeLiteral: OAUTH_PROTECTED_RESOURCE_METADATA_PATH,
220
+ routeLiteral: metadataUrlPath,
189
221
  handlerFactory: "createTanStackOAuthProtectedResourceMetadataHandler"
190
222
  });
191
223
  }
@@ -207,7 +239,7 @@ function mcpPlugin(options = {}) {
207
239
  }
208
240
  for (const route of routes) {
209
241
  expected.add(route.file);
210
- writeIfChanged(route.file, buildRouteSource(route, urlPath, mcpEntry, projectRoot));
242
+ writeIfChanged(route.file, buildRouteSource(route, urlPath, injectedMetadataPath, mcpEntry, projectRoot));
211
243
  }
212
244
  }
213
245
  for (const file of findAdoptedFiles(routesDir)) {
@@ -241,7 +273,8 @@ function mcpPlugin(options = {}) {
241
273
  ({ routesDir, mcpRouteFile, metadataRouteFile, listToolsRouteFile, invokeToolRouteFile } = resolveAllRoutes(
242
274
  projectRoot,
243
275
  routesDirOption,
244
- routeFileName
276
+ routeFileName,
277
+ metadataUrlPath
245
278
  ));
246
279
  regenerate();
247
280
  },
@@ -267,6 +300,7 @@ function mcpPlugin(options = {}) {
267
300
  }
268
301
  var vite_default = mcpPlugin;
269
302
  export {
303
+ assertMetadataPathShape,
270
304
  assertUrlPathShape,
271
305
  vite_default as default,
272
306
  deriveRouteFileName,
@@ -52,6 +52,10 @@ interface McpAuthConfig {
52
52
  * `WWW-Authenticate: resource_metadata` challenge and does not generate or
53
53
  * serve its own metadata (the `/.well-known/oauth-protected-resource` handler
54
54
  * 404s). Use it when the resource server already publishes its own PRM.
55
+ *
56
+ * Mutually exclusive with the Vite plugin's `metadataPath`: that option has
57
+ * the SDK serve the document itself at a custom path, so configuring both is
58
+ * rejected at handler construction.
55
59
  */
56
60
  readonly protectedResourceMetadataUrl?: string;
57
61
  readonly requireOAuthClientClaim?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovable.dev/mcp-js",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Author MCP servers for Lovable apps. Declare tools with defineTool, register them in defineMcp, and the framework adapter (TanStack today, Supabase Edge Functions next) emits the route(s) at build time.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,9 +0,0 @@
1
- interface McpRuntimeOptions {
2
- /**
3
- * Public MCP protocol path. REST companion handlers pass this so OAuth
4
- * resource/audience checks bind to `/mcp`, not internal `/.mcp/*` RPC URLs.
5
- */
6
- resourcePath?: string;
7
- }
8
-
9
- export type { McpRuntimeOptions as M };