@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.
- package/README.internal.md +6 -2
- package/README.md +13 -1
- package/dist/authorize-HTd0GKmB.d.ts +18 -0
- package/dist/{chunk-W7F6JRDB.js → chunk-3GQXQBYQ.js} +1 -1
- package/dist/chunk-BG3HOZXZ.js +57 -0
- package/dist/{chunk-3XPKR4G6.js → chunk-CJMHLE7F.js} +1 -1
- package/dist/{chunk-HRMLGCXV.js → chunk-FQLM4JDU.js} +36 -12
- package/dist/chunk-QJ7XEKT3.js +8 -0
- package/dist/{chunk-LMT6RXUF.js → chunk-SBKIKJNB.js} +3 -47
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/protocols/mcp/index.cjs +36 -12
- package/dist/protocols/mcp/index.d.cts +2 -2
- package/dist/protocols/mcp/index.d.ts +2 -2
- package/dist/protocols/mcp/index.js +2 -2
- package/dist/protocols/oauth-metadata.cjs +10 -3
- package/dist/protocols/oauth-metadata.d.cts +2 -2
- package/dist/protocols/oauth-metadata.d.ts +2 -2
- package/dist/protocols/oauth-metadata.js +2 -2
- package/dist/protocols/rest/index.cjs +50 -24
- package/dist/protocols/rest/index.d.cts +2 -2
- package/dist/protocols/rest/index.d.ts +2 -2
- package/dist/protocols/rest/index.js +6 -4
- package/dist/stacks/tanstack/cli/extract-manifest.cjs +214 -0
- package/dist/stacks/tanstack/cli/extract-manifest.d.cts +1 -0
- package/dist/stacks/tanstack/cli/extract-manifest.d.ts +1 -0
- package/dist/stacks/tanstack/cli/extract-manifest.js +170 -0
- package/dist/stacks/tanstack/index.cjs +50 -24
- package/dist/stacks/tanstack/index.d.cts +2 -2
- package/dist/stacks/tanstack/index.d.ts +2 -2
- package/dist/stacks/tanstack/index.js +8 -6
- package/dist/stacks/tanstack/vite.cjs +67 -19
- package/dist/stacks/tanstack/vite.d.cts +26 -2
- package/dist/stacks/tanstack/vite.d.ts +26 -2
- package/dist/stacks/tanstack/vite.js +65 -20
- package/dist/{types-zMlr1mOK.d.ts → types-C5SSORxe.d.ts} +4 -0
- package/package.json +6 -3
- 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:
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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-
|
|
2
|
-
import { d as McpDefinition } from '../../types-
|
|
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-
|
|
2
|
-
import { d as McpDefinition } from '../../types-
|
|
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-
|
|
3
|
+
} from "../../chunk-CJMHLE7F.js";
|
|
4
4
|
import {
|
|
5
5
|
createOAuthProtectedResourceMetadataHandler
|
|
6
|
-
} from "../../chunk-
|
|
6
|
+
} from "../../chunk-3GQXQBYQ.js";
|
|
7
7
|
import {
|
|
8
|
-
createInvokeToolHandler
|
|
9
|
-
|
|
10
|
-
} from "../../chunk-LMT6RXUF.js";
|
|
8
|
+
createInvokeToolHandler
|
|
9
|
+
} from "../../chunk-SBKIKJNB.js";
|
|
11
10
|
import "../../chunk-MA5H6PSF.js";
|
|
12
|
-
import
|
|
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/
|
|
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
|
|
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,
|
|
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:
|
|
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
|
|
266
|
-
|
|
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",
|
|
272
|
-
server.watcher.on("change",
|
|
273
|
-
server.watcher.on("unlink",
|
|
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",
|
|
276
|
-
server.watcher.off("change",
|
|
277
|
-
server.watcher.off("unlink",
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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:
|
|
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
|
|
238
|
-
|
|
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",
|
|
244
|
-
server.watcher.on("change",
|
|
245
|
-
server.watcher.on("unlink",
|
|
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",
|
|
248
|
-
server.watcher.off("change",
|
|
249
|
-
server.watcher.off("unlink",
|
|
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;
|