@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 +3 -1
- package/dist/{authorize-HTd0GKmB.d.ts → authorize-BMeXd_Sh.d.ts} +7 -0
- package/dist/{chunk-Y2FTHVL3.js → chunk-NOFPRSSI.js} +1 -1
- package/dist/chunk-UQK5UO6C.js +26 -0
- package/dist/cli/extract-manifest.cjs +1 -1
- package/dist/cli/extract-manifest.js +1 -1
- package/dist/protocols/mcp/index.d.cts +1 -1
- package/dist/protocols/mcp/index.d.ts +1 -1
- package/dist/protocols/oauth-metadata.d.cts +1 -1
- package/dist/protocols/oauth-metadata.d.ts +1 -1
- package/dist/protocols/rest/index.d.cts +1 -1
- package/dist/protocols/rest/index.d.ts +1 -1
- package/dist/stacks/supabase/index.cjs +27 -11
- package/dist/stacks/supabase/index.d.cts +9 -0
- package/dist/stacks/supabase/index.d.ts +9 -0
- package/dist/stacks/supabase/index.js +7 -11
- package/dist/stacks/supabase/vite.cjs +1 -1
- package/dist/stacks/supabase/vite.js +1 -1
- package/dist/stacks/tanstack/index.cjs +30 -4
- package/dist/stacks/tanstack/index.d.cts +1 -1
- package/dist/stacks/tanstack/index.d.ts +1 -1
- package/dist/stacks/tanstack/index.js +10 -4
- package/dist/stacks/tanstack/vite.cjs +48 -25
- package/dist/stacks/tanstack/vite.d.cts +20 -7
- package/dist/stacks/tanstack/vite.d.ts +20 -7
- package/dist/stacks/tanstack/vite.js +48 -25
- package/package.json +1 -1
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
|
-
|
|
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 };
|
|
@@ -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
|
+
};
|
|
@@ -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 =
|
|
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 =
|
|
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":
|
|
@@ -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,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
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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 {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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:
|
|
276
|
+
routeLiteral: listToolsLiteral,
|
|
260
277
|
handlerFactory: "createTanStackListToolsHandler",
|
|
261
278
|
spaFallbackNote: true
|
|
262
279
|
},
|
|
263
280
|
{
|
|
264
281
|
file: invokeToolRouteFile,
|
|
265
|
-
routeLiteral:
|
|
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(
|
|
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
|
-
({
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
19
|
+
* Public URL path for the MCP-protocol route.
|
|
20
20
|
*
|
|
21
|
-
* The REST companion routes
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
*
|
|
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
|
|
19
|
+
* Public URL path for the MCP-protocol route.
|
|
20
20
|
*
|
|
21
|
-
* The REST companion routes
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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 {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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:
|
|
245
|
+
routeLiteral: listToolsLiteral,
|
|
229
246
|
handlerFactory: "createTanStackListToolsHandler",
|
|
230
247
|
spaFallbackNote: true
|
|
231
248
|
},
|
|
232
249
|
{
|
|
233
250
|
file: invokeToolRouteFile,
|
|
234
|
-
routeLiteral:
|
|
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(
|
|
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
|
-
({
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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.
|
|
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": {
|