@lovable.dev/mcp-js 0.7.1 → 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.
@@ -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
11
  } from "./chunk-HRMLGCXV.js";
13
12
 
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
- }
55
-
56
13
  // src/protocols/rest/invoke-tool.ts
57
- import { getParseErrorMessage, objectFromShape as objectFromShape2, safeParseAsync } from "@modelcontextprotocol/sdk/server/zod-compat.js";
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 = objectFromShape2(tool.inputSchema);
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
+ };
@@ -0,0 +1,8 @@
1
+ // src/stacks/tanstack/fs-errors.ts
2
+ function isFileMissing(err) {
3
+ return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
4
+ }
5
+
6
+ export {
7
+ isFileMissing
8
+ };
@@ -550,6 +550,19 @@ function shapeToJsonSchema(shape) {
550
550
  return null;
551
551
  }
552
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
+ }
553
566
  function createListToolsHandler(mcp, options = {}) {
554
567
  assertRestResourceBinding(mcp, options);
555
568
  const authorizer = createRequestAuthorizer(mcp, options);
@@ -559,18 +572,7 @@ function createListToolsHandler(mcp, options = {}) {
559
572
  return authResult.response;
560
573
  if (request.method !== "GET" && request.method !== "HEAD")
561
574
  return methodNotAllowed("GET, HEAD, OPTIONS");
562
- const body = {
563
- server: { name: mcp.name, version: mcp.version, title: mcp.title },
564
- tools: mcp.tools.map((tool) => ({
565
- name: tool.name,
566
- title: tool.title,
567
- description: tool.description,
568
- annotations: tool.annotations,
569
- inputSchema: shapeToJsonSchema(tool.inputSchema),
570
- outputSchema: shapeToJsonSchema(tool.outputSchema)
571
- }))
572
- };
573
- const response = Response.json(body);
575
+ const response = Response.json(buildMcpListing(mcp));
574
576
  return request.method === "HEAD" ? headResponse(response) : response;
575
577
  };
576
578
  return async (request) => {
@@ -1,8 +1,10 @@
1
1
  import {
2
- createInvokeToolHandler,
3
- createListToolsHandler
4
- } from "../../chunk-LMT6RXUF.js";
2
+ createInvokeToolHandler
3
+ } from "../../chunk-4Y72OCPS.js";
5
4
  import "../../chunk-MA5H6PSF.js";
5
+ import {
6
+ createListToolsHandler
7
+ } from "../../chunk-E335BBVM.js";
6
8
  import "../../chunk-HRMLGCXV.js";
7
9
  import "../../chunk-QC3DXQTH.js";
8
10
  import "../../chunk-6DXGZZA4.js";
@@ -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
+ });
@@ -718,6 +718,19 @@ function shapeToJsonSchema(shape) {
718
718
  return null;
719
719
  }
720
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
+ }
721
734
  function createListToolsHandler(mcp, options = {}) {
722
735
  assertRestResourceBinding(mcp, options);
723
736
  const authorizer = createRequestAuthorizer(mcp, options);
@@ -727,18 +740,7 @@ function createListToolsHandler(mcp, options = {}) {
727
740
  return authResult.response;
728
741
  if (request.method !== "GET" && request.method !== "HEAD")
729
742
  return methodNotAllowed("GET, HEAD, OPTIONS");
730
- const body = {
731
- server: { name: mcp.name, version: mcp.version, title: mcp.title },
732
- tools: mcp.tools.map((tool) => ({
733
- name: tool.name,
734
- title: tool.title,
735
- description: tool.description,
736
- annotations: tool.annotations,
737
- inputSchema: shapeToJsonSchema(tool.inputSchema),
738
- outputSchema: shapeToJsonSchema(tool.outputSchema)
739
- }))
740
- };
741
- const response = Response.json(body);
743
+ const response = Response.json(buildMcpListing(mcp));
742
744
  return request.method === "HEAD" ? headResponse(response) : response;
743
745
  };
744
746
  return async (request) => {
@@ -5,10 +5,12 @@ import {
5
5
  createOAuthProtectedResourceMetadataHandler
6
6
  } from "../../chunk-W7F6JRDB.js";
7
7
  import {
8
- createInvokeToolHandler,
9
- createListToolsHandler
10
- } from "../../chunk-LMT6RXUF.js";
8
+ createInvokeToolHandler
9
+ } from "../../chunk-4Y72OCPS.js";
11
10
  import "../../chunk-MA5H6PSF.js";
11
+ import {
12
+ createListToolsHandler
13
+ } from "../../chunk-E335BBVM.js";
12
14
  import "../../chunk-HRMLGCXV.js";
13
15
  import "../../chunk-QC3DXQTH.js";
14
16
  import "../../chunk-6DXGZZA4.js";
@@ -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/vite.ts
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 watchedEntry = mcpEntry;
266
- const onEntryChange = (file) => {
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", onEntryChange);
272
- server.watcher.on("change", onEntryChange);
273
- server.watcher.on("unlink", onEntryChange);
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", onEntryChange);
276
- server.watcher.off("change", onEntryChange);
277
- server.watcher.off("unlink", onEntryChange);
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 changes during dev.
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 changes during dev.
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 watchedEntry = mcpEntry;
238
- const onEntryChange = (file) => {
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", onEntryChange);
244
- server.watcher.on("change", onEntryChange);
245
- server.watcher.on("unlink", onEntryChange);
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", onEntryChange);
248
- server.watcher.off("change", onEntryChange);
249
- server.watcher.off("unlink", onEntryChange);
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.7.1",
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/",