@lovable.dev/mcp-js 0.8.0 → 0.9.1
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 +18 -0
- package/dist/{chunk-E335BBVM.js → chunk-2RGXCDBU.js} +1 -1
- package/dist/{chunk-W7F6JRDB.js → chunk-4JO3BZQG.js} +1 -1
- package/dist/{chunk-4Y72OCPS.js → chunk-6N7B7NZY.js} +1 -1
- package/dist/{chunk-3XPKR4G6.js → chunk-OXSSMKQE.js} +1 -1
- package/dist/{chunk-HRMLGCXV.js → chunk-YQBOGY7D.js} +37 -12
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/protocols/mcp/index.cjs +37 -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 +11 -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 +37 -12
- package/dist/protocols/rest/index.d.cts +2 -2
- package/dist/protocols/rest/index.d.ts +2 -2
- package/dist/protocols/rest/index.js +3 -3
- package/dist/stacks/tanstack/cli/extract-manifest.cjs +1 -1
- package/dist/stacks/tanstack/cli/extract-manifest.js +3 -3
- package/dist/stacks/tanstack/index.cjs +37 -12
- package/dist/stacks/tanstack/index.d.cts +2 -2
- package/dist/stacks/tanstack/index.d.ts +2 -2
- package/dist/stacks/tanstack/index.js +5 -5
- package/dist/stacks/tanstack/vite.cjs +43 -8
- package/dist/stacks/tanstack/vite.d.cts +17 -1
- package/dist/stacks/tanstack/vite.d.ts +17 -1
- package/dist/stacks/tanstack/vite.js +42 -8
- package/dist/{types-zMlr1mOK.d.ts → types-C5SSORxe.d.ts} +4 -0
- package/package.json +1 -1
- package/dist/authorize-DpTEIFvs.d.ts +0 -9
package/README.md
CHANGED
|
@@ -53,6 +53,8 @@ MCP (`POST /mcp`) is the public wire format clients speak directly. REST (`/.mcp
|
|
|
53
53
|
|
|
54
54
|
The OAuth metadata route is emitted by default and returns `404` until OAuth auth is configured. Disable it with `mcpPlugin({ protectedResourceMetadataRoute: false })` only if the app owns `/.well-known/oauth-protected-resource` itself.
|
|
55
55
|
|
|
56
|
+
To serve the metadata from a different path — e.g. a workspace-private project where only `/api/public/*` is anonymously reachable and `/.well-known/...` is gated — pass `mcpPlugin({ metadataPath: "/api/public/.well-known/oauth-protected-resource" })`. The plugin emits the route there and injects the same path into every handler, so the 401 `WWW-Authenticate` challenge advertises exactly what it serves. `metadataPath` is mutually exclusive with `auth.protectedResourceMetadataUrl` (and with `protectedResourceMetadataRoute: false`).
|
|
57
|
+
|
|
56
58
|
### `.lovable/mcp/manifest.json`
|
|
57
59
|
|
|
58
60
|
`.lovable/mcp/manifest.json` is a snapshot the Lovable platform reads to register the MCP server. Envelope fields: `version` (manifest schema version), `sdk_version` (the `@lovable.dev/mcp-js` release that wrote the snapshot), `path`, and `auth`. `auth` is the server's auth configuration, lifted into the envelope (not into `mcp`): `{ "type": "none" }`, or `{ "type": "oauth", ... }` mirroring the `defineMcp({ auth })` config (snake_case — `issuer`, `accepted_audiences`, `required_scopes`, `resource`, …). The `mcp` field is **exactly the `GET /.mcp/list-tools` body** — `server` plus the tool catalog (`name`/`title`/`description`/`annotations` and JSON-Schema `inputSchema`/`outputSchema`) — so the committed snapshot can't drift from what the live route serves. It's produced by loading the entry and reading the catalog off the `defineMcp` result, so the manifest reflects exactly what the server exposes, including tools built programmatically (spreads, computed names, tools from npm). **The `lovable-mcp-extract-manifest` CLI writes it** (loading the entry through Vite's SSR module loader, so it works under Node/Bun), and removes it when the entry is deleted. The Lovable platform runs the CLI in its commit pipeline; the Vite plugin only generates routes. Commit it: the platform reads the committed file.
|
|
@@ -182,7 +184,7 @@ Use an OAuth access token from the configured authorization server for MCP calls
|
|
|
182
184
|
| `algorithms` | Accepted JWT signing algorithms. Defaults to asymmetric algorithms (`RS*`, `ES*`, `EdDSA`); set explicitly only for a narrower policy. |
|
|
183
185
|
| `clockToleranceSeconds` | Allowed JWT clock skew for `exp`/`nbf` checks, in seconds. Defaults to `30`; max `300`. |
|
|
184
186
|
| `resourceName`, `resourceDocumentation` | Optional metadata fields for MCP clients and humans. `resourceName` defaults to the MCP server `title`. |
|
|
185
|
-
| `protectedResourceMetadataUrl` | Absolute HTTPS URL of an externally hosted RFC 9728 protected-resource metadata document. When set, the SDK advertises this URL in the 401 `WWW-Authenticate: resource_metadata` challenge and stops generating its own metadata — the `/.well-known/oauth-protected-resource` handler returns 404. Use it when the resource server already publishes its own PRM; you'll typically also pass `protectedResourceMetadataRoute: false` to the Vite plugin so the now-unused route isn't emitted. |
|
|
187
|
+
| `protectedResourceMetadataUrl` | Absolute HTTPS URL of an externally hosted RFC 9728 protected-resource metadata document. When set, the SDK advertises this URL in the 401 `WWW-Authenticate: resource_metadata` challenge and stops generating its own metadata — the `/.well-known/oauth-protected-resource` handler returns 404. Use it when the resource server already publishes its own PRM; you'll typically also pass `protectedResourceMetadataRoute: false` to the Vite plugin so the now-unused route isn't emitted. Mutually exclusive with the plugin's `metadataPath` (which has the SDK serve its own document at a custom path); configuring both throws at handler construction. |
|
|
186
188
|
|
|
187
189
|
Protected-resource metadata is derived from the same config. When `resource` is omitted, the published resource is the incoming request origin plus the MCP route path the stack adapter supplies as `resourcePath` (for example, `https://my-app.lovable.app/mcp`); `authorization_servers` is the configured issuer, and `resource_name` is `defineMcp({ title })` unless `resourceName` overrides it. Because the metadata endpoint is served from `/.well-known/...`, it can't infer the resource from its own path — the handler throws at construction when neither `resource` nor `resourcePath` is set (the generated TanStack routes always pass `resourcePath`). For Supabase auth, this metadata still names the MCP resource even though Supabase JWTs use the project audience `"authenticated"`.
|
|
188
190
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface McpRuntimeOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Public MCP protocol path. REST companion handlers pass this so OAuth
|
|
4
|
+
* resource/audience checks bind to `/mcp`, not internal `/.mcp/*` RPC URLs.
|
|
5
|
+
*/
|
|
6
|
+
resourcePath?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Host-relative path of the SDK-served protected-resource metadata document.
|
|
9
|
+
* The Vite plugin injects it into every emitted handler when it generates the
|
|
10
|
+
* PRM route, so the 401 challenge advertises the same path the route serves.
|
|
11
|
+
* Its presence is the signal that the SDK owns the PRM document, so it is
|
|
12
|
+
* mutually exclusive with `auth.protectedResourceMetadataUrl` (which points at
|
|
13
|
+
* an externally hosted document). Defaults to the well-known path when absent.
|
|
14
|
+
*/
|
|
15
|
+
metadataPath?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type { McpRuntimeOptions as M };
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
createRequestAuthorizer,
|
|
9
9
|
methodNotAllowed,
|
|
10
10
|
withCors
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-YQBOGY7D.js";
|
|
12
12
|
|
|
13
13
|
// src/protocols/rest/invoke-tool.ts
|
|
14
14
|
import { getParseErrorMessage, objectFromShape, safeParseAsync } from "@modelcontextprotocol/sdk/server/zod-compat.js";
|
|
@@ -30,10 +30,10 @@ function resolveProtectedResource(auth, request, options) {
|
|
|
30
30
|
resourceURL.hash = "";
|
|
31
31
|
return trimTrailingSlash(resourceURL.toString());
|
|
32
32
|
}
|
|
33
|
-
function assertResourcePathShape(resourcePath) {
|
|
33
|
+
function assertResourcePathShape(resourcePath, label = "resourcePath") {
|
|
34
34
|
if (!resourcePath.startsWith("/") || resourcePath.startsWith("//") || resourcePath.includes("\\") || /(^|\/)\.\.(\/|$)/.test(resourcePath)) {
|
|
35
35
|
throw new Error(
|
|
36
|
-
`@lovable.dev/mcp-js:
|
|
36
|
+
`@lovable.dev/mcp-js: ${label} must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -225,6 +225,7 @@ async function fetchVerificationKeySet(jwksUri) {
|
|
|
225
225
|
async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
226
226
|
try {
|
|
227
227
|
const { payload } = await jwtVerify(token, keySet, {
|
|
228
|
+
typ: "at+jwt",
|
|
228
229
|
// `issuer` is trimmed of any trailing slash; accept both forms so a token whose
|
|
229
230
|
// `iss` carries the slash the AS publishes still verifies.
|
|
230
231
|
issuer: [issuer, `${issuer}/`],
|
|
@@ -299,16 +300,16 @@ function quoteAuthenticateParam(value) {
|
|
|
299
300
|
const sanitized = value.replace(/[\u0000-\u001F\u007F]/g, "");
|
|
300
301
|
return `"${sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
301
302
|
}
|
|
302
|
-
function resolveProtectedResourceMetadataUrl(auth, request) {
|
|
303
|
+
function resolveProtectedResourceMetadataUrl(auth, request, options) {
|
|
303
304
|
if (auth.protectedResourceMetadataUrl !== void 0)
|
|
304
305
|
return auth.protectedResourceMetadataUrl;
|
|
305
306
|
const base = auth.resource ?? request.url;
|
|
306
|
-
return new URL(OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
|
|
307
|
+
return new URL(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
|
|
307
308
|
}
|
|
308
|
-
function wwwAuthenticateHeader(auth, request, params = {}) {
|
|
309
|
+
function wwwAuthenticateHeader(auth, request, options, params = {}) {
|
|
309
310
|
const values = [
|
|
310
311
|
`realm=${quoteAuthenticateParam("mcp")}`,
|
|
311
|
-
`resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request))}`
|
|
312
|
+
`resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request, options))}`
|
|
312
313
|
];
|
|
313
314
|
if (auth.requiredScopes && auth.requiredScopes.length > 0) {
|
|
314
315
|
values.push(`scope=${quoteAuthenticateParam(auth.requiredScopes.join(" "))}`);
|
|
@@ -319,9 +320,12 @@ function wwwAuthenticateHeader(auth, request, params = {}) {
|
|
|
319
320
|
values.push(`error_description=${quoteAuthenticateParam(params.errorDescription)}`);
|
|
320
321
|
return `Bearer ${values.join(", ")}`;
|
|
321
322
|
}
|
|
322
|
-
function challengeResponse(auth, request, status, oauthError, errorDescription) {
|
|
323
|
+
function challengeResponse(auth, request, options, status, oauthError, errorDescription) {
|
|
323
324
|
const headers = new Headers(JSON_HEADERS);
|
|
324
|
-
headers.set(
|
|
325
|
+
headers.set(
|
|
326
|
+
"WWW-Authenticate",
|
|
327
|
+
wwwAuthenticateHeader(auth, request, options, { error: oauthError, errorDescription })
|
|
328
|
+
);
|
|
325
329
|
headers.set("Cache-Control", "no-store");
|
|
326
330
|
return new Response(JSON.stringify({ error: status === 403 ? "forbidden" : "unauthorized" }), {
|
|
327
331
|
status,
|
|
@@ -349,10 +353,17 @@ function parseBearerToken(request) {
|
|
|
349
353
|
function getOAuthRuntime(mcp, options = {}) {
|
|
350
354
|
if (options.resourcePath !== void 0)
|
|
351
355
|
assertResourcePathShape(options.resourcePath);
|
|
352
|
-
|
|
356
|
+
if (options.metadataPath !== void 0)
|
|
357
|
+
assertResourcePathShape(options.metadataPath, "metadataPath");
|
|
358
|
+
const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
|
|
353
359
|
const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
|
|
354
360
|
if (!auth)
|
|
355
361
|
return { kind: "unconfigured", options: stableOptions };
|
|
362
|
+
if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`@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.`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
356
367
|
const discovery = createOAuthDiscoveryResolver(auth);
|
|
357
368
|
return {
|
|
358
369
|
kind: "configured",
|
|
@@ -391,7 +402,7 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
391
402
|
const token = parseBearerToken(request);
|
|
392
403
|
if (!token) {
|
|
393
404
|
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
394
|
-
return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
|
|
405
|
+
return { ok: false, response: challengeResponse(runtime.auth, request, runtime.options, 401) };
|
|
395
406
|
}
|
|
396
407
|
try {
|
|
397
408
|
const auth = await runtime.verify(token, request, runtime.options);
|
|
@@ -406,13 +417,27 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
406
417
|
log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
|
|
407
418
|
return {
|
|
408
419
|
ok: false,
|
|
409
|
-
response: challengeResponse(
|
|
420
|
+
response: challengeResponse(
|
|
421
|
+
runtime.auth,
|
|
422
|
+
request,
|
|
423
|
+
runtime.options,
|
|
424
|
+
err.status,
|
|
425
|
+
err.oauthError,
|
|
426
|
+
err.message
|
|
427
|
+
)
|
|
410
428
|
};
|
|
411
429
|
}
|
|
412
430
|
log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
|
|
413
431
|
return {
|
|
414
432
|
ok: false,
|
|
415
|
-
response: challengeResponse(
|
|
433
|
+
response: challengeResponse(
|
|
434
|
+
runtime.auth,
|
|
435
|
+
request,
|
|
436
|
+
runtime.options,
|
|
437
|
+
401,
|
|
438
|
+
"invalid_token",
|
|
439
|
+
"Invalid access token"
|
|
440
|
+
)
|
|
416
441
|
};
|
|
417
442
|
}
|
|
418
443
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, j as ToolDefinition, M as McpAuthConfig } from './types-
|
|
2
|
-
export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink, f as ResourceLinkIcon, T as TextContent, g as ToolAnnotations, h as ToolContent, i as ToolContext, k as ToolHandlerResult, l as ZodSchema } from './types-
|
|
1
|
+
import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, j as ToolDefinition, M as McpAuthConfig } from './types-C5SSORxe.js';
|
|
2
|
+
export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink, f as ResourceLinkIcon, T as TextContent, g as ToolAnnotations, h as ToolContent, i as ToolContext, k as ToolHandlerResult, l as ZodSchema } from './types-C5SSORxe.js';
|
|
3
3
|
import 'zod';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, j as ToolDefinition, M as McpAuthConfig } from './types-
|
|
2
|
-
export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink, f as ResourceLinkIcon, T as TextContent, g as ToolAnnotations, h as ToolContent, i as ToolContext, k as ToolHandlerResult, l as ZodSchema } from './types-
|
|
1
|
+
import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, j as ToolDefinition, M as McpAuthConfig } from './types-C5SSORxe.js';
|
|
2
|
+
export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink, f as ResourceLinkIcon, T as TextContent, g as ToolAnnotations, h as ToolContent, i as ToolContext, k as ToolHandlerResult, l as ZodSchema } from './types-C5SSORxe.js';
|
|
3
3
|
import 'zod';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -246,10 +246,10 @@ function resolveProtectedResource(auth, request, options) {
|
|
|
246
246
|
resourceURL.hash = "";
|
|
247
247
|
return trimTrailingSlash(resourceURL.toString());
|
|
248
248
|
}
|
|
249
|
-
function assertResourcePathShape(resourcePath) {
|
|
249
|
+
function assertResourcePathShape(resourcePath, label = "resourcePath") {
|
|
250
250
|
if (!resourcePath.startsWith("/") || resourcePath.startsWith("//") || resourcePath.includes("\\") || /(^|\/)\.\.(\/|$)/.test(resourcePath)) {
|
|
251
251
|
throw new Error(
|
|
252
|
-
`@lovable.dev/mcp-js:
|
|
252
|
+
`@lovable.dev/mcp-js: ${label} must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`
|
|
253
253
|
);
|
|
254
254
|
}
|
|
255
255
|
}
|
|
@@ -316,6 +316,7 @@ async function fetchVerificationKeySet(jwksUri) {
|
|
|
316
316
|
async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
317
317
|
try {
|
|
318
318
|
const { payload } = await (0, import_jose.jwtVerify)(token, keySet, {
|
|
319
|
+
typ: "at+jwt",
|
|
319
320
|
// `issuer` is trimmed of any trailing slash; accept both forms so a token whose
|
|
320
321
|
// `iss` carries the slash the AS publishes still verifies.
|
|
321
322
|
issuer: [issuer, `${issuer}/`],
|
|
@@ -390,16 +391,16 @@ function quoteAuthenticateParam(value) {
|
|
|
390
391
|
const sanitized = value.replace(/[\u0000-\u001F\u007F]/g, "");
|
|
391
392
|
return `"${sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
392
393
|
}
|
|
393
|
-
function resolveProtectedResourceMetadataUrl(auth, request) {
|
|
394
|
+
function resolveProtectedResourceMetadataUrl(auth, request, options) {
|
|
394
395
|
if (auth.protectedResourceMetadataUrl !== void 0)
|
|
395
396
|
return auth.protectedResourceMetadataUrl;
|
|
396
397
|
const base = auth.resource ?? request.url;
|
|
397
|
-
return new URL(OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
|
|
398
|
+
return new URL(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
|
|
398
399
|
}
|
|
399
|
-
function wwwAuthenticateHeader(auth, request, params = {}) {
|
|
400
|
+
function wwwAuthenticateHeader(auth, request, options, params = {}) {
|
|
400
401
|
const values = [
|
|
401
402
|
`realm=${quoteAuthenticateParam("mcp")}`,
|
|
402
|
-
`resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request))}`
|
|
403
|
+
`resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request, options))}`
|
|
403
404
|
];
|
|
404
405
|
if (auth.requiredScopes && auth.requiredScopes.length > 0) {
|
|
405
406
|
values.push(`scope=${quoteAuthenticateParam(auth.requiredScopes.join(" "))}`);
|
|
@@ -410,9 +411,12 @@ function wwwAuthenticateHeader(auth, request, params = {}) {
|
|
|
410
411
|
values.push(`error_description=${quoteAuthenticateParam(params.errorDescription)}`);
|
|
411
412
|
return `Bearer ${values.join(", ")}`;
|
|
412
413
|
}
|
|
413
|
-
function challengeResponse(auth, request, status, oauthError, errorDescription) {
|
|
414
|
+
function challengeResponse(auth, request, options, status, oauthError, errorDescription) {
|
|
414
415
|
const headers = new Headers(JSON_HEADERS);
|
|
415
|
-
headers.set(
|
|
416
|
+
headers.set(
|
|
417
|
+
"WWW-Authenticate",
|
|
418
|
+
wwwAuthenticateHeader(auth, request, options, { error: oauthError, errorDescription })
|
|
419
|
+
);
|
|
416
420
|
headers.set("Cache-Control", "no-store");
|
|
417
421
|
return new Response(JSON.stringify({ error: status === 403 ? "forbidden" : "unauthorized" }), {
|
|
418
422
|
status,
|
|
@@ -440,10 +444,17 @@ function parseBearerToken(request) {
|
|
|
440
444
|
function getOAuthRuntime(mcp, options = {}) {
|
|
441
445
|
if (options.resourcePath !== void 0)
|
|
442
446
|
assertResourcePathShape(options.resourcePath);
|
|
443
|
-
|
|
447
|
+
if (options.metadataPath !== void 0)
|
|
448
|
+
assertResourcePathShape(options.metadataPath, "metadataPath");
|
|
449
|
+
const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
|
|
444
450
|
const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
|
|
445
451
|
if (!auth)
|
|
446
452
|
return { kind: "unconfigured", options: stableOptions };
|
|
453
|
+
if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
`@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.`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
447
458
|
const discovery = createOAuthDiscoveryResolver(auth);
|
|
448
459
|
return {
|
|
449
460
|
kind: "configured",
|
|
@@ -474,7 +485,7 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
474
485
|
const token = parseBearerToken(request);
|
|
475
486
|
if (!token) {
|
|
476
487
|
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
477
|
-
return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
|
|
488
|
+
return { ok: false, response: challengeResponse(runtime.auth, request, runtime.options, 401) };
|
|
478
489
|
}
|
|
479
490
|
try {
|
|
480
491
|
const auth = await runtime.verify(token, request, runtime.options);
|
|
@@ -489,13 +500,27 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
489
500
|
log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
|
|
490
501
|
return {
|
|
491
502
|
ok: false,
|
|
492
|
-
response: challengeResponse(
|
|
503
|
+
response: challengeResponse(
|
|
504
|
+
runtime.auth,
|
|
505
|
+
request,
|
|
506
|
+
runtime.options,
|
|
507
|
+
err.status,
|
|
508
|
+
err.oauthError,
|
|
509
|
+
err.message
|
|
510
|
+
)
|
|
493
511
|
};
|
|
494
512
|
}
|
|
495
513
|
log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
|
|
496
514
|
return {
|
|
497
515
|
ok: false,
|
|
498
|
-
response: challengeResponse(
|
|
516
|
+
response: challengeResponse(
|
|
517
|
+
runtime.auth,
|
|
518
|
+
request,
|
|
519
|
+
runtime.options,
|
|
520
|
+
401,
|
|
521
|
+
"invalid_token",
|
|
522
|
+
"Invalid access token"
|
|
523
|
+
)
|
|
499
524
|
};
|
|
500
525
|
}
|
|
501
526
|
}
|
|
@@ -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
|
type McpProtocolHandler = (request: Request) => Promise<Response>;
|
|
@@ -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
|
type McpProtocolHandler = (request: Request) => Promise<Response>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpProtocolHandler
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-OXSSMKQE.js";
|
|
4
4
|
import "../../chunk-MA5H6PSF.js";
|
|
5
|
-
import "../../chunk-
|
|
5
|
+
import "../../chunk-YQBOGY7D.js";
|
|
6
6
|
import "../../chunk-QC3DXQTH.js";
|
|
7
7
|
import "../../chunk-6DXGZZA4.js";
|
|
8
8
|
export {
|
|
@@ -248,10 +248,10 @@ function resolveProtectedResource(auth, request, options) {
|
|
|
248
248
|
resourceURL.hash = "";
|
|
249
249
|
return trimTrailingSlash(resourceURL.toString());
|
|
250
250
|
}
|
|
251
|
-
function assertResourcePathShape(resourcePath) {
|
|
251
|
+
function assertResourcePathShape(resourcePath, label = "resourcePath") {
|
|
252
252
|
if (!resourcePath.startsWith("/") || resourcePath.startsWith("//") || resourcePath.includes("\\") || /(^|\/)\.\.(\/|$)/.test(resourcePath)) {
|
|
253
253
|
throw new Error(
|
|
254
|
-
`@lovable.dev/mcp-js:
|
|
254
|
+
`@lovable.dev/mcp-js: ${label} must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`
|
|
255
255
|
);
|
|
256
256
|
}
|
|
257
257
|
}
|
|
@@ -318,6 +318,7 @@ async function fetchVerificationKeySet(jwksUri) {
|
|
|
318
318
|
async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
319
319
|
try {
|
|
320
320
|
const { payload } = await (0, import_jose.jwtVerify)(token, keySet, {
|
|
321
|
+
typ: "at+jwt",
|
|
321
322
|
// `issuer` is trimmed of any trailing slash; accept both forms so a token whose
|
|
322
323
|
// `iss` carries the slash the AS publishes still verifies.
|
|
323
324
|
issuer: [issuer, `${issuer}/`],
|
|
@@ -397,10 +398,17 @@ function oauthConfigurationErrorResponse() {
|
|
|
397
398
|
function getOAuthRuntime(mcp, options = {}) {
|
|
398
399
|
if (options.resourcePath !== void 0)
|
|
399
400
|
assertResourcePathShape(options.resourcePath);
|
|
400
|
-
|
|
401
|
+
if (options.metadataPath !== void 0)
|
|
402
|
+
assertResourcePathShape(options.metadataPath, "metadataPath");
|
|
403
|
+
const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
|
|
401
404
|
const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
|
|
402
405
|
if (!auth)
|
|
403
406
|
return { kind: "unconfigured", options: stableOptions };
|
|
407
|
+
if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`@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.`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
404
412
|
const discovery = createOAuthDiscoveryResolver(auth);
|
|
405
413
|
return {
|
|
406
414
|
kind: "configured",
|
|
@@ -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
|
type OAuthProtectedResourceMetadataHandler = (request: Request) => Promise<Response>;
|
|
@@ -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
|
type OAuthProtectedResourceMetadataHandler = (request: Request) => Promise<Response>;
|
|
@@ -256,10 +256,10 @@ function resolveProtectedResource(auth, request, options) {
|
|
|
256
256
|
resourceURL.hash = "";
|
|
257
257
|
return trimTrailingSlash(resourceURL.toString());
|
|
258
258
|
}
|
|
259
|
-
function assertResourcePathShape(resourcePath) {
|
|
259
|
+
function assertResourcePathShape(resourcePath, label = "resourcePath") {
|
|
260
260
|
if (!resourcePath.startsWith("/") || resourcePath.startsWith("//") || resourcePath.includes("\\") || /(^|\/)\.\.(\/|$)/.test(resourcePath)) {
|
|
261
261
|
throw new Error(
|
|
262
|
-
`@lovable.dev/mcp-js:
|
|
262
|
+
`@lovable.dev/mcp-js: ${label} must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`
|
|
263
263
|
);
|
|
264
264
|
}
|
|
265
265
|
}
|
|
@@ -326,6 +326,7 @@ async function fetchVerificationKeySet(jwksUri) {
|
|
|
326
326
|
async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
327
327
|
try {
|
|
328
328
|
const { payload } = await (0, import_jose.jwtVerify)(token, keySet, {
|
|
329
|
+
typ: "at+jwt",
|
|
329
330
|
// `issuer` is trimmed of any trailing slash; accept both forms so a token whose
|
|
330
331
|
// `iss` carries the slash the AS publishes still verifies.
|
|
331
332
|
issuer: [issuer, `${issuer}/`],
|
|
@@ -400,16 +401,16 @@ function quoteAuthenticateParam(value) {
|
|
|
400
401
|
const sanitized = value.replace(/[\u0000-\u001F\u007F]/g, "");
|
|
401
402
|
return `"${sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
402
403
|
}
|
|
403
|
-
function resolveProtectedResourceMetadataUrl(auth, request) {
|
|
404
|
+
function resolveProtectedResourceMetadataUrl(auth, request, options) {
|
|
404
405
|
if (auth.protectedResourceMetadataUrl !== void 0)
|
|
405
406
|
return auth.protectedResourceMetadataUrl;
|
|
406
407
|
const base = auth.resource ?? request.url;
|
|
407
|
-
return new URL(OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
|
|
408
|
+
return new URL(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
|
|
408
409
|
}
|
|
409
|
-
function wwwAuthenticateHeader(auth, request, params = {}) {
|
|
410
|
+
function wwwAuthenticateHeader(auth, request, options, params = {}) {
|
|
410
411
|
const values = [
|
|
411
412
|
`realm=${quoteAuthenticateParam("mcp")}`,
|
|
412
|
-
`resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request))}`
|
|
413
|
+
`resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request, options))}`
|
|
413
414
|
];
|
|
414
415
|
if (auth.requiredScopes && auth.requiredScopes.length > 0) {
|
|
415
416
|
values.push(`scope=${quoteAuthenticateParam(auth.requiredScopes.join(" "))}`);
|
|
@@ -420,9 +421,12 @@ function wwwAuthenticateHeader(auth, request, params = {}) {
|
|
|
420
421
|
values.push(`error_description=${quoteAuthenticateParam(params.errorDescription)}`);
|
|
421
422
|
return `Bearer ${values.join(", ")}`;
|
|
422
423
|
}
|
|
423
|
-
function challengeResponse(auth, request, status, oauthError, errorDescription) {
|
|
424
|
+
function challengeResponse(auth, request, options, status, oauthError, errorDescription) {
|
|
424
425
|
const headers = new Headers(JSON_HEADERS);
|
|
425
|
-
headers.set(
|
|
426
|
+
headers.set(
|
|
427
|
+
"WWW-Authenticate",
|
|
428
|
+
wwwAuthenticateHeader(auth, request, options, { error: oauthError, errorDescription })
|
|
429
|
+
);
|
|
426
430
|
headers.set("Cache-Control", "no-store");
|
|
427
431
|
return new Response(JSON.stringify({ error: status === 403 ? "forbidden" : "unauthorized" }), {
|
|
428
432
|
status,
|
|
@@ -450,10 +454,17 @@ function parseBearerToken(request) {
|
|
|
450
454
|
function getOAuthRuntime(mcp, options = {}) {
|
|
451
455
|
if (options.resourcePath !== void 0)
|
|
452
456
|
assertResourcePathShape(options.resourcePath);
|
|
453
|
-
|
|
457
|
+
if (options.metadataPath !== void 0)
|
|
458
|
+
assertResourcePathShape(options.metadataPath, "metadataPath");
|
|
459
|
+
const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
|
|
454
460
|
const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
|
|
455
461
|
if (!auth)
|
|
456
462
|
return { kind: "unconfigured", options: stableOptions };
|
|
463
|
+
if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`@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.`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
457
468
|
const discovery = createOAuthDiscoveryResolver(auth);
|
|
458
469
|
return {
|
|
459
470
|
kind: "configured",
|
|
@@ -492,7 +503,7 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
492
503
|
const token = parseBearerToken(request);
|
|
493
504
|
if (!token) {
|
|
494
505
|
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
495
|
-
return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
|
|
506
|
+
return { ok: false, response: challengeResponse(runtime.auth, request, runtime.options, 401) };
|
|
496
507
|
}
|
|
497
508
|
try {
|
|
498
509
|
const auth = await runtime.verify(token, request, runtime.options);
|
|
@@ -507,13 +518,27 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
507
518
|
log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
|
|
508
519
|
return {
|
|
509
520
|
ok: false,
|
|
510
|
-
response: challengeResponse(
|
|
521
|
+
response: challengeResponse(
|
|
522
|
+
runtime.auth,
|
|
523
|
+
request,
|
|
524
|
+
runtime.options,
|
|
525
|
+
err.status,
|
|
526
|
+
err.oauthError,
|
|
527
|
+
err.message
|
|
528
|
+
)
|
|
511
529
|
};
|
|
512
530
|
}
|
|
513
531
|
log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
|
|
514
532
|
return {
|
|
515
533
|
ok: false,
|
|
516
|
-
response: challengeResponse(
|
|
534
|
+
response: challengeResponse(
|
|
535
|
+
runtime.auth,
|
|
536
|
+
request,
|
|
537
|
+
runtime.options,
|
|
538
|
+
401,
|
|
539
|
+
"invalid_token",
|
|
540
|
+
"Invalid access token"
|
|
541
|
+
)
|
|
517
542
|
};
|
|
518
543
|
}
|
|
519
544
|
}
|
|
@@ -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
|
type RestListToolsHandler = (request: Request) => Promise<Response>;
|
|
@@ -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
|
type RestListToolsHandler = (request: Request) => Promise<Response>;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createInvokeToolHandler
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-6N7B7NZY.js";
|
|
4
4
|
import "../../chunk-MA5H6PSF.js";
|
|
5
5
|
import {
|
|
6
6
|
createListToolsHandler
|
|
7
|
-
} from "../../chunk-
|
|
8
|
-
import "../../chunk-
|
|
7
|
+
} from "../../chunk-2RGXCDBU.js";
|
|
8
|
+
import "../../chunk-YQBOGY7D.js";
|
|
9
9
|
import "../../chunk-QC3DXQTH.js";
|
|
10
10
|
import "../../chunk-6DXGZZA4.js";
|
|
11
11
|
export {
|
|
@@ -8,7 +8,7 @@ var import_node_path = require("path");
|
|
|
8
8
|
var import_vite2 = require("vite");
|
|
9
9
|
|
|
10
10
|
// package.json
|
|
11
|
-
var version = "0.
|
|
11
|
+
var version = "0.9.1";
|
|
12
12
|
|
|
13
13
|
// src/protocols/rest/list-tools.ts
|
|
14
14
|
var import_zod_compat = require("@modelcontextprotocol/sdk/server/zod-compat.js");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
buildMcpListing
|
|
4
|
-
} from "../../../chunk-
|
|
5
|
-
import "../../../chunk-
|
|
4
|
+
} from "../../../chunk-2RGXCDBU.js";
|
|
5
|
+
import "../../../chunk-YQBOGY7D.js";
|
|
6
6
|
import "../../../chunk-QC3DXQTH.js";
|
|
7
7
|
import {
|
|
8
8
|
isFileMissing
|
|
@@ -16,7 +16,7 @@ import { dirname, resolve } from "path";
|
|
|
16
16
|
import { resolveConfig } from "vite";
|
|
17
17
|
|
|
18
18
|
// package.json
|
|
19
|
-
var version = "0.
|
|
19
|
+
var version = "0.9.1";
|
|
20
20
|
|
|
21
21
|
// src/stacks/tanstack/extract-manifest.ts
|
|
22
22
|
var MANIFEST_VERSION = 1;
|
|
@@ -258,10 +258,10 @@ function resolveProtectedResource(auth, request, options) {
|
|
|
258
258
|
resourceURL.hash = "";
|
|
259
259
|
return trimTrailingSlash(resourceURL.toString());
|
|
260
260
|
}
|
|
261
|
-
function assertResourcePathShape(resourcePath) {
|
|
261
|
+
function assertResourcePathShape(resourcePath, label = "resourcePath") {
|
|
262
262
|
if (!resourcePath.startsWith("/") || resourcePath.startsWith("//") || resourcePath.includes("\\") || /(^|\/)\.\.(\/|$)/.test(resourcePath)) {
|
|
263
263
|
throw new Error(
|
|
264
|
-
`@lovable.dev/mcp-js:
|
|
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
|
}
|
|
@@ -328,6 +328,7 @@ async function fetchVerificationKeySet(jwksUri) {
|
|
|
328
328
|
async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
329
329
|
try {
|
|
330
330
|
const { payload } = await (0, import_jose.jwtVerify)(token, keySet, {
|
|
331
|
+
typ: "at+jwt",
|
|
331
332
|
// `issuer` is trimmed of any trailing slash; accept both forms so a token whose
|
|
332
333
|
// `iss` carries the slash the AS publishes still verifies.
|
|
333
334
|
issuer: [issuer, `${issuer}/`],
|
|
@@ -402,16 +403,16 @@ function quoteAuthenticateParam(value) {
|
|
|
402
403
|
const sanitized = value.replace(/[\u0000-\u001F\u007F]/g, "");
|
|
403
404
|
return `"${sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
404
405
|
}
|
|
405
|
-
function resolveProtectedResourceMetadataUrl(auth, request) {
|
|
406
|
+
function resolveProtectedResourceMetadataUrl(auth, request, options) {
|
|
406
407
|
if (auth.protectedResourceMetadataUrl !== void 0)
|
|
407
408
|
return auth.protectedResourceMetadataUrl;
|
|
408
409
|
const base = auth.resource ?? request.url;
|
|
409
|
-
return new URL(OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
|
|
410
|
+
return new URL(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
|
|
410
411
|
}
|
|
411
|
-
function wwwAuthenticateHeader(auth, request, params = {}) {
|
|
412
|
+
function wwwAuthenticateHeader(auth, request, options, params = {}) {
|
|
412
413
|
const values = [
|
|
413
414
|
`realm=${quoteAuthenticateParam("mcp")}`,
|
|
414
|
-
`resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request))}`
|
|
415
|
+
`resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request, options))}`
|
|
415
416
|
];
|
|
416
417
|
if (auth.requiredScopes && auth.requiredScopes.length > 0) {
|
|
417
418
|
values.push(`scope=${quoteAuthenticateParam(auth.requiredScopes.join(" "))}`);
|
|
@@ -422,9 +423,12 @@ function wwwAuthenticateHeader(auth, request, params = {}) {
|
|
|
422
423
|
values.push(`error_description=${quoteAuthenticateParam(params.errorDescription)}`);
|
|
423
424
|
return `Bearer ${values.join(", ")}`;
|
|
424
425
|
}
|
|
425
|
-
function challengeResponse(auth, request, status, oauthError, errorDescription) {
|
|
426
|
+
function challengeResponse(auth, request, options, status, oauthError, errorDescription) {
|
|
426
427
|
const headers = new Headers(JSON_HEADERS);
|
|
427
|
-
headers.set(
|
|
428
|
+
headers.set(
|
|
429
|
+
"WWW-Authenticate",
|
|
430
|
+
wwwAuthenticateHeader(auth, request, options, { error: oauthError, errorDescription })
|
|
431
|
+
);
|
|
428
432
|
headers.set("Cache-Control", "no-store");
|
|
429
433
|
return new Response(JSON.stringify({ error: status === 403 ? "forbidden" : "unauthorized" }), {
|
|
430
434
|
status,
|
|
@@ -452,10 +456,17 @@ function parseBearerToken(request) {
|
|
|
452
456
|
function getOAuthRuntime(mcp, options = {}) {
|
|
453
457
|
if (options.resourcePath !== void 0)
|
|
454
458
|
assertResourcePathShape(options.resourcePath);
|
|
455
|
-
|
|
459
|
+
if (options.metadataPath !== void 0)
|
|
460
|
+
assertResourcePathShape(options.metadataPath, "metadataPath");
|
|
461
|
+
const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
|
|
456
462
|
const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
|
|
457
463
|
if (!auth)
|
|
458
464
|
return { kind: "unconfigured", options: stableOptions };
|
|
465
|
+
if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`@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.`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
459
470
|
const discovery = createOAuthDiscoveryResolver(auth);
|
|
460
471
|
return {
|
|
461
472
|
kind: "configured",
|
|
@@ -494,7 +505,7 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
494
505
|
const token = parseBearerToken(request);
|
|
495
506
|
if (!token) {
|
|
496
507
|
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
497
|
-
return { ok: false, response: challengeResponse(runtime.auth, request, 401) };
|
|
508
|
+
return { ok: false, response: challengeResponse(runtime.auth, request, runtime.options, 401) };
|
|
498
509
|
}
|
|
499
510
|
try {
|
|
500
511
|
const auth = await runtime.verify(token, request, runtime.options);
|
|
@@ -509,13 +520,27 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
509
520
|
log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
|
|
510
521
|
return {
|
|
511
522
|
ok: false,
|
|
512
|
-
response: challengeResponse(
|
|
523
|
+
response: challengeResponse(
|
|
524
|
+
runtime.auth,
|
|
525
|
+
request,
|
|
526
|
+
runtime.options,
|
|
527
|
+
err.status,
|
|
528
|
+
err.oauthError,
|
|
529
|
+
err.message
|
|
530
|
+
)
|
|
513
531
|
};
|
|
514
532
|
}
|
|
515
533
|
log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
|
|
516
534
|
return {
|
|
517
535
|
ok: false,
|
|
518
|
-
response: challengeResponse(
|
|
536
|
+
response: challengeResponse(
|
|
537
|
+
runtime.auth,
|
|
538
|
+
request,
|
|
539
|
+
runtime.options,
|
|
540
|
+
401,
|
|
541
|
+
"invalid_token",
|
|
542
|
+
"Invalid access token"
|
|
543
|
+
)
|
|
519
544
|
};
|
|
520
545
|
}
|
|
521
546
|
}
|
|
@@ -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,17 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpProtocolHandler
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-OXSSMKQE.js";
|
|
4
4
|
import {
|
|
5
5
|
createOAuthProtectedResourceMetadataHandler
|
|
6
|
-
} from "../../chunk-
|
|
6
|
+
} from "../../chunk-4JO3BZQG.js";
|
|
7
7
|
import {
|
|
8
8
|
createInvokeToolHandler
|
|
9
|
-
} from "../../chunk-
|
|
9
|
+
} from "../../chunk-6N7B7NZY.js";
|
|
10
10
|
import "../../chunk-MA5H6PSF.js";
|
|
11
11
|
import {
|
|
12
12
|
createListToolsHandler
|
|
13
|
-
} from "../../chunk-
|
|
14
|
-
import "../../chunk-
|
|
13
|
+
} from "../../chunk-2RGXCDBU.js";
|
|
14
|
+
import "../../chunk-YQBOGY7D.js";
|
|
15
15
|
import "../../chunk-QC3DXQTH.js";
|
|
16
16
|
import "../../chunk-6DXGZZA4.js";
|
|
17
17
|
|
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/stacks/tanstack/vite.ts
|
|
21
21
|
var vite_exports = {};
|
|
22
22
|
__export(vite_exports, {
|
|
23
|
+
assertMetadataPathShape: () => assertMetadataPathShape,
|
|
23
24
|
assertUrlPathShape: () => assertUrlPathShape,
|
|
24
25
|
default: () => vite_default,
|
|
25
26
|
deriveRouteFileName: () => deriveRouteFileName,
|
|
@@ -55,15 +56,17 @@ function wellKnownRouteFile(routesDir, urlPath) {
|
|
|
55
56
|
});
|
|
56
57
|
return (0, import_node_path.resolve)(routesDir, ...fileSegments);
|
|
57
58
|
}
|
|
58
|
-
function resolveAllRoutes(projectRoot, routesDirOption, routeFileName) {
|
|
59
|
+
function resolveAllRoutes(projectRoot, routesDirOption, routeFileName, metadataUrlPath) {
|
|
59
60
|
const routesDir = (0, import_node_path.resolve)(projectRoot, routesDirOption);
|
|
60
61
|
assertContains(projectRoot, routesDir, `routesDir "${routesDirOption}"`);
|
|
61
62
|
const mcpRouteFile = (0, import_node_path.resolve)(routesDir, routeFileName);
|
|
62
63
|
assertContains(routesDir, mcpRouteFile, `routeFileName "${routeFileName}"`);
|
|
64
|
+
const metadataRouteFile = wellKnownRouteFile(routesDir, metadataUrlPath);
|
|
65
|
+
assertContains(routesDir, metadataRouteFile, `metadataPath "${metadataUrlPath}"`);
|
|
63
66
|
return {
|
|
64
67
|
routesDir,
|
|
65
68
|
mcpRouteFile,
|
|
66
|
-
metadataRouteFile
|
|
69
|
+
metadataRouteFile,
|
|
67
70
|
listToolsRouteFile: (0, import_node_path.resolve)(routesDir, "[.mcp]", "list-tools.ts"),
|
|
68
71
|
invokeToolRouteFile: (0, import_node_path.resolve)(routesDir, "[.mcp]", "invoke-tool", "$tool.ts")
|
|
69
72
|
};
|
|
@@ -87,6 +90,24 @@ function assertUrlPathShape(urlPath) {
|
|
|
87
90
|
function deriveRouteFileName(urlPath) {
|
|
88
91
|
return `${assertUrlPathShape(urlPath)}.ts`;
|
|
89
92
|
}
|
|
93
|
+
function assertMetadataPathShape(metadataPath) {
|
|
94
|
+
if (!metadataPath.startsWith("/")) {
|
|
95
|
+
throw new Error(`@lovable.dev/mcp-js: metadataPath must start with "/", got "${metadataPath}"`);
|
|
96
|
+
}
|
|
97
|
+
if (/\/{2,}/.test(metadataPath)) {
|
|
98
|
+
throw new Error(`@lovable.dev/mcp-js: metadataPath "${metadataPath}" contains empty segments ("//")`);
|
|
99
|
+
}
|
|
100
|
+
if (/(^|\/)\.\.(\/|$)/.test(metadataPath)) {
|
|
101
|
+
throw new Error(`@lovable.dev/mcp-js: metadataPath "${metadataPath}" must not contain ".." segments`);
|
|
102
|
+
}
|
|
103
|
+
const trimmed = metadataPath.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
104
|
+
if (!trimmed) {
|
|
105
|
+
throw new Error(`@lovable.dev/mcp-js: metadataPath "${metadataPath}" has no route segment`);
|
|
106
|
+
}
|
|
107
|
+
if (!/^[a-zA-Z0-9_\-./]+$/.test(trimmed)) {
|
|
108
|
+
throw new Error(`@lovable.dev/mcp-js: metadataPath "${metadataPath}" contains unsupported characters`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
90
111
|
function canonicalUrlPath(urlPath) {
|
|
91
112
|
return `/${urlPath.replace(/^\/+/, "").replace(/\/+$/, "")}`;
|
|
92
113
|
}
|
|
@@ -95,10 +116,11 @@ function relativeImportSpecifier(routeFile, target) {
|
|
|
95
116
|
const withoutExt = rel.replace(/\.ts$/, "");
|
|
96
117
|
return withoutExt.startsWith(".") ? withoutExt : `./${withoutExt}`;
|
|
97
118
|
}
|
|
98
|
-
function buildRouteSource(route, resourcePath, mcpEntry, projectRoot) {
|
|
119
|
+
function buildRouteSource(route, resourcePath, metadataPath, mcpEntry, projectRoot) {
|
|
99
120
|
const mcpImport = relativeImportSpecifier(route.file, mcpEntry);
|
|
100
121
|
const routeRel = normalizePath((0, import_node_path.relative)(projectRoot, route.file));
|
|
101
122
|
const note = route.spaFallbackNote ? " // ANY: TanStack returns SPA HTML for methods not in `handlers`; the SDK 405s instead.\n" : "";
|
|
123
|
+
const runtimeOptions = metadataPath === void 0 ? `{ resourcePath: "${resourcePath}" }` : `{ resourcePath: "${resourcePath}", metadataPath: "${metadataPath}" }`;
|
|
102
124
|
return `${GENERATED_BANNER}
|
|
103
125
|
// route: ${route.routeLiteral}
|
|
104
126
|
// emitted to: ${routeRel}
|
|
@@ -112,7 +134,7 @@ import mcp from "${mcpImport}";
|
|
|
112
134
|
export const Route = createFileRoute("${route.routeLiteral}")({
|
|
113
135
|
server: {
|
|
114
136
|
handlers: {
|
|
115
|
-
${note} ANY: ${route.handlerFactory}(mcp,
|
|
137
|
+
${note} ANY: ${route.handlerFactory}(mcp, ${runtimeOptions}),
|
|
116
138
|
},
|
|
117
139
|
},
|
|
118
140
|
});
|
|
@@ -191,12 +213,23 @@ function mcpPlugin(options = {}) {
|
|
|
191
213
|
const urlPath = canonicalUrlPath(rawUrlPath);
|
|
192
214
|
const emitRestRoutes = options.restRoutes !== false;
|
|
193
215
|
const emitProtectedResourceMetadataRoute = options.protectedResourceMetadataRoute !== false;
|
|
216
|
+
if (options.metadataPath !== void 0) {
|
|
217
|
+
if (!emitProtectedResourceMetadataRoute) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`@lovable.dev/mcp-js: metadataPath sets where the protected-resource metadata route is served, so it can't be combined with protectedResourceMetadataRoute: false.`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
assertMetadataPathShape(options.metadataPath);
|
|
223
|
+
}
|
|
224
|
+
const metadataUrlPath = canonicalUrlPath(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH);
|
|
225
|
+
const injectedMetadataPath = emitProtectedResourceMetadataRoute ? metadataUrlPath : void 0;
|
|
194
226
|
let projectRoot = process.cwd();
|
|
195
227
|
let mcpEntry = (0, import_node_path.resolve)(projectRoot, mcpEntryOption);
|
|
196
228
|
let { routesDir, mcpRouteFile, metadataRouteFile, listToolsRouteFile, invokeToolRouteFile } = resolveAllRoutes(
|
|
197
229
|
projectRoot,
|
|
198
230
|
routesDirOption,
|
|
199
|
-
routeFileName
|
|
231
|
+
routeFileName,
|
|
232
|
+
metadataUrlPath
|
|
200
233
|
);
|
|
201
234
|
const regenerate = () => {
|
|
202
235
|
let mcpEntryExists = true;
|
|
@@ -215,7 +248,7 @@ function mcpPlugin(options = {}) {
|
|
|
215
248
|
if (emitProtectedResourceMetadataRoute) {
|
|
216
249
|
routes.push({
|
|
217
250
|
file: metadataRouteFile,
|
|
218
|
-
routeLiteral:
|
|
251
|
+
routeLiteral: metadataUrlPath,
|
|
219
252
|
handlerFactory: "createTanStackOAuthProtectedResourceMetadataHandler"
|
|
220
253
|
});
|
|
221
254
|
}
|
|
@@ -237,7 +270,7 @@ function mcpPlugin(options = {}) {
|
|
|
237
270
|
}
|
|
238
271
|
for (const route of routes) {
|
|
239
272
|
expected.add(route.file);
|
|
240
|
-
writeIfChanged(route.file, buildRouteSource(route, urlPath, mcpEntry, projectRoot));
|
|
273
|
+
writeIfChanged(route.file, buildRouteSource(route, urlPath, injectedMetadataPath, mcpEntry, projectRoot));
|
|
241
274
|
}
|
|
242
275
|
}
|
|
243
276
|
for (const file of findAdoptedFiles(routesDir)) {
|
|
@@ -271,7 +304,8 @@ function mcpPlugin(options = {}) {
|
|
|
271
304
|
({ routesDir, mcpRouteFile, metadataRouteFile, listToolsRouteFile, invokeToolRouteFile } = resolveAllRoutes(
|
|
272
305
|
projectRoot,
|
|
273
306
|
routesDirOption,
|
|
274
|
-
routeFileName
|
|
307
|
+
routeFileName,
|
|
308
|
+
metadataUrlPath
|
|
275
309
|
));
|
|
276
310
|
regenerate();
|
|
277
311
|
},
|
|
@@ -298,6 +332,7 @@ function mcpPlugin(options = {}) {
|
|
|
298
332
|
var vite_default = mcpPlugin;
|
|
299
333
|
// Annotate the CommonJS export names for ESM import in node:
|
|
300
334
|
0 && (module.exports = {
|
|
335
|
+
assertMetadataPathShape,
|
|
301
336
|
assertUrlPathShape,
|
|
302
337
|
deriveRouteFileName,
|
|
303
338
|
mcpPlugin
|
|
@@ -48,6 +48,21 @@ interface McpPluginOptions {
|
|
|
48
48
|
* @default true
|
|
49
49
|
*/
|
|
50
50
|
protectedResourceMetadataRoute?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Public URL path for the OAuth protected-resource metadata route (RFC 9728).
|
|
53
|
+
* Both the emitted route file and the runtime `WWW-Authenticate` challenge use
|
|
54
|
+
* this path, so they can't drift. Use it when a host gate exposes only a
|
|
55
|
+
* sub-prefix (e.g. workspace-private projects where only `/api/public/*` is
|
|
56
|
+
* anonymously reachable) and the default well-known path is blocked.
|
|
57
|
+
*
|
|
58
|
+
* Leading-dot segments are bracket-escaped for TanStack file routing
|
|
59
|
+
* (`.well-known` → `[.well-known]`); the final segment becomes the route file.
|
|
60
|
+
* Mutually exclusive with `auth.protectedResourceMetadataUrl` (that option
|
|
61
|
+
* advertises an externally hosted document instead of serving one), and
|
|
62
|
+
* cannot be combined with `protectedResourceMetadataRoute: false`.
|
|
63
|
+
* @default "/.well-known/oauth-protected-resource"
|
|
64
|
+
*/
|
|
65
|
+
metadataPath?: string;
|
|
51
66
|
}
|
|
52
67
|
interface McpPluginApi {
|
|
53
68
|
readonly mcpEntry: string;
|
|
@@ -55,6 +70,7 @@ interface McpPluginApi {
|
|
|
55
70
|
}
|
|
56
71
|
declare function assertUrlPathShape(urlPath: string): string;
|
|
57
72
|
declare function deriveRouteFileName(urlPath: string): string;
|
|
73
|
+
declare function assertMetadataPathShape(metadataPath: string): void;
|
|
58
74
|
/**
|
|
59
75
|
* Generate the TanStack route(s) that mount the MCP server.
|
|
60
76
|
*
|
|
@@ -69,4 +85,4 @@ declare function deriveRouteFileName(urlPath: string): string;
|
|
|
69
85
|
*/
|
|
70
86
|
declare function mcpPlugin(options?: McpPluginOptions): Plugin;
|
|
71
87
|
|
|
72
|
-
export { type McpPluginApi, type McpPluginOptions, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
|
|
88
|
+
export { type McpPluginApi, type McpPluginOptions, assertMetadataPathShape, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
|
|
@@ -48,6 +48,21 @@ interface McpPluginOptions {
|
|
|
48
48
|
* @default true
|
|
49
49
|
*/
|
|
50
50
|
protectedResourceMetadataRoute?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Public URL path for the OAuth protected-resource metadata route (RFC 9728).
|
|
53
|
+
* Both the emitted route file and the runtime `WWW-Authenticate` challenge use
|
|
54
|
+
* this path, so they can't drift. Use it when a host gate exposes only a
|
|
55
|
+
* sub-prefix (e.g. workspace-private projects where only `/api/public/*` is
|
|
56
|
+
* anonymously reachable) and the default well-known path is blocked.
|
|
57
|
+
*
|
|
58
|
+
* Leading-dot segments are bracket-escaped for TanStack file routing
|
|
59
|
+
* (`.well-known` → `[.well-known]`); the final segment becomes the route file.
|
|
60
|
+
* Mutually exclusive with `auth.protectedResourceMetadataUrl` (that option
|
|
61
|
+
* advertises an externally hosted document instead of serving one), and
|
|
62
|
+
* cannot be combined with `protectedResourceMetadataRoute: false`.
|
|
63
|
+
* @default "/.well-known/oauth-protected-resource"
|
|
64
|
+
*/
|
|
65
|
+
metadataPath?: string;
|
|
51
66
|
}
|
|
52
67
|
interface McpPluginApi {
|
|
53
68
|
readonly mcpEntry: string;
|
|
@@ -55,6 +70,7 @@ interface McpPluginApi {
|
|
|
55
70
|
}
|
|
56
71
|
declare function assertUrlPathShape(urlPath: string): string;
|
|
57
72
|
declare function deriveRouteFileName(urlPath: string): string;
|
|
73
|
+
declare function assertMetadataPathShape(metadataPath: string): void;
|
|
58
74
|
/**
|
|
59
75
|
* Generate the TanStack route(s) that mount the MCP server.
|
|
60
76
|
*
|
|
@@ -69,4 +85,4 @@ declare function deriveRouteFileName(urlPath: string): string;
|
|
|
69
85
|
*/
|
|
70
86
|
declare function mcpPlugin(options?: McpPluginOptions): Plugin;
|
|
71
87
|
|
|
72
|
-
export { type McpPluginApi, type McpPluginOptions, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
|
|
88
|
+
export { type McpPluginApi, type McpPluginOptions, assertMetadataPathShape, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
|
|
@@ -25,15 +25,17 @@ function wellKnownRouteFile(routesDir, urlPath) {
|
|
|
25
25
|
});
|
|
26
26
|
return resolve(routesDir, ...fileSegments);
|
|
27
27
|
}
|
|
28
|
-
function resolveAllRoutes(projectRoot, routesDirOption, routeFileName) {
|
|
28
|
+
function resolveAllRoutes(projectRoot, routesDirOption, routeFileName, metadataUrlPath) {
|
|
29
29
|
const routesDir = resolve(projectRoot, routesDirOption);
|
|
30
30
|
assertContains(projectRoot, routesDir, `routesDir "${routesDirOption}"`);
|
|
31
31
|
const mcpRouteFile = resolve(routesDir, routeFileName);
|
|
32
32
|
assertContains(routesDir, mcpRouteFile, `routeFileName "${routeFileName}"`);
|
|
33
|
+
const metadataRouteFile = wellKnownRouteFile(routesDir, metadataUrlPath);
|
|
34
|
+
assertContains(routesDir, metadataRouteFile, `metadataPath "${metadataUrlPath}"`);
|
|
33
35
|
return {
|
|
34
36
|
routesDir,
|
|
35
37
|
mcpRouteFile,
|
|
36
|
-
metadataRouteFile
|
|
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)) {
|
|
@@ -241,7 +273,8 @@ function mcpPlugin(options = {}) {
|
|
|
241
273
|
({ routesDir, mcpRouteFile, metadataRouteFile, listToolsRouteFile, invokeToolRouteFile } = resolveAllRoutes(
|
|
242
274
|
projectRoot,
|
|
243
275
|
routesDirOption,
|
|
244
|
-
routeFileName
|
|
276
|
+
routeFileName,
|
|
277
|
+
metadataUrlPath
|
|
245
278
|
));
|
|
246
279
|
regenerate();
|
|
247
280
|
},
|
|
@@ -267,6 +300,7 @@ function mcpPlugin(options = {}) {
|
|
|
267
300
|
}
|
|
268
301
|
var vite_default = mcpPlugin;
|
|
269
302
|
export {
|
|
303
|
+
assertMetadataPathShape,
|
|
270
304
|
assertUrlPathShape,
|
|
271
305
|
vite_default as default,
|
|
272
306
|
deriveRouteFileName,
|
|
@@ -52,6 +52,10 @@ interface McpAuthConfig {
|
|
|
52
52
|
* `WWW-Authenticate: resource_metadata` challenge and does not generate or
|
|
53
53
|
* serve its own metadata (the `/.well-known/oauth-protected-resource` handler
|
|
54
54
|
* 404s). Use it when the resource server already publishes its own PRM.
|
|
55
|
+
*
|
|
56
|
+
* Mutually exclusive with the Vite plugin's `metadataPath`: that option has
|
|
57
|
+
* the SDK serve the document itself at a custom path, so configuring both is
|
|
58
|
+
* rejected at handler construction.
|
|
55
59
|
*/
|
|
56
60
|
readonly protectedResourceMetadataUrl?: string;
|
|
57
61
|
readonly requireOAuthClientClaim?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovable.dev/mcp-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Author MCP servers for Lovable apps. Declare tools with defineTool, register them in defineMcp, and the framework adapter (TanStack today, Supabase Edge Functions next) emits the route(s) at build time.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
interface McpRuntimeOptions {
|
|
2
|
-
/**
|
|
3
|
-
* Public MCP protocol path. REST companion handlers pass this so OAuth
|
|
4
|
-
* resource/audience checks bind to `/mcp`, not internal `/.mcp/*` RPC URLs.
|
|
5
|
-
*/
|
|
6
|
-
resourcePath?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export type { McpRuntimeOptions as M };
|