@lovable.dev/mcp-js 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,7 +49,9 @@ That's the whole authoring surface. No imperative server construction, no route
49
49
  | `GET /.mcp/list-tools` | `src/routes/[.mcp]/list-tools.ts` | Tool catalog with JSON Schemas |
50
50
  | `POST /.mcp/invoke-tool/<tool>` | `src/routes/[.mcp]/invoke-tool/$tool.ts` | REST dispatcher; one handler call per tool |
51
51
 
52
- MCP (`POST /mcp`) is the public wire format clients speak directly. REST (`/.mcp/list-tools`, `/.mcp/invoke-tool/<tool>`) is internal RPC — only an upstream MCP proxy calls it; never a browser or a hand-rolled client. All runtime routes import the same `defineMcp` result, so they stay in sync on which tools exist and which auth policy protects them. If `defineMcp({ auth: ... })` is omitted, the handlers stay unauthenticated; if OAuth auth is configured, MCP and REST both require the proxy/client to pass `Authorization: Bearer <token>`. CORS and rate limiting still belong at the app host or edge.
52
+ The table shows the default `path: "/mcp"`. The REST companions co-locate under the parent of the configured `path`, so `mcpPlugin({ path: "/api/public/mcp" })` emits them at `/api/public/.mcp/list-tools` and `/api/public/.mcp/invoke-tool/<tool>` (file: `src/routes/api/public/[.mcp]/...`). Pass `restRoutes: false` to drop them.
53
+
54
+ MCP (`POST <path>`, `/mcp` by default) is the public wire format clients speak directly. REST (the `.mcp/*` companions, co-located under the `path` parent) is internal RPC — only an upstream MCP proxy calls it; never a browser or a hand-rolled client. All runtime routes import the same `defineMcp` result, so they stay in sync on which tools exist and which auth policy protects them. If `defineMcp({ auth: ... })` is omitted, the handlers stay unauthenticated; if OAuth auth is configured, MCP and REST both require the proxy/client to pass `Authorization: Bearer <token>`. CORS and rate limiting still belong at the app host or edge.
53
55
 
54
56
  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
57
 
@@ -13,6 +13,13 @@ interface McpRuntimeOptions {
13
13
  * an externally hosted document). Defaults to the well-known path when absent.
14
14
  */
15
15
  metadataPath?: string;
16
+ /**
17
+ * Derive the request origin (and thus the advertised `resource`/metadata URLs)
18
+ * from `X-Forwarded-Host` rather than the rewritten `Host`. Stack adapters read
19
+ * this before invoking the handler, so the OAuth runtime never sees it; safe
20
+ * only behind a proxy that overwrites the header (see `core/forwarded.ts`).
21
+ */
22
+ trustForwardedHost?: boolean;
16
23
  }
17
24
 
18
25
  export type { McpRuntimeOptions as M };
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "0.12.1";
2
+ var version = "0.14.0";
3
3
 
4
4
  export {
5
5
  version
@@ -0,0 +1,26 @@
1
+ // src/core/forwarded.ts
2
+ function applyForwardedOrigin(request, options) {
3
+ const proto = options.trustForwardedProto ? firstForwardedValue(request, "x-forwarded-proto") : void 0;
4
+ const host = options.trustForwardedHost ? firstForwardedValue(request, "x-forwarded-host") : void 0;
5
+ if (proto === void 0 && host === void 0)
6
+ return request;
7
+ const url = new URL(request.url);
8
+ let changed = false;
9
+ if (proto !== void 0 && `${proto}:` !== url.protocol) {
10
+ url.protocol = `${proto}:`;
11
+ changed = true;
12
+ }
13
+ if (host !== void 0 && host !== url.host) {
14
+ url.host = host;
15
+ changed = true;
16
+ }
17
+ return changed ? new Request(url.href, request) : request;
18
+ }
19
+ function firstForwardedValue(request, header) {
20
+ const value = request.headers.get(header)?.split(",")[0]?.trim();
21
+ return value ? value : void 0;
22
+ }
23
+
24
+ export {
25
+ applyForwardedOrigin
26
+ };
@@ -13,7 +13,7 @@ function isFileMissing(err) {
13
13
  }
14
14
 
15
15
  // package.json
16
- var version = "0.12.1";
16
+ var version = "0.14.0";
17
17
 
18
18
  // src/protocols/rest/list-tools.ts
19
19
  var import_zod_compat = require("@modelcontextprotocol/sdk/server/zod-compat.js");
@@ -7,7 +7,7 @@ import "../chunk-QC3DXQTH.js";
7
7
  import "../chunk-6DXGZZA4.js";
8
8
  import {
9
9
  version
10
- } from "../chunk-Y2FTHVL3.js";
10
+ } from "../chunk-NOFPRSSI.js";
11
11
  import {
12
12
  isFileMissing
13
13
  } from "../chunk-Y3ZFPEQH.js";
@@ -1,4 +1,4 @@
1
- import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
1
+ import { M as McpRuntimeOptions } from '../../authorize-BMeXd_Sh.js';
2
2
  import { d as McpDefinition } from '../../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
@@ -1,4 +1,4 @@
1
- import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
1
+ import { M as McpRuntimeOptions } from '../../authorize-BMeXd_Sh.js';
2
2
  import { d as McpDefinition } from '../../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
@@ -1,4 +1,4 @@
1
- import { M as McpRuntimeOptions } from '../authorize-HTd0GKmB.js';
1
+ import { M as McpRuntimeOptions } from '../authorize-BMeXd_Sh.js';
2
2
  import { d as McpDefinition } from '../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
@@ -1,4 +1,4 @@
1
- import { M as McpRuntimeOptions } from '../authorize-HTd0GKmB.js';
1
+ import { M as McpRuntimeOptions } from '../authorize-BMeXd_Sh.js';
2
2
  import { d as McpDefinition } from '../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
@@ -1,4 +1,4 @@
1
- import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
1
+ import { M as McpRuntimeOptions } from '../../authorize-BMeXd_Sh.js';
2
2
  import { d as McpDefinition } from '../../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
@@ -1,4 +1,4 @@
1
- import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
1
+ import { M as McpRuntimeOptions } from '../../authorize-BMeXd_Sh.js';
2
2
  import { d as McpDefinition } from '../../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
@@ -72,6 +72,29 @@ function resolveResourcePath(resourcePath, request) {
72
72
  return resourcePath;
73
73
  }
74
74
 
75
+ // src/core/forwarded.ts
76
+ function applyForwardedOrigin(request, options) {
77
+ const proto = options.trustForwardedProto ? firstForwardedValue(request, "x-forwarded-proto") : void 0;
78
+ const host = options.trustForwardedHost ? firstForwardedValue(request, "x-forwarded-host") : void 0;
79
+ if (proto === void 0 && host === void 0)
80
+ return request;
81
+ const url = new URL(request.url);
82
+ let changed = false;
83
+ if (proto !== void 0 && `${proto}:` !== url.protocol) {
84
+ url.protocol = `${proto}:`;
85
+ changed = true;
86
+ }
87
+ if (host !== void 0 && host !== url.host) {
88
+ url.host = host;
89
+ changed = true;
90
+ }
91
+ return changed ? new Request(url.href, request) : request;
92
+ }
93
+ function firstForwardedValue(request, header) {
94
+ const value = request.headers.get(header)?.split(",")[0]?.trim();
95
+ return value ? value : void 0;
96
+ }
97
+
75
98
  // src/protocols/mcp/protocol.ts
76
99
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
77
100
  var import_webStandardStreamableHttp = require("@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js");
@@ -924,16 +947,6 @@ function dispatchFor(pathname) {
924
947
  }
925
948
  return { kind: "mcp" };
926
949
  }
927
- function applyForwardedProto(request) {
928
- const proto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
929
- if (!proto)
930
- return request;
931
- const url = new URL(request.url);
932
- if (`${proto}:` === url.protocol)
933
- return request;
934
- url.protocol = `${proto}:`;
935
- return new Request(url.href, request);
936
- }
937
950
  function createSupabaseHandler(mcp, options = {}) {
938
951
  const resourcePath = deriveResourcePath(options);
939
952
  if (resourcePath !== void 0)
@@ -946,7 +959,10 @@ function createSupabaseHandler(mcp, options = {}) {
946
959
  const invokeToolHandler = createInvokeToolHandler(mcp, runtimeOptions);
947
960
  const metadataHandler = createOAuthProtectedResourceMetadataHandler(mcp, runtimeOptions);
948
961
  return async (request) => {
949
- const req = applyForwardedProto(request);
962
+ const req = applyForwardedOrigin(request, {
963
+ trustForwardedProto: true,
964
+ trustForwardedHost: options.trustForwardedHost
965
+ });
950
966
  const target = dispatchFor(new URL(req.url).pathname);
951
967
  switch (target.kind) {
952
968
  case "metadata":
@@ -19,6 +19,15 @@ interface SupabaseHandlerOptions {
19
19
  * a Lovable Cloud–style host that rewrites the public URL.
20
20
  */
21
21
  resourcePath?: string;
22
+ /**
23
+ * Derive the advertised `resource`/metadata host from `X-Forwarded-Host`
24
+ * instead of the request `Host`, so a function fronted by a custom-domain
25
+ * proxy advertises the public host. Off by default: enable only behind a
26
+ * proxy that overwrites the header (an unsanitized one lets a client spoof
27
+ * the advertised resource). `X-Forwarded-Proto` is trusted regardless —
28
+ * Supabase terminates TLS upstream and forwards over http.
29
+ */
30
+ trustForwardedHost?: boolean;
22
31
  }
23
32
  /**
24
33
  * Build a single Web-Standard handler that serves the full MCP surface from
@@ -19,6 +19,15 @@ interface SupabaseHandlerOptions {
19
19
  * a Lovable Cloud–style host that rewrites the public URL.
20
20
  */
21
21
  resourcePath?: string;
22
+ /**
23
+ * Derive the advertised `resource`/metadata host from `X-Forwarded-Host`
24
+ * instead of the request `Host`, so a function fronted by a custom-domain
25
+ * proxy advertises the public host. Off by default: enable only behind a
26
+ * proxy that overwrites the header (an unsanitized one lets a client spoof
27
+ * the advertised resource). `X-Forwarded-Proto` is trusted regardless —
28
+ * Supabase terminates TLS upstream and forwards over http.
29
+ */
30
+ trustForwardedHost?: boolean;
22
31
  }
23
32
  /**
24
33
  * Build a single Web-Standard handler that serves the full MCP surface from
@@ -1,3 +1,6 @@
1
+ import {
2
+ applyForwardedOrigin
3
+ } from "../../chunk-UQK5UO6C.js";
1
4
  import {
2
5
  createMcpProtocolHandler
3
6
  } from "../../chunk-WLNT2FX7.js";
@@ -54,16 +57,6 @@ function dispatchFor(pathname) {
54
57
  }
55
58
  return { kind: "mcp" };
56
59
  }
57
- function applyForwardedProto(request) {
58
- const proto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
59
- if (!proto)
60
- return request;
61
- const url = new URL(request.url);
62
- if (`${proto}:` === url.protocol)
63
- return request;
64
- url.protocol = `${proto}:`;
65
- return new Request(url.href, request);
66
- }
67
60
  function createSupabaseHandler(mcp, options = {}) {
68
61
  const resourcePath = deriveResourcePath(options);
69
62
  if (resourcePath !== void 0)
@@ -76,7 +69,10 @@ function createSupabaseHandler(mcp, options = {}) {
76
69
  const invokeToolHandler = createInvokeToolHandler(mcp, runtimeOptions);
77
70
  const metadataHandler = createOAuthProtectedResourceMetadataHandler(mcp, runtimeOptions);
78
71
  return async (request) => {
79
- const req = applyForwardedProto(request);
72
+ const req = applyForwardedOrigin(request, {
73
+ trustForwardedProto: true,
74
+ trustForwardedHost: options.trustForwardedHost
75
+ });
80
76
  const target = dispatchFor(new URL(req.url).pathname);
81
77
  switch (target.kind) {
82
78
  case "metadata":
@@ -32,7 +32,7 @@ var import_node_fs = require("fs");
32
32
  var import_node_path = require("path");
33
33
 
34
34
  // package.json
35
- var version = "0.12.1";
35
+ var version = "0.14.0";
36
36
 
37
37
  // src/core/fs-errors.ts
38
38
  function isFileMissing(err) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  version
3
- } from "../../chunk-Y2FTHVL3.js";
3
+ } from "../../chunk-NOFPRSSI.js";
4
4
  import {
5
5
  isFileMissing
6
6
  } from "../../chunk-Y3ZFPEQH.js";
@@ -27,6 +27,29 @@ __export(tanstack_exports, {
27
27
  });
28
28
  module.exports = __toCommonJS(tanstack_exports);
29
29
 
30
+ // src/core/forwarded.ts
31
+ function applyForwardedOrigin(request, options) {
32
+ const proto = options.trustForwardedProto ? firstForwardedValue(request, "x-forwarded-proto") : void 0;
33
+ const host = options.trustForwardedHost ? firstForwardedValue(request, "x-forwarded-host") : void 0;
34
+ if (proto === void 0 && host === void 0)
35
+ return request;
36
+ const url = new URL(request.url);
37
+ let changed = false;
38
+ if (proto !== void 0 && `${proto}:` !== url.protocol) {
39
+ url.protocol = `${proto}:`;
40
+ changed = true;
41
+ }
42
+ if (host !== void 0 && host !== url.host) {
43
+ url.host = host;
44
+ changed = true;
45
+ }
46
+ return changed ? new Request(url.href, request) : request;
47
+ }
48
+ function firstForwardedValue(request, header) {
49
+ const value = request.headers.get(header)?.split(",")[0]?.trim();
50
+ return value ? value : void 0;
51
+ }
52
+
30
53
  // src/protocols/mcp/protocol.ts
31
54
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
32
55
  var import_webStandardStreamableHttp = require("@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js");
@@ -889,21 +912,24 @@ function createInvokeToolHandler(mcp, options = {}) {
889
912
  }
890
913
 
891
914
  // src/stacks/tanstack/handlers.ts
915
+ function forwarded(request, options) {
916
+ return applyForwardedOrigin(request, { trustForwardedHost: options.trustForwardedHost });
917
+ }
892
918
  function createTanStackMcpHandler(mcp, options = {}) {
893
919
  const handler = createMcpProtocolHandler(mcp, options);
894
- return ({ request }) => handler(request);
920
+ return ({ request }) => handler(forwarded(request, options));
895
921
  }
896
922
  function createTanStackListToolsHandler(mcp, options = {}) {
897
923
  const handler = createListToolsHandler(mcp, options);
898
- return ({ request }) => handler(request);
924
+ return ({ request }) => handler(forwarded(request, options));
899
925
  }
900
926
  function createTanStackInvokeToolHandler(mcp, options = {}) {
901
927
  const handler = createInvokeToolHandler(mcp, options);
902
- return ({ request, params }) => handler(request, params.tool);
928
+ return ({ request, params }) => handler(forwarded(request, options), params.tool);
903
929
  }
904
930
  function createTanStackOAuthProtectedResourceMetadataHandler(mcp, options = {}) {
905
931
  const handler = createOAuthProtectedResourceMetadataHandler(mcp, options);
906
- return ({ request }) => handler(request);
932
+ return ({ request }) => handler(forwarded(request, options));
907
933
  }
908
934
  // Annotate the CommonJS export names for ESM import in node:
909
935
  0 && (module.exports = {
@@ -1,4 +1,4 @@
1
- import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
1
+ import { M as McpRuntimeOptions } from '../../authorize-BMeXd_Sh.js';
2
2
  import { d as McpDefinition } from '../../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
@@ -1,4 +1,4 @@
1
- import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
1
+ import { M as McpRuntimeOptions } from '../../authorize-BMeXd_Sh.js';
2
2
  import { d as McpDefinition } from '../../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
@@ -1,3 +1,6 @@
1
+ import {
2
+ applyForwardedOrigin
3
+ } from "../../chunk-UQK5UO6C.js";
1
4
  import {
2
5
  createMcpProtocolHandler
3
6
  } from "../../chunk-WLNT2FX7.js";
@@ -16,21 +19,24 @@ import "../../chunk-QC3DXQTH.js";
16
19
  import "../../chunk-6DXGZZA4.js";
17
20
 
18
21
  // src/stacks/tanstack/handlers.ts
22
+ function forwarded(request, options) {
23
+ return applyForwardedOrigin(request, { trustForwardedHost: options.trustForwardedHost });
24
+ }
19
25
  function createTanStackMcpHandler(mcp, options = {}) {
20
26
  const handler = createMcpProtocolHandler(mcp, options);
21
- return ({ request }) => handler(request);
27
+ return ({ request }) => handler(forwarded(request, options));
22
28
  }
23
29
  function createTanStackListToolsHandler(mcp, options = {}) {
24
30
  const handler = createListToolsHandler(mcp, options);
25
- return ({ request }) => handler(request);
31
+ return ({ request }) => handler(forwarded(request, options));
26
32
  }
27
33
  function createTanStackInvokeToolHandler(mcp, options = {}) {
28
34
  const handler = createInvokeToolHandler(mcp, options);
29
- return ({ request, params }) => handler(request, params.tool);
35
+ return ({ request, params }) => handler(forwarded(request, options), params.tool);
30
36
  }
31
37
  function createTanStackOAuthProtectedResourceMetadataHandler(mcp, options = {}) {
32
38
  const handler = createOAuthProtectedResourceMetadataHandler(mcp, options);
33
- return ({ request }) => handler(request);
39
+ return ({ request }) => handler(forwarded(request, options));
34
40
  }
35
41
  export {
36
42
  createTanStackInvokeToolHandler,
@@ -56,20 +56,28 @@ function wellKnownRouteFile(routesDir, urlPath) {
56
56
  });
57
57
  return (0, import_node_path.resolve)(routesDir, ...fileSegments);
58
58
  }
59
- function resolveAllRoutes(projectRoot, routesDirOption, routeFileName, metadataUrlPath) {
59
+ function resolveRestRoutes(routesDir, mcpUrlPath) {
60
+ const parentSegments = mcpUrlPath.replace(/^\/+/, "").replace(/\/+$/, "").split("/").slice(0, -1);
61
+ const restDir = (0, import_node_path.resolve)(routesDir, ...parentSegments, "[.mcp]");
62
+ const restUrlBase = `/${[...parentSegments, ".mcp"].join("/")}`;
63
+ return {
64
+ listToolsRouteFile: (0, import_node_path.resolve)(restDir, "list-tools.ts"),
65
+ invokeToolRouteFile: (0, import_node_path.resolve)(restDir, "invoke-tool", "$tool.ts"),
66
+ listToolsLiteral: `${restUrlBase}/list-tools`,
67
+ invokeToolLiteral: `${restUrlBase}/invoke-tool/$tool`
68
+ };
69
+ }
70
+ function resolveAllRoutes(projectRoot, routesDirOption, routeFileName, mcpUrlPath, metadataUrlPath) {
60
71
  const routesDir = (0, import_node_path.resolve)(projectRoot, routesDirOption);
61
72
  assertContains(projectRoot, routesDir, `routesDir "${routesDirOption}"`);
62
73
  const mcpRouteFile = (0, import_node_path.resolve)(routesDir, routeFileName);
63
74
  assertContains(routesDir, mcpRouteFile, `routeFileName "${routeFileName}"`);
64
75
  const metadataRouteFile = wellKnownRouteFile(routesDir, metadataUrlPath);
65
76
  assertContains(routesDir, metadataRouteFile, `metadataPath "${metadataUrlPath}"`);
66
- return {
67
- routesDir,
68
- mcpRouteFile,
69
- metadataRouteFile,
70
- listToolsRouteFile: (0, import_node_path.resolve)(routesDir, "[.mcp]", "list-tools.ts"),
71
- invokeToolRouteFile: (0, import_node_path.resolve)(routesDir, "[.mcp]", "invoke-tool", "$tool.ts")
72
- };
77
+ const rest = resolveRestRoutes(routesDir, mcpUrlPath);
78
+ assertContains(routesDir, rest.listToolsRouteFile, `REST routes for path "${mcpUrlPath}"`);
79
+ assertContains(routesDir, rest.invokeToolRouteFile, `REST routes for path "${mcpUrlPath}"`);
80
+ return { routesDir, mcpRouteFile, metadataRouteFile, ...rest };
73
81
  }
74
82
  function assertUrlPathShape(urlPath) {
75
83
  if (!urlPath.startsWith("/")) {
@@ -116,11 +124,16 @@ function relativeImportSpecifier(routeFile, target) {
116
124
  const withoutExt = rel.replace(/\.ts$/, "");
117
125
  return withoutExt.startsWith(".") ? withoutExt : `./${withoutExt}`;
118
126
  }
119
- function buildRouteSource(route, resourcePath, metadataPath, mcpEntry, projectRoot) {
127
+ function buildRouteSource(route, resourcePath, metadataPath, trustForwardedHost, mcpEntry, projectRoot) {
120
128
  const mcpImport = relativeImportSpecifier(route.file, mcpEntry);
121
129
  const routeRel = normalizePath((0, import_node_path.relative)(projectRoot, route.file));
122
130
  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}" }`;
131
+ const optionEntries = [`resourcePath: "${resourcePath}"`];
132
+ if (metadataPath !== void 0)
133
+ optionEntries.push(`metadataPath: "${metadataPath}"`);
134
+ if (trustForwardedHost)
135
+ optionEntries.push("trustForwardedHost: true");
136
+ const runtimeOptions = `{ ${optionEntries.join(", ")} }`;
124
137
  return `${GENERATED_BANNER}
125
138
  // route: ${route.routeLiteral}
126
139
  // emitted to: ${routeRel}
@@ -223,14 +236,18 @@ function mcpPlugin(options = {}) {
223
236
  }
224
237
  const metadataUrlPath = canonicalUrlPath(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH);
225
238
  const injectedMetadataPath = emitProtectedResourceMetadataRoute ? metadataUrlPath : void 0;
239
+ const trustForwardedHost = options.trustForwardedHost !== false;
226
240
  let projectRoot = process.cwd();
227
241
  let mcpEntry = (0, import_node_path.resolve)(projectRoot, mcpEntryOption);
228
- let { routesDir, mcpRouteFile, metadataRouteFile, listToolsRouteFile, invokeToolRouteFile } = resolveAllRoutes(
229
- projectRoot,
230
- routesDirOption,
231
- routeFileName,
232
- metadataUrlPath
233
- );
242
+ let {
243
+ routesDir,
244
+ mcpRouteFile,
245
+ metadataRouteFile,
246
+ listToolsRouteFile,
247
+ invokeToolRouteFile,
248
+ listToolsLiteral,
249
+ invokeToolLiteral
250
+ } = resolveAllRoutes(projectRoot, routesDirOption, routeFileName, urlPath, metadataUrlPath);
234
251
  const regenerate = () => {
235
252
  let mcpEntryExists = true;
236
253
  try {
@@ -256,13 +273,13 @@ function mcpPlugin(options = {}) {
256
273
  routes.push(
257
274
  {
258
275
  file: listToolsRouteFile,
259
- routeLiteral: "/.mcp/list-tools",
276
+ routeLiteral: listToolsLiteral,
260
277
  handlerFactory: "createTanStackListToolsHandler",
261
278
  spaFallbackNote: true
262
279
  },
263
280
  {
264
281
  file: invokeToolRouteFile,
265
- routeLiteral: "/.mcp/invoke-tool/$tool",
282
+ routeLiteral: invokeToolLiteral,
266
283
  handlerFactory: "createTanStackInvokeToolHandler",
267
284
  spaFallbackNote: true
268
285
  }
@@ -270,7 +287,10 @@ function mcpPlugin(options = {}) {
270
287
  }
271
288
  for (const route of routes) {
272
289
  expected.add(route.file);
273
- writeIfChanged(route.file, buildRouteSource(route, urlPath, injectedMetadataPath, mcpEntry, projectRoot));
290
+ writeIfChanged(
291
+ route.file,
292
+ buildRouteSource(route, urlPath, injectedMetadataPath, trustForwardedHost, mcpEntry, projectRoot)
293
+ );
274
294
  }
275
295
  }
276
296
  for (const file of findAdoptedFiles(routesDir)) {
@@ -301,12 +321,15 @@ function mcpPlugin(options = {}) {
301
321
  configResolved(config) {
302
322
  projectRoot = config.root;
303
323
  mcpEntry = (0, import_node_path.resolve)(projectRoot, mcpEntryOption);
304
- ({ routesDir, mcpRouteFile, metadataRouteFile, listToolsRouteFile, invokeToolRouteFile } = resolveAllRoutes(
305
- projectRoot,
306
- routesDirOption,
307
- routeFileName,
308
- metadataUrlPath
309
- ));
324
+ ({
325
+ routesDir,
326
+ mcpRouteFile,
327
+ metadataRouteFile,
328
+ listToolsRouteFile,
329
+ invokeToolRouteFile,
330
+ listToolsLiteral,
331
+ invokeToolLiteral
332
+ } = resolveAllRoutes(projectRoot, routesDirOption, routeFileName, urlPath, metadataUrlPath));
310
333
  regenerate();
311
334
  },
312
335
  configureServer(server) {
@@ -16,13 +16,15 @@ interface McpPluginOptions {
16
16
  */
17
17
  routesDir?: string;
18
18
  /**
19
- * Public URL path for the MCP-protocol route only.
19
+ * Public URL path for the MCP-protocol route.
20
20
  *
21
- * The REST companion routes are fixed at `/.mcp/list-tools` and
22
- * `/.mcp/invoke-tool/$tool` and are not affected by this option the
23
- * `[.mcp]` namespace exists to keep them from colliding with
24
- * user-defined URLs, so making them follow `path` would defeat the
25
- * purpose. Set `restRoutes: false` to drop the companions entirely.
21
+ * The REST companion routes co-locate with this path under a `.mcp`
22
+ * sub-segment of the same parent prefix: `/mcp` keeps them at
23
+ * `/.mcp/list-tools` and `/.mcp/invoke-tool/$tool`, while
24
+ * `/api/public/mcp` moves them to `/api/public/.mcp/*`. The `.mcp`
25
+ * namespace keeps them from colliding with user-defined URLs without
26
+ * anchoring them at the root. Set `restRoutes: false` to drop the
27
+ * companions entirely.
26
28
  *
27
29
  * Must start with `/` and use only `/mcp`-style segments; anything more
28
30
  * exotic doesn't map cleanly to a TanStack file name.
@@ -37,7 +39,7 @@ interface McpPluginOptions {
37
39
  routeFileName?: string;
38
40
  /**
39
41
  * Set to `false` to skip emitting the REST companion routes at
40
- * `/.mcp/list-tools` and `/.mcp/<tool>`. Set
42
+ * `<path-parent>/.mcp/list-tools` and `<path-parent>/.mcp/<tool>`. Set
41
43
  * `protectedResourceMetadataRoute: false` as well if you want the MCP
42
44
  * protocol route to be the only emitted surface.
43
45
  * @default true
@@ -64,6 +66,17 @@ interface McpPluginOptions {
64
66
  * @default "/.well-known/oauth-protected-resource"
65
67
  */
66
68
  metadataPath?: string;
69
+ /**
70
+ * Derive the public origin of the advertised OAuth `resource`/metadata URLs
71
+ * from `X-Forwarded-Host` instead of the rewritten `Host`. Lovable's web-proxy
72
+ * serves a custom domain by rewriting `Host` to the internal backend and
73
+ * overwriting `X-Forwarded-Host` with the real public host, so the default is
74
+ * `true` to make custom domains work out of the box. Set `false` only when
75
+ * deploying behind a proxy that does not overwrite the header (a client could
76
+ * then spoof it to redirect the advertised resource).
77
+ * @default true
78
+ */
79
+ trustForwardedHost?: boolean;
67
80
  }
68
81
 
69
82
  declare function assertUrlPathShape(urlPath: string): string;
@@ -16,13 +16,15 @@ interface McpPluginOptions {
16
16
  */
17
17
  routesDir?: string;
18
18
  /**
19
- * Public URL path for the MCP-protocol route only.
19
+ * Public URL path for the MCP-protocol route.
20
20
  *
21
- * The REST companion routes are fixed at `/.mcp/list-tools` and
22
- * `/.mcp/invoke-tool/$tool` and are not affected by this option the
23
- * `[.mcp]` namespace exists to keep them from colliding with
24
- * user-defined URLs, so making them follow `path` would defeat the
25
- * purpose. Set `restRoutes: false` to drop the companions entirely.
21
+ * The REST companion routes co-locate with this path under a `.mcp`
22
+ * sub-segment of the same parent prefix: `/mcp` keeps them at
23
+ * `/.mcp/list-tools` and `/.mcp/invoke-tool/$tool`, while
24
+ * `/api/public/mcp` moves them to `/api/public/.mcp/*`. The `.mcp`
25
+ * namespace keeps them from colliding with user-defined URLs without
26
+ * anchoring them at the root. Set `restRoutes: false` to drop the
27
+ * companions entirely.
26
28
  *
27
29
  * Must start with `/` and use only `/mcp`-style segments; anything more
28
30
  * exotic doesn't map cleanly to a TanStack file name.
@@ -37,7 +39,7 @@ interface McpPluginOptions {
37
39
  routeFileName?: string;
38
40
  /**
39
41
  * Set to `false` to skip emitting the REST companion routes at
40
- * `/.mcp/list-tools` and `/.mcp/<tool>`. Set
42
+ * `<path-parent>/.mcp/list-tools` and `<path-parent>/.mcp/<tool>`. Set
41
43
  * `protectedResourceMetadataRoute: false` as well if you want the MCP
42
44
  * protocol route to be the only emitted surface.
43
45
  * @default true
@@ -64,6 +66,17 @@ interface McpPluginOptions {
64
66
  * @default "/.well-known/oauth-protected-resource"
65
67
  */
66
68
  metadataPath?: string;
69
+ /**
70
+ * Derive the public origin of the advertised OAuth `resource`/metadata URLs
71
+ * from `X-Forwarded-Host` instead of the rewritten `Host`. Lovable's web-proxy
72
+ * serves a custom domain by rewriting `Host` to the internal backend and
73
+ * overwriting `X-Forwarded-Host` with the real public host, so the default is
74
+ * `true` to make custom domains work out of the box. Set `false` only when
75
+ * deploying behind a proxy that does not overwrite the header (a client could
76
+ * then spoof it to redirect the advertised resource).
77
+ * @default true
78
+ */
79
+ trustForwardedHost?: boolean;
67
80
  }
68
81
 
69
82
  declare function assertUrlPathShape(urlPath: string): string;
@@ -25,20 +25,28 @@ function wellKnownRouteFile(routesDir, urlPath) {
25
25
  });
26
26
  return resolve(routesDir, ...fileSegments);
27
27
  }
28
- function resolveAllRoutes(projectRoot, routesDirOption, routeFileName, metadataUrlPath) {
28
+ function resolveRestRoutes(routesDir, mcpUrlPath) {
29
+ const parentSegments = mcpUrlPath.replace(/^\/+/, "").replace(/\/+$/, "").split("/").slice(0, -1);
30
+ const restDir = resolve(routesDir, ...parentSegments, "[.mcp]");
31
+ const restUrlBase = `/${[...parentSegments, ".mcp"].join("/")}`;
32
+ return {
33
+ listToolsRouteFile: resolve(restDir, "list-tools.ts"),
34
+ invokeToolRouteFile: resolve(restDir, "invoke-tool", "$tool.ts"),
35
+ listToolsLiteral: `${restUrlBase}/list-tools`,
36
+ invokeToolLiteral: `${restUrlBase}/invoke-tool/$tool`
37
+ };
38
+ }
39
+ function resolveAllRoutes(projectRoot, routesDirOption, routeFileName, mcpUrlPath, metadataUrlPath) {
29
40
  const routesDir = resolve(projectRoot, routesDirOption);
30
41
  assertContains(projectRoot, routesDir, `routesDir "${routesDirOption}"`);
31
42
  const mcpRouteFile = resolve(routesDir, routeFileName);
32
43
  assertContains(routesDir, mcpRouteFile, `routeFileName "${routeFileName}"`);
33
44
  const metadataRouteFile = wellKnownRouteFile(routesDir, metadataUrlPath);
34
45
  assertContains(routesDir, metadataRouteFile, `metadataPath "${metadataUrlPath}"`);
35
- return {
36
- routesDir,
37
- mcpRouteFile,
38
- metadataRouteFile,
39
- listToolsRouteFile: resolve(routesDir, "[.mcp]", "list-tools.ts"),
40
- invokeToolRouteFile: resolve(routesDir, "[.mcp]", "invoke-tool", "$tool.ts")
41
- };
46
+ const rest = resolveRestRoutes(routesDir, mcpUrlPath);
47
+ assertContains(routesDir, rest.listToolsRouteFile, `REST routes for path "${mcpUrlPath}"`);
48
+ assertContains(routesDir, rest.invokeToolRouteFile, `REST routes for path "${mcpUrlPath}"`);
49
+ return { routesDir, mcpRouteFile, metadataRouteFile, ...rest };
42
50
  }
43
51
  function assertUrlPathShape(urlPath) {
44
52
  if (!urlPath.startsWith("/")) {
@@ -85,11 +93,16 @@ function relativeImportSpecifier(routeFile, target) {
85
93
  const withoutExt = rel.replace(/\.ts$/, "");
86
94
  return withoutExt.startsWith(".") ? withoutExt : `./${withoutExt}`;
87
95
  }
88
- function buildRouteSource(route, resourcePath, metadataPath, mcpEntry, projectRoot) {
96
+ function buildRouteSource(route, resourcePath, metadataPath, trustForwardedHost, mcpEntry, projectRoot) {
89
97
  const mcpImport = relativeImportSpecifier(route.file, mcpEntry);
90
98
  const routeRel = normalizePath(relative(projectRoot, route.file));
91
99
  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}" }`;
100
+ const optionEntries = [`resourcePath: "${resourcePath}"`];
101
+ if (metadataPath !== void 0)
102
+ optionEntries.push(`metadataPath: "${metadataPath}"`);
103
+ if (trustForwardedHost)
104
+ optionEntries.push("trustForwardedHost: true");
105
+ const runtimeOptions = `{ ${optionEntries.join(", ")} }`;
93
106
  return `${GENERATED_BANNER}
94
107
  // route: ${route.routeLiteral}
95
108
  // emitted to: ${routeRel}
@@ -192,14 +205,18 @@ function mcpPlugin(options = {}) {
192
205
  }
193
206
  const metadataUrlPath = canonicalUrlPath(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH);
194
207
  const injectedMetadataPath = emitProtectedResourceMetadataRoute ? metadataUrlPath : void 0;
208
+ const trustForwardedHost = options.trustForwardedHost !== false;
195
209
  let projectRoot = process.cwd();
196
210
  let mcpEntry = resolve(projectRoot, mcpEntryOption);
197
- let { routesDir, mcpRouteFile, metadataRouteFile, listToolsRouteFile, invokeToolRouteFile } = resolveAllRoutes(
198
- projectRoot,
199
- routesDirOption,
200
- routeFileName,
201
- metadataUrlPath
202
- );
211
+ let {
212
+ routesDir,
213
+ mcpRouteFile,
214
+ metadataRouteFile,
215
+ listToolsRouteFile,
216
+ invokeToolRouteFile,
217
+ listToolsLiteral,
218
+ invokeToolLiteral
219
+ } = resolveAllRoutes(projectRoot, routesDirOption, routeFileName, urlPath, metadataUrlPath);
203
220
  const regenerate = () => {
204
221
  let mcpEntryExists = true;
205
222
  try {
@@ -225,13 +242,13 @@ function mcpPlugin(options = {}) {
225
242
  routes.push(
226
243
  {
227
244
  file: listToolsRouteFile,
228
- routeLiteral: "/.mcp/list-tools",
245
+ routeLiteral: listToolsLiteral,
229
246
  handlerFactory: "createTanStackListToolsHandler",
230
247
  spaFallbackNote: true
231
248
  },
232
249
  {
233
250
  file: invokeToolRouteFile,
234
- routeLiteral: "/.mcp/invoke-tool/$tool",
251
+ routeLiteral: invokeToolLiteral,
235
252
  handlerFactory: "createTanStackInvokeToolHandler",
236
253
  spaFallbackNote: true
237
254
  }
@@ -239,7 +256,10 @@ function mcpPlugin(options = {}) {
239
256
  }
240
257
  for (const route of routes) {
241
258
  expected.add(route.file);
242
- writeIfChanged(route.file, buildRouteSource(route, urlPath, injectedMetadataPath, mcpEntry, projectRoot));
259
+ writeIfChanged(
260
+ route.file,
261
+ buildRouteSource(route, urlPath, injectedMetadataPath, trustForwardedHost, mcpEntry, projectRoot)
262
+ );
243
263
  }
244
264
  }
245
265
  for (const file of findAdoptedFiles(routesDir)) {
@@ -270,12 +290,15 @@ function mcpPlugin(options = {}) {
270
290
  configResolved(config) {
271
291
  projectRoot = config.root;
272
292
  mcpEntry = resolve(projectRoot, mcpEntryOption);
273
- ({ routesDir, mcpRouteFile, metadataRouteFile, listToolsRouteFile, invokeToolRouteFile } = resolveAllRoutes(
274
- projectRoot,
275
- routesDirOption,
276
- routeFileName,
277
- metadataUrlPath
278
- ));
293
+ ({
294
+ routesDir,
295
+ mcpRouteFile,
296
+ metadataRouteFile,
297
+ listToolsRouteFile,
298
+ invokeToolRouteFile,
299
+ listToolsLiteral,
300
+ invokeToolLiteral
301
+ } = resolveAllRoutes(projectRoot, routesDirOption, routeFileName, urlPath, metadataUrlPath));
279
302
  regenerate();
280
303
  },
281
304
  configureServer(server) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovable.dev/mcp-js",
3
- "version": "0.12.1",
3
+ "version": "0.14.0",
4
4
  "description": "Author MCP servers for Lovable apps. Declare tools with defineTool, register them in defineMcp, and a framework adapter (TanStack or Supabase Edge Functions) emits the route(s) at build time.",
5
5
  "type": "module",
6
6
  "repository": {