@lovable.dev/mcp-js 0.7.0 → 0.8.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 +10 -0
- package/dist/{chunk-G5GWSV5D.js → chunk-3XPKR4G6.js} +1 -1
- package/dist/{chunk-53M2X7FU.js → chunk-4Y72OCPS.js} +3 -47
- package/dist/chunk-E335BBVM.js +57 -0
- package/dist/{chunk-NTXHOUK6.js → chunk-HRMLGCXV.js} +15 -13
- package/dist/chunk-QJ7XEKT3.js +8 -0
- package/dist/{chunk-722HKLIU.js → chunk-W7F6JRDB.js} +1 -1
- package/dist/protocols/mcp/index.cjs +14 -12
- package/dist/protocols/mcp/index.js +2 -2
- package/dist/protocols/oauth-metadata.cjs +14 -12
- package/dist/protocols/oauth-metadata.js +2 -2
- package/dist/protocols/rest/index.cjs +28 -24
- 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 +28 -24
- package/dist/stacks/tanstack/index.js +8 -6
- package/dist/stacks/tanstack/vite.cjs +24 -11
- package/dist/stacks/tanstack/vite.d.cts +10 -2
- package/dist/stacks/tanstack/vite.d.ts +10 -2
- package/dist/stacks/tanstack/vite.js +23 -12
- package/package.json +6 -3
package/README.internal.md
CHANGED
|
@@ -133,7 +133,11 @@ src/
|
|
|
133
133
|
stacks/
|
|
134
134
|
tanstack/
|
|
135
135
|
handlers.ts # TanStack route-ctx adapters
|
|
136
|
-
vite.ts # mcpPlugin (route emission + stale-file cleanup)
|
|
136
|
+
vite.ts # mcpPlugin (route emission + stale-file cleanup; no manifest)
|
|
137
|
+
ssr-loader.ts # produceViaSsr: load the entry via a throwaway Vite SSR server
|
|
138
|
+
extract-manifest.ts # project a defineMcp result to the manifest (types + manifestFromDefinition)
|
|
139
|
+
manifest-io.ts # runExtract/syncManifest: write/remove .lovable/mcp/manifest.json
|
|
140
|
+
cli/extract-manifest.ts # lovable-mcp-extract-manifest bin (runs runExtract on cwd)
|
|
137
141
|
index.ts # barrel
|
|
138
142
|
tests/
|
|
139
143
|
core/{define,promise,url}.test.ts
|
|
@@ -143,7 +147,7 @@ tests/
|
|
|
143
147
|
rest/{list-tools,invoke-tool}.test.ts
|
|
144
148
|
parity.test.ts # REST↔MCP equivalence contract
|
|
145
149
|
stacks/
|
|
146
|
-
tanstack/{handlers,vite}.test.ts
|
|
150
|
+
tanstack/{handlers,vite,extract-manifest,manifest-io,ssr-loader}.test.ts
|
|
147
151
|
integration/ # boots an example app, hits live HTTP
|
|
148
152
|
global-setup.ts # spawns example/tanstack on :8080
|
|
149
153
|
tools.test.ts # non-OAuth example
|
package/README.md
CHANGED
|
@@ -53,6 +53,16 @@ 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
|
+
### `.lovable/mcp/manifest.json`
|
|
57
|
+
|
|
58
|
+
`.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.
|
|
59
|
+
|
|
60
|
+
Three caveats:
|
|
61
|
+
|
|
62
|
+
- **The entry must import cleanly.** Because extraction imports the entry, it must not throw at module load — read env vars and do I/O *inside* tool handlers, not at module top level. A top-level throw also breaks Worker cold-start, so this is correct authoring regardless. An entry that can't be imported (or doesn't `export default defineMcp(...)`) **makes the extract CLI exit non-zero with a clear error** rather than emitting a partial manifest.
|
|
63
|
+
- **Tool `title`/`description` and the `auth` config are serialized verbatim.** They land in a committed file. The `auth` config is whatever the entry resolves at build time — an issuer read from env resolves to its build-time value (e.g. a fallback like `https://supabase.invalid/auth/v1` when the env var is unset), so don't put secrets in it.
|
|
64
|
+
- **The `version` field is a wire-format version, not the package version.** The platform reader must accept a manifest `version` before this package starts emitting it, so bumping it is a coordinated deploy with the Lovable backend — not something an app author changes.
|
|
65
|
+
|
|
56
66
|
## OAuth resource-server auth
|
|
57
67
|
|
|
58
68
|
`@lovable.dev/mcp-js` can protect an app-hosted MCP server as an OAuth 2.1 resource server. The package does not implement `/authorize`, `/token`, client registration, refresh tokens, or consent UI; those stay with the authorization server (for today's Supabase-backed Lovable Cloud apps, Supabase Auth). The MCP runtime validates bearer JWTs, publishes RFC 9728 protected-resource metadata, returns `WWW-Authenticate: Bearer ... resource_metadata="..."` challenges, and passes verified claims to tools.
|
|
@@ -6,55 +6,12 @@ import {
|
|
|
6
6
|
assertRestResourceBinding,
|
|
7
7
|
corsPreflightResponse,
|
|
8
8
|
createRequestAuthorizer,
|
|
9
|
-
headResponse,
|
|
10
9
|
methodNotAllowed,
|
|
11
10
|
withCors
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
|
|
14
|
-
// src/protocols/rest/list-tools.ts
|
|
15
|
-
import { objectFromShape } from "@modelcontextprotocol/sdk/server/zod-compat.js";
|
|
16
|
-
import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
|
|
17
|
-
function shapeToJsonSchema(shape) {
|
|
18
|
-
if (!shape)
|
|
19
|
-
return null;
|
|
20
|
-
try {
|
|
21
|
-
return toJsonSchemaCompat(objectFromShape(shape));
|
|
22
|
-
} catch {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
function createListToolsHandler(mcp, options = {}) {
|
|
27
|
-
assertRestResourceBinding(mcp, options);
|
|
28
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
29
|
-
const handle = async (request) => {
|
|
30
|
-
const authResult = await authorizer.authorize(request);
|
|
31
|
-
if (!authResult.ok)
|
|
32
|
-
return authResult.response;
|
|
33
|
-
if (request.method !== "GET" && request.method !== "HEAD")
|
|
34
|
-
return methodNotAllowed("GET, HEAD, OPTIONS");
|
|
35
|
-
const body = {
|
|
36
|
-
server: { name: mcp.name, version: mcp.version, title: mcp.title },
|
|
37
|
-
tools: mcp.tools.map((tool) => ({
|
|
38
|
-
name: tool.name,
|
|
39
|
-
title: tool.title,
|
|
40
|
-
description: tool.description,
|
|
41
|
-
annotations: tool.annotations,
|
|
42
|
-
inputSchema: shapeToJsonSchema(tool.inputSchema),
|
|
43
|
-
outputSchema: shapeToJsonSchema(tool.outputSchema)
|
|
44
|
-
}))
|
|
45
|
-
};
|
|
46
|
-
const response = Response.json(body);
|
|
47
|
-
return request.method === "HEAD" ? headResponse(response) : response;
|
|
48
|
-
};
|
|
49
|
-
return async (request) => {
|
|
50
|
-
if (request.method === "OPTIONS")
|
|
51
|
-
return corsPreflightResponse("GET, HEAD, OPTIONS");
|
|
52
|
-
return withCors(await handle(request));
|
|
53
|
-
};
|
|
54
|
-
}
|
|
11
|
+
} from "./chunk-HRMLGCXV.js";
|
|
55
12
|
|
|
56
13
|
// src/protocols/rest/invoke-tool.ts
|
|
57
|
-
import { getParseErrorMessage, objectFromShape
|
|
14
|
+
import { getParseErrorMessage, objectFromShape, safeParseAsync } from "@modelcontextprotocol/sdk/server/zod-compat.js";
|
|
58
15
|
var MAX_REFLECTED_TOOL_NAME = 256;
|
|
59
16
|
function safeReflectName(name) {
|
|
60
17
|
const text = String(name);
|
|
@@ -98,7 +55,7 @@ function createInvokeToolHandler(mcp, options = {}) {
|
|
|
98
55
|
let args = rawArgs;
|
|
99
56
|
if (tool.inputSchema) {
|
|
100
57
|
try {
|
|
101
|
-
const schema =
|
|
58
|
+
const schema = objectFromShape(tool.inputSchema);
|
|
102
59
|
const parsed = await safeParseAsync(schema, rawArgs);
|
|
103
60
|
if (!parsed.success) {
|
|
104
61
|
return new Response(
|
|
@@ -151,6 +108,5 @@ function createInvokeToolHandler(mcp, options = {}) {
|
|
|
151
108
|
}
|
|
152
109
|
|
|
153
110
|
export {
|
|
154
|
-
createListToolsHandler,
|
|
155
111
|
createInvokeToolHandler
|
|
156
112
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertRestResourceBinding,
|
|
3
|
+
corsPreflightResponse,
|
|
4
|
+
createRequestAuthorizer,
|
|
5
|
+
headResponse,
|
|
6
|
+
methodNotAllowed,
|
|
7
|
+
withCors
|
|
8
|
+
} from "./chunk-HRMLGCXV.js";
|
|
9
|
+
|
|
10
|
+
// src/protocols/rest/list-tools.ts
|
|
11
|
+
import { objectFromShape } from "@modelcontextprotocol/sdk/server/zod-compat.js";
|
|
12
|
+
import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
|
|
13
|
+
function shapeToJsonSchema(shape) {
|
|
14
|
+
if (!shape)
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
return toJsonSchemaCompat(objectFromShape(shape));
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function buildMcpListing(mcp) {
|
|
23
|
+
return {
|
|
24
|
+
server: { name: mcp.name, version: mcp.version, title: mcp.title },
|
|
25
|
+
tools: mcp.tools.map((tool) => ({
|
|
26
|
+
name: tool.name,
|
|
27
|
+
title: tool.title,
|
|
28
|
+
description: tool.description,
|
|
29
|
+
annotations: tool.annotations,
|
|
30
|
+
inputSchema: shapeToJsonSchema(tool.inputSchema),
|
|
31
|
+
outputSchema: shapeToJsonSchema(tool.outputSchema)
|
|
32
|
+
}))
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function createListToolsHandler(mcp, options = {}) {
|
|
36
|
+
assertRestResourceBinding(mcp, options);
|
|
37
|
+
const authorizer = createRequestAuthorizer(mcp, options);
|
|
38
|
+
const handle = async (request) => {
|
|
39
|
+
const authResult = await authorizer.authorize(request);
|
|
40
|
+
if (!authResult.ok)
|
|
41
|
+
return authResult.response;
|
|
42
|
+
if (request.method !== "GET" && request.method !== "HEAD")
|
|
43
|
+
return methodNotAllowed("GET, HEAD, OPTIONS");
|
|
44
|
+
const response = Response.json(buildMcpListing(mcp));
|
|
45
|
+
return request.method === "HEAD" ? headResponse(response) : response;
|
|
46
|
+
};
|
|
47
|
+
return async (request) => {
|
|
48
|
+
if (request.method === "OPTIONS")
|
|
49
|
+
return corsPreflightResponse("GET, HEAD, OPTIONS");
|
|
50
|
+
return withCors(await handle(request));
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
buildMcpListing,
|
|
56
|
+
createListToolsHandler
|
|
57
|
+
};
|
|
@@ -170,7 +170,7 @@ function createOAuthDiscoveryResolver(auth) {
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
// src/auth/verifier.ts
|
|
173
|
-
import {
|
|
173
|
+
import { createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
|
|
174
174
|
|
|
175
175
|
// src/auth/claims.ts
|
|
176
176
|
function readString(value) {
|
|
@@ -190,6 +190,7 @@ function stringClaim(claims, name) {
|
|
|
190
190
|
// src/auth/verifier.ts
|
|
191
191
|
var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
|
|
192
192
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
193
|
+
var JWKS_FETCH_TIMEOUT_MS = 5e3;
|
|
193
194
|
var OAuthTokenError = class extends Error {
|
|
194
195
|
constructor(status, oauthError, message) {
|
|
195
196
|
super(message);
|
|
@@ -209,12 +210,17 @@ function tokenHeaderFields(token) {
|
|
|
209
210
|
return { tokenLength: token.length };
|
|
210
211
|
}
|
|
211
212
|
}
|
|
212
|
-
function
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
213
|
+
async function fetchVerificationKeySet(jwksUri) {
|
|
214
|
+
try {
|
|
215
|
+
const response = await fetch(jwksUri, { signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS), redirect: "manual" });
|
|
216
|
+
if (!response.ok)
|
|
217
|
+
throw new Error(`JWKS endpoint returned ${response.status}`);
|
|
218
|
+
const json = await response.json();
|
|
219
|
+
return createLocalJWKSet(json);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
222
|
+
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
223
|
+
}
|
|
218
224
|
}
|
|
219
225
|
async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
220
226
|
try {
|
|
@@ -229,10 +235,6 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
|
229
235
|
});
|
|
230
236
|
return payload;
|
|
231
237
|
} catch (err) {
|
|
232
|
-
if (isJwksFetchFailure(err)) {
|
|
233
|
-
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
234
|
-
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
235
|
-
}
|
|
236
238
|
log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
|
|
237
239
|
throw err;
|
|
238
240
|
}
|
|
@@ -277,8 +279,8 @@ function createOAuthTokenVerifier(auth, discovery) {
|
|
|
277
279
|
const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
|
|
278
280
|
log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
|
|
279
281
|
const jwksUri = await discovery.resolveJwksUri();
|
|
280
|
-
log.debug("oauth.jwks.
|
|
281
|
-
const keySet =
|
|
282
|
+
log.debug("oauth.jwks.fetch", { jwksUri });
|
|
283
|
+
const keySet = await fetchVerificationKeySet(jwksUri);
|
|
282
284
|
const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
|
|
283
285
|
assertNonEmptySubject(claims);
|
|
284
286
|
const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
|
|
@@ -281,6 +281,7 @@ function stringClaim(claims, name) {
|
|
|
281
281
|
// src/auth/verifier.ts
|
|
282
282
|
var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
|
|
283
283
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
284
|
+
var JWKS_FETCH_TIMEOUT_MS = 5e3;
|
|
284
285
|
var OAuthTokenError = class extends Error {
|
|
285
286
|
constructor(status, oauthError, message) {
|
|
286
287
|
super(message);
|
|
@@ -300,12 +301,17 @@ function tokenHeaderFields(token) {
|
|
|
300
301
|
return { tokenLength: token.length };
|
|
301
302
|
}
|
|
302
303
|
}
|
|
303
|
-
function
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
304
|
+
async function fetchVerificationKeySet(jwksUri) {
|
|
305
|
+
try {
|
|
306
|
+
const response = await fetch(jwksUri, { signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS), redirect: "manual" });
|
|
307
|
+
if (!response.ok)
|
|
308
|
+
throw new Error(`JWKS endpoint returned ${response.status}`);
|
|
309
|
+
const json = await response.json();
|
|
310
|
+
return (0, import_jose.createLocalJWKSet)(json);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
313
|
+
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
314
|
+
}
|
|
309
315
|
}
|
|
310
316
|
async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
311
317
|
try {
|
|
@@ -320,10 +326,6 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
|
320
326
|
});
|
|
321
327
|
return payload;
|
|
322
328
|
} catch (err) {
|
|
323
|
-
if (isJwksFetchFailure(err)) {
|
|
324
|
-
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
325
|
-
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
326
|
-
}
|
|
327
329
|
log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
|
|
328
330
|
throw err;
|
|
329
331
|
}
|
|
@@ -368,8 +370,8 @@ function createOAuthTokenVerifier(auth, discovery) {
|
|
|
368
370
|
const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
|
|
369
371
|
log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
|
|
370
372
|
const jwksUri = await discovery.resolveJwksUri();
|
|
371
|
-
log.debug("oauth.jwks.
|
|
372
|
-
const keySet =
|
|
373
|
+
log.debug("oauth.jwks.fetch", { jwksUri });
|
|
374
|
+
const keySet = await fetchVerificationKeySet(jwksUri);
|
|
373
375
|
const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
|
|
374
376
|
assertNonEmptySubject(claims);
|
|
375
377
|
const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpProtocolHandler
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-3XPKR4G6.js";
|
|
4
4
|
import "../../chunk-MA5H6PSF.js";
|
|
5
|
-
import "../../chunk-
|
|
5
|
+
import "../../chunk-HRMLGCXV.js";
|
|
6
6
|
import "../../chunk-QC3DXQTH.js";
|
|
7
7
|
import "../../chunk-6DXGZZA4.js";
|
|
8
8
|
export {
|
|
@@ -283,6 +283,7 @@ function stringClaim(claims, name) {
|
|
|
283
283
|
// src/auth/verifier.ts
|
|
284
284
|
var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
|
|
285
285
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
286
|
+
var JWKS_FETCH_TIMEOUT_MS = 5e3;
|
|
286
287
|
var OAuthTokenError = class extends Error {
|
|
287
288
|
constructor(status, oauthError, message) {
|
|
288
289
|
super(message);
|
|
@@ -302,12 +303,17 @@ function tokenHeaderFields(token) {
|
|
|
302
303
|
return { tokenLength: token.length };
|
|
303
304
|
}
|
|
304
305
|
}
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
306
|
+
async function fetchVerificationKeySet(jwksUri) {
|
|
307
|
+
try {
|
|
308
|
+
const response = await fetch(jwksUri, { signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS), redirect: "manual" });
|
|
309
|
+
if (!response.ok)
|
|
310
|
+
throw new Error(`JWKS endpoint returned ${response.status}`);
|
|
311
|
+
const json = await response.json();
|
|
312
|
+
return (0, import_jose.createLocalJWKSet)(json);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
315
|
+
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
316
|
+
}
|
|
311
317
|
}
|
|
312
318
|
async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
313
319
|
try {
|
|
@@ -322,10 +328,6 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
|
322
328
|
});
|
|
323
329
|
return payload;
|
|
324
330
|
} catch (err) {
|
|
325
|
-
if (isJwksFetchFailure(err)) {
|
|
326
|
-
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
327
|
-
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
328
|
-
}
|
|
329
331
|
log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
|
|
330
332
|
throw err;
|
|
331
333
|
}
|
|
@@ -370,8 +372,8 @@ function createOAuthTokenVerifier(auth, discovery) {
|
|
|
370
372
|
const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
|
|
371
373
|
log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
|
|
372
374
|
const jwksUri = await discovery.resolveJwksUri();
|
|
373
|
-
log.debug("oauth.jwks.
|
|
374
|
-
const keySet =
|
|
375
|
+
log.debug("oauth.jwks.fetch", { jwksUri });
|
|
376
|
+
const keySet = await fetchVerificationKeySet(jwksUri);
|
|
375
377
|
const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
|
|
376
378
|
assertNonEmptySubject(claims);
|
|
377
379
|
const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
|
|
@@ -291,6 +291,7 @@ function stringClaim(claims, name) {
|
|
|
291
291
|
// src/auth/verifier.ts
|
|
292
292
|
var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
|
|
293
293
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
294
|
+
var JWKS_FETCH_TIMEOUT_MS = 5e3;
|
|
294
295
|
var OAuthTokenError = class extends Error {
|
|
295
296
|
constructor(status, oauthError, message) {
|
|
296
297
|
super(message);
|
|
@@ -310,12 +311,17 @@ function tokenHeaderFields(token) {
|
|
|
310
311
|
return { tokenLength: token.length };
|
|
311
312
|
}
|
|
312
313
|
}
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
314
|
+
async function fetchVerificationKeySet(jwksUri) {
|
|
315
|
+
try {
|
|
316
|
+
const response = await fetch(jwksUri, { signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS), redirect: "manual" });
|
|
317
|
+
if (!response.ok)
|
|
318
|
+
throw new Error(`JWKS endpoint returned ${response.status}`);
|
|
319
|
+
const json = await response.json();
|
|
320
|
+
return (0, import_jose.createLocalJWKSet)(json);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
323
|
+
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
324
|
+
}
|
|
319
325
|
}
|
|
320
326
|
async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
321
327
|
try {
|
|
@@ -330,10 +336,6 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
|
330
336
|
});
|
|
331
337
|
return payload;
|
|
332
338
|
} catch (err) {
|
|
333
|
-
if (isJwksFetchFailure(err)) {
|
|
334
|
-
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
335
|
-
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
336
|
-
}
|
|
337
339
|
log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
|
|
338
340
|
throw err;
|
|
339
341
|
}
|
|
@@ -378,8 +380,8 @@ function createOAuthTokenVerifier(auth, discovery) {
|
|
|
378
380
|
const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
|
|
379
381
|
log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
|
|
380
382
|
const jwksUri = await discovery.resolveJwksUri();
|
|
381
|
-
log.debug("oauth.jwks.
|
|
382
|
-
const keySet =
|
|
383
|
+
log.debug("oauth.jwks.fetch", { jwksUri });
|
|
384
|
+
const keySet = await fetchVerificationKeySet(jwksUri);
|
|
383
385
|
const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
|
|
384
386
|
assertNonEmptySubject(claims);
|
|
385
387
|
const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
|
|
@@ -548,6 +550,19 @@ function shapeToJsonSchema(shape) {
|
|
|
548
550
|
return null;
|
|
549
551
|
}
|
|
550
552
|
}
|
|
553
|
+
function buildMcpListing(mcp) {
|
|
554
|
+
return {
|
|
555
|
+
server: { name: mcp.name, version: mcp.version, title: mcp.title },
|
|
556
|
+
tools: mcp.tools.map((tool) => ({
|
|
557
|
+
name: tool.name,
|
|
558
|
+
title: tool.title,
|
|
559
|
+
description: tool.description,
|
|
560
|
+
annotations: tool.annotations,
|
|
561
|
+
inputSchema: shapeToJsonSchema(tool.inputSchema),
|
|
562
|
+
outputSchema: shapeToJsonSchema(tool.outputSchema)
|
|
563
|
+
}))
|
|
564
|
+
};
|
|
565
|
+
}
|
|
551
566
|
function createListToolsHandler(mcp, options = {}) {
|
|
552
567
|
assertRestResourceBinding(mcp, options);
|
|
553
568
|
const authorizer = createRequestAuthorizer(mcp, options);
|
|
@@ -557,18 +572,7 @@ function createListToolsHandler(mcp, options = {}) {
|
|
|
557
572
|
return authResult.response;
|
|
558
573
|
if (request.method !== "GET" && request.method !== "HEAD")
|
|
559
574
|
return methodNotAllowed("GET, HEAD, OPTIONS");
|
|
560
|
-
const
|
|
561
|
-
server: { name: mcp.name, version: mcp.version, title: mcp.title },
|
|
562
|
-
tools: mcp.tools.map((tool) => ({
|
|
563
|
-
name: tool.name,
|
|
564
|
-
title: tool.title,
|
|
565
|
-
description: tool.description,
|
|
566
|
-
annotations: tool.annotations,
|
|
567
|
-
inputSchema: shapeToJsonSchema(tool.inputSchema),
|
|
568
|
-
outputSchema: shapeToJsonSchema(tool.outputSchema)
|
|
569
|
-
}))
|
|
570
|
-
};
|
|
571
|
-
const response = Response.json(body);
|
|
575
|
+
const response = Response.json(buildMcpListing(mcp));
|
|
572
576
|
return request.method === "HEAD" ? headResponse(response) : response;
|
|
573
577
|
};
|
|
574
578
|
return async (request) => {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createInvokeToolHandler
|
|
3
|
-
|
|
4
|
-
} from "../../chunk-53M2X7FU.js";
|
|
2
|
+
createInvokeToolHandler
|
|
3
|
+
} from "../../chunk-4Y72OCPS.js";
|
|
5
4
|
import "../../chunk-MA5H6PSF.js";
|
|
6
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
createListToolsHandler
|
|
7
|
+
} from "../../chunk-E335BBVM.js";
|
|
8
|
+
import "../../chunk-HRMLGCXV.js";
|
|
7
9
|
import "../../chunk-QC3DXQTH.js";
|
|
8
10
|
import "../../chunk-6DXGZZA4.js";
|
|
9
11
|
export {
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/stacks/tanstack/manifest-io.ts
|
|
5
|
+
var import_node_crypto = require("crypto");
|
|
6
|
+
var import_node_fs = require("fs");
|
|
7
|
+
var import_node_path = require("path");
|
|
8
|
+
var import_vite2 = require("vite");
|
|
9
|
+
|
|
10
|
+
// package.json
|
|
11
|
+
var version = "0.8.0";
|
|
12
|
+
|
|
13
|
+
// src/protocols/rest/list-tools.ts
|
|
14
|
+
var import_zod_compat = require("@modelcontextprotocol/sdk/server/zod-compat.js");
|
|
15
|
+
var import_zod_json_schema_compat = require("@modelcontextprotocol/sdk/server/zod-json-schema-compat.js");
|
|
16
|
+
|
|
17
|
+
// src/core/logger.ts
|
|
18
|
+
var LEVEL_RANK = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 };
|
|
19
|
+
function isLogLevel(value) {
|
|
20
|
+
return typeof value === "string" && value in LEVEL_RANK;
|
|
21
|
+
}
|
|
22
|
+
function readEnvLevel() {
|
|
23
|
+
try {
|
|
24
|
+
const raw = typeof process !== "undefined" ? process.env?.["LOVABLE_MCP_LOG_LEVEL"] : void 0;
|
|
25
|
+
const normalized = raw?.trim().toLowerCase();
|
|
26
|
+
return isLogLevel(normalized) ? normalized : void 0;
|
|
27
|
+
} catch {
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
var currentLevel = readEnvLevel() ?? "silent";
|
|
32
|
+
|
|
33
|
+
// src/auth/verifier.ts
|
|
34
|
+
var import_jose = require("jose");
|
|
35
|
+
|
|
36
|
+
// src/protocols/rest/list-tools.ts
|
|
37
|
+
function shapeToJsonSchema(shape) {
|
|
38
|
+
if (!shape)
|
|
39
|
+
return null;
|
|
40
|
+
try {
|
|
41
|
+
return (0, import_zod_json_schema_compat.toJsonSchemaCompat)((0, import_zod_compat.objectFromShape)(shape));
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function buildMcpListing(mcp) {
|
|
47
|
+
return {
|
|
48
|
+
server: { name: mcp.name, version: mcp.version, title: mcp.title },
|
|
49
|
+
tools: mcp.tools.map((tool) => ({
|
|
50
|
+
name: tool.name,
|
|
51
|
+
title: tool.title,
|
|
52
|
+
description: tool.description,
|
|
53
|
+
annotations: tool.annotations,
|
|
54
|
+
inputSchema: shapeToJsonSchema(tool.inputSchema),
|
|
55
|
+
outputSchema: shapeToJsonSchema(tool.outputSchema)
|
|
56
|
+
}))
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/stacks/tanstack/extract-manifest.ts
|
|
61
|
+
var MANIFEST_VERSION = 1;
|
|
62
|
+
var MANIFEST_RELATIVE_PATH = ".lovable/mcp/manifest.json";
|
|
63
|
+
function manifestAuth(auth) {
|
|
64
|
+
if (!auth)
|
|
65
|
+
return { type: "none" };
|
|
66
|
+
const oauth = { type: "oauth", issuer: auth.issuer };
|
|
67
|
+
if (auth.resource !== void 0)
|
|
68
|
+
oauth.resource = auth.resource;
|
|
69
|
+
if (auth.resourceName !== void 0)
|
|
70
|
+
oauth.resource_name = auth.resourceName;
|
|
71
|
+
if (auth.resourceDocumentation !== void 0)
|
|
72
|
+
oauth.resource_documentation = auth.resourceDocumentation;
|
|
73
|
+
if (auth.protectedResourceMetadataUrl !== void 0)
|
|
74
|
+
oauth.protected_resource_metadata_url = auth.protectedResourceMetadataUrl;
|
|
75
|
+
if (auth.requireOAuthClientClaim !== void 0)
|
|
76
|
+
oauth.require_oauth_client_claim = auth.requireOAuthClientClaim;
|
|
77
|
+
if (auth.acceptedAudiences !== void 0)
|
|
78
|
+
oauth.accepted_audiences = auth.acceptedAudiences;
|
|
79
|
+
if (auth.requiredScopes !== void 0)
|
|
80
|
+
oauth.required_scopes = auth.requiredScopes;
|
|
81
|
+
if (auth.jwksUri !== void 0)
|
|
82
|
+
oauth.jwks_uri = auth.jwksUri;
|
|
83
|
+
if (auth.algorithms !== void 0)
|
|
84
|
+
oauth.algorithms = auth.algorithms;
|
|
85
|
+
if (auth.clockToleranceSeconds !== void 0)
|
|
86
|
+
oauth.clock_tolerance_seconds = auth.clockToleranceSeconds;
|
|
87
|
+
return oauth;
|
|
88
|
+
}
|
|
89
|
+
function isToolShape(tool) {
|
|
90
|
+
if (typeof tool !== "object" || tool === null)
|
|
91
|
+
return false;
|
|
92
|
+
const t = tool;
|
|
93
|
+
return typeof t.name === "string" && typeof t.title === "string" && typeof t.description === "string";
|
|
94
|
+
}
|
|
95
|
+
function isAuthShape(auth) {
|
|
96
|
+
if (auth === void 0)
|
|
97
|
+
return true;
|
|
98
|
+
if (typeof auth !== "object" || auth === null)
|
|
99
|
+
return false;
|
|
100
|
+
const a = auth;
|
|
101
|
+
return a.type === "oauth" && typeof a.issuer === "string" && a.issuer.length > 0;
|
|
102
|
+
}
|
|
103
|
+
function manifestFromDefinition(definition, urlPath, source = "the MCP entry") {
|
|
104
|
+
const def = definition;
|
|
105
|
+
const validShape = !!def && typeof def === "object" && typeof def.name === "string" && typeof def.title === "string" && typeof def.version === "string" && Array.isArray(def.tools) && def.tools.every(isToolShape) && isAuthShape(def.auth);
|
|
106
|
+
if (!validShape) {
|
|
107
|
+
const got = definition == null ? "no default export" : typeof definition;
|
|
108
|
+
throw new Error(`@lovable.dev/mcp-js: ${source} must \`export default defineMcp(...)\` (got ${got}).`);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
version: MANIFEST_VERSION,
|
|
112
|
+
sdk_version: version,
|
|
113
|
+
path: urlPath,
|
|
114
|
+
auth: manifestAuth(def.auth),
|
|
115
|
+
mcp: buildMcpListing(def)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/stacks/tanstack/fs-errors.ts
|
|
120
|
+
function isFileMissing(err) {
|
|
121
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/stacks/tanstack/ssr-loader.ts
|
|
125
|
+
var import_vite = require("vite");
|
|
126
|
+
async function produceViaSsr(projectRoot2, alias, entryAbs, urlPath) {
|
|
127
|
+
const server = await (0, import_vite.createServer)({
|
|
128
|
+
configFile: false,
|
|
129
|
+
root: projectRoot2,
|
|
130
|
+
logLevel: "silent",
|
|
131
|
+
server: { middlewareMode: true, hmr: false, watch: null },
|
|
132
|
+
optimizeDeps: { noDiscovery: true },
|
|
133
|
+
resolve: { alias }
|
|
134
|
+
});
|
|
135
|
+
try {
|
|
136
|
+
const mod = await server.ssrLoadModule(entryAbs);
|
|
137
|
+
return manifestFromDefinition(mod.default, urlPath, entryAbs);
|
|
138
|
+
} finally {
|
|
139
|
+
await server.close();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/stacks/tanstack/manifest-io.ts
|
|
144
|
+
var MCP_PLUGIN_NAME = "@lovable.dev/mcp-js";
|
|
145
|
+
var DEFAULT_MCP_ENTRY = "src/lib/mcp/index.ts";
|
|
146
|
+
var DEFAULT_MCP_PATH = "/mcp";
|
|
147
|
+
function writeManifestIfChanged(file, manifest) {
|
|
148
|
+
const content = JSON.stringify(manifest, null, 2) + "\n";
|
|
149
|
+
let existing;
|
|
150
|
+
try {
|
|
151
|
+
existing = (0, import_node_fs.readFileSync)(file, "utf8");
|
|
152
|
+
} catch (err) {
|
|
153
|
+
if (!isFileMissing(err))
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
if (existing === content)
|
|
157
|
+
return;
|
|
158
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(file), { recursive: true });
|
|
159
|
+
const tmp = `${file}.${(0, import_node_crypto.randomUUID)()}.tmp`;
|
|
160
|
+
try {
|
|
161
|
+
(0, import_node_fs.writeFileSync)(tmp, content, "utf8");
|
|
162
|
+
(0, import_node_fs.renameSync)(tmp, file);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
try {
|
|
165
|
+
(0, import_node_fs.unlinkSync)(tmp);
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function removeManifest(file) {
|
|
172
|
+
try {
|
|
173
|
+
(0, import_node_fs.unlinkSync)(file);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (!isFileMissing(err))
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function syncManifest(manifestFile, entryAbs, urlPath, produce) {
|
|
180
|
+
let entryExists = true;
|
|
181
|
+
try {
|
|
182
|
+
(0, import_node_fs.lstatSync)(entryAbs);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (!isFileMissing(err))
|
|
185
|
+
throw err;
|
|
186
|
+
entryExists = false;
|
|
187
|
+
}
|
|
188
|
+
if (!entryExists) {
|
|
189
|
+
removeManifest(manifestFile);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const manifest = await produce(entryAbs, urlPath);
|
|
193
|
+
writeManifestIfChanged(manifestFile, manifest);
|
|
194
|
+
}
|
|
195
|
+
async function runExtract(projectRoot2) {
|
|
196
|
+
const resolved = await (0, import_vite2.resolveConfig)({ root: projectRoot2, logLevel: "silent" }, "serve");
|
|
197
|
+
const alias = resolved.resolve?.alias ?? [];
|
|
198
|
+
const pluginApi = resolved.plugins.find((p) => p.name === MCP_PLUGIN_NAME)?.api;
|
|
199
|
+
const entryAbs = pluginApi?.mcpEntry ?? (0, import_node_path.resolve)(projectRoot2, DEFAULT_MCP_ENTRY);
|
|
200
|
+
const urlPath = pluginApi?.urlPath ?? DEFAULT_MCP_PATH;
|
|
201
|
+
await syncManifest(
|
|
202
|
+
(0, import_node_path.resolve)(projectRoot2, MANIFEST_RELATIVE_PATH),
|
|
203
|
+
entryAbs,
|
|
204
|
+
urlPath,
|
|
205
|
+
(entry, path) => produceViaSsr(projectRoot2, alias, entry, path)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/stacks/tanstack/cli/extract-manifest.ts
|
|
210
|
+
var projectRoot = process.argv[2] ?? process.cwd();
|
|
211
|
+
runExtract(projectRoot).catch((err) => {
|
|
212
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildMcpListing
|
|
4
|
+
} from "../../../chunk-E335BBVM.js";
|
|
5
|
+
import "../../../chunk-HRMLGCXV.js";
|
|
6
|
+
import "../../../chunk-QC3DXQTH.js";
|
|
7
|
+
import {
|
|
8
|
+
isFileMissing
|
|
9
|
+
} from "../../../chunk-QJ7XEKT3.js";
|
|
10
|
+
import "../../../chunk-6DXGZZA4.js";
|
|
11
|
+
|
|
12
|
+
// src/stacks/tanstack/manifest-io.ts
|
|
13
|
+
import { randomUUID } from "crypto";
|
|
14
|
+
import { lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
15
|
+
import { dirname, resolve } from "path";
|
|
16
|
+
import { resolveConfig } from "vite";
|
|
17
|
+
|
|
18
|
+
// package.json
|
|
19
|
+
var version = "0.8.0";
|
|
20
|
+
|
|
21
|
+
// src/stacks/tanstack/extract-manifest.ts
|
|
22
|
+
var MANIFEST_VERSION = 1;
|
|
23
|
+
var MANIFEST_RELATIVE_PATH = ".lovable/mcp/manifest.json";
|
|
24
|
+
function manifestAuth(auth) {
|
|
25
|
+
if (!auth)
|
|
26
|
+
return { type: "none" };
|
|
27
|
+
const oauth = { type: "oauth", issuer: auth.issuer };
|
|
28
|
+
if (auth.resource !== void 0)
|
|
29
|
+
oauth.resource = auth.resource;
|
|
30
|
+
if (auth.resourceName !== void 0)
|
|
31
|
+
oauth.resource_name = auth.resourceName;
|
|
32
|
+
if (auth.resourceDocumentation !== void 0)
|
|
33
|
+
oauth.resource_documentation = auth.resourceDocumentation;
|
|
34
|
+
if (auth.protectedResourceMetadataUrl !== void 0)
|
|
35
|
+
oauth.protected_resource_metadata_url = auth.protectedResourceMetadataUrl;
|
|
36
|
+
if (auth.requireOAuthClientClaim !== void 0)
|
|
37
|
+
oauth.require_oauth_client_claim = auth.requireOAuthClientClaim;
|
|
38
|
+
if (auth.acceptedAudiences !== void 0)
|
|
39
|
+
oauth.accepted_audiences = auth.acceptedAudiences;
|
|
40
|
+
if (auth.requiredScopes !== void 0)
|
|
41
|
+
oauth.required_scopes = auth.requiredScopes;
|
|
42
|
+
if (auth.jwksUri !== void 0)
|
|
43
|
+
oauth.jwks_uri = auth.jwksUri;
|
|
44
|
+
if (auth.algorithms !== void 0)
|
|
45
|
+
oauth.algorithms = auth.algorithms;
|
|
46
|
+
if (auth.clockToleranceSeconds !== void 0)
|
|
47
|
+
oauth.clock_tolerance_seconds = auth.clockToleranceSeconds;
|
|
48
|
+
return oauth;
|
|
49
|
+
}
|
|
50
|
+
function isToolShape(tool) {
|
|
51
|
+
if (typeof tool !== "object" || tool === null)
|
|
52
|
+
return false;
|
|
53
|
+
const t = tool;
|
|
54
|
+
return typeof t.name === "string" && typeof t.title === "string" && typeof t.description === "string";
|
|
55
|
+
}
|
|
56
|
+
function isAuthShape(auth) {
|
|
57
|
+
if (auth === void 0)
|
|
58
|
+
return true;
|
|
59
|
+
if (typeof auth !== "object" || auth === null)
|
|
60
|
+
return false;
|
|
61
|
+
const a = auth;
|
|
62
|
+
return a.type === "oauth" && typeof a.issuer === "string" && a.issuer.length > 0;
|
|
63
|
+
}
|
|
64
|
+
function manifestFromDefinition(definition, urlPath, source = "the MCP entry") {
|
|
65
|
+
const def = definition;
|
|
66
|
+
const validShape = !!def && typeof def === "object" && typeof def.name === "string" && typeof def.title === "string" && typeof def.version === "string" && Array.isArray(def.tools) && def.tools.every(isToolShape) && isAuthShape(def.auth);
|
|
67
|
+
if (!validShape) {
|
|
68
|
+
const got = definition == null ? "no default export" : typeof definition;
|
|
69
|
+
throw new Error(`@lovable.dev/mcp-js: ${source} must \`export default defineMcp(...)\` (got ${got}).`);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
version: MANIFEST_VERSION,
|
|
73
|
+
sdk_version: version,
|
|
74
|
+
path: urlPath,
|
|
75
|
+
auth: manifestAuth(def.auth),
|
|
76
|
+
mcp: buildMcpListing(def)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/stacks/tanstack/ssr-loader.ts
|
|
81
|
+
import { createServer } from "vite";
|
|
82
|
+
async function produceViaSsr(projectRoot2, alias, entryAbs, urlPath) {
|
|
83
|
+
const server = await createServer({
|
|
84
|
+
configFile: false,
|
|
85
|
+
root: projectRoot2,
|
|
86
|
+
logLevel: "silent",
|
|
87
|
+
server: { middlewareMode: true, hmr: false, watch: null },
|
|
88
|
+
optimizeDeps: { noDiscovery: true },
|
|
89
|
+
resolve: { alias }
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
const mod = await server.ssrLoadModule(entryAbs);
|
|
93
|
+
return manifestFromDefinition(mod.default, urlPath, entryAbs);
|
|
94
|
+
} finally {
|
|
95
|
+
await server.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/stacks/tanstack/manifest-io.ts
|
|
100
|
+
var MCP_PLUGIN_NAME = "@lovable.dev/mcp-js";
|
|
101
|
+
var DEFAULT_MCP_ENTRY = "src/lib/mcp/index.ts";
|
|
102
|
+
var DEFAULT_MCP_PATH = "/mcp";
|
|
103
|
+
function writeManifestIfChanged(file, manifest) {
|
|
104
|
+
const content = JSON.stringify(manifest, null, 2) + "\n";
|
|
105
|
+
let existing;
|
|
106
|
+
try {
|
|
107
|
+
existing = readFileSync(file, "utf8");
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (!isFileMissing(err))
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
if (existing === content)
|
|
113
|
+
return;
|
|
114
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
115
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
116
|
+
try {
|
|
117
|
+
writeFileSync(tmp, content, "utf8");
|
|
118
|
+
renameSync(tmp, file);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
try {
|
|
121
|
+
unlinkSync(tmp);
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function removeManifest(file) {
|
|
128
|
+
try {
|
|
129
|
+
unlinkSync(file);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (!isFileMissing(err))
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function syncManifest(manifestFile, entryAbs, urlPath, produce) {
|
|
136
|
+
let entryExists = true;
|
|
137
|
+
try {
|
|
138
|
+
lstatSync(entryAbs);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (!isFileMissing(err))
|
|
141
|
+
throw err;
|
|
142
|
+
entryExists = false;
|
|
143
|
+
}
|
|
144
|
+
if (!entryExists) {
|
|
145
|
+
removeManifest(manifestFile);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const manifest = await produce(entryAbs, urlPath);
|
|
149
|
+
writeManifestIfChanged(manifestFile, manifest);
|
|
150
|
+
}
|
|
151
|
+
async function runExtract(projectRoot2) {
|
|
152
|
+
const resolved = await resolveConfig({ root: projectRoot2, logLevel: "silent" }, "serve");
|
|
153
|
+
const alias = resolved.resolve?.alias ?? [];
|
|
154
|
+
const pluginApi = resolved.plugins.find((p) => p.name === MCP_PLUGIN_NAME)?.api;
|
|
155
|
+
const entryAbs = pluginApi?.mcpEntry ?? resolve(projectRoot2, DEFAULT_MCP_ENTRY);
|
|
156
|
+
const urlPath = pluginApi?.urlPath ?? DEFAULT_MCP_PATH;
|
|
157
|
+
await syncManifest(
|
|
158
|
+
resolve(projectRoot2, MANIFEST_RELATIVE_PATH),
|
|
159
|
+
entryAbs,
|
|
160
|
+
urlPath,
|
|
161
|
+
(entry, path) => produceViaSsr(projectRoot2, alias, entry, path)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/stacks/tanstack/cli/extract-manifest.ts
|
|
166
|
+
var projectRoot = process.argv[2] ?? process.cwd();
|
|
167
|
+
runExtract(projectRoot).catch((err) => {
|
|
168
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
169
|
+
process.exit(1);
|
|
170
|
+
});
|
|
@@ -293,6 +293,7 @@ function stringClaim(claims, name) {
|
|
|
293
293
|
// src/auth/verifier.ts
|
|
294
294
|
var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
|
|
295
295
|
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
296
|
+
var JWKS_FETCH_TIMEOUT_MS = 5e3;
|
|
296
297
|
var OAuthTokenError = class extends Error {
|
|
297
298
|
constructor(status, oauthError, message) {
|
|
298
299
|
super(message);
|
|
@@ -312,12 +313,17 @@ function tokenHeaderFields(token) {
|
|
|
312
313
|
return { tokenLength: token.length };
|
|
313
314
|
}
|
|
314
315
|
}
|
|
315
|
-
function
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
316
|
+
async function fetchVerificationKeySet(jwksUri) {
|
|
317
|
+
try {
|
|
318
|
+
const response = await fetch(jwksUri, { signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS), redirect: "manual" });
|
|
319
|
+
if (!response.ok)
|
|
320
|
+
throw new Error(`JWKS endpoint returned ${response.status}`);
|
|
321
|
+
const json = await response.json();
|
|
322
|
+
return (0, import_jose.createLocalJWKSet)(json);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
325
|
+
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
326
|
+
}
|
|
321
327
|
}
|
|
322
328
|
async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
323
329
|
try {
|
|
@@ -332,10 +338,6 @@ async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
|
|
|
332
338
|
});
|
|
333
339
|
return payload;
|
|
334
340
|
} catch (err) {
|
|
335
|
-
if (isJwksFetchFailure(err)) {
|
|
336
|
-
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
337
|
-
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
338
|
-
}
|
|
339
341
|
log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
|
|
340
342
|
throw err;
|
|
341
343
|
}
|
|
@@ -380,8 +382,8 @@ function createOAuthTokenVerifier(auth, discovery) {
|
|
|
380
382
|
const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
|
|
381
383
|
log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
|
|
382
384
|
const jwksUri = await discovery.resolveJwksUri();
|
|
383
|
-
log.debug("oauth.jwks.
|
|
384
|
-
const keySet =
|
|
385
|
+
log.debug("oauth.jwks.fetch", { jwksUri });
|
|
386
|
+
const keySet = await fetchVerificationKeySet(jwksUri);
|
|
385
387
|
const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
|
|
386
388
|
assertNonEmptySubject(claims);
|
|
387
389
|
const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
|
|
@@ -716,6 +718,19 @@ function shapeToJsonSchema(shape) {
|
|
|
716
718
|
return null;
|
|
717
719
|
}
|
|
718
720
|
}
|
|
721
|
+
function buildMcpListing(mcp) {
|
|
722
|
+
return {
|
|
723
|
+
server: { name: mcp.name, version: mcp.version, title: mcp.title },
|
|
724
|
+
tools: mcp.tools.map((tool) => ({
|
|
725
|
+
name: tool.name,
|
|
726
|
+
title: tool.title,
|
|
727
|
+
description: tool.description,
|
|
728
|
+
annotations: tool.annotations,
|
|
729
|
+
inputSchema: shapeToJsonSchema(tool.inputSchema),
|
|
730
|
+
outputSchema: shapeToJsonSchema(tool.outputSchema)
|
|
731
|
+
}))
|
|
732
|
+
};
|
|
733
|
+
}
|
|
719
734
|
function createListToolsHandler(mcp, options = {}) {
|
|
720
735
|
assertRestResourceBinding(mcp, options);
|
|
721
736
|
const authorizer = createRequestAuthorizer(mcp, options);
|
|
@@ -725,18 +740,7 @@ function createListToolsHandler(mcp, options = {}) {
|
|
|
725
740
|
return authResult.response;
|
|
726
741
|
if (request.method !== "GET" && request.method !== "HEAD")
|
|
727
742
|
return methodNotAllowed("GET, HEAD, OPTIONS");
|
|
728
|
-
const
|
|
729
|
-
server: { name: mcp.name, version: mcp.version, title: mcp.title },
|
|
730
|
-
tools: mcp.tools.map((tool) => ({
|
|
731
|
-
name: tool.name,
|
|
732
|
-
title: tool.title,
|
|
733
|
-
description: tool.description,
|
|
734
|
-
annotations: tool.annotations,
|
|
735
|
-
inputSchema: shapeToJsonSchema(tool.inputSchema),
|
|
736
|
-
outputSchema: shapeToJsonSchema(tool.outputSchema)
|
|
737
|
-
}))
|
|
738
|
-
};
|
|
739
|
-
const response = Response.json(body);
|
|
743
|
+
const response = Response.json(buildMcpListing(mcp));
|
|
740
744
|
return request.method === "HEAD" ? headResponse(response) : response;
|
|
741
745
|
};
|
|
742
746
|
return async (request) => {
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpProtocolHandler
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-3XPKR4G6.js";
|
|
4
4
|
import {
|
|
5
5
|
createOAuthProtectedResourceMetadataHandler
|
|
6
|
-
} from "../../chunk-
|
|
6
|
+
} from "../../chunk-W7F6JRDB.js";
|
|
7
7
|
import {
|
|
8
|
-
createInvokeToolHandler
|
|
9
|
-
|
|
10
|
-
} from "../../chunk-53M2X7FU.js";
|
|
8
|
+
createInvokeToolHandler
|
|
9
|
+
} from "../../chunk-4Y72OCPS.js";
|
|
11
10
|
import "../../chunk-MA5H6PSF.js";
|
|
12
|
-
import
|
|
11
|
+
import {
|
|
12
|
+
createListToolsHandler
|
|
13
|
+
} from "../../chunk-E335BBVM.js";
|
|
14
|
+
import "../../chunk-HRMLGCXV.js";
|
|
13
15
|
import "../../chunk-QC3DXQTH.js";
|
|
14
16
|
import "../../chunk-6DXGZZA4.js";
|
|
15
17
|
|
|
@@ -32,11 +32,13 @@ var import_node_path = require("path");
|
|
|
32
32
|
// src/auth/metadata-path.ts
|
|
33
33
|
var OAUTH_PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
34
34
|
|
|
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.";
|
|
35
|
+
// src/stacks/tanstack/fs-errors.ts
|
|
37
36
|
function isFileMissing(err) {
|
|
38
37
|
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
39
38
|
}
|
|
39
|
+
|
|
40
|
+
// src/stacks/tanstack/vite.ts
|
|
41
|
+
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
42
|
function normalizePath(p) {
|
|
41
43
|
return p.split(import_node_path.sep).join("/");
|
|
42
44
|
}
|
|
@@ -251,6 +253,18 @@ function mcpPlugin(options = {}) {
|
|
|
251
253
|
};
|
|
252
254
|
return {
|
|
253
255
|
name: "@lovable.dev/mcp-js",
|
|
256
|
+
// The extract CLI reads the resolved entry + URL path back through this
|
|
257
|
+
// (via `resolveConfig().plugins`) so the manifest honors custom
|
|
258
|
+
// `mcpEntry`/`path` instead of assuming the defaults. Getters because
|
|
259
|
+
// `mcpEntry` is finalized in `configResolved`.
|
|
260
|
+
api: {
|
|
261
|
+
get mcpEntry() {
|
|
262
|
+
return mcpEntry;
|
|
263
|
+
},
|
|
264
|
+
get urlPath() {
|
|
265
|
+
return urlPath;
|
|
266
|
+
}
|
|
267
|
+
},
|
|
254
268
|
configResolved(config) {
|
|
255
269
|
projectRoot = config.root;
|
|
256
270
|
mcpEntry = (0, import_node_path.resolve)(projectRoot, mcpEntryOption);
|
|
@@ -262,19 +276,18 @@ function mcpPlugin(options = {}) {
|
|
|
262
276
|
regenerate();
|
|
263
277
|
},
|
|
264
278
|
configureServer(server) {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
if (normalizePath(file) === normalizePath(watchedEntry)) {
|
|
279
|
+
const onChange = (file) => {
|
|
280
|
+
if (normalizePath(file) === normalizePath(mcpEntry)) {
|
|
268
281
|
regenerate();
|
|
269
282
|
}
|
|
270
283
|
};
|
|
271
|
-
server.watcher.on("add",
|
|
272
|
-
server.watcher.on("change",
|
|
273
|
-
server.watcher.on("unlink",
|
|
284
|
+
server.watcher.on("add", onChange);
|
|
285
|
+
server.watcher.on("change", onChange);
|
|
286
|
+
server.watcher.on("unlink", onChange);
|
|
274
287
|
server.watcher.once("close", () => {
|
|
275
|
-
server.watcher.off("add",
|
|
276
|
-
server.watcher.off("change",
|
|
277
|
-
server.watcher.off("unlink",
|
|
288
|
+
server.watcher.off("add", onChange);
|
|
289
|
+
server.watcher.off("change", onChange);
|
|
290
|
+
server.watcher.off("unlink", onChange);
|
|
278
291
|
});
|
|
279
292
|
},
|
|
280
293
|
buildStart() {
|
|
@@ -49,6 +49,10 @@ interface McpPluginOptions {
|
|
|
49
49
|
*/
|
|
50
50
|
protectedResourceMetadataRoute?: boolean;
|
|
51
51
|
}
|
|
52
|
+
interface McpPluginApi {
|
|
53
|
+
readonly mcpEntry: string;
|
|
54
|
+
readonly urlPath: string;
|
|
55
|
+
}
|
|
52
56
|
declare function assertUrlPathShape(urlPath: string): string;
|
|
53
57
|
declare function deriveRouteFileName(urlPath: string): string;
|
|
54
58
|
/**
|
|
@@ -57,8 +61,12 @@ declare function deriveRouteFileName(urlPath: string): string;
|
|
|
57
61
|
* Place this in your `vite.config.ts` plugins array. Emits during
|
|
58
62
|
* `configResolved` (so dev-server boot and the TanStack file-router both
|
|
59
63
|
* see the routes on the first request) and re-emits when the MCP entry
|
|
60
|
-
* file
|
|
64
|
+
* file appears or disappears during dev.
|
|
65
|
+
*
|
|
66
|
+
* The `.lovable/mcp/manifest.json` snapshot is produced separately by the
|
|
67
|
+
* `lovable-mcp-extract-manifest` CLI (run by a host platform at build/commit
|
|
68
|
+
* time) — not by this plugin.
|
|
61
69
|
*/
|
|
62
70
|
declare function mcpPlugin(options?: McpPluginOptions): Plugin;
|
|
63
71
|
|
|
64
|
-
export { type McpPluginOptions, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
|
|
72
|
+
export { type McpPluginApi, type McpPluginOptions, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
|
|
@@ -49,6 +49,10 @@ interface McpPluginOptions {
|
|
|
49
49
|
*/
|
|
50
50
|
protectedResourceMetadataRoute?: boolean;
|
|
51
51
|
}
|
|
52
|
+
interface McpPluginApi {
|
|
53
|
+
readonly mcpEntry: string;
|
|
54
|
+
readonly urlPath: string;
|
|
55
|
+
}
|
|
52
56
|
declare function assertUrlPathShape(urlPath: string): string;
|
|
53
57
|
declare function deriveRouteFileName(urlPath: string): string;
|
|
54
58
|
/**
|
|
@@ -57,8 +61,12 @@ declare function deriveRouteFileName(urlPath: string): string;
|
|
|
57
61
|
* Place this in your `vite.config.ts` plugins array. Emits during
|
|
58
62
|
* `configResolved` (so dev-server boot and the TanStack file-router both
|
|
59
63
|
* see the routes on the first request) and re-emits when the MCP entry
|
|
60
|
-
* file
|
|
64
|
+
* file appears or disappears during dev.
|
|
65
|
+
*
|
|
66
|
+
* The `.lovable/mcp/manifest.json` snapshot is produced separately by the
|
|
67
|
+
* `lovable-mcp-extract-manifest` CLI (run by a host platform at build/commit
|
|
68
|
+
* time) — not by this plugin.
|
|
61
69
|
*/
|
|
62
70
|
declare function mcpPlugin(options?: McpPluginOptions): Plugin;
|
|
63
71
|
|
|
64
|
-
export { type McpPluginOptions, assertUrlPathShape, mcpPlugin as default, deriveRouteFileName, mcpPlugin };
|
|
72
|
+
export { type McpPluginApi, type McpPluginOptions, 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
|
}
|
|
@@ -223,6 +223,18 @@ function mcpPlugin(options = {}) {
|
|
|
223
223
|
};
|
|
224
224
|
return {
|
|
225
225
|
name: "@lovable.dev/mcp-js",
|
|
226
|
+
// The extract CLI reads the resolved entry + URL path back through this
|
|
227
|
+
// (via `resolveConfig().plugins`) so the manifest honors custom
|
|
228
|
+
// `mcpEntry`/`path` instead of assuming the defaults. Getters because
|
|
229
|
+
// `mcpEntry` is finalized in `configResolved`.
|
|
230
|
+
api: {
|
|
231
|
+
get mcpEntry() {
|
|
232
|
+
return mcpEntry;
|
|
233
|
+
},
|
|
234
|
+
get urlPath() {
|
|
235
|
+
return urlPath;
|
|
236
|
+
}
|
|
237
|
+
},
|
|
226
238
|
configResolved(config) {
|
|
227
239
|
projectRoot = config.root;
|
|
228
240
|
mcpEntry = resolve(projectRoot, mcpEntryOption);
|
|
@@ -234,19 +246,18 @@ function mcpPlugin(options = {}) {
|
|
|
234
246
|
regenerate();
|
|
235
247
|
},
|
|
236
248
|
configureServer(server) {
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
if (normalizePath(file) === normalizePath(watchedEntry)) {
|
|
249
|
+
const onChange = (file) => {
|
|
250
|
+
if (normalizePath(file) === normalizePath(mcpEntry)) {
|
|
240
251
|
regenerate();
|
|
241
252
|
}
|
|
242
253
|
};
|
|
243
|
-
server.watcher.on("add",
|
|
244
|
-
server.watcher.on("change",
|
|
245
|
-
server.watcher.on("unlink",
|
|
254
|
+
server.watcher.on("add", onChange);
|
|
255
|
+
server.watcher.on("change", onChange);
|
|
256
|
+
server.watcher.on("unlink", onChange);
|
|
246
257
|
server.watcher.once("close", () => {
|
|
247
|
-
server.watcher.off("add",
|
|
248
|
-
server.watcher.off("change",
|
|
249
|
-
server.watcher.off("unlink",
|
|
258
|
+
server.watcher.off("add", onChange);
|
|
259
|
+
server.watcher.off("change", onChange);
|
|
260
|
+
server.watcher.off("unlink", onChange);
|
|
250
261
|
});
|
|
251
262
|
},
|
|
252
263
|
buildStart() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovable.dev/mcp-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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": {
|
|
@@ -50,6 +50,9 @@
|
|
|
50
50
|
"require": "./dist/stacks/tanstack/vite.cjs"
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
|
+
"bin": {
|
|
54
|
+
"lovable-mcp-extract-manifest": "./dist/stacks/tanstack/cli/extract-manifest.cjs"
|
|
55
|
+
},
|
|
53
56
|
"files": [
|
|
54
57
|
"dist"
|
|
55
58
|
],
|
|
@@ -79,8 +82,8 @@
|
|
|
79
82
|
"zod": "^4.1.13"
|
|
80
83
|
},
|
|
81
84
|
"scripts": {
|
|
82
|
-
"build": "tsup src/index.ts src/protocols/mcp/index.ts src/protocols/oauth-metadata.ts src/protocols/rest/index.ts src/stacks/tanstack/index.ts src/stacks/tanstack/vite.ts --format cjs,esm --dts --outDir dist",
|
|
83
|
-
"dev": "tsup src/index.ts src/protocols/mcp/index.ts src/protocols/oauth-metadata.ts src/protocols/rest/index.ts src/stacks/tanstack/index.ts src/stacks/tanstack/vite.ts --format cjs,esm --dts --watch",
|
|
85
|
+
"build": "tsup src/index.ts src/protocols/mcp/index.ts src/protocols/oauth-metadata.ts src/protocols/rest/index.ts src/stacks/tanstack/index.ts src/stacks/tanstack/vite.ts src/stacks/tanstack/cli/extract-manifest.ts --format cjs,esm --dts --outDir dist",
|
|
86
|
+
"dev": "tsup src/index.ts src/protocols/mcp/index.ts src/protocols/oauth-metadata.ts src/protocols/rest/index.ts src/stacks/tanstack/index.ts src/stacks/tanstack/vite.ts src/stacks/tanstack/cli/extract-manifest.ts --format cjs,esm --dts --watch",
|
|
84
87
|
"typecheck": "tsgo --noEmit",
|
|
85
88
|
"format": "oxfmt --write src/ tests/",
|
|
86
89
|
"format:check": "oxfmt --check src/ tests/",
|