@lovable.dev/mcp-js 0.7.1 → 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 (38) hide show
  1. package/README.internal.md +6 -2
  2. package/README.md +13 -1
  3. package/dist/authorize-HTd0GKmB.d.ts +18 -0
  4. package/dist/{chunk-W7F6JRDB.js → chunk-3GQXQBYQ.js} +1 -1
  5. package/dist/chunk-BG3HOZXZ.js +57 -0
  6. package/dist/{chunk-3XPKR4G6.js → chunk-CJMHLE7F.js} +1 -1
  7. package/dist/{chunk-HRMLGCXV.js → chunk-FQLM4JDU.js} +36 -12
  8. package/dist/chunk-QJ7XEKT3.js +8 -0
  9. package/dist/{chunk-LMT6RXUF.js → chunk-SBKIKJNB.js} +3 -47
  10. package/dist/index.d.cts +2 -2
  11. package/dist/index.d.ts +2 -2
  12. package/dist/protocols/mcp/index.cjs +36 -12
  13. package/dist/protocols/mcp/index.d.cts +2 -2
  14. package/dist/protocols/mcp/index.d.ts +2 -2
  15. package/dist/protocols/mcp/index.js +2 -2
  16. package/dist/protocols/oauth-metadata.cjs +10 -3
  17. package/dist/protocols/oauth-metadata.d.cts +2 -2
  18. package/dist/protocols/oauth-metadata.d.ts +2 -2
  19. package/dist/protocols/oauth-metadata.js +2 -2
  20. package/dist/protocols/rest/index.cjs +50 -24
  21. package/dist/protocols/rest/index.d.cts +2 -2
  22. package/dist/protocols/rest/index.d.ts +2 -2
  23. package/dist/protocols/rest/index.js +6 -4
  24. package/dist/stacks/tanstack/cli/extract-manifest.cjs +214 -0
  25. package/dist/stacks/tanstack/cli/extract-manifest.d.cts +1 -0
  26. package/dist/stacks/tanstack/cli/extract-manifest.d.ts +1 -0
  27. package/dist/stacks/tanstack/cli/extract-manifest.js +170 -0
  28. package/dist/stacks/tanstack/index.cjs +50 -24
  29. package/dist/stacks/tanstack/index.d.cts +2 -2
  30. package/dist/stacks/tanstack/index.d.ts +2 -2
  31. package/dist/stacks/tanstack/index.js +8 -6
  32. package/dist/stacks/tanstack/vite.cjs +67 -19
  33. package/dist/stacks/tanstack/vite.d.cts +26 -2
  34. package/dist/stacks/tanstack/vite.d.ts +26 -2
  35. package/dist/stacks/tanstack/vite.js +65 -20
  36. package/dist/{types-zMlr1mOK.d.ts → types-C5SSORxe.d.ts} +4 -0
  37. package/package.json +6 -3
  38. package/dist/authorize-DpTEIFvs.d.ts +0 -9
@@ -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
  }
@@ -718,6 +742,19 @@ function shapeToJsonSchema(shape) {
718
742
  return null;
719
743
  }
720
744
  }
745
+ function buildMcpListing(mcp) {
746
+ return {
747
+ server: { name: mcp.name, version: mcp.version, title: mcp.title },
748
+ tools: mcp.tools.map((tool) => ({
749
+ name: tool.name,
750
+ title: tool.title,
751
+ description: tool.description,
752
+ annotations: tool.annotations,
753
+ inputSchema: shapeToJsonSchema(tool.inputSchema),
754
+ outputSchema: shapeToJsonSchema(tool.outputSchema)
755
+ }))
756
+ };
757
+ }
721
758
  function createListToolsHandler(mcp, options = {}) {
722
759
  assertRestResourceBinding(mcp, options);
723
760
  const authorizer = createRequestAuthorizer(mcp, options);
@@ -727,18 +764,7 @@ function createListToolsHandler(mcp, options = {}) {
727
764
  return authResult.response;
728
765
  if (request.method !== "GET" && request.method !== "HEAD")
729
766
  return methodNotAllowed("GET, HEAD, OPTIONS");
730
- const body = {
731
- server: { name: mcp.name, version: mcp.version, title: mcp.title },
732
- tools: mcp.tools.map((tool) => ({
733
- name: tool.name,
734
- title: tool.title,
735
- description: tool.description,
736
- annotations: tool.annotations,
737
- inputSchema: shapeToJsonSchema(tool.inputSchema),
738
- outputSchema: shapeToJsonSchema(tool.outputSchema)
739
- }))
740
- };
741
- const response = Response.json(body);
767
+ const response = Response.json(buildMcpListing(mcp));
742
768
  return request.method === "HEAD" ? headResponse(response) : response;
743
769
  };
744
770
  return async (request) => {
@@ -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,15 +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
- createInvokeToolHandler,
9
- createListToolsHandler
10
- } from "../../chunk-LMT6RXUF.js";
8
+ createInvokeToolHandler
9
+ } from "../../chunk-SBKIKJNB.js";
11
10
  import "../../chunk-MA5H6PSF.js";
12
- import "../../chunk-HRMLGCXV.js";
11
+ import {
12
+ createListToolsHandler
13
+ } from "../../chunk-BG3HOZXZ.js";
14
+ import "../../chunk-FQLM4JDU.js";
13
15
  import "../../chunk-QC3DXQTH.js";
14
16
  import "../../chunk-6DXGZZA4.js";
15
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,
@@ -32,11 +33,13 @@ var import_node_path = require("path");
32
33
  // src/auth/metadata-path.ts
33
34
  var OAUTH_PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
34
35
 
35
- // src/stacks/tanstack/vite.ts
36
- var GENERATED_BANNER = "// AUTO-GENERATED by @lovable.dev/mcp-js \u2014 do not edit. Regenerated by the Vite plugin.\n// To take ownership, delete this banner line; the plugin then leaves the file alone.";
36
+ // src/stacks/tanstack/fs-errors.ts
37
37
  function isFileMissing(err) {
38
38
  return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
39
39
  }
40
+
41
+ // src/stacks/tanstack/vite.ts
42
+ var GENERATED_BANNER = "// AUTO-GENERATED by @lovable.dev/mcp-js \u2014 do not edit. Regenerated by the Vite plugin.\n// To take ownership, delete this banner line; the plugin then leaves the file alone.";
40
43
  function normalizePath(p) {
41
44
  return p.split(import_node_path.sep).join("/");
42
45
  }
@@ -53,15 +56,17 @@ function wellKnownRouteFile(routesDir, urlPath) {
53
56
  });
54
57
  return (0, import_node_path.resolve)(routesDir, ...fileSegments);
55
58
  }
56
- function resolveAllRoutes(projectRoot, routesDirOption, routeFileName) {
59
+ function resolveAllRoutes(projectRoot, routesDirOption, routeFileName, metadataUrlPath) {
57
60
  const routesDir = (0, import_node_path.resolve)(projectRoot, routesDirOption);
58
61
  assertContains(projectRoot, routesDir, `routesDir "${routesDirOption}"`);
59
62
  const mcpRouteFile = (0, import_node_path.resolve)(routesDir, routeFileName);
60
63
  assertContains(routesDir, mcpRouteFile, `routeFileName "${routeFileName}"`);
64
+ const metadataRouteFile = wellKnownRouteFile(routesDir, metadataUrlPath);
65
+ assertContains(routesDir, metadataRouteFile, `metadataPath "${metadataUrlPath}"`);
61
66
  return {
62
67
  routesDir,
63
68
  mcpRouteFile,
64
- metadataRouteFile: wellKnownRouteFile(routesDir, OAUTH_PROTECTED_RESOURCE_METADATA_PATH),
69
+ metadataRouteFile,
65
70
  listToolsRouteFile: (0, import_node_path.resolve)(routesDir, "[.mcp]", "list-tools.ts"),
66
71
  invokeToolRouteFile: (0, import_node_path.resolve)(routesDir, "[.mcp]", "invoke-tool", "$tool.ts")
67
72
  };
@@ -85,6 +90,24 @@ function assertUrlPathShape(urlPath) {
85
90
  function deriveRouteFileName(urlPath) {
86
91
  return `${assertUrlPathShape(urlPath)}.ts`;
87
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
+ }
88
111
  function canonicalUrlPath(urlPath) {
89
112
  return `/${urlPath.replace(/^\/+/, "").replace(/\/+$/, "")}`;
90
113
  }
@@ -93,10 +116,11 @@ function relativeImportSpecifier(routeFile, target) {
93
116
  const withoutExt = rel.replace(/\.ts$/, "");
94
117
  return withoutExt.startsWith(".") ? withoutExt : `./${withoutExt}`;
95
118
  }
96
- function buildRouteSource(route, resourcePath, mcpEntry, projectRoot) {
119
+ function buildRouteSource(route, resourcePath, metadataPath, mcpEntry, projectRoot) {
97
120
  const mcpImport = relativeImportSpecifier(route.file, mcpEntry);
98
121
  const routeRel = normalizePath((0, import_node_path.relative)(projectRoot, route.file));
99
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}" }`;
100
124
  return `${GENERATED_BANNER}
101
125
  // route: ${route.routeLiteral}
102
126
  // emitted to: ${routeRel}
@@ -110,7 +134,7 @@ import mcp from "${mcpImport}";
110
134
  export const Route = createFileRoute("${route.routeLiteral}")({
111
135
  server: {
112
136
  handlers: {
113
- ${note} ANY: ${route.handlerFactory}(mcp, { resourcePath: "${resourcePath}" }),
137
+ ${note} ANY: ${route.handlerFactory}(mcp, ${runtimeOptions}),
114
138
  },
115
139
  },
116
140
  });
@@ -189,12 +213,23 @@ function mcpPlugin(options = {}) {
189
213
  const urlPath = canonicalUrlPath(rawUrlPath);
190
214
  const emitRestRoutes = options.restRoutes !== false;
191
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;
192
226
  let projectRoot = process.cwd();
193
227
  let mcpEntry = (0, import_node_path.resolve)(projectRoot, mcpEntryOption);
194
228
  let { routesDir, mcpRouteFile, metadataRouteFile, listToolsRouteFile, invokeToolRouteFile } = resolveAllRoutes(
195
229
  projectRoot,
196
230
  routesDirOption,
197
- routeFileName
231
+ routeFileName,
232
+ metadataUrlPath
198
233
  );
199
234
  const regenerate = () => {
200
235
  let mcpEntryExists = true;
@@ -213,7 +248,7 @@ function mcpPlugin(options = {}) {
213
248
  if (emitProtectedResourceMetadataRoute) {
214
249
  routes.push({
215
250
  file: metadataRouteFile,
216
- routeLiteral: OAUTH_PROTECTED_RESOURCE_METADATA_PATH,
251
+ routeLiteral: metadataUrlPath,
217
252
  handlerFactory: "createTanStackOAuthProtectedResourceMetadataHandler"
218
253
  });
219
254
  }
@@ -235,7 +270,7 @@ function mcpPlugin(options = {}) {
235
270
  }
236
271
  for (const route of routes) {
237
272
  expected.add(route.file);
238
- writeIfChanged(route.file, buildRouteSource(route, urlPath, mcpEntry, projectRoot));
273
+ writeIfChanged(route.file, buildRouteSource(route, urlPath, injectedMetadataPath, mcpEntry, projectRoot));
239
274
  }
240
275
  }
241
276
  for (const file of findAdoptedFiles(routesDir)) {
@@ -251,30 +286,42 @@ function mcpPlugin(options = {}) {
251
286
  };
252
287
  return {
253
288
  name: "@lovable.dev/mcp-js",
289
+ // The extract CLI reads the resolved entry + URL path back through this
290
+ // (via `resolveConfig().plugins`) so the manifest honors custom
291
+ // `mcpEntry`/`path` instead of assuming the defaults. Getters because
292
+ // `mcpEntry` is finalized in `configResolved`.
293
+ api: {
294
+ get mcpEntry() {
295
+ return mcpEntry;
296
+ },
297
+ get urlPath() {
298
+ return urlPath;
299
+ }
300
+ },
254
301
  configResolved(config) {
255
302
  projectRoot = config.root;
256
303
  mcpEntry = (0, import_node_path.resolve)(projectRoot, mcpEntryOption);
257
304
  ({ routesDir, mcpRouteFile, metadataRouteFile, listToolsRouteFile, invokeToolRouteFile } = resolveAllRoutes(
258
305
  projectRoot,
259
306
  routesDirOption,
260
- routeFileName
307
+ routeFileName,
308
+ metadataUrlPath
261
309
  ));
262
310
  regenerate();
263
311
  },
264
312
  configureServer(server) {
265
- const watchedEntry = mcpEntry;
266
- const onEntryChange = (file) => {
267
- if (normalizePath(file) === normalizePath(watchedEntry)) {
313
+ const onChange = (file) => {
314
+ if (normalizePath(file) === normalizePath(mcpEntry)) {
268
315
  regenerate();
269
316
  }
270
317
  };
271
- server.watcher.on("add", onEntryChange);
272
- server.watcher.on("change", onEntryChange);
273
- server.watcher.on("unlink", onEntryChange);
318
+ server.watcher.on("add", onChange);
319
+ server.watcher.on("change", onChange);
320
+ server.watcher.on("unlink", onChange);
274
321
  server.watcher.once("close", () => {
275
- server.watcher.off("add", onEntryChange);
276
- server.watcher.off("change", onEntryChange);
277
- server.watcher.off("unlink", onEntryChange);
322
+ server.watcher.off("add", onChange);
323
+ server.watcher.off("change", onChange);
324
+ server.watcher.off("unlink", onChange);
278
325
  });
279
326
  },
280
327
  buildStart() {
@@ -285,6 +332,7 @@ function mcpPlugin(options = {}) {
285
332
  var vite_default = mcpPlugin;
286
333
  // Annotate the CommonJS export names for ESM import in node:
287
334
  0 && (module.exports = {
335
+ assertMetadataPathShape,
288
336
  assertUrlPathShape,
289
337
  deriveRouteFileName,
290
338
  mcpPlugin
@@ -48,17 +48,41 @@ 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;
66
+ }
67
+ interface McpPluginApi {
68
+ readonly mcpEntry: string;
69
+ readonly urlPath: string;
51
70
  }
52
71
  declare function assertUrlPathShape(urlPath: string): string;
53
72
  declare function deriveRouteFileName(urlPath: string): string;
73
+ declare function assertMetadataPathShape(metadataPath: string): void;
54
74
  /**
55
75
  * Generate the TanStack route(s) that mount the MCP server.
56
76
  *
57
77
  * Place this in your `vite.config.ts` plugins array. Emits during
58
78
  * `configResolved` (so dev-server boot and the TanStack file-router both
59
79
  * see the routes on the first request) and re-emits when the MCP entry
60
- * file changes during dev.
80
+ * file appears or disappears during dev.
81
+ *
82
+ * The `.lovable/mcp/manifest.json` snapshot is produced separately by the
83
+ * `lovable-mcp-extract-manifest` CLI (run by a host platform at build/commit
84
+ * time) — not by this plugin.
61
85
  */
62
86
  declare function mcpPlugin(options?: McpPluginOptions): Plugin;
63
87
 
64
- export { type McpPluginOptions, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
88
+ export { type McpPluginApi, type McpPluginOptions, assertMetadataPathShape, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
@@ -48,17 +48,41 @@ 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;
66
+ }
67
+ interface McpPluginApi {
68
+ readonly mcpEntry: string;
69
+ readonly urlPath: string;
51
70
  }
52
71
  declare function assertUrlPathShape(urlPath: string): string;
53
72
  declare function deriveRouteFileName(urlPath: string): string;
73
+ declare function assertMetadataPathShape(metadataPath: string): void;
54
74
  /**
55
75
  * Generate the TanStack route(s) that mount the MCP server.
56
76
  *
57
77
  * Place this in your `vite.config.ts` plugins array. Emits during
58
78
  * `configResolved` (so dev-server boot and the TanStack file-router both
59
79
  * see the routes on the first request) and re-emits when the MCP entry
60
- * file changes during dev.
80
+ * file appears or disappears during dev.
81
+ *
82
+ * The `.lovable/mcp/manifest.json` snapshot is produced separately by the
83
+ * `lovable-mcp-extract-manifest` CLI (run by a host platform at build/commit
84
+ * time) — not by this plugin.
61
85
  */
62
86
  declare function mcpPlugin(options?: McpPluginOptions): Plugin;
63
87
 
64
- export { type McpPluginOptions, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
88
+ export { type McpPluginApi, type McpPluginOptions, assertMetadataPathShape, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
@@ -1,3 +1,6 @@
1
+ import {
2
+ isFileMissing
3
+ } from "../../chunk-QJ7XEKT3.js";
1
4
  import {
2
5
  OAUTH_PROTECTED_RESOURCE_METADATA_PATH
3
6
  } from "../../chunk-6DXGZZA4.js";
@@ -6,9 +9,6 @@ import {
6
9
  import { lstatSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
7
10
  import { dirname, join, relative, resolve, sep } from "path";
8
11
  var GENERATED_BANNER = "// AUTO-GENERATED by @lovable.dev/mcp-js \u2014 do not edit. Regenerated by the Vite plugin.\n// To take ownership, delete this banner line; the plugin then leaves the file alone.";
9
- function isFileMissing(err) {
10
- return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
11
- }
12
12
  function normalizePath(p) {
13
13
  return p.split(sep).join("/");
14
14
  }
@@ -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)) {
@@ -223,30 +255,42 @@ function mcpPlugin(options = {}) {
223
255
  };
224
256
  return {
225
257
  name: "@lovable.dev/mcp-js",
258
+ // The extract CLI reads the resolved entry + URL path back through this
259
+ // (via `resolveConfig().plugins`) so the manifest honors custom
260
+ // `mcpEntry`/`path` instead of assuming the defaults. Getters because
261
+ // `mcpEntry` is finalized in `configResolved`.
262
+ api: {
263
+ get mcpEntry() {
264
+ return mcpEntry;
265
+ },
266
+ get urlPath() {
267
+ return urlPath;
268
+ }
269
+ },
226
270
  configResolved(config) {
227
271
  projectRoot = config.root;
228
272
  mcpEntry = resolve(projectRoot, mcpEntryOption);
229
273
  ({ routesDir, mcpRouteFile, metadataRouteFile, listToolsRouteFile, invokeToolRouteFile } = resolveAllRoutes(
230
274
  projectRoot,
231
275
  routesDirOption,
232
- routeFileName
276
+ routeFileName,
277
+ metadataUrlPath
233
278
  ));
234
279
  regenerate();
235
280
  },
236
281
  configureServer(server) {
237
- const watchedEntry = mcpEntry;
238
- const onEntryChange = (file) => {
239
- if (normalizePath(file) === normalizePath(watchedEntry)) {
282
+ const onChange = (file) => {
283
+ if (normalizePath(file) === normalizePath(mcpEntry)) {
240
284
  regenerate();
241
285
  }
242
286
  };
243
- server.watcher.on("add", onEntryChange);
244
- server.watcher.on("change", onEntryChange);
245
- server.watcher.on("unlink", onEntryChange);
287
+ server.watcher.on("add", onChange);
288
+ server.watcher.on("change", onChange);
289
+ server.watcher.on("unlink", onChange);
246
290
  server.watcher.once("close", () => {
247
- server.watcher.off("add", onEntryChange);
248
- server.watcher.off("change", onEntryChange);
249
- server.watcher.off("unlink", onEntryChange);
291
+ server.watcher.off("add", onChange);
292
+ server.watcher.off("change", onChange);
293
+ server.watcher.off("unlink", onChange);
250
294
  });
251
295
  },
252
296
  buildStart() {
@@ -256,6 +300,7 @@ function mcpPlugin(options = {}) {
256
300
  }
257
301
  var vite_default = mcpPlugin;
258
302
  export {
303
+ assertMetadataPathShape,
259
304
  assertUrlPathShape,
260
305
  vite_default as default,
261
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;