@lovable.dev/mcp-js 0.11.0 → 0.12.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 +1 -1
- package/README.md +3 -3
- package/dist/{chunk-ENWRS6AT.js → chunk-S3YHTZKJ.js} +1 -1
- package/dist/cli/extract-manifest.cjs +1 -1
- package/dist/cli/extract-manifest.js +1 -1
- package/dist/stacks/supabase/vite.cjs +80 -49
- package/dist/stacks/supabase/vite.d.cts +7 -4
- package/dist/stacks/supabase/vite.d.ts +7 -4
- package/dist/stacks/supabase/vite.js +80 -49
- package/package.json +2 -1
package/README.internal.md
CHANGED
|
@@ -145,7 +145,7 @@ src/
|
|
|
145
145
|
index.ts # barrel
|
|
146
146
|
supabase/
|
|
147
147
|
handler.ts # createSupabaseHandler (URL-suffix dispatcher to protocols/*)
|
|
148
|
-
emit.ts # syncSupabaseFunction (
|
|
148
|
+
emit.ts # syncSupabaseFunction (esbuild-bundles supabase/functions/<name>/index.ts)
|
|
149
149
|
vite.ts # mcpPlugin (Vite-lifecycle trigger; exposes McpPluginApi with urlPath=/functions/v1/<functionName>)
|
|
150
150
|
index.ts # barrel
|
|
151
151
|
tests/
|
package/README.md
CHANGED
|
@@ -217,7 +217,7 @@ A `500` on an OAuth-protected route is always one of two causes, logged at `erro
|
|
|
217
217
|
|
|
218
218
|
## Supabase Edge Functions
|
|
219
219
|
|
|
220
|
-
Ship the same `defineMcp` server as a single Supabase Edge Function. Authoring is identical to the TanStack flow; only the build-time step differs — a different Vite plugin
|
|
220
|
+
Ship the same `defineMcp` server as a single Supabase Edge Function. Authoring is identical to the TanStack flow; only the build-time step differs — a different Vite plugin bundles your MCP entry and its local imports into one self-contained Deno function (with esbuild) instead of emitting a tree of file-router routes. Third-party dependencies are left as `npm:` specifiers that Deno resolves at runtime, so no import map / `deno.json` is needed.
|
|
221
221
|
|
|
222
222
|
```ts
|
|
223
223
|
// src/lib/mcp/index.ts — same as the TanStack flow
|
|
@@ -249,7 +249,7 @@ import { mcpPlugin } from "@lovable.dev/mcp-js/stacks/supabase/vite";
|
|
|
249
249
|
export default defineConfig({ plugins: [mcpPlugin()] });
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
-
The plugin
|
|
252
|
+
The plugin bundles `src/lib/mcp/index.ts` (and the tool files it imports) into `supabase/functions/mcp/index.ts` on `configResolved`, re-emits on `buildStart`, and watches the bundled source files for changes during `vite dev`. The emitted function calls `Deno.serve(createSupabaseHandler(mcp, { functionName: "mcp" }))`. The handler is one Web-Standard `Request → Response` that dispatches by URL suffix to the same protocol handlers the TanStack stack uses:
|
|
253
253
|
|
|
254
254
|
| URL suffix | Target |
|
|
255
255
|
| --- | --- |
|
|
@@ -260,7 +260,7 @@ The plugin emits `supabase/functions/mcp/index.ts` and `supabase/functions/mcp/d
|
|
|
260
260
|
|
|
261
261
|
In production the function is mounted at `https://<project-ref>.supabase.co/functions/v1/mcp` and the dispatcher binds the OAuth `resource` to that URL via `functionName`. Locally, run `supabase functions serve mcp --no-verify-jwt` so Supabase's edge-runtime doesn't 401 before the SDK's OAuth verifier sees the bearer.
|
|
262
262
|
|
|
263
|
-
Ownership:
|
|
263
|
+
Ownership: the emitted `index.ts` carries a `// AUTO-GENERATED by @lovable.dev/mcp-js` banner. The plugin rewrites the banner-tagged file in place and cleans up orphans (including a `deno.json` left by a pre-bundling SDK version); banner-less files (yours) are never touched. Delete the banner line to take ownership. Deleting the `defineMcp` entry and re-running removes the SDK-owned function file.
|
|
264
264
|
|
|
265
265
|
## Subpath exports
|
|
266
266
|
|
|
@@ -27,11 +27,12 @@ module.exports = __toCommonJS(vite_exports);
|
|
|
27
27
|
var import_node_path2 = require("path");
|
|
28
28
|
|
|
29
29
|
// src/stacks/supabase/emit.ts
|
|
30
|
+
var import_esbuild = require("esbuild");
|
|
30
31
|
var import_node_fs = require("fs");
|
|
31
32
|
var import_node_path = require("path");
|
|
32
33
|
|
|
33
34
|
// package.json
|
|
34
|
-
var version = "0.
|
|
35
|
+
var version = "0.12.0";
|
|
35
36
|
|
|
36
37
|
// src/core/fs-errors.ts
|
|
37
38
|
function isFileMissing(err) {
|
|
@@ -54,10 +55,6 @@ var DEFAULT_FUNCTIONS_DIR = "supabase/functions";
|
|
|
54
55
|
var DEFAULT_FUNCTION_NAME = "mcp";
|
|
55
56
|
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.";
|
|
56
57
|
var GENERATED_JSON_MARKER_KEY = "_lovableMcpGenerated";
|
|
57
|
-
var GENERATED_JSON_MARKER_VALUE = "AUTO-GENERATED by @lovable.dev/mcp-js \u2014 do not edit. Regenerated by the Vite plugin. To take ownership, remove this key; the plugin then leaves the file alone.";
|
|
58
|
-
function normalizePath(p) {
|
|
59
|
-
return p.split(import_node_path.sep).join("/");
|
|
60
|
-
}
|
|
61
58
|
function fileExists(path) {
|
|
62
59
|
try {
|
|
63
60
|
(0, import_node_fs.lstatSync)(path);
|
|
@@ -100,32 +97,63 @@ function writeIfChanged(file, content, ownershipCheck) {
|
|
|
100
97
|
(0, import_node_fs.writeFileSync)(file, content, "utf8");
|
|
101
98
|
return true;
|
|
102
99
|
}
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
|
|
100
|
+
function readProjectDependencyVersions(projectRoot) {
|
|
101
|
+
try {
|
|
102
|
+
const pkg = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.join)(projectRoot, "package.json"), "utf8"));
|
|
103
|
+
return { ...pkg.peerDependencies, ...pkg.devDependencies, ...pkg.dependencies };
|
|
104
|
+
} catch {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
106
107
|
}
|
|
107
|
-
function
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
import mcp from "${entryImport}";
|
|
116
|
-
|
|
117
|
-
Deno.serve(createSupabaseHandler(mcp, { functionName: ${JSON.stringify(functionName)} }));
|
|
118
|
-
`;
|
|
108
|
+
function npmSpecifierFor(bare, versions) {
|
|
109
|
+
const pkg = bare.startsWith("@") ? bare.split("/").slice(0, 2).join("/") : bare.split("/")[0];
|
|
110
|
+
const subpath = bare.slice(pkg.length);
|
|
111
|
+
if (pkg === "@lovable.dev/mcp-js")
|
|
112
|
+
return `npm:${pkg}@${version}${subpath}`;
|
|
113
|
+
const range = versions[pkg];
|
|
114
|
+
const version2 = range && /^[\dv^~>=]|^latest$/.test(range) ? range : void 0;
|
|
115
|
+
return version2 ? `npm:${pkg}@${version2}${subpath}` : `npm:${pkg}${subpath}`;
|
|
119
116
|
}
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
117
|
+
function externalizeBareAsNpm(versions) {
|
|
118
|
+
return {
|
|
119
|
+
name: "lovable-mcp-externalize-bare-as-npm",
|
|
120
|
+
setup(pluginBuild) {
|
|
121
|
+
pluginBuild.onResolve({ filter: /.*/ }, (args) => {
|
|
122
|
+
const p = args.path;
|
|
123
|
+
if (p.startsWith(".") || p.startsWith("/"))
|
|
124
|
+
return null;
|
|
125
|
+
if (/^(npm|node|jsr|https?|data):/.test(p))
|
|
126
|
+
return { path: p, external: true };
|
|
127
|
+
return { path: npmSpecifierFor(p, versions), external: true };
|
|
128
|
+
});
|
|
125
129
|
}
|
|
126
130
|
};
|
|
127
|
-
|
|
131
|
+
}
|
|
132
|
+
async function bundleFunctionEntry(projectRoot, mcpEntryAbs, functionName) {
|
|
133
|
+
const versions = readProjectDependencyVersions(projectRoot);
|
|
134
|
+
const wrapper = `import mcp from ${JSON.stringify(mcpEntryAbs)};
|
|
135
|
+
import { createSupabaseHandler } from "@lovable.dev/mcp-js/stacks/supabase";
|
|
136
|
+
Deno.serve(createSupabaseHandler(mcp, { functionName: ${JSON.stringify(functionName)} }));
|
|
128
137
|
`;
|
|
138
|
+
const result = await (0, import_esbuild.build)({
|
|
139
|
+
stdin: { contents: wrapper, resolveDir: projectRoot, sourcefile: "lovable-mcp-supabase-entry.ts", loader: "ts" },
|
|
140
|
+
bundle: true,
|
|
141
|
+
write: false,
|
|
142
|
+
metafile: true,
|
|
143
|
+
format: "esm",
|
|
144
|
+
platform: "neutral",
|
|
145
|
+
target: "esnext",
|
|
146
|
+
absWorkingDir: projectRoot,
|
|
147
|
+
logLevel: "silent",
|
|
148
|
+
plugins: [externalizeBareAsNpm(versions)]
|
|
149
|
+
});
|
|
150
|
+
const out = result.outputFiles[0]?.text ?? "";
|
|
151
|
+
const inputs = Object.keys(result.metafile?.inputs ?? {}).filter((p) => !p.startsWith("<") && !p.startsWith("npm:")).map((p) => (0, import_node_path.resolve)(projectRoot, p));
|
|
152
|
+
const code = `${GENERATED_BANNER}
|
|
153
|
+
// supabase function: ${functionName}
|
|
154
|
+
// Bundled from ${(0, import_node_path.relative)(projectRoot, mcpEntryAbs).split(import_node_path.sep).join("/")} by @lovable.dev/mcp-js.
|
|
155
|
+
` + out;
|
|
156
|
+
return { code, inputs };
|
|
129
157
|
}
|
|
130
158
|
function resolvePaths(options) {
|
|
131
159
|
const projectRoot = (0, import_node_path.resolve)(options.projectRoot);
|
|
@@ -142,31 +170,25 @@ function resolvePaths(options) {
|
|
|
142
170
|
return {
|
|
143
171
|
functionDir,
|
|
144
172
|
functionEntry: (0, import_node_path.join)(functionDir, "index.ts"),
|
|
145
|
-
denoJson: (0, import_node_path.join)(functionDir, "deno.json"),
|
|
146
173
|
mcpEntryAbs: (0, import_node_path.resolve)(projectRoot, mcpEntry)
|
|
147
174
|
};
|
|
148
175
|
}
|
|
149
|
-
function syncSupabaseFunction(options) {
|
|
176
|
+
async function syncSupabaseFunction(options) {
|
|
177
|
+
const projectRoot = (0, import_node_path.resolve)(options.projectRoot);
|
|
150
178
|
const paths = resolvePaths(options);
|
|
151
179
|
const functionName = options.functionName ?? DEFAULT_FUNCTION_NAME;
|
|
152
180
|
const written = [];
|
|
153
181
|
const removed = [];
|
|
154
182
|
if (!fileExists(paths.mcpEntryAbs)) {
|
|
155
183
|
cleanupOrphans(paths.functionDir, /* @__PURE__ */ new Set(), removed);
|
|
156
|
-
return { written, removed };
|
|
184
|
+
return { written, removed, inputs: [] };
|
|
157
185
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
buildFunctionEntry(paths.mcpEntryAbs, paths.functionEntry, functionName),
|
|
161
|
-
hasGeneratedBanner
|
|
162
|
-
)) {
|
|
186
|
+
const { code, inputs } = await bundleFunctionEntry(projectRoot, paths.mcpEntryAbs, functionName);
|
|
187
|
+
if (writeIfChanged(paths.functionEntry, code, hasGeneratedBanner)) {
|
|
163
188
|
written.push(paths.functionEntry);
|
|
164
189
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
cleanupOrphans(paths.functionDir, /* @__PURE__ */ new Set([paths.functionEntry, paths.denoJson]), removed);
|
|
169
|
-
return { written, removed };
|
|
190
|
+
cleanupOrphans(paths.functionDir, /* @__PURE__ */ new Set([paths.functionEntry]), removed);
|
|
191
|
+
return { written, removed, inputs };
|
|
170
192
|
}
|
|
171
193
|
function cleanupOrphans(functionDir, expected, removed) {
|
|
172
194
|
let entries;
|
|
@@ -202,7 +224,7 @@ function cleanupOrphans(functionDir, expected, removed) {
|
|
|
202
224
|
}
|
|
203
225
|
|
|
204
226
|
// src/stacks/supabase/vite.ts
|
|
205
|
-
function
|
|
227
|
+
function normalizePath(p) {
|
|
206
228
|
return p.split(import_node_path2.sep).join("/");
|
|
207
229
|
}
|
|
208
230
|
function mcpPlugin(options = {}) {
|
|
@@ -212,13 +234,15 @@ function mcpPlugin(options = {}) {
|
|
|
212
234
|
const urlPath = `${FUNCTIONS_MOUNT_PREFIX}${functionName}`;
|
|
213
235
|
let projectRoot = process.cwd();
|
|
214
236
|
let mcpEntryAbs = (0, import_node_path2.resolve)(projectRoot, mcpEntryOption);
|
|
215
|
-
|
|
216
|
-
|
|
237
|
+
let watchedInputs = /* @__PURE__ */ new Set();
|
|
238
|
+
const regenerate = async () => {
|
|
239
|
+
const { inputs } = await syncSupabaseFunction({
|
|
217
240
|
projectRoot,
|
|
218
241
|
mcpEntry: mcpEntryOption,
|
|
219
242
|
functionsDir: functionsDirOption,
|
|
220
243
|
functionName
|
|
221
244
|
});
|
|
245
|
+
watchedInputs = new Set(inputs.map(normalizePath));
|
|
222
246
|
};
|
|
223
247
|
return {
|
|
224
248
|
name: "@lovable.dev/mcp-js/supabase",
|
|
@@ -231,16 +255,23 @@ function mcpPlugin(options = {}) {
|
|
|
231
255
|
},
|
|
232
256
|
urlPath
|
|
233
257
|
},
|
|
234
|
-
configResolved(config) {
|
|
258
|
+
async configResolved(config) {
|
|
235
259
|
projectRoot = config.root;
|
|
236
260
|
mcpEntryAbs = (0, import_node_path2.resolve)(projectRoot, mcpEntryOption);
|
|
237
|
-
regenerate();
|
|
261
|
+
await regenerate();
|
|
238
262
|
},
|
|
239
263
|
configureServer(server) {
|
|
240
264
|
const onChange = (file) => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
265
|
+
const norm = normalizePath(file);
|
|
266
|
+
if (norm !== normalizePath(mcpEntryAbs) && !watchedInputs.has(norm))
|
|
267
|
+
return;
|
|
268
|
+
regenerate().catch((err) => {
|
|
269
|
+
const message = `[@lovable.dev/mcp-js] failed to regenerate the Supabase function: ${err instanceof Error ? err.message : String(err)}`;
|
|
270
|
+
if (server.config?.logger?.error)
|
|
271
|
+
server.config.logger.error(message);
|
|
272
|
+
else
|
|
273
|
+
console.error(message);
|
|
274
|
+
});
|
|
244
275
|
};
|
|
245
276
|
server.watcher.on("add", onChange);
|
|
246
277
|
server.watcher.on("change", onChange);
|
|
@@ -251,8 +282,8 @@ function mcpPlugin(options = {}) {
|
|
|
251
282
|
server.watcher.off("unlink", onChange);
|
|
252
283
|
});
|
|
253
284
|
},
|
|
254
|
-
buildStart() {
|
|
255
|
-
regenerate();
|
|
285
|
+
async buildStart() {
|
|
286
|
+
await regenerate();
|
|
256
287
|
}
|
|
257
288
|
};
|
|
258
289
|
}
|
|
@@ -34,11 +34,14 @@ interface McpPluginOptions {
|
|
|
34
34
|
* Lifecycle, mirroring `stacks/tanstack/vite`:
|
|
35
35
|
* - `configResolved` finalizes the project root and runs the first emit.
|
|
36
36
|
* - `buildStart` re-runs the emit so a `vite build` always picks up the
|
|
37
|
-
* latest
|
|
38
|
-
* - `configureServer` re-emits when the entry
|
|
39
|
-
*
|
|
40
|
-
* function
|
|
37
|
+
* latest source.
|
|
38
|
+
* - `configureServer` re-emits when the entry or any bundled source file
|
|
39
|
+
* (tools, local helpers) changes during `vite dev`, so editing a tool
|
|
40
|
+
* regenerates the function without restarting the server.
|
|
41
41
|
*
|
|
42
|
+
* The entry and its local import graph are bundled by esbuild into one
|
|
43
|
+
* self-contained Deno module (deps left as `npm:` specifiers), so the deployed
|
|
44
|
+
* function carries no cross-project imports the Supabase deploy can't upload.
|
|
42
45
|
* Idempotent: `syncSupabaseFunction` writes only on byte-level change and
|
|
43
46
|
* cleans up SDK-owned files when the entry is deleted.
|
|
44
47
|
*/
|
|
@@ -34,11 +34,14 @@ interface McpPluginOptions {
|
|
|
34
34
|
* Lifecycle, mirroring `stacks/tanstack/vite`:
|
|
35
35
|
* - `configResolved` finalizes the project root and runs the first emit.
|
|
36
36
|
* - `buildStart` re-runs the emit so a `vite build` always picks up the
|
|
37
|
-
* latest
|
|
38
|
-
* - `configureServer` re-emits when the entry
|
|
39
|
-
*
|
|
40
|
-
* function
|
|
37
|
+
* latest source.
|
|
38
|
+
* - `configureServer` re-emits when the entry or any bundled source file
|
|
39
|
+
* (tools, local helpers) changes during `vite dev`, so editing a tool
|
|
40
|
+
* regenerates the function without restarting the server.
|
|
41
41
|
*
|
|
42
|
+
* The entry and its local import graph are bundled by esbuild into one
|
|
43
|
+
* self-contained Deno module (deps left as `npm:` specifiers), so the deployed
|
|
44
|
+
* function carries no cross-project imports the Supabase deploy can't upload.
|
|
42
45
|
* Idempotent: `syncSupabaseFunction` writes only on byte-level change and
|
|
43
46
|
* cleans up SDK-owned files when the entry is deleted.
|
|
44
47
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
version
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-S3YHTZKJ.js";
|
|
4
4
|
import {
|
|
5
5
|
isFileMissing
|
|
6
6
|
} from "../../chunk-Y3ZFPEQH.js";
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { resolve as resolve2, sep as sep2 } from "path";
|
|
14
14
|
|
|
15
15
|
// src/stacks/supabase/emit.ts
|
|
16
|
+
import { build } from "esbuild";
|
|
16
17
|
import { lstatSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
|
|
17
18
|
import { dirname, join, relative, resolve, sep } from "path";
|
|
18
19
|
var DEFAULT_MCP_ENTRY = "src/lib/mcp/index.ts";
|
|
@@ -20,10 +21,6 @@ var DEFAULT_FUNCTIONS_DIR = "supabase/functions";
|
|
|
20
21
|
var DEFAULT_FUNCTION_NAME = "mcp";
|
|
21
22
|
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.";
|
|
22
23
|
var GENERATED_JSON_MARKER_KEY = "_lovableMcpGenerated";
|
|
23
|
-
var GENERATED_JSON_MARKER_VALUE = "AUTO-GENERATED by @lovable.dev/mcp-js \u2014 do not edit. Regenerated by the Vite plugin. To take ownership, remove this key; the plugin then leaves the file alone.";
|
|
24
|
-
function normalizePath(p) {
|
|
25
|
-
return p.split(sep).join("/");
|
|
26
|
-
}
|
|
27
24
|
function fileExists(path) {
|
|
28
25
|
try {
|
|
29
26
|
lstatSync(path);
|
|
@@ -66,32 +63,63 @@ function writeIfChanged(file, content, ownershipCheck) {
|
|
|
66
63
|
writeFileSync(file, content, "utf8");
|
|
67
64
|
return true;
|
|
68
65
|
}
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
function readProjectDependencyVersions(projectRoot) {
|
|
67
|
+
try {
|
|
68
|
+
const pkg = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf8"));
|
|
69
|
+
return { ...pkg.peerDependencies, ...pkg.devDependencies, ...pkg.dependencies };
|
|
70
|
+
} catch {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
72
73
|
}
|
|
73
|
-
function
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
import mcp from "${entryImport}";
|
|
82
|
-
|
|
83
|
-
Deno.serve(createSupabaseHandler(mcp, { functionName: ${JSON.stringify(functionName)} }));
|
|
84
|
-
`;
|
|
74
|
+
function npmSpecifierFor(bare, versions) {
|
|
75
|
+
const pkg = bare.startsWith("@") ? bare.split("/").slice(0, 2).join("/") : bare.split("/")[0];
|
|
76
|
+
const subpath = bare.slice(pkg.length);
|
|
77
|
+
if (pkg === "@lovable.dev/mcp-js")
|
|
78
|
+
return `npm:${pkg}@${version}${subpath}`;
|
|
79
|
+
const range = versions[pkg];
|
|
80
|
+
const version2 = range && /^[\dv^~>=]|^latest$/.test(range) ? range : void 0;
|
|
81
|
+
return version2 ? `npm:${pkg}@${version2}${subpath}` : `npm:${pkg}${subpath}`;
|
|
85
82
|
}
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
function externalizeBareAsNpm(versions) {
|
|
84
|
+
return {
|
|
85
|
+
name: "lovable-mcp-externalize-bare-as-npm",
|
|
86
|
+
setup(pluginBuild) {
|
|
87
|
+
pluginBuild.onResolve({ filter: /.*/ }, (args) => {
|
|
88
|
+
const p = args.path;
|
|
89
|
+
if (p.startsWith(".") || p.startsWith("/"))
|
|
90
|
+
return null;
|
|
91
|
+
if (/^(npm|node|jsr|https?|data):/.test(p))
|
|
92
|
+
return { path: p, external: true };
|
|
93
|
+
return { path: npmSpecifierFor(p, versions), external: true };
|
|
94
|
+
});
|
|
91
95
|
}
|
|
92
96
|
};
|
|
93
|
-
|
|
97
|
+
}
|
|
98
|
+
async function bundleFunctionEntry(projectRoot, mcpEntryAbs, functionName) {
|
|
99
|
+
const versions = readProjectDependencyVersions(projectRoot);
|
|
100
|
+
const wrapper = `import mcp from ${JSON.stringify(mcpEntryAbs)};
|
|
101
|
+
import { createSupabaseHandler } from "@lovable.dev/mcp-js/stacks/supabase";
|
|
102
|
+
Deno.serve(createSupabaseHandler(mcp, { functionName: ${JSON.stringify(functionName)} }));
|
|
94
103
|
`;
|
|
104
|
+
const result = await build({
|
|
105
|
+
stdin: { contents: wrapper, resolveDir: projectRoot, sourcefile: "lovable-mcp-supabase-entry.ts", loader: "ts" },
|
|
106
|
+
bundle: true,
|
|
107
|
+
write: false,
|
|
108
|
+
metafile: true,
|
|
109
|
+
format: "esm",
|
|
110
|
+
platform: "neutral",
|
|
111
|
+
target: "esnext",
|
|
112
|
+
absWorkingDir: projectRoot,
|
|
113
|
+
logLevel: "silent",
|
|
114
|
+
plugins: [externalizeBareAsNpm(versions)]
|
|
115
|
+
});
|
|
116
|
+
const out = result.outputFiles[0]?.text ?? "";
|
|
117
|
+
const inputs = Object.keys(result.metafile?.inputs ?? {}).filter((p) => !p.startsWith("<") && !p.startsWith("npm:")).map((p) => resolve(projectRoot, p));
|
|
118
|
+
const code = `${GENERATED_BANNER}
|
|
119
|
+
// supabase function: ${functionName}
|
|
120
|
+
// Bundled from ${relative(projectRoot, mcpEntryAbs).split(sep).join("/")} by @lovable.dev/mcp-js.
|
|
121
|
+
` + out;
|
|
122
|
+
return { code, inputs };
|
|
95
123
|
}
|
|
96
124
|
function resolvePaths(options) {
|
|
97
125
|
const projectRoot = resolve(options.projectRoot);
|
|
@@ -108,31 +136,25 @@ function resolvePaths(options) {
|
|
|
108
136
|
return {
|
|
109
137
|
functionDir,
|
|
110
138
|
functionEntry: join(functionDir, "index.ts"),
|
|
111
|
-
denoJson: join(functionDir, "deno.json"),
|
|
112
139
|
mcpEntryAbs: resolve(projectRoot, mcpEntry)
|
|
113
140
|
};
|
|
114
141
|
}
|
|
115
|
-
function syncSupabaseFunction(options) {
|
|
142
|
+
async function syncSupabaseFunction(options) {
|
|
143
|
+
const projectRoot = resolve(options.projectRoot);
|
|
116
144
|
const paths = resolvePaths(options);
|
|
117
145
|
const functionName = options.functionName ?? DEFAULT_FUNCTION_NAME;
|
|
118
146
|
const written = [];
|
|
119
147
|
const removed = [];
|
|
120
148
|
if (!fileExists(paths.mcpEntryAbs)) {
|
|
121
149
|
cleanupOrphans(paths.functionDir, /* @__PURE__ */ new Set(), removed);
|
|
122
|
-
return { written, removed };
|
|
150
|
+
return { written, removed, inputs: [] };
|
|
123
151
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
buildFunctionEntry(paths.mcpEntryAbs, paths.functionEntry, functionName),
|
|
127
|
-
hasGeneratedBanner
|
|
128
|
-
)) {
|
|
152
|
+
const { code, inputs } = await bundleFunctionEntry(projectRoot, paths.mcpEntryAbs, functionName);
|
|
153
|
+
if (writeIfChanged(paths.functionEntry, code, hasGeneratedBanner)) {
|
|
129
154
|
written.push(paths.functionEntry);
|
|
130
155
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
cleanupOrphans(paths.functionDir, /* @__PURE__ */ new Set([paths.functionEntry, paths.denoJson]), removed);
|
|
135
|
-
return { written, removed };
|
|
156
|
+
cleanupOrphans(paths.functionDir, /* @__PURE__ */ new Set([paths.functionEntry]), removed);
|
|
157
|
+
return { written, removed, inputs };
|
|
136
158
|
}
|
|
137
159
|
function cleanupOrphans(functionDir, expected, removed) {
|
|
138
160
|
let entries;
|
|
@@ -168,7 +190,7 @@ function cleanupOrphans(functionDir, expected, removed) {
|
|
|
168
190
|
}
|
|
169
191
|
|
|
170
192
|
// src/stacks/supabase/vite.ts
|
|
171
|
-
function
|
|
193
|
+
function normalizePath(p) {
|
|
172
194
|
return p.split(sep2).join("/");
|
|
173
195
|
}
|
|
174
196
|
function mcpPlugin(options = {}) {
|
|
@@ -178,13 +200,15 @@ function mcpPlugin(options = {}) {
|
|
|
178
200
|
const urlPath = `${FUNCTIONS_MOUNT_PREFIX}${functionName}`;
|
|
179
201
|
let projectRoot = process.cwd();
|
|
180
202
|
let mcpEntryAbs = resolve2(projectRoot, mcpEntryOption);
|
|
181
|
-
|
|
182
|
-
|
|
203
|
+
let watchedInputs = /* @__PURE__ */ new Set();
|
|
204
|
+
const regenerate = async () => {
|
|
205
|
+
const { inputs } = await syncSupabaseFunction({
|
|
183
206
|
projectRoot,
|
|
184
207
|
mcpEntry: mcpEntryOption,
|
|
185
208
|
functionsDir: functionsDirOption,
|
|
186
209
|
functionName
|
|
187
210
|
});
|
|
211
|
+
watchedInputs = new Set(inputs.map(normalizePath));
|
|
188
212
|
};
|
|
189
213
|
return {
|
|
190
214
|
name: "@lovable.dev/mcp-js/supabase",
|
|
@@ -197,16 +221,23 @@ function mcpPlugin(options = {}) {
|
|
|
197
221
|
},
|
|
198
222
|
urlPath
|
|
199
223
|
},
|
|
200
|
-
configResolved(config) {
|
|
224
|
+
async configResolved(config) {
|
|
201
225
|
projectRoot = config.root;
|
|
202
226
|
mcpEntryAbs = resolve2(projectRoot, mcpEntryOption);
|
|
203
|
-
regenerate();
|
|
227
|
+
await regenerate();
|
|
204
228
|
},
|
|
205
229
|
configureServer(server) {
|
|
206
230
|
const onChange = (file) => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
231
|
+
const norm = normalizePath(file);
|
|
232
|
+
if (norm !== normalizePath(mcpEntryAbs) && !watchedInputs.has(norm))
|
|
233
|
+
return;
|
|
234
|
+
regenerate().catch((err) => {
|
|
235
|
+
const message = `[@lovable.dev/mcp-js] failed to regenerate the Supabase function: ${err instanceof Error ? err.message : String(err)}`;
|
|
236
|
+
if (server.config?.logger?.error)
|
|
237
|
+
server.config.logger.error(message);
|
|
238
|
+
else
|
|
239
|
+
console.error(message);
|
|
240
|
+
});
|
|
210
241
|
};
|
|
211
242
|
server.watcher.on("add", onChange);
|
|
212
243
|
server.watcher.on("change", onChange);
|
|
@@ -217,8 +248,8 @@ function mcpPlugin(options = {}) {
|
|
|
217
248
|
server.watcher.off("unlink", onChange);
|
|
218
249
|
});
|
|
219
250
|
},
|
|
220
|
-
buildStart() {
|
|
221
|
-
regenerate();
|
|
251
|
+
async buildStart() {
|
|
252
|
+
await regenerate();
|
|
222
253
|
}
|
|
223
254
|
};
|
|
224
255
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovable.dev/mcp-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Author MCP servers for Lovable apps. Declare tools with defineTool, register them in defineMcp, and a framework adapter (TanStack or Supabase Edge Functions) emits the route(s) at build time.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"license": "MIT",
|
|
77
77
|
"dependencies": {
|
|
78
78
|
"@modelcontextprotocol/sdk": "1.28.0",
|
|
79
|
+
"esbuild": "^0.27.0",
|
|
79
80
|
"jose": "^6.2.2"
|
|
80
81
|
},
|
|
81
82
|
"peerDependencies": {
|