@lovable.dev/mcp-js 0.8.0 → 0.9.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.
Files changed (34) hide show
  1. package/README.md +3 -1
  2. package/dist/authorize-HTd0GKmB.d.ts +18 -0
  3. package/dist/{chunk-W7F6JRDB.js → chunk-3GQXQBYQ.js} +1 -1
  4. package/dist/{chunk-E335BBVM.js → chunk-BG3HOZXZ.js} +1 -1
  5. package/dist/{chunk-3XPKR4G6.js → chunk-CJMHLE7F.js} +1 -1
  6. package/dist/{chunk-HRMLGCXV.js → chunk-FQLM4JDU.js} +36 -12
  7. package/dist/{chunk-4Y72OCPS.js → chunk-SBKIKJNB.js} +1 -1
  8. package/dist/index.d.cts +2 -2
  9. package/dist/index.d.ts +2 -2
  10. package/dist/protocols/mcp/index.cjs +36 -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 +10 -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 +36 -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 +36 -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 };
@@ -7,7 +7,7 @@ import {
7
7
  oauthConfigurationErrorResponse,
8
8
  resolveProtectedResource,
9
9
  withCors
10
- } from "./chunk-HRMLGCXV.js";
10
+ } from "./chunk-FQLM4JDU.js";
11
11
  import {
12
12
  describeError,
13
13
  log
@@ -5,7 +5,7 @@ import {
5
5
  headResponse,
6
6
  methodNotAllowed,
7
7
  withCors
8
- } from "./chunk-HRMLGCXV.js";
8
+ } from "./chunk-FQLM4JDU.js";
9
9
 
10
10
  // src/protocols/rest/list-tools.ts
11
11
  import { objectFromShape } 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-FQLM4JDU.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
  }
@@ -299,16 +299,16 @@ function quoteAuthenticateParam(value) {
299
299
  const sanitized = value.replace(/[\u0000-\u001F\u007F]/g, "");
300
300
  return `"${sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
301
301
  }
302
- function resolveProtectedResourceMetadataUrl(auth, request) {
302
+ function resolveProtectedResourceMetadataUrl(auth, request, options) {
303
303
  if (auth.protectedResourceMetadataUrl !== void 0)
304
304
  return auth.protectedResourceMetadataUrl;
305
305
  const base = auth.resource ?? request.url;
306
- return new URL(OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
306
+ return new URL(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
307
307
  }
308
- function wwwAuthenticateHeader(auth, request, params = {}) {
308
+ function wwwAuthenticateHeader(auth, request, options, params = {}) {
309
309
  const values = [
310
310
  `realm=${quoteAuthenticateParam("mcp")}`,
311
- `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request))}`
311
+ `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request, options))}`
312
312
  ];
313
313
  if (auth.requiredScopes && auth.requiredScopes.length > 0) {
314
314
  values.push(`scope=${quoteAuthenticateParam(auth.requiredScopes.join(" "))}`);
@@ -319,9 +319,12 @@ function wwwAuthenticateHeader(auth, request, params = {}) {
319
319
  values.push(`error_description=${quoteAuthenticateParam(params.errorDescription)}`);
320
320
  return `Bearer ${values.join(", ")}`;
321
321
  }
322
- function challengeResponse(auth, request, status, oauthError, errorDescription) {
322
+ function challengeResponse(auth, request, options, status, oauthError, errorDescription) {
323
323
  const headers = new Headers(JSON_HEADERS);
324
- headers.set("WWW-Authenticate", wwwAuthenticateHeader(auth, request, { error: oauthError, errorDescription }));
324
+ headers.set(
325
+ "WWW-Authenticate",
326
+ wwwAuthenticateHeader(auth, request, options, { error: oauthError, errorDescription })
327
+ );
325
328
  headers.set("Cache-Control", "no-store");
326
329
  return new Response(JSON.stringify({ error: status === 403 ? "forbidden" : "unauthorized" }), {
327
330
  status,
@@ -349,10 +352,17 @@ function parseBearerToken(request) {
349
352
  function getOAuthRuntime(mcp, options = {}) {
350
353
  if (options.resourcePath !== void 0)
351
354
  assertResourcePathShape(options.resourcePath);
352
- const stableOptions = { resourcePath: options.resourcePath };
355
+ if (options.metadataPath !== void 0)
356
+ assertResourcePathShape(options.metadataPath, "metadataPath");
357
+ const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
353
358
  const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
354
359
  if (!auth)
355
360
  return { kind: "unconfigured", options: stableOptions };
361
+ if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
362
+ throw new Error(
363
+ `@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.`
364
+ );
365
+ }
356
366
  const discovery = createOAuthDiscoveryResolver(auth);
357
367
  return {
358
368
  kind: "configured",
@@ -391,7 +401,7 @@ function createRequestAuthorizer(mcp, options = {}) {
391
401
  const token = parseBearerToken(request);
392
402
  if (!token) {
393
403
  log.info("auth.no_bearer_token", { outcome: "401" });
394
- return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
404
+ return { ok: false, response: challengeResponse(runtime.auth, request, runtime.options, 401) };
395
405
  }
396
406
  try {
397
407
  const auth = await runtime.verify(token, request, runtime.options);
@@ -406,13 +416,27 @@ function createRequestAuthorizer(mcp, options = {}) {
406
416
  log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
407
417
  return {
408
418
  ok: false,
409
- response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
419
+ response: challengeResponse(
420
+ runtime.auth,
421
+ request,
422
+ runtime.options,
423
+ err.status,
424
+ err.oauthError,
425
+ err.message
426
+ )
410
427
  };
411
428
  }
412
429
  log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
413
430
  return {
414
431
  ok: false,
415
- response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
432
+ response: challengeResponse(
433
+ runtime.auth,
434
+ request,
435
+ runtime.options,
436
+ 401,
437
+ "invalid_token",
438
+ "Invalid access token"
439
+ )
416
440
  };
417
441
  }
418
442
  }
@@ -8,7 +8,7 @@ import {
8
8
  createRequestAuthorizer,
9
9
  methodNotAllowed,
10
10
  withCors
11
- } from "./chunk-HRMLGCXV.js";
11
+ } from "./chunk-FQLM4JDU.js";
12
12
 
13
13
  // src/protocols/rest/invoke-tool.ts
14
14
  import { getParseErrorMessage, objectFromShape, safeParseAsync } from "@modelcontextprotocol/sdk/server/zod-compat.js";
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
  }
@@ -390,16 +390,16 @@ function quoteAuthenticateParam(value) {
390
390
  const sanitized = value.replace(/[\u0000-\u001F\u007F]/g, "");
391
391
  return `"${sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
392
392
  }
393
- function resolveProtectedResourceMetadataUrl(auth, request) {
393
+ function resolveProtectedResourceMetadataUrl(auth, request, options) {
394
394
  if (auth.protectedResourceMetadataUrl !== void 0)
395
395
  return auth.protectedResourceMetadataUrl;
396
396
  const base = auth.resource ?? request.url;
397
- return new URL(OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
397
+ return new URL(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
398
398
  }
399
- function wwwAuthenticateHeader(auth, request, params = {}) {
399
+ function wwwAuthenticateHeader(auth, request, options, params = {}) {
400
400
  const values = [
401
401
  `realm=${quoteAuthenticateParam("mcp")}`,
402
- `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request))}`
402
+ `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request, options))}`
403
403
  ];
404
404
  if (auth.requiredScopes && auth.requiredScopes.length > 0) {
405
405
  values.push(`scope=${quoteAuthenticateParam(auth.requiredScopes.join(" "))}`);
@@ -410,9 +410,12 @@ function wwwAuthenticateHeader(auth, request, params = {}) {
410
410
  values.push(`error_description=${quoteAuthenticateParam(params.errorDescription)}`);
411
411
  return `Bearer ${values.join(", ")}`;
412
412
  }
413
- function challengeResponse(auth, request, status, oauthError, errorDescription) {
413
+ function challengeResponse(auth, request, options, status, oauthError, errorDescription) {
414
414
  const headers = new Headers(JSON_HEADERS);
415
- headers.set("WWW-Authenticate", wwwAuthenticateHeader(auth, request, { error: oauthError, errorDescription }));
415
+ headers.set(
416
+ "WWW-Authenticate",
417
+ wwwAuthenticateHeader(auth, request, options, { error: oauthError, errorDescription })
418
+ );
416
419
  headers.set("Cache-Control", "no-store");
417
420
  return new Response(JSON.stringify({ error: status === 403 ? "forbidden" : "unauthorized" }), {
418
421
  status,
@@ -440,10 +443,17 @@ function parseBearerToken(request) {
440
443
  function getOAuthRuntime(mcp, options = {}) {
441
444
  if (options.resourcePath !== void 0)
442
445
  assertResourcePathShape(options.resourcePath);
443
- const stableOptions = { resourcePath: options.resourcePath };
446
+ if (options.metadataPath !== void 0)
447
+ assertResourcePathShape(options.metadataPath, "metadataPath");
448
+ const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
444
449
  const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
445
450
  if (!auth)
446
451
  return { kind: "unconfigured", options: stableOptions };
452
+ if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
453
+ throw new Error(
454
+ `@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.`
455
+ );
456
+ }
447
457
  const discovery = createOAuthDiscoveryResolver(auth);
448
458
  return {
449
459
  kind: "configured",
@@ -474,7 +484,7 @@ function createRequestAuthorizer(mcp, options = {}) {
474
484
  const token = parseBearerToken(request);
475
485
  if (!token) {
476
486
  log.info("auth.no_bearer_token", { outcome: "401" });
477
- return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
487
+ return { ok: false, response: challengeResponse(runtime.auth, request, runtime.options, 401) };
478
488
  }
479
489
  try {
480
490
  const auth = await runtime.verify(token, request, runtime.options);
@@ -489,13 +499,27 @@ function createRequestAuthorizer(mcp, options = {}) {
489
499
  log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
490
500
  return {
491
501
  ok: false,
492
- response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
502
+ response: challengeResponse(
503
+ runtime.auth,
504
+ request,
505
+ runtime.options,
506
+ err.status,
507
+ err.oauthError,
508
+ err.message
509
+ )
493
510
  };
494
511
  }
495
512
  log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
496
513
  return {
497
514
  ok: false,
498
- response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
515
+ response: challengeResponse(
516
+ runtime.auth,
517
+ request,
518
+ runtime.options,
519
+ 401,
520
+ "invalid_token",
521
+ "Invalid access token"
522
+ )
499
523
  };
500
524
  }
501
525
  }
@@ -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-CJMHLE7F.js";
4
4
  import "../../chunk-MA5H6PSF.js";
5
- import "../../chunk-HRMLGCXV.js";
5
+ import "../../chunk-FQLM4JDU.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
  }
@@ -397,10 +397,17 @@ function oauthConfigurationErrorResponse() {
397
397
  function getOAuthRuntime(mcp, options = {}) {
398
398
  if (options.resourcePath !== void 0)
399
399
  assertResourcePathShape(options.resourcePath);
400
- const stableOptions = { resourcePath: options.resourcePath };
400
+ if (options.metadataPath !== void 0)
401
+ assertResourcePathShape(options.metadataPath, "metadataPath");
402
+ const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
401
403
  const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
402
404
  if (!auth)
403
405
  return { kind: "unconfigured", options: stableOptions };
406
+ if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
407
+ throw new Error(
408
+ `@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.`
409
+ );
410
+ }
404
411
  const discovery = createOAuthDiscoveryResolver(auth);
405
412
  return {
406
413
  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-3GQXQBYQ.js";
4
+ import "../chunk-FQLM4JDU.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
  }
@@ -400,16 +400,16 @@ function quoteAuthenticateParam(value) {
400
400
  const sanitized = value.replace(/[\u0000-\u001F\u007F]/g, "");
401
401
  return `"${sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
402
402
  }
403
- function resolveProtectedResourceMetadataUrl(auth, request) {
403
+ function resolveProtectedResourceMetadataUrl(auth, request, options) {
404
404
  if (auth.protectedResourceMetadataUrl !== void 0)
405
405
  return auth.protectedResourceMetadataUrl;
406
406
  const base = auth.resource ?? request.url;
407
- return new URL(OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
407
+ return new URL(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
408
408
  }
409
- function wwwAuthenticateHeader(auth, request, params = {}) {
409
+ function wwwAuthenticateHeader(auth, request, options, params = {}) {
410
410
  const values = [
411
411
  `realm=${quoteAuthenticateParam("mcp")}`,
412
- `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request))}`
412
+ `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request, options))}`
413
413
  ];
414
414
  if (auth.requiredScopes && auth.requiredScopes.length > 0) {
415
415
  values.push(`scope=${quoteAuthenticateParam(auth.requiredScopes.join(" "))}`);
@@ -420,9 +420,12 @@ function wwwAuthenticateHeader(auth, request, params = {}) {
420
420
  values.push(`error_description=${quoteAuthenticateParam(params.errorDescription)}`);
421
421
  return `Bearer ${values.join(", ")}`;
422
422
  }
423
- function challengeResponse(auth, request, status, oauthError, errorDescription) {
423
+ function challengeResponse(auth, request, options, status, oauthError, errorDescription) {
424
424
  const headers = new Headers(JSON_HEADERS);
425
- headers.set("WWW-Authenticate", wwwAuthenticateHeader(auth, request, { error: oauthError, errorDescription }));
425
+ headers.set(
426
+ "WWW-Authenticate",
427
+ wwwAuthenticateHeader(auth, request, options, { error: oauthError, errorDescription })
428
+ );
426
429
  headers.set("Cache-Control", "no-store");
427
430
  return new Response(JSON.stringify({ error: status === 403 ? "forbidden" : "unauthorized" }), {
428
431
  status,
@@ -450,10 +453,17 @@ function parseBearerToken(request) {
450
453
  function getOAuthRuntime(mcp, options = {}) {
451
454
  if (options.resourcePath !== void 0)
452
455
  assertResourcePathShape(options.resourcePath);
453
- const stableOptions = { resourcePath: options.resourcePath };
456
+ if (options.metadataPath !== void 0)
457
+ assertResourcePathShape(options.metadataPath, "metadataPath");
458
+ const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
454
459
  const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
455
460
  if (!auth)
456
461
  return { kind: "unconfigured", options: stableOptions };
462
+ if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
463
+ throw new Error(
464
+ `@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.`
465
+ );
466
+ }
457
467
  const discovery = createOAuthDiscoveryResolver(auth);
458
468
  return {
459
469
  kind: "configured",
@@ -492,7 +502,7 @@ function createRequestAuthorizer(mcp, options = {}) {
492
502
  const token = parseBearerToken(request);
493
503
  if (!token) {
494
504
  log.info("auth.no_bearer_token", { outcome: "401" });
495
- return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
505
+ return { ok: false, response: challengeResponse(runtime.auth, request, runtime.options, 401) };
496
506
  }
497
507
  try {
498
508
  const auth = await runtime.verify(token, request, runtime.options);
@@ -507,13 +517,27 @@ function createRequestAuthorizer(mcp, options = {}) {
507
517
  log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
508
518
  return {
509
519
  ok: false,
510
- response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
520
+ response: challengeResponse(
521
+ runtime.auth,
522
+ request,
523
+ runtime.options,
524
+ err.status,
525
+ err.oauthError,
526
+ err.message
527
+ )
511
528
  };
512
529
  }
513
530
  log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
514
531
  return {
515
532
  ok: false,
516
- response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
533
+ response: challengeResponse(
534
+ runtime.auth,
535
+ request,
536
+ runtime.options,
537
+ 401,
538
+ "invalid_token",
539
+ "Invalid access token"
540
+ )
517
541
  };
518
542
  }
519
543
  }
@@ -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-SBKIKJNB.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-BG3HOZXZ.js";
8
+ import "../../chunk-FQLM4JDU.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.0";
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-BG3HOZXZ.js";
5
+ import "../../../chunk-FQLM4JDU.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.0";
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
  }
@@ -402,16 +402,16 @@ function quoteAuthenticateParam(value) {
402
402
  const sanitized = value.replace(/[\u0000-\u001F\u007F]/g, "");
403
403
  return `"${sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
404
404
  }
405
- function resolveProtectedResourceMetadataUrl(auth, request) {
405
+ function resolveProtectedResourceMetadataUrl(auth, request, options) {
406
406
  if (auth.protectedResourceMetadataUrl !== void 0)
407
407
  return auth.protectedResourceMetadataUrl;
408
408
  const base = auth.resource ?? request.url;
409
- return new URL(OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
409
+ return new URL(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
410
410
  }
411
- function wwwAuthenticateHeader(auth, request, params = {}) {
411
+ function wwwAuthenticateHeader(auth, request, options, params = {}) {
412
412
  const values = [
413
413
  `realm=${quoteAuthenticateParam("mcp")}`,
414
- `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request))}`
414
+ `resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request, options))}`
415
415
  ];
416
416
  if (auth.requiredScopes && auth.requiredScopes.length > 0) {
417
417
  values.push(`scope=${quoteAuthenticateParam(auth.requiredScopes.join(" "))}`);
@@ -422,9 +422,12 @@ function wwwAuthenticateHeader(auth, request, params = {}) {
422
422
  values.push(`error_description=${quoteAuthenticateParam(params.errorDescription)}`);
423
423
  return `Bearer ${values.join(", ")}`;
424
424
  }
425
- function challengeResponse(auth, request, status, oauthError, errorDescription) {
425
+ function challengeResponse(auth, request, options, status, oauthError, errorDescription) {
426
426
  const headers = new Headers(JSON_HEADERS);
427
- headers.set("WWW-Authenticate", wwwAuthenticateHeader(auth, request, { error: oauthError, errorDescription }));
427
+ headers.set(
428
+ "WWW-Authenticate",
429
+ wwwAuthenticateHeader(auth, request, options, { error: oauthError, errorDescription })
430
+ );
428
431
  headers.set("Cache-Control", "no-store");
429
432
  return new Response(JSON.stringify({ error: status === 403 ? "forbidden" : "unauthorized" }), {
430
433
  status,
@@ -452,10 +455,17 @@ function parseBearerToken(request) {
452
455
  function getOAuthRuntime(mcp, options = {}) {
453
456
  if (options.resourcePath !== void 0)
454
457
  assertResourcePathShape(options.resourcePath);
455
- const stableOptions = { resourcePath: options.resourcePath };
458
+ if (options.metadataPath !== void 0)
459
+ assertResourcePathShape(options.metadataPath, "metadataPath");
460
+ const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
456
461
  const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
457
462
  if (!auth)
458
463
  return { kind: "unconfigured", options: stableOptions };
464
+ if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
465
+ throw new Error(
466
+ `@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.`
467
+ );
468
+ }
459
469
  const discovery = createOAuthDiscoveryResolver(auth);
460
470
  return {
461
471
  kind: "configured",
@@ -494,7 +504,7 @@ function createRequestAuthorizer(mcp, options = {}) {
494
504
  const token = parseBearerToken(request);
495
505
  if (!token) {
496
506
  log.info("auth.no_bearer_token", { outcome: "401" });
497
- return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
507
+ return { ok: false, response: challengeResponse(runtime.auth, request, runtime.options, 401) };
498
508
  }
499
509
  try {
500
510
  const auth = await runtime.verify(token, request, runtime.options);
@@ -509,13 +519,27 @@ function createRequestAuthorizer(mcp, options = {}) {
509
519
  log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
510
520
  return {
511
521
  ok: false,
512
- response: challengeResponse(runtime.auth, request, err.status, err.oauthError, err.message)
522
+ response: challengeResponse(
523
+ runtime.auth,
524
+ request,
525
+ runtime.options,
526
+ err.status,
527
+ err.oauthError,
528
+ err.message
529
+ )
513
530
  };
514
531
  }
515
532
  log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
516
533
  return {
517
534
  ok: false,
518
- response: challengeResponse(runtime.auth, request, 401, "invalid_token", "Invalid access token")
535
+ response: challengeResponse(
536
+ runtime.auth,
537
+ request,
538
+ runtime.options,
539
+ 401,
540
+ "invalid_token",
541
+ "Invalid access token"
542
+ )
519
543
  };
520
544
  }
521
545
  }
@@ -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-CJMHLE7F.js";
4
4
  import {
5
5
  createOAuthProtectedResourceMetadataHandler
6
- } from "../../chunk-W7F6JRDB.js";
6
+ } from "../../chunk-3GQXQBYQ.js";
7
7
  import {
8
8
  createInvokeToolHandler
9
- } from "../../chunk-4Y72OCPS.js";
9
+ } from "../../chunk-SBKIKJNB.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-BG3HOZXZ.js";
14
+ import "../../chunk-FQLM4JDU.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.0",
4
4
  "description": "Author MCP servers for Lovable apps. Declare tools with defineTool, register them in defineMcp, and the framework adapter (TanStack today, Supabase Edge Functions next) emits the route(s) at build time.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,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 };