@lovable.dev/mcp-js 0.13.0 → 0.15.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.
Files changed (39) hide show
  1. package/README.md +31 -1
  2. package/dist/{chunk-SIITHGUD.js → chunk-LWHVXXKQ.js} +1 -1
  3. package/dist/{chunk-MA5H6PSF.js → chunk-P3F324UC.js} +34 -0
  4. package/dist/{chunk-WLNT2FX7.js → chunk-RHDEW56Y.js} +18 -4
  5. package/dist/{chunk-YWLMMDET.js → chunk-T2RED2TG.js} +15 -2
  6. package/dist/chunk-XQSTN54Y.js +189 -0
  7. package/dist/cli/extract-manifest.cjs +1 -1
  8. package/dist/cli/extract-manifest.js +3 -3
  9. package/dist/index.cjs +33 -0
  10. package/dist/index.d.cts +2 -2
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.js +4 -2
  13. package/dist/protocols/mcp/index.cjs +221 -3
  14. package/dist/protocols/mcp/index.d.cts +3 -2
  15. package/dist/protocols/mcp/index.d.ts +3 -2
  16. package/dist/protocols/mcp/index.js +4 -2
  17. package/dist/protocols/oauth-metadata.d.cts +1 -1
  18. package/dist/protocols/oauth-metadata.d.ts +1 -1
  19. package/dist/protocols/rest/index.cjs +218 -1
  20. package/dist/protocols/rest/index.d.cts +3 -2
  21. package/dist/protocols/rest/index.d.ts +3 -2
  22. package/dist/protocols/rest/index.js +4 -2
  23. package/dist/recorder-B-eJU4Qu.d.ts +21 -0
  24. package/dist/stacks/supabase/index.cjs +250 -22
  25. package/dist/stacks/supabase/index.d.cts +1 -1
  26. package/dist/stacks/supabase/index.d.ts +1 -1
  27. package/dist/stacks/supabase/index.js +10 -5
  28. package/dist/stacks/supabase/vite.cjs +1 -1
  29. package/dist/stacks/supabase/vite.js +3 -3
  30. package/dist/stacks/tanstack/index.cjs +231 -4
  31. package/dist/stacks/tanstack/index.d.cts +1 -1
  32. package/dist/stacks/tanstack/index.d.ts +1 -1
  33. package/dist/stacks/tanstack/index.js +5 -3
  34. package/dist/stacks/tanstack/vite.cjs +36 -22
  35. package/dist/stacks/tanstack/vite.d.cts +9 -7
  36. package/dist/stacks/tanstack/vite.d.ts +9 -7
  37. package/dist/stacks/tanstack/vite.js +36 -22
  38. package/dist/{types-BanzncFh.d.ts → types-COY42xux.d.ts} +30 -1
  39. package/package.json +1 -1
package/README.md CHANGED
@@ -49,7 +49,9 @@ That's the whole authoring surface. No imperative server construction, no route
49
49
  | `GET /.mcp/list-tools` | `src/routes/[.mcp]/list-tools.ts` | Tool catalog with JSON Schemas |
50
50
  | `POST /.mcp/invoke-tool/<tool>` | `src/routes/[.mcp]/invoke-tool/$tool.ts` | REST dispatcher; one handler call per tool |
51
51
 
52
- MCP (`POST /mcp`) is the public wire format clients speak directly. REST (`/.mcp/list-tools`, `/.mcp/invoke-tool/<tool>`) is internal RPC — only an upstream MCP proxy calls it; never a browser or a hand-rolled client. All runtime routes import the same `defineMcp` result, so they stay in sync on which tools exist and which auth policy protects them. If `defineMcp({ auth: ... })` is omitted, the handlers stay unauthenticated; if OAuth auth is configured, MCP and REST both require the proxy/client to pass `Authorization: Bearer <token>`. CORS and rate limiting still belong at the app host or edge.
52
+ The table shows the default `path: "/mcp"`. The REST companions co-locate under the parent of the configured `path`, so `mcpPlugin({ path: "/api/public/mcp" })` emits them at `/api/public/.mcp/list-tools` and `/api/public/.mcp/invoke-tool/<tool>` (file: `src/routes/api/public/[.mcp]/...`). Pass `restRoutes: false` to drop them.
53
+
54
+ MCP (`POST <path>`, `/mcp` by default) is the public wire format clients speak directly. REST (the `.mcp/*` companions, co-located under the `path` parent) is internal RPC — only an upstream MCP proxy calls it; never a browser or a hand-rolled client. All runtime routes import the same `defineMcp` result, so they stay in sync on which tools exist and which auth policy protects them. If `defineMcp({ auth: ... })` is omitted, the handlers stay unauthenticated; if OAuth auth is configured, MCP and REST both require the proxy/client to pass `Authorization: Bearer <token>`. CORS and rate limiting still belong at the app host or edge.
53
55
 
54
56
  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
57
 
@@ -215,6 +217,34 @@ LOVABLE_MCP_LOG_LEVEL=debug
215
217
 
216
218
  A `500` on an OAuth-protected route is always one of two causes, logged at `error`: an `OAuthConfigurationError` from issuer-metadata discovery or a JWKS *fetch* failure (`oauth.discovery.config_error` / `oauth.jwks.fetch_failed` → `auth.config_error`), or a transport-level fault in the MCP handler (`mcp.transport_error`). Auth outcomes are logged at `info`: a granted request as `oauth.verify.ok`, and a `401`/`403` as `auth.token_rejected` (with the `jose` reason) or `auth.no_bearer_token` — so a request rejected for insufficient scope shows both `oauth.verify.ok` and the `auth.token_rejected` that follows it.
217
219
 
220
+ ## Usage metrics
221
+
222
+ Each MCP server records per-invocation telemetry — tool name, JSON-RPC method, outcome (`ok` / `handler_error` / `transport_error`), and latency — and POSTs it in fire-and-forget batches as **OTLP/HTTP JSON logs**. No tool arguments or response payloads are ever captured. Emission is **on by default**, self-disables at runtime when `LOVABLE_API_KEY` is absent (so a local `vite dev` stays silent), and degrades silently — a failed flush never blocks or fails a tool call.
223
+
224
+ Configure it on the MCP definition:
225
+
226
+ ```ts
227
+ import { defineMcp } from "@lovable.dev/mcp-js";
228
+
229
+ export default defineMcp({
230
+ name: "my-app-mcp",
231
+ // ...
232
+ tools: [],
233
+ metrics: {
234
+ endpoint: "https://your-collector.example.com/v1/logs",
235
+ headers: { authorization: "Bearer your-token" },
236
+ },
237
+ });
238
+ ```
239
+
240
+ | Use case | Config |
241
+ | --- | --- |
242
+ | **Default (recommended)** | Omit `metrics`, or `metrics: true` — batches to Lovable, authenticated with `LOVABLE_API_KEY` |
243
+ | **Opt out entirely** | `metrics: false` |
244
+ | **Self-hosted collector** | `metrics: { endpoint, headers? }` — your own OTLP/HTTP (JSON) endpoint |
245
+
246
+ **Key scoping:** `LOVABLE_API_KEY` is read and sent (as the bearer) **only** for the default Lovable endpoint; a custom collector is reached with your own `headers` and never receives the workspace key. `headers` are baked into the build output, so source any token from your own build-time env. `content-type` is forced to `application/json` and can't be overridden.
247
+
218
248
  ## Supabase Edge Functions
219
249
 
220
250
  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.
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "0.13.0";
2
+ var version = "0.15.0";
3
3
 
4
4
  export {
5
5
  version
@@ -41,6 +41,40 @@ var ToolContext = class {
41
41
  }
42
42
  };
43
43
 
44
+ // src/metrics/config.ts
45
+ var DEFAULT_METRICS_ENDPOINT = "https://api.lovable.dev/v1/app-mcp-usage";
46
+ var METRICS_API_KEY_ENV_VAR = "LOVABLE_API_KEY";
47
+ var METRICS_SAMPLE_RATE = 1;
48
+ var METRICS_FLUSH_INTERVAL_MS = 5e3;
49
+ var METRICS_MAX_BATCH_SIZE = 50;
50
+ function assertMetricsEndpoint(endpoint) {
51
+ let url;
52
+ try {
53
+ url = new URL(endpoint);
54
+ } catch {
55
+ throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}`);
56
+ }
57
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
58
+ throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be http(s), got ${JSON.stringify(endpoint)}`);
59
+ }
60
+ }
61
+ function resolveMetricsConfig(config = true) {
62
+ const options = typeof config === "boolean" ? { enabled: config } : config;
63
+ const endpoint = options.endpoint ?? DEFAULT_METRICS_ENDPOINT;
64
+ assertMetricsEndpoint(endpoint);
65
+ return Object.freeze({
66
+ enabled: options.enabled ?? true,
67
+ endpoint,
68
+ headers: options.headers ?? {},
69
+ apiKeyEnvVar: METRICS_API_KEY_ENV_VAR,
70
+ sampleRate: METRICS_SAMPLE_RATE,
71
+ flushIntervalMs: METRICS_FLUSH_INTERVAL_MS,
72
+ maxBatchSize: METRICS_MAX_BATCH_SIZE
73
+ });
74
+ }
75
+
44
76
  export {
77
+ DEFAULT_METRICS_ENDPOINT,
78
+ resolveMetricsConfig,
45
79
  ToolContext
46
80
  };
@@ -1,6 +1,10 @@
1
+ import {
2
+ createRecorderForRuntime,
3
+ nowMs
4
+ } from "./chunk-XQSTN54Y.js";
1
5
  import {
2
6
  ToolContext
3
- } from "./chunk-MA5H6PSF.js";
7
+ } from "./chunk-P3F324UC.js";
4
8
  import {
5
9
  corsPreflightResponse,
6
10
  createRequestAuthorizer,
@@ -14,22 +18,31 @@ import {
14
18
  // src/protocols/mcp/protocol.ts
15
19
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
20
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
17
- function adaptToolToSdkCallback(tool, auth) {
21
+ function adaptToolToSdkCallback(tool, auth, recorder) {
18
22
  return async (first) => {
19
23
  const args = tool.inputSchema ? first ?? {} : {};
24
+ const start = nowMs();
20
25
  let result;
21
26
  try {
22
27
  result = await tool.handler(args, new ToolContext(auth));
23
28
  } catch {
29
+ recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
24
30
  return { content: [{ type: "text", text: "tool execution failed" }], isError: true };
25
31
  }
26
32
  if (result == null) {
33
+ recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
27
34
  return { content: [{ type: "text", text: `tool "${tool.name}" returned no result` }], isError: true };
28
35
  }
36
+ recorder.record({
37
+ tool: tool.name,
38
+ method: "tools/call",
39
+ outcome: result.isError ? "tool_error" : "ok",
40
+ durationMs: nowMs() - start
41
+ });
29
42
  return { content: result.content ?? [], structuredContent: result.structuredContent, isError: result.isError };
30
43
  };
31
44
  }
32
- function createMcpProtocolHandler(mcp, options = {}) {
45
+ function createMcpProtocolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
33
46
  const authorizer = createRequestAuthorizer(mcp, options);
34
47
  const handle = async (request) => {
35
48
  const authResult = await authorizer.authorize(request);
@@ -50,7 +63,7 @@ function createMcpProtocolHandler(mcp, options = {}) {
50
63
  outputSchema: tool.outputSchema,
51
64
  annotations: tool.annotations
52
65
  },
53
- adaptToolToSdkCallback(tool, authResult.auth)
66
+ adaptToolToSdkCallback(tool, authResult.auth, recorder)
54
67
  );
55
68
  }
56
69
  const transport = new WebStandardStreamableHTTPServerTransport({
@@ -59,6 +72,7 @@ function createMcpProtocolHandler(mcp, options = {}) {
59
72
  await server.connect(transport);
60
73
  return await transport.handleRequest(request);
61
74
  } catch (err) {
75
+ recorder.record({ tool: null, method: "transport", outcome: "transport_error", durationMs: 0 });
62
76
  log.error("mcp.transport_error", { ...describeError(err), outcome: "500 internal error" });
63
77
  return Response.json(
64
78
  { jsonrpc: "2.0", id: null, error: { code: -32603, message: "internal error" } },
@@ -1,6 +1,10 @@
1
+ import {
2
+ createRecorderForRuntime,
3
+ nowMs
4
+ } from "./chunk-XQSTN54Y.js";
1
5
  import {
2
6
  ToolContext
3
- } from "./chunk-MA5H6PSF.js";
7
+ } from "./chunk-P3F324UC.js";
4
8
  import {
5
9
  JSON_HEADERS,
6
10
  assertRestResourceBinding,
@@ -24,7 +28,7 @@ function isEmptyArgs(value) {
24
28
  return false;
25
29
  return Object.keys(value).length === 0;
26
30
  }
27
- function createInvokeToolHandler(mcp, options = {}) {
31
+ function createInvokeToolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
28
32
  assertRestResourceBinding(mcp, options);
29
33
  const authorizer = createRequestAuthorizer(mcp, options);
30
34
  const handle = async (request, toolName) => {
@@ -80,20 +84,29 @@ function createInvokeToolHandler(mcp, options = {}) {
80
84
  });
81
85
  }
82
86
  let result;
87
+ const start = nowMs();
83
88
  try {
84
89
  result = await tool.handler(args, new ToolContext(authResult.auth));
85
90
  } catch {
91
+ recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
86
92
  return new Response(JSON.stringify({ error: "handler threw", tool: toolName }), {
87
93
  status: 500,
88
94
  headers: JSON_HEADERS
89
95
  });
90
96
  }
91
97
  if (result == null) {
98
+ recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
92
99
  return new Response(JSON.stringify({ error: `tool "${toolName}" returned no result` }), {
93
100
  status: 500,
94
101
  headers: JSON_HEADERS
95
102
  });
96
103
  }
104
+ recorder.record({
105
+ tool: tool.name,
106
+ method: "tools/call",
107
+ outcome: result.isError ? "tool_error" : "ok",
108
+ durationMs: nowMs() - start
109
+ });
97
110
  return Response.json({
98
111
  content: result.content ?? [],
99
112
  structuredContent: result.structuredContent,
@@ -0,0 +1,189 @@
1
+ import {
2
+ DEFAULT_METRICS_ENDPOINT,
3
+ resolveMetricsConfig
4
+ } from "./chunk-P3F324UC.js";
5
+ import {
6
+ describeError,
7
+ log
8
+ } from "./chunk-QC3DXQTH.js";
9
+ import {
10
+ version
11
+ } from "./chunk-LWHVXXKQ.js";
12
+
13
+ // src/metrics/otlp.ts
14
+ var SCOPE_NAME = "@lovable.dev/mcp-js";
15
+ var EVENT_NAME = "mcp.tool.invocation";
16
+ var SEVERITY_INFO = 9;
17
+ function strAttr(key, value) {
18
+ return { key, value: { stringValue: value } };
19
+ }
20
+ function intAttr(key, value) {
21
+ return { key, value: { intValue: String(Math.round(value)) } };
22
+ }
23
+ function toLogRecord(rec) {
24
+ const attributes = [
25
+ strAttr("event.name", EVENT_NAME),
26
+ strAttr("mcp.method", rec.method),
27
+ strAttr("mcp.outcome", rec.outcome),
28
+ intAttr("mcp.duration_ms", rec.durationMs)
29
+ ];
30
+ if (rec.tool !== null)
31
+ attributes.push(strAttr("mcp.tool", rec.tool));
32
+ if (rec.reqBytes !== void 0)
33
+ attributes.push(intAttr("mcp.request_size_bytes", rec.reqBytes));
34
+ if (rec.resBytes !== void 0)
35
+ attributes.push(intAttr("mcp.response_size_bytes", rec.resBytes));
36
+ return {
37
+ timeUnixNano: rec.timeUnixNano,
38
+ observedTimeUnixNano: rec.timeUnixNano,
39
+ severityNumber: SEVERITY_INFO,
40
+ severityText: "INFO",
41
+ body: { stringValue: EVENT_NAME },
42
+ attributes
43
+ };
44
+ }
45
+ function buildLogsPayload(records, server) {
46
+ return JSON.stringify({
47
+ resourceLogs: [
48
+ {
49
+ resource: {
50
+ attributes: [
51
+ strAttr("service.name", server.name),
52
+ strAttr("service.version", server.version),
53
+ strAttr("telemetry.sdk.name", SCOPE_NAME),
54
+ strAttr("telemetry.sdk.version", version),
55
+ strAttr("telemetry.sdk.language", "webjs")
56
+ ]
57
+ },
58
+ scopeLogs: [
59
+ {
60
+ scope: { name: SCOPE_NAME, version },
61
+ logRecords: records.map(toLogRecord)
62
+ }
63
+ ]
64
+ }
65
+ ]
66
+ });
67
+ }
68
+ function nowUnixNano() {
69
+ return `${Date.now()}000000`;
70
+ }
71
+
72
+ // src/metrics/recorder.ts
73
+ function nowMs() {
74
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
75
+ }
76
+ var NOOP_RECORDER = {
77
+ record() {
78
+ },
79
+ async flush() {
80
+ }
81
+ };
82
+ function createNoopRecorder() {
83
+ return NOOP_RECORDER;
84
+ }
85
+ function readRuntimeEnv(name) {
86
+ try {
87
+ const denoEnv = globalThis.Deno?.env;
88
+ const value = denoEnv?.get?.(name);
89
+ if (value)
90
+ return value;
91
+ } catch {
92
+ }
93
+ try {
94
+ if (typeof process !== "undefined") {
95
+ const value = process.env?.[name];
96
+ if (value)
97
+ return value;
98
+ }
99
+ } catch {
100
+ }
101
+ return void 0;
102
+ }
103
+ function probeWaitUntil() {
104
+ const fn = globalThis.EdgeRuntime?.waitUntil;
105
+ return typeof fn === "function" ? fn.bind(globalThis.EdgeRuntime) : void 0;
106
+ }
107
+ function createMetricsRecorder(ctx, deps = {}) {
108
+ const { config, server } = ctx;
109
+ if (!config.enabled)
110
+ return NOOP_RECORDER;
111
+ const doFetch = deps.fetch ?? globalThis.fetch;
112
+ if (!doFetch)
113
+ return NOOP_RECORDER;
114
+ const usesLovableEndpoint = config.endpoint === DEFAULT_METRICS_ENDPOINT;
115
+ const apiKey = usesLovableEndpoint ? (deps.getApiKey ?? (() => readRuntimeEnv(config.apiKeyEnvVar)))() : void 0;
116
+ if (usesLovableEndpoint && !apiKey) {
117
+ log.debug("metrics.disabled_no_api_key", { envVar: config.apiKeyEnvVar });
118
+ return NOOP_RECORDER;
119
+ }
120
+ const waitUntil = deps.waitUntil ?? probeWaitUntil();
121
+ const buffer = [];
122
+ let timer;
123
+ const schedule = (p) => {
124
+ if (waitUntil) {
125
+ try {
126
+ waitUntil(p);
127
+ return;
128
+ } catch {
129
+ }
130
+ }
131
+ void p.catch(() => {
132
+ });
133
+ };
134
+ const headers = {};
135
+ if (!usesLovableEndpoint) {
136
+ for (const [key, value] of Object.entries(config.headers)) {
137
+ if (key.toLowerCase() !== "content-type")
138
+ headers[key] = value;
139
+ }
140
+ }
141
+ headers["content-type"] = "application/json";
142
+ if (apiKey)
143
+ headers.authorization = `Bearer ${apiKey}`;
144
+ const flush = async () => {
145
+ if (buffer.length === 0)
146
+ return;
147
+ const records = buffer.splice(0, buffer.length);
148
+ try {
149
+ const res = await doFetch(config.endpoint, {
150
+ method: "POST",
151
+ headers,
152
+ body: buildLogsPayload(records, server),
153
+ keepalive: true
154
+ });
155
+ if (!res.ok)
156
+ log.debug("metrics.flush_rejected", { status: res.status });
157
+ } catch (err) {
158
+ log.debug("metrics.flush_failed", describeError(err));
159
+ }
160
+ };
161
+ const ensureTimer = () => {
162
+ if (timer !== void 0)
163
+ return;
164
+ timer = setInterval(() => schedule(flush()), config.flushIntervalMs);
165
+ timer.unref?.();
166
+ };
167
+ return {
168
+ record(ev) {
169
+ if (config.sampleRate < 1 && Math.random() >= config.sampleRate)
170
+ return;
171
+ buffer.push({ ...ev, timeUnixNano: nowUnixNano() });
172
+ ensureTimer();
173
+ if (buffer.length >= config.maxBatchSize)
174
+ schedule(flush());
175
+ },
176
+ flush
177
+ };
178
+ }
179
+ function createRecorderForRuntime(mcp) {
180
+ const config = resolveMetricsConfig(mcp.metrics);
181
+ if (!config.enabled)
182
+ return createNoopRecorder();
183
+ return createMetricsRecorder({ config, server: { name: mcp.name, version: mcp.version } });
184
+ }
185
+
186
+ export {
187
+ nowMs,
188
+ createRecorderForRuntime
189
+ };
@@ -13,7 +13,7 @@ function isFileMissing(err) {
13
13
  }
14
14
 
15
15
  // package.json
16
- var version = "0.13.0";
16
+ var version = "0.15.0";
17
17
 
18
18
  // src/protocols/rest/list-tools.ts
19
19
  var import_zod_compat = require("@modelcontextprotocol/sdk/server/zod-compat.js");
@@ -5,12 +5,12 @@ import {
5
5
  import "../chunk-G4XA7IJM.js";
6
6
  import "../chunk-QC3DXQTH.js";
7
7
  import "../chunk-6DXGZZA4.js";
8
- import {
9
- version
10
- } from "../chunk-SIITHGUD.js";
11
8
  import {
12
9
  isFileMissing
13
10
  } from "../chunk-Y3ZFPEQH.js";
11
+ import {
12
+ version
13
+ } from "../chunk-LWHVXXKQ.js";
14
14
 
15
15
  // src/manifest/io.ts
16
16
  import { randomUUID } from "crypto";
package/dist/index.cjs CHANGED
@@ -28,6 +28,38 @@ __export(src_exports, {
28
28
  });
29
29
  module.exports = __toCommonJS(src_exports);
30
30
 
31
+ // src/metrics/config.ts
32
+ var DEFAULT_METRICS_ENDPOINT = "https://api.lovable.dev/v1/app-mcp-usage";
33
+ var METRICS_API_KEY_ENV_VAR = "LOVABLE_API_KEY";
34
+ var METRICS_SAMPLE_RATE = 1;
35
+ var METRICS_FLUSH_INTERVAL_MS = 5e3;
36
+ var METRICS_MAX_BATCH_SIZE = 50;
37
+ function assertMetricsEndpoint(endpoint) {
38
+ let url;
39
+ try {
40
+ url = new URL(endpoint);
41
+ } catch {
42
+ throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}`);
43
+ }
44
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
45
+ throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be http(s), got ${JSON.stringify(endpoint)}`);
46
+ }
47
+ }
48
+ function resolveMetricsConfig(config = true) {
49
+ const options = typeof config === "boolean" ? { enabled: config } : config;
50
+ const endpoint = options.endpoint ?? DEFAULT_METRICS_ENDPOINT;
51
+ assertMetricsEndpoint(endpoint);
52
+ return Object.freeze({
53
+ enabled: options.enabled ?? true,
54
+ endpoint,
55
+ headers: options.headers ?? {},
56
+ apiKeyEnvVar: METRICS_API_KEY_ENV_VAR,
57
+ sampleRate: METRICS_SAMPLE_RATE,
58
+ flushIntervalMs: METRICS_FLUSH_INTERVAL_MS,
59
+ maxBatchSize: METRICS_MAX_BATCH_SIZE
60
+ });
61
+ }
62
+
31
63
  // src/core/url.ts
32
64
  function isLocalHTTPHost(hostname) {
33
65
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
@@ -202,6 +234,7 @@ function defineMcp(def) {
202
234
  assertUniqueNames(def);
203
235
  if (def.auth)
204
236
  assertOAuthConfig(def.auth);
237
+ resolveMetricsConfig(def.metrics);
205
238
  freezeAuth(def.auth);
206
239
  Object.freeze(def.tools);
207
240
  for (const tool of def.tools)
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, j as ToolDefinition, M as McpAuthConfig } from './types-BanzncFh.js';
2
- export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink, f as ResourceLinkIcon, T as TextContent, g as ToolAnnotations, h as ToolContent, i as ToolContext, k as ToolHandlerResult, l as ZodSchema } from './types-BanzncFh.js';
1
+ import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, l as ToolDefinition, M as McpAuthConfig } from './types-COY42xux.js';
2
+ export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, f as MetricsConfig, g as MetricsOptions, R as ResourceLink, h as ResourceLinkIcon, T as TextContent, i as ToolAnnotations, j as ToolContent, k as ToolContext, m as ToolHandlerResult, n as ZodSchema } from './types-COY42xux.js';
3
3
  import 'zod';
4
4
 
5
5
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, j as ToolDefinition, M as McpAuthConfig } from './types-BanzncFh.js';
2
- export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink, f as ResourceLinkIcon, T as TextContent, g as ToolAnnotations, h as ToolContent, i as ToolContext, k as ToolHandlerResult, l as ZodSchema } from './types-BanzncFh.js';
1
+ import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, l as ToolDefinition, M as McpAuthConfig } from './types-COY42xux.js';
2
+ export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, f as MetricsConfig, g as MetricsOptions, R as ResourceLink, h as ResourceLinkIcon, T as TextContent, i as ToolAnnotations, j as ToolContent, k as ToolContext, m as ToolHandlerResult, n as ZodSchema } from './types-COY42xux.js';
3
3
  import 'zod';
4
4
 
5
5
  /**
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
- ToolContext
3
- } from "./chunk-MA5H6PSF.js";
2
+ ToolContext,
3
+ resolveMetricsConfig
4
+ } from "./chunk-P3F324UC.js";
4
5
  import {
5
6
  parseSafeUrl,
6
7
  setLogLevel
@@ -147,6 +148,7 @@ function defineMcp(def) {
147
148
  assertUniqueNames(def);
148
149
  if (def.auth)
149
150
  assertOAuthConfig(def.auth);
151
+ resolveMetricsConfig(def.metrics);
150
152
  freezeAuth(def.auth);
151
153
  Object.freeze(def.tools);
152
154
  for (const tool of def.tools)