@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.
@@ -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.0";
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
- async function bundleFunctionEntry(projectRoot, mcpEntryAbs, functionName) {
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-LWHVXXKQ.js";
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
- async function bundleFunctionEntry(projectRoot, mcpEntryAbs, functionName) {
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-RHDEW56Y.js";
6
+ } from "../../chunk-UCPYLSNN.js";
7
7
  import {
8
8
  createOAuthProtectedResourceMetadataHandler
9
- } from "../../chunk-QDKOF4UF.js";
9
+ } from "../../chunk-ILYTJXDR.js";
10
10
  import {
11
11
  createInvokeToolHandler
12
- } from "../../chunk-T2RED2TG.js";
12
+ } from "../../chunk-CEDD57G2.js";
13
13
  import {
14
14
  createListToolsHandler
15
- } from "../../chunk-VNRQPA4K.js";
16
- import "../../chunk-XQSTN54Y.js";
17
- import "../../chunk-P3F324UC.js";
18
- import "../../chunk-G4XA7IJM.js";
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-LWHVXXKQ.js";
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.0",
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": {