@lovable.dev/mcp-js 0.15.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-T2RED2TG.js → chunk-CEDD57G2.js} +5 -7
- package/dist/{chunk-QC3DXQTH.js → chunk-DCWYK4CA.js} +34 -0
- package/dist/{chunk-LWHVXXKQ.js → chunk-GIHCQT6L.js} +1 -1
- package/dist/{chunk-QDKOF4UF.js → chunk-ILYTJXDR.js} +2 -2
- package/dist/{chunk-P3F324UC.js → chunk-MA5H6PSF.js} +0 -34
- package/dist/{chunk-VNRQPA4K.js → chunk-QT5L4CS3.js} +4 -3
- package/dist/{chunk-RHDEW56Y.js → chunk-UCPYLSNN.js} +6 -8
- package/dist/{chunk-G4XA7IJM.js → chunk-YTOMGJV5.js} +189 -2
- package/dist/cli/extract-manifest.cjs +1 -1
- package/dist/cli/extract-manifest.js +4 -4
- package/dist/index.js +4 -4
- package/dist/protocols/mcp/index.cjs +217 -210
- package/dist/protocols/mcp/index.d.cts +1 -1
- package/dist/protocols/mcp/index.d.ts +1 -1
- package/dist/protocols/mcp/index.js +5 -6
- package/dist/protocols/oauth-metadata.js +4 -3
- package/dist/protocols/rest/index.cjs +219 -212
- package/dist/protocols/rest/index.d.cts +2 -2
- package/dist/protocols/rest/index.d.ts +2 -2
- package/dist/protocols/rest/index.js +6 -7
- package/dist/{recorder-B-eJU4Qu.d.ts → recorder-qlzUKFxG.d.ts} +3 -3
- package/dist/stacks/supabase/index.cjs +14 -7
- package/dist/stacks/supabase/index.js +10 -12
- package/dist/stacks/supabase/vite.cjs +28 -4
- package/dist/stacks/supabase/vite.js +28 -4
- package/dist/stacks/tanstack/index.cjs +220 -213
- package/dist/stacks/tanstack/index.js +8 -9
- package/package.json +1 -1
- package/dist/chunk-XQSTN54Y.js +0 -189
|
@@ -25,6 +25,7 @@ __export(vite_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(vite_exports);
|
|
27
27
|
var import_node_path2 = require("path");
|
|
28
|
+
var import_vite = require("vite");
|
|
28
29
|
|
|
29
30
|
// src/stacks/supabase/emit.ts
|
|
30
31
|
var import_esbuild = require("esbuild");
|
|
@@ -32,7 +33,7 @@ var import_node_fs = require("fs");
|
|
|
32
33
|
var import_node_path = require("path");
|
|
33
34
|
|
|
34
35
|
// package.json
|
|
35
|
-
var version = "0.15.
|
|
36
|
+
var version = "0.15.1";
|
|
36
37
|
|
|
37
38
|
// src/core/fs-errors.ts
|
|
38
39
|
function isFileMissing(err) {
|
|
@@ -129,7 +130,16 @@ function externalizeBareAsNpm(versions) {
|
|
|
129
130
|
}
|
|
130
131
|
};
|
|
131
132
|
}
|
|
132
|
-
|
|
133
|
+
function importMetaEnvDefines(inlineEnv) {
|
|
134
|
+
const env = inlineEnv ?? {};
|
|
135
|
+
const defines = { "import.meta.env": JSON.stringify(env) };
|
|
136
|
+
for (const [key, value] of Object.entries(env)) {
|
|
137
|
+
if (/^[A-Za-z_$][\w$]*$/.test(key))
|
|
138
|
+
defines[`import.meta.env.${key}`] = JSON.stringify(value);
|
|
139
|
+
}
|
|
140
|
+
return defines;
|
|
141
|
+
}
|
|
142
|
+
async function bundleFunctionEntry(projectRoot, mcpEntryAbs, functionName, inlineEnv) {
|
|
133
143
|
const versions = readProjectDependencyVersions(projectRoot);
|
|
134
144
|
const wrapper = `import mcp from ${JSON.stringify(mcpEntryAbs)};
|
|
135
145
|
import { createSupabaseHandler } from "@lovable.dev/mcp-js/stacks/supabase";
|
|
@@ -145,6 +155,7 @@ Deno.serve(createSupabaseHandler(mcp, { functionName: ${JSON.stringify(functionN
|
|
|
145
155
|
target: "esnext",
|
|
146
156
|
absWorkingDir: projectRoot,
|
|
147
157
|
logLevel: "silent",
|
|
158
|
+
define: importMetaEnvDefines(inlineEnv),
|
|
148
159
|
plugins: [externalizeBareAsNpm(versions)]
|
|
149
160
|
});
|
|
150
161
|
const out = result.outputFiles[0]?.text ?? "";
|
|
@@ -183,7 +194,7 @@ async function syncSupabaseFunction(options) {
|
|
|
183
194
|
cleanupOrphans(paths.functionDir, /* @__PURE__ */ new Set(), removed);
|
|
184
195
|
return { written, removed, inputs: [] };
|
|
185
196
|
}
|
|
186
|
-
const { code, inputs } = await bundleFunctionEntry(projectRoot, paths.mcpEntryAbs, functionName);
|
|
197
|
+
const { code, inputs } = await bundleFunctionEntry(projectRoot, paths.mcpEntryAbs, functionName, options.inlineEnv);
|
|
187
198
|
if (writeIfChanged(paths.functionEntry, code, hasGeneratedBanner)) {
|
|
188
199
|
written.push(paths.functionEntry);
|
|
189
200
|
}
|
|
@@ -227,6 +238,16 @@ function cleanupOrphans(functionDir, expected, removed) {
|
|
|
227
238
|
function normalizePath(p) {
|
|
228
239
|
return p.split(import_node_path2.sep).join("/");
|
|
229
240
|
}
|
|
241
|
+
function productionImportMetaEnv(config) {
|
|
242
|
+
return {
|
|
243
|
+
...(0, import_vite.loadEnv)("production", config.envDir, config.envPrefix),
|
|
244
|
+
MODE: "production",
|
|
245
|
+
BASE_URL: config.base ?? "/",
|
|
246
|
+
DEV: false,
|
|
247
|
+
PROD: true,
|
|
248
|
+
SSR: false
|
|
249
|
+
};
|
|
250
|
+
}
|
|
230
251
|
function mcpPlugin(options = {}) {
|
|
231
252
|
const mcpEntryOption = options.mcpEntry ?? DEFAULT_MCP_ENTRY;
|
|
232
253
|
const functionsDirOption = options.functionsDir ?? DEFAULT_FUNCTIONS_DIR;
|
|
@@ -234,13 +255,15 @@ function mcpPlugin(options = {}) {
|
|
|
234
255
|
const urlPath = `${FUNCTIONS_MOUNT_PREFIX}${functionName}`;
|
|
235
256
|
let projectRoot = process.cwd();
|
|
236
257
|
let mcpEntryAbs = (0, import_node_path2.resolve)(projectRoot, mcpEntryOption);
|
|
258
|
+
let inlineEnv = {};
|
|
237
259
|
let watchedInputs = /* @__PURE__ */ new Set();
|
|
238
260
|
const regenerate = async () => {
|
|
239
261
|
const { inputs } = await syncSupabaseFunction({
|
|
240
262
|
projectRoot,
|
|
241
263
|
mcpEntry: mcpEntryOption,
|
|
242
264
|
functionsDir: functionsDirOption,
|
|
243
|
-
functionName
|
|
265
|
+
functionName,
|
|
266
|
+
inlineEnv
|
|
244
267
|
});
|
|
245
268
|
watchedInputs = new Set(inputs.map(normalizePath));
|
|
246
269
|
};
|
|
@@ -258,6 +281,7 @@ function mcpPlugin(options = {}) {
|
|
|
258
281
|
async configResolved(config) {
|
|
259
282
|
projectRoot = config.root;
|
|
260
283
|
mcpEntryAbs = (0, import_node_path2.resolve)(projectRoot, mcpEntryOption);
|
|
284
|
+
inlineEnv = productionImportMetaEnv(config);
|
|
261
285
|
await regenerate();
|
|
262
286
|
},
|
|
263
287
|
configureServer(server) {
|
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
} from "../../chunk-XQWJN6DC.js";
|
|
8
8
|
import {
|
|
9
9
|
version
|
|
10
|
-
} from "../../chunk-
|
|
10
|
+
} from "../../chunk-GIHCQT6L.js";
|
|
11
11
|
|
|
12
12
|
// src/stacks/supabase/vite.ts
|
|
13
13
|
import { resolve as resolve2, sep as sep2 } from "path";
|
|
14
|
+
import { loadEnv } from "vite";
|
|
14
15
|
|
|
15
16
|
// src/stacks/supabase/emit.ts
|
|
16
17
|
import { build } from "esbuild";
|
|
@@ -95,7 +96,16 @@ function externalizeBareAsNpm(versions) {
|
|
|
95
96
|
}
|
|
96
97
|
};
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
+
function importMetaEnvDefines(inlineEnv) {
|
|
100
|
+
const env = inlineEnv ?? {};
|
|
101
|
+
const defines = { "import.meta.env": JSON.stringify(env) };
|
|
102
|
+
for (const [key, value] of Object.entries(env)) {
|
|
103
|
+
if (/^[A-Za-z_$][\w$]*$/.test(key))
|
|
104
|
+
defines[`import.meta.env.${key}`] = JSON.stringify(value);
|
|
105
|
+
}
|
|
106
|
+
return defines;
|
|
107
|
+
}
|
|
108
|
+
async function bundleFunctionEntry(projectRoot, mcpEntryAbs, functionName, inlineEnv) {
|
|
99
109
|
const versions = readProjectDependencyVersions(projectRoot);
|
|
100
110
|
const wrapper = `import mcp from ${JSON.stringify(mcpEntryAbs)};
|
|
101
111
|
import { createSupabaseHandler } from "@lovable.dev/mcp-js/stacks/supabase";
|
|
@@ -111,6 +121,7 @@ Deno.serve(createSupabaseHandler(mcp, { functionName: ${JSON.stringify(functionN
|
|
|
111
121
|
target: "esnext",
|
|
112
122
|
absWorkingDir: projectRoot,
|
|
113
123
|
logLevel: "silent",
|
|
124
|
+
define: importMetaEnvDefines(inlineEnv),
|
|
114
125
|
plugins: [externalizeBareAsNpm(versions)]
|
|
115
126
|
});
|
|
116
127
|
const out = result.outputFiles[0]?.text ?? "";
|
|
@@ -149,7 +160,7 @@ async function syncSupabaseFunction(options) {
|
|
|
149
160
|
cleanupOrphans(paths.functionDir, /* @__PURE__ */ new Set(), removed);
|
|
150
161
|
return { written, removed, inputs: [] };
|
|
151
162
|
}
|
|
152
|
-
const { code, inputs } = await bundleFunctionEntry(projectRoot, paths.mcpEntryAbs, functionName);
|
|
163
|
+
const { code, inputs } = await bundleFunctionEntry(projectRoot, paths.mcpEntryAbs, functionName, options.inlineEnv);
|
|
153
164
|
if (writeIfChanged(paths.functionEntry, code, hasGeneratedBanner)) {
|
|
154
165
|
written.push(paths.functionEntry);
|
|
155
166
|
}
|
|
@@ -193,6 +204,16 @@ function cleanupOrphans(functionDir, expected, removed) {
|
|
|
193
204
|
function normalizePath(p) {
|
|
194
205
|
return p.split(sep2).join("/");
|
|
195
206
|
}
|
|
207
|
+
function productionImportMetaEnv(config) {
|
|
208
|
+
return {
|
|
209
|
+
...loadEnv("production", config.envDir, config.envPrefix),
|
|
210
|
+
MODE: "production",
|
|
211
|
+
BASE_URL: config.base ?? "/",
|
|
212
|
+
DEV: false,
|
|
213
|
+
PROD: true,
|
|
214
|
+
SSR: false
|
|
215
|
+
};
|
|
216
|
+
}
|
|
196
217
|
function mcpPlugin(options = {}) {
|
|
197
218
|
const mcpEntryOption = options.mcpEntry ?? DEFAULT_MCP_ENTRY;
|
|
198
219
|
const functionsDirOption = options.functionsDir ?? DEFAULT_FUNCTIONS_DIR;
|
|
@@ -200,13 +221,15 @@ function mcpPlugin(options = {}) {
|
|
|
200
221
|
const urlPath = `${FUNCTIONS_MOUNT_PREFIX}${functionName}`;
|
|
201
222
|
let projectRoot = process.cwd();
|
|
202
223
|
let mcpEntryAbs = resolve2(projectRoot, mcpEntryOption);
|
|
224
|
+
let inlineEnv = {};
|
|
203
225
|
let watchedInputs = /* @__PURE__ */ new Set();
|
|
204
226
|
const regenerate = async () => {
|
|
205
227
|
const { inputs } = await syncSupabaseFunction({
|
|
206
228
|
projectRoot,
|
|
207
229
|
mcpEntry: mcpEntryOption,
|
|
208
230
|
functionsDir: functionsDirOption,
|
|
209
|
-
functionName
|
|
231
|
+
functionName,
|
|
232
|
+
inlineEnv
|
|
210
233
|
});
|
|
211
234
|
watchedInputs = new Set(inputs.map(normalizePath));
|
|
212
235
|
};
|
|
@@ -224,6 +247,7 @@ function mcpPlugin(options = {}) {
|
|
|
224
247
|
async configResolved(config) {
|
|
225
248
|
projectRoot = config.root;
|
|
226
249
|
mcpEntryAbs = resolve2(projectRoot, mcpEntryOption);
|
|
250
|
+
inlineEnv = productionImportMetaEnv(config);
|
|
227
251
|
await regenerate();
|
|
228
252
|
},
|
|
229
253
|
configureServer(server) {
|
|
@@ -107,6 +107,214 @@ function describeError(err) {
|
|
|
107
107
|
return { value: String(err) };
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// src/metrics/config.ts
|
|
111
|
+
var DEFAULT_METRICS_ENDPOINT = "https://api.lovable.dev/v1/app-mcp-usage";
|
|
112
|
+
var METRICS_API_KEY_ENV_VAR = "LOVABLE_API_KEY";
|
|
113
|
+
var METRICS_SAMPLE_RATE = 1;
|
|
114
|
+
var METRICS_FLUSH_INTERVAL_MS = 5e3;
|
|
115
|
+
var METRICS_MAX_BATCH_SIZE = 50;
|
|
116
|
+
function assertMetricsEndpoint(endpoint) {
|
|
117
|
+
let url;
|
|
118
|
+
try {
|
|
119
|
+
url = new URL(endpoint);
|
|
120
|
+
} catch {
|
|
121
|
+
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}`);
|
|
122
|
+
}
|
|
123
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
124
|
+
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be http(s), got ${JSON.stringify(endpoint)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function resolveMetricsConfig(config = true) {
|
|
128
|
+
const options = typeof config === "boolean" ? { enabled: config } : config;
|
|
129
|
+
const endpoint = options.endpoint ?? DEFAULT_METRICS_ENDPOINT;
|
|
130
|
+
assertMetricsEndpoint(endpoint);
|
|
131
|
+
return Object.freeze({
|
|
132
|
+
enabled: options.enabled ?? true,
|
|
133
|
+
endpoint,
|
|
134
|
+
headers: options.headers ?? {},
|
|
135
|
+
apiKeyEnvVar: METRICS_API_KEY_ENV_VAR,
|
|
136
|
+
sampleRate: METRICS_SAMPLE_RATE,
|
|
137
|
+
flushIntervalMs: METRICS_FLUSH_INTERVAL_MS,
|
|
138
|
+
maxBatchSize: METRICS_MAX_BATCH_SIZE
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// package.json
|
|
143
|
+
var version = "0.15.1";
|
|
144
|
+
|
|
145
|
+
// src/metrics/otlp.ts
|
|
146
|
+
var SCOPE_NAME = "@lovable.dev/mcp-js";
|
|
147
|
+
var EVENT_NAME = "mcp.tool.invocation";
|
|
148
|
+
var SEVERITY_INFO = 9;
|
|
149
|
+
function strAttr(key, value) {
|
|
150
|
+
return { key, value: { stringValue: value } };
|
|
151
|
+
}
|
|
152
|
+
function intAttr(key, value) {
|
|
153
|
+
return { key, value: { intValue: String(Math.round(value)) } };
|
|
154
|
+
}
|
|
155
|
+
function toLogRecord(rec) {
|
|
156
|
+
const attributes = [
|
|
157
|
+
strAttr("event.name", EVENT_NAME),
|
|
158
|
+
strAttr("mcp.method", rec.method),
|
|
159
|
+
strAttr("mcp.outcome", rec.outcome),
|
|
160
|
+
intAttr("mcp.duration_ms", rec.durationMs)
|
|
161
|
+
];
|
|
162
|
+
if (rec.tool !== null)
|
|
163
|
+
attributes.push(strAttr("mcp.tool", rec.tool));
|
|
164
|
+
if (rec.reqBytes !== void 0)
|
|
165
|
+
attributes.push(intAttr("mcp.request_size_bytes", rec.reqBytes));
|
|
166
|
+
if (rec.resBytes !== void 0)
|
|
167
|
+
attributes.push(intAttr("mcp.response_size_bytes", rec.resBytes));
|
|
168
|
+
return {
|
|
169
|
+
timeUnixNano: rec.timeUnixNano,
|
|
170
|
+
observedTimeUnixNano: rec.timeUnixNano,
|
|
171
|
+
severityNumber: SEVERITY_INFO,
|
|
172
|
+
severityText: "INFO",
|
|
173
|
+
body: { stringValue: EVENT_NAME },
|
|
174
|
+
attributes
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function buildLogsPayload(records, server) {
|
|
178
|
+
return JSON.stringify({
|
|
179
|
+
resourceLogs: [
|
|
180
|
+
{
|
|
181
|
+
resource: {
|
|
182
|
+
attributes: [
|
|
183
|
+
strAttr("service.name", server.name),
|
|
184
|
+
strAttr("service.version", server.version),
|
|
185
|
+
strAttr("telemetry.sdk.name", SCOPE_NAME),
|
|
186
|
+
strAttr("telemetry.sdk.version", version),
|
|
187
|
+
strAttr("telemetry.sdk.language", "webjs")
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
scopeLogs: [
|
|
191
|
+
{
|
|
192
|
+
scope: { name: SCOPE_NAME, version },
|
|
193
|
+
logRecords: records.map(toLogRecord)
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function nowUnixNano() {
|
|
201
|
+
return `${Date.now()}000000`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/metrics/recorder.ts
|
|
205
|
+
function nowMs() {
|
|
206
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
207
|
+
}
|
|
208
|
+
var NOOP_RECORDER = {
|
|
209
|
+
record() {
|
|
210
|
+
},
|
|
211
|
+
async flush() {
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
function createNoopRecorder() {
|
|
215
|
+
return NOOP_RECORDER;
|
|
216
|
+
}
|
|
217
|
+
function readRuntimeEnv(name) {
|
|
218
|
+
try {
|
|
219
|
+
const denoEnv = globalThis.Deno?.env;
|
|
220
|
+
const value = denoEnv?.get?.(name);
|
|
221
|
+
if (value)
|
|
222
|
+
return value;
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
if (typeof process !== "undefined") {
|
|
227
|
+
const value = process.env?.[name];
|
|
228
|
+
if (value)
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
return void 0;
|
|
234
|
+
}
|
|
235
|
+
function probeWaitUntil() {
|
|
236
|
+
const fn = globalThis.EdgeRuntime?.waitUntil;
|
|
237
|
+
return typeof fn === "function" ? fn.bind(globalThis.EdgeRuntime) : void 0;
|
|
238
|
+
}
|
|
239
|
+
function createMetricsRecorder(ctx, deps = {}) {
|
|
240
|
+
const { config, server } = ctx;
|
|
241
|
+
if (!config.enabled)
|
|
242
|
+
return NOOP_RECORDER;
|
|
243
|
+
const doFetch = deps.fetch ?? globalThis.fetch;
|
|
244
|
+
if (!doFetch)
|
|
245
|
+
return NOOP_RECORDER;
|
|
246
|
+
const usesLovableEndpoint = config.endpoint === DEFAULT_METRICS_ENDPOINT;
|
|
247
|
+
const apiKey = usesLovableEndpoint ? (deps.getApiKey ?? (() => readRuntimeEnv(config.apiKeyEnvVar)))() : void 0;
|
|
248
|
+
if (usesLovableEndpoint && !apiKey) {
|
|
249
|
+
log.debug("metrics.disabled_no_api_key", { envVar: config.apiKeyEnvVar });
|
|
250
|
+
return NOOP_RECORDER;
|
|
251
|
+
}
|
|
252
|
+
const waitUntil = deps.waitUntil ?? probeWaitUntil();
|
|
253
|
+
const buffer = [];
|
|
254
|
+
let timer;
|
|
255
|
+
const schedule = (p) => {
|
|
256
|
+
if (waitUntil) {
|
|
257
|
+
try {
|
|
258
|
+
waitUntil(p);
|
|
259
|
+
return;
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
void p.catch(() => {
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
const headers = {};
|
|
267
|
+
if (!usesLovableEndpoint) {
|
|
268
|
+
for (const [key, value] of Object.entries(config.headers)) {
|
|
269
|
+
if (key.toLowerCase() !== "content-type")
|
|
270
|
+
headers[key] = value;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
headers["content-type"] = "application/json";
|
|
274
|
+
if (apiKey)
|
|
275
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
276
|
+
const flush = async () => {
|
|
277
|
+
if (buffer.length === 0)
|
|
278
|
+
return;
|
|
279
|
+
const records = buffer.splice(0, buffer.length);
|
|
280
|
+
try {
|
|
281
|
+
const res = await doFetch(config.endpoint, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers,
|
|
284
|
+
body: buildLogsPayload(records, server),
|
|
285
|
+
keepalive: true
|
|
286
|
+
});
|
|
287
|
+
if (!res.ok)
|
|
288
|
+
log.debug("metrics.flush_rejected", { status: res.status });
|
|
289
|
+
} catch (err) {
|
|
290
|
+
log.debug("metrics.flush_failed", describeError(err));
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const ensureTimer = () => {
|
|
294
|
+
if (timer !== void 0)
|
|
295
|
+
return;
|
|
296
|
+
timer = setInterval(() => schedule(flush()), config.flushIntervalMs);
|
|
297
|
+
timer.unref?.();
|
|
298
|
+
};
|
|
299
|
+
return {
|
|
300
|
+
record(ev) {
|
|
301
|
+
if (config.sampleRate < 1 && Math.random() >= config.sampleRate)
|
|
302
|
+
return;
|
|
303
|
+
buffer.push({ ...ev, timeUnixNano: nowUnixNano() });
|
|
304
|
+
ensureTimer();
|
|
305
|
+
if (buffer.length >= config.maxBatchSize)
|
|
306
|
+
schedule(flush());
|
|
307
|
+
},
|
|
308
|
+
flush
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function createRecorderForRuntime(mcp) {
|
|
312
|
+
const config = resolveMetricsConfig(mcp.metrics);
|
|
313
|
+
if (!config.enabled)
|
|
314
|
+
return createNoopRecorder();
|
|
315
|
+
return createMetricsRecorder({ config, server: { name: mcp.name, version: mcp.version } });
|
|
316
|
+
}
|
|
317
|
+
|
|
110
318
|
// src/core/promise.ts
|
|
111
319
|
function cachedPromise(load, label) {
|
|
112
320
|
let settled = false;
|
|
@@ -535,12 +743,13 @@ function assertRequiredScopes(auth, context) {
|
|
|
535
743
|
throw new OAuthTokenError(403, "insufficient_scope", "Additional OAuth scope is required");
|
|
536
744
|
}
|
|
537
745
|
}
|
|
538
|
-
function createRequestAuthorizer(mcp, options = {}) {
|
|
746
|
+
function createRequestAuthorizer(mcp, options = {}, recorder) {
|
|
539
747
|
const runtime = getOAuthRuntime(mcp, options);
|
|
540
748
|
return {
|
|
541
749
|
async authorize(request) {
|
|
542
750
|
if (runtime.kind === "unconfigured")
|
|
543
751
|
return { ok: true };
|
|
752
|
+
const startedAt = nowMs();
|
|
544
753
|
const token = parseBearerToken(request);
|
|
545
754
|
if (!token) {
|
|
546
755
|
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
@@ -553,6 +762,12 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
553
762
|
} catch (err) {
|
|
554
763
|
if (err instanceof OAuthConfigurationError) {
|
|
555
764
|
log.error("auth.config_error", { ...describeError(err), outcome: "500" });
|
|
765
|
+
recorder?.record({
|
|
766
|
+
tool: null,
|
|
767
|
+
method: "authorize",
|
|
768
|
+
outcome: "auth_config_error",
|
|
769
|
+
durationMs: nowMs() - startedAt
|
|
770
|
+
});
|
|
556
771
|
return { ok: false, response: oauthConfigurationErrorResponse() };
|
|
557
772
|
}
|
|
558
773
|
if (err instanceof OAuthTokenError) {
|
|
@@ -649,214 +864,6 @@ function corsPreflightResponse(allowMethods) {
|
|
|
649
864
|
});
|
|
650
865
|
}
|
|
651
866
|
|
|
652
|
-
// src/metrics/config.ts
|
|
653
|
-
var DEFAULT_METRICS_ENDPOINT = "https://api.lovable.dev/v1/app-mcp-usage";
|
|
654
|
-
var METRICS_API_KEY_ENV_VAR = "LOVABLE_API_KEY";
|
|
655
|
-
var METRICS_SAMPLE_RATE = 1;
|
|
656
|
-
var METRICS_FLUSH_INTERVAL_MS = 5e3;
|
|
657
|
-
var METRICS_MAX_BATCH_SIZE = 50;
|
|
658
|
-
function assertMetricsEndpoint(endpoint) {
|
|
659
|
-
let url;
|
|
660
|
-
try {
|
|
661
|
-
url = new URL(endpoint);
|
|
662
|
-
} catch {
|
|
663
|
-
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}`);
|
|
664
|
-
}
|
|
665
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
666
|
-
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be http(s), got ${JSON.stringify(endpoint)}`);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
function resolveMetricsConfig(config = true) {
|
|
670
|
-
const options = typeof config === "boolean" ? { enabled: config } : config;
|
|
671
|
-
const endpoint = options.endpoint ?? DEFAULT_METRICS_ENDPOINT;
|
|
672
|
-
assertMetricsEndpoint(endpoint);
|
|
673
|
-
return Object.freeze({
|
|
674
|
-
enabled: options.enabled ?? true,
|
|
675
|
-
endpoint,
|
|
676
|
-
headers: options.headers ?? {},
|
|
677
|
-
apiKeyEnvVar: METRICS_API_KEY_ENV_VAR,
|
|
678
|
-
sampleRate: METRICS_SAMPLE_RATE,
|
|
679
|
-
flushIntervalMs: METRICS_FLUSH_INTERVAL_MS,
|
|
680
|
-
maxBatchSize: METRICS_MAX_BATCH_SIZE
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// package.json
|
|
685
|
-
var version = "0.15.0";
|
|
686
|
-
|
|
687
|
-
// src/metrics/otlp.ts
|
|
688
|
-
var SCOPE_NAME = "@lovable.dev/mcp-js";
|
|
689
|
-
var EVENT_NAME = "mcp.tool.invocation";
|
|
690
|
-
var SEVERITY_INFO = 9;
|
|
691
|
-
function strAttr(key, value) {
|
|
692
|
-
return { key, value: { stringValue: value } };
|
|
693
|
-
}
|
|
694
|
-
function intAttr(key, value) {
|
|
695
|
-
return { key, value: { intValue: String(Math.round(value)) } };
|
|
696
|
-
}
|
|
697
|
-
function toLogRecord(rec) {
|
|
698
|
-
const attributes = [
|
|
699
|
-
strAttr("event.name", EVENT_NAME),
|
|
700
|
-
strAttr("mcp.method", rec.method),
|
|
701
|
-
strAttr("mcp.outcome", rec.outcome),
|
|
702
|
-
intAttr("mcp.duration_ms", rec.durationMs)
|
|
703
|
-
];
|
|
704
|
-
if (rec.tool !== null)
|
|
705
|
-
attributes.push(strAttr("mcp.tool", rec.tool));
|
|
706
|
-
if (rec.reqBytes !== void 0)
|
|
707
|
-
attributes.push(intAttr("mcp.request_size_bytes", rec.reqBytes));
|
|
708
|
-
if (rec.resBytes !== void 0)
|
|
709
|
-
attributes.push(intAttr("mcp.response_size_bytes", rec.resBytes));
|
|
710
|
-
return {
|
|
711
|
-
timeUnixNano: rec.timeUnixNano,
|
|
712
|
-
observedTimeUnixNano: rec.timeUnixNano,
|
|
713
|
-
severityNumber: SEVERITY_INFO,
|
|
714
|
-
severityText: "INFO",
|
|
715
|
-
body: { stringValue: EVENT_NAME },
|
|
716
|
-
attributes
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
function buildLogsPayload(records, server) {
|
|
720
|
-
return JSON.stringify({
|
|
721
|
-
resourceLogs: [
|
|
722
|
-
{
|
|
723
|
-
resource: {
|
|
724
|
-
attributes: [
|
|
725
|
-
strAttr("service.name", server.name),
|
|
726
|
-
strAttr("service.version", server.version),
|
|
727
|
-
strAttr("telemetry.sdk.name", SCOPE_NAME),
|
|
728
|
-
strAttr("telemetry.sdk.version", version),
|
|
729
|
-
strAttr("telemetry.sdk.language", "webjs")
|
|
730
|
-
]
|
|
731
|
-
},
|
|
732
|
-
scopeLogs: [
|
|
733
|
-
{
|
|
734
|
-
scope: { name: SCOPE_NAME, version },
|
|
735
|
-
logRecords: records.map(toLogRecord)
|
|
736
|
-
}
|
|
737
|
-
]
|
|
738
|
-
}
|
|
739
|
-
]
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
function nowUnixNano() {
|
|
743
|
-
return `${Date.now()}000000`;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// src/metrics/recorder.ts
|
|
747
|
-
function nowMs() {
|
|
748
|
-
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
749
|
-
}
|
|
750
|
-
var NOOP_RECORDER = {
|
|
751
|
-
record() {
|
|
752
|
-
},
|
|
753
|
-
async flush() {
|
|
754
|
-
}
|
|
755
|
-
};
|
|
756
|
-
function createNoopRecorder() {
|
|
757
|
-
return NOOP_RECORDER;
|
|
758
|
-
}
|
|
759
|
-
function readRuntimeEnv(name) {
|
|
760
|
-
try {
|
|
761
|
-
const denoEnv = globalThis.Deno?.env;
|
|
762
|
-
const value = denoEnv?.get?.(name);
|
|
763
|
-
if (value)
|
|
764
|
-
return value;
|
|
765
|
-
} catch {
|
|
766
|
-
}
|
|
767
|
-
try {
|
|
768
|
-
if (typeof process !== "undefined") {
|
|
769
|
-
const value = process.env?.[name];
|
|
770
|
-
if (value)
|
|
771
|
-
return value;
|
|
772
|
-
}
|
|
773
|
-
} catch {
|
|
774
|
-
}
|
|
775
|
-
return void 0;
|
|
776
|
-
}
|
|
777
|
-
function probeWaitUntil() {
|
|
778
|
-
const fn = globalThis.EdgeRuntime?.waitUntil;
|
|
779
|
-
return typeof fn === "function" ? fn.bind(globalThis.EdgeRuntime) : void 0;
|
|
780
|
-
}
|
|
781
|
-
function createMetricsRecorder(ctx, deps = {}) {
|
|
782
|
-
const { config, server } = ctx;
|
|
783
|
-
if (!config.enabled)
|
|
784
|
-
return NOOP_RECORDER;
|
|
785
|
-
const doFetch = deps.fetch ?? globalThis.fetch;
|
|
786
|
-
if (!doFetch)
|
|
787
|
-
return NOOP_RECORDER;
|
|
788
|
-
const usesLovableEndpoint = config.endpoint === DEFAULT_METRICS_ENDPOINT;
|
|
789
|
-
const apiKey = usesLovableEndpoint ? (deps.getApiKey ?? (() => readRuntimeEnv(config.apiKeyEnvVar)))() : void 0;
|
|
790
|
-
if (usesLovableEndpoint && !apiKey) {
|
|
791
|
-
log.debug("metrics.disabled_no_api_key", { envVar: config.apiKeyEnvVar });
|
|
792
|
-
return NOOP_RECORDER;
|
|
793
|
-
}
|
|
794
|
-
const waitUntil = deps.waitUntil ?? probeWaitUntil();
|
|
795
|
-
const buffer = [];
|
|
796
|
-
let timer;
|
|
797
|
-
const schedule = (p) => {
|
|
798
|
-
if (waitUntil) {
|
|
799
|
-
try {
|
|
800
|
-
waitUntil(p);
|
|
801
|
-
return;
|
|
802
|
-
} catch {
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
void p.catch(() => {
|
|
806
|
-
});
|
|
807
|
-
};
|
|
808
|
-
const headers = {};
|
|
809
|
-
if (!usesLovableEndpoint) {
|
|
810
|
-
for (const [key, value] of Object.entries(config.headers)) {
|
|
811
|
-
if (key.toLowerCase() !== "content-type")
|
|
812
|
-
headers[key] = value;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
headers["content-type"] = "application/json";
|
|
816
|
-
if (apiKey)
|
|
817
|
-
headers.authorization = `Bearer ${apiKey}`;
|
|
818
|
-
const flush = async () => {
|
|
819
|
-
if (buffer.length === 0)
|
|
820
|
-
return;
|
|
821
|
-
const records = buffer.splice(0, buffer.length);
|
|
822
|
-
try {
|
|
823
|
-
const res = await doFetch(config.endpoint, {
|
|
824
|
-
method: "POST",
|
|
825
|
-
headers,
|
|
826
|
-
body: buildLogsPayload(records, server),
|
|
827
|
-
keepalive: true
|
|
828
|
-
});
|
|
829
|
-
if (!res.ok)
|
|
830
|
-
log.debug("metrics.flush_rejected", { status: res.status });
|
|
831
|
-
} catch (err) {
|
|
832
|
-
log.debug("metrics.flush_failed", describeError(err));
|
|
833
|
-
}
|
|
834
|
-
};
|
|
835
|
-
const ensureTimer = () => {
|
|
836
|
-
if (timer !== void 0)
|
|
837
|
-
return;
|
|
838
|
-
timer = setInterval(() => schedule(flush()), config.flushIntervalMs);
|
|
839
|
-
timer.unref?.();
|
|
840
|
-
};
|
|
841
|
-
return {
|
|
842
|
-
record(ev) {
|
|
843
|
-
if (config.sampleRate < 1 && Math.random() >= config.sampleRate)
|
|
844
|
-
return;
|
|
845
|
-
buffer.push({ ...ev, timeUnixNano: nowUnixNano() });
|
|
846
|
-
ensureTimer();
|
|
847
|
-
if (buffer.length >= config.maxBatchSize)
|
|
848
|
-
schedule(flush());
|
|
849
|
-
},
|
|
850
|
-
flush
|
|
851
|
-
};
|
|
852
|
-
}
|
|
853
|
-
function createRecorderForRuntime(mcp) {
|
|
854
|
-
const config = resolveMetricsConfig(mcp.metrics);
|
|
855
|
-
if (!config.enabled)
|
|
856
|
-
return createNoopRecorder();
|
|
857
|
-
return createMetricsRecorder({ config, server: { name: mcp.name, version: mcp.version } });
|
|
858
|
-
}
|
|
859
|
-
|
|
860
867
|
// src/protocols/mcp/protocol.ts
|
|
861
868
|
function adaptToolToSdkCallback(tool, auth, recorder) {
|
|
862
869
|
return async (first) => {
|
|
@@ -883,7 +890,7 @@ function adaptToolToSdkCallback(tool, auth, recorder) {
|
|
|
883
890
|
};
|
|
884
891
|
}
|
|
885
892
|
function createMcpProtocolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
|
|
886
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
893
|
+
const authorizer = createRequestAuthorizer(mcp, options, recorder);
|
|
887
894
|
const handle = async (request) => {
|
|
888
895
|
const authResult = await authorizer.authorize(request);
|
|
889
896
|
if (!authResult.ok)
|
|
@@ -1013,9 +1020,9 @@ function buildMcpListing(mcp) {
|
|
|
1013
1020
|
}))
|
|
1014
1021
|
};
|
|
1015
1022
|
}
|
|
1016
|
-
function createListToolsHandler(mcp, options = {}) {
|
|
1023
|
+
function createListToolsHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
|
|
1017
1024
|
assertRestResourceBinding(mcp, options);
|
|
1018
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
1025
|
+
const authorizer = createRequestAuthorizer(mcp, options, recorder);
|
|
1019
1026
|
const handle = async (request) => {
|
|
1020
1027
|
const authResult = await authorizer.authorize(request);
|
|
1021
1028
|
if (!authResult.ok)
|
|
@@ -1048,7 +1055,7 @@ function isEmptyArgs(value) {
|
|
|
1048
1055
|
}
|
|
1049
1056
|
function createInvokeToolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
|
|
1050
1057
|
assertRestResourceBinding(mcp, options);
|
|
1051
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
1058
|
+
const authorizer = createRequestAuthorizer(mcp, options, recorder);
|
|
1052
1059
|
const handle = async (request, toolName) => {
|
|
1053
1060
|
const authResult = await authorizer.authorize(request);
|
|
1054
1061
|
if (!authResult.ok)
|
|
@@ -3,22 +3,21 @@ import {
|
|
|
3
3
|
} from "../../chunk-UQK5UO6C.js";
|
|
4
4
|
import {
|
|
5
5
|
createMcpProtocolHandler
|
|
6
|
-
} from "../../chunk-
|
|
6
|
+
} from "../../chunk-UCPYLSNN.js";
|
|
7
7
|
import {
|
|
8
8
|
createOAuthProtectedResourceMetadataHandler
|
|
9
|
-
} from "../../chunk-
|
|
9
|
+
} from "../../chunk-ILYTJXDR.js";
|
|
10
10
|
import {
|
|
11
11
|
createInvokeToolHandler
|
|
12
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-CEDD57G2.js";
|
|
13
13
|
import {
|
|
14
14
|
createListToolsHandler
|
|
15
|
-
} from "../../chunk-
|
|
16
|
-
import "../../chunk-
|
|
17
|
-
import "../../chunk-
|
|
18
|
-
import "../../chunk-
|
|
19
|
-
import "../../chunk-QC3DXQTH.js";
|
|
15
|
+
} from "../../chunk-QT5L4CS3.js";
|
|
16
|
+
import "../../chunk-MA5H6PSF.js";
|
|
17
|
+
import "../../chunk-YTOMGJV5.js";
|
|
18
|
+
import "../../chunk-DCWYK4CA.js";
|
|
20
19
|
import "../../chunk-6DXGZZA4.js";
|
|
21
|
-
import "../../chunk-
|
|
20
|
+
import "../../chunk-GIHCQT6L.js";
|
|
22
21
|
|
|
23
22
|
// src/stacks/tanstack/handlers.ts
|
|
24
23
|
function forwarded(request, options) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovable.dev/mcp-js",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.1",
|
|
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": {
|