@lovable.dev/mcp-js 0.15.0 → 0.16.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.
@@ -42,6 +42,38 @@ function describeError(err) {
42
42
  return { value: String(err) };
43
43
  }
44
44
 
45
+ // src/metrics/config.ts
46
+ var DEFAULT_METRICS_ENDPOINT = "https://api.lovable.dev/v1/app-mcp-usage";
47
+ var METRICS_API_KEY_ENV_VAR = "LOVABLE_API_KEY";
48
+ var METRICS_SAMPLE_RATE = 1;
49
+ var METRICS_FLUSH_INTERVAL_MS = 5e3;
50
+ var METRICS_MAX_BATCH_SIZE = 50;
51
+ function assertMetricsEndpoint(endpoint) {
52
+ let url;
53
+ try {
54
+ url = new URL(endpoint);
55
+ } catch {
56
+ throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}`);
57
+ }
58
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
59
+ throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be http(s), got ${JSON.stringify(endpoint)}`);
60
+ }
61
+ }
62
+ function resolveMetricsConfig(config = true) {
63
+ const options = typeof config === "boolean" ? { enabled: config } : config;
64
+ const endpoint = options.endpoint ?? DEFAULT_METRICS_ENDPOINT;
65
+ assertMetricsEndpoint(endpoint);
66
+ return Object.freeze({
67
+ enabled: options.enabled ?? true,
68
+ endpoint,
69
+ headers: options.headers ?? {},
70
+ apiKeyEnvVar: METRICS_API_KEY_ENV_VAR,
71
+ sampleRate: METRICS_SAMPLE_RATE,
72
+ flushIntervalMs: METRICS_FLUSH_INTERVAL_MS,
73
+ maxBatchSize: METRICS_MAX_BATCH_SIZE
74
+ });
75
+ }
76
+
45
77
  // src/core/url.ts
46
78
  function trimTrailingSlash(value) {
47
79
  return value.replace(/\/+$/, "");
@@ -79,6 +111,8 @@ function parseSafeUrl(subject, raw, ErrorClass = Error) {
79
111
  }
80
112
 
81
113
  export {
114
+ DEFAULT_METRICS_ENDPOINT,
115
+ resolveMetricsConfig,
82
116
  trimTrailingSlash,
83
117
  parseSafeUrl,
84
118
  setLogLevel,
@@ -1,19 +1,18 @@
1
- import {
2
- createRecorderForRuntime,
3
- nowMs
4
- } from "./chunk-XQSTN54Y.js";
5
1
  import {
6
2
  ToolContext
7
- } from "./chunk-P3F324UC.js";
3
+ } from "./chunk-MA5H6PSF.js";
8
4
  import {
9
5
  corsPreflightResponse,
6
+ createRecorderForRuntime,
10
7
  createRequestAuthorizer,
8
+ nowMs,
9
+ reportInvocation,
11
10
  withCors
12
- } from "./chunk-G4XA7IJM.js";
11
+ } from "./chunk-H6BKTNJY.js";
13
12
  import {
14
13
  describeError,
15
14
  log
16
- } from "./chunk-QC3DXQTH.js";
15
+ } from "./chunk-DCWYK4CA.js";
17
16
 
18
17
  // src/protocols/mcp/protocol.ts
19
18
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -26,14 +25,24 @@ function adaptToolToSdkCallback(tool, auth, recorder) {
26
25
  try {
27
26
  result = await tool.handler(args, new ToolContext(auth));
28
27
  } catch {
29
- recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
28
+ reportInvocation(recorder, {
29
+ tool: tool.name,
30
+ method: "tools/call",
31
+ outcome: "handler_error",
32
+ durationMs: nowMs() - start
33
+ });
30
34
  return { content: [{ type: "text", text: "tool execution failed" }], isError: true };
31
35
  }
32
36
  if (result == null) {
33
- recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
37
+ reportInvocation(recorder, {
38
+ tool: tool.name,
39
+ method: "tools/call",
40
+ outcome: "handler_error",
41
+ durationMs: nowMs() - start
42
+ });
34
43
  return { content: [{ type: "text", text: `tool "${tool.name}" returned no result` }], isError: true };
35
44
  }
36
- recorder.record({
45
+ reportInvocation(recorder, {
37
46
  tool: tool.name,
38
47
  method: "tools/call",
39
48
  outcome: result.isError ? "tool_error" : "ok",
@@ -43,7 +52,7 @@ function adaptToolToSdkCallback(tool, auth, recorder) {
43
52
  };
44
53
  }
45
54
  function createMcpProtocolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
46
- const authorizer = createRequestAuthorizer(mcp, options);
55
+ const authorizer = createRequestAuthorizer(mcp, options, recorder);
47
56
  const handle = async (request) => {
48
57
  const authResult = await authorizer.authorize(request);
49
58
  if (!authResult.ok)
@@ -1,12 +1,17 @@
1
1
  import {
2
+ DEFAULT_METRICS_ENDPOINT,
2
3
  describeError,
3
4
  log,
4
5
  parseSafeUrl,
6
+ resolveMetricsConfig,
5
7
  trimTrailingSlash
6
- } from "./chunk-QC3DXQTH.js";
8
+ } from "./chunk-DCWYK4CA.js";
7
9
  import {
8
10
  OAUTH_PROTECTED_RESOURCE_METADATA_PATH
9
11
  } from "./chunk-6DXGZZA4.js";
12
+ import {
13
+ version
14
+ } from "./chunk-SKSKC747.js";
10
15
 
11
16
  // src/core/http.ts
12
17
  var JSON_HEADERS = { "Content-Type": "application/json" };
@@ -44,6 +49,188 @@ function resolveResourcePath(resourcePath, request) {
44
49
  return resourcePath;
45
50
  }
46
51
 
52
+ // src/metrics/otlp.ts
53
+ var SCOPE_NAME = "@lovable.dev/mcp-js";
54
+ var EVENT_NAME = "mcp.tool.invocation";
55
+ var SEVERITY_INFO = 9;
56
+ function strAttr(key, value) {
57
+ return { key, value: { stringValue: value } };
58
+ }
59
+ function intAttr(key, value) {
60
+ return { key, value: { intValue: String(Math.round(value)) } };
61
+ }
62
+ function toLogRecord(rec) {
63
+ const attributes = [
64
+ strAttr("event.name", EVENT_NAME),
65
+ strAttr("mcp.method", rec.method),
66
+ strAttr("mcp.outcome", rec.outcome),
67
+ intAttr("mcp.duration_ms", rec.durationMs)
68
+ ];
69
+ if (rec.tool !== null)
70
+ attributes.push(strAttr("mcp.tool", rec.tool));
71
+ if (rec.reqBytes !== void 0)
72
+ attributes.push(intAttr("mcp.request_size_bytes", rec.reqBytes));
73
+ if (rec.resBytes !== void 0)
74
+ attributes.push(intAttr("mcp.response_size_bytes", rec.resBytes));
75
+ return {
76
+ timeUnixNano: rec.timeUnixNano,
77
+ observedTimeUnixNano: rec.timeUnixNano,
78
+ severityNumber: SEVERITY_INFO,
79
+ severityText: "INFO",
80
+ body: { stringValue: EVENT_NAME },
81
+ attributes
82
+ };
83
+ }
84
+ function buildLogsPayload(records, server) {
85
+ return JSON.stringify({
86
+ resourceLogs: [
87
+ {
88
+ resource: {
89
+ attributes: [
90
+ strAttr("service.name", server.name),
91
+ strAttr("service.version", server.version),
92
+ strAttr("telemetry.sdk.name", SCOPE_NAME),
93
+ strAttr("telemetry.sdk.version", version),
94
+ strAttr("telemetry.sdk.language", "webjs")
95
+ ]
96
+ },
97
+ scopeLogs: [
98
+ {
99
+ scope: { name: SCOPE_NAME, version },
100
+ logRecords: records.map(toLogRecord)
101
+ }
102
+ ]
103
+ }
104
+ ]
105
+ });
106
+ }
107
+ function nowUnixNano() {
108
+ return `${Date.now()}000000`;
109
+ }
110
+
111
+ // src/metrics/recorder.ts
112
+ function nowMs() {
113
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
114
+ }
115
+ function reportInvocation(recorder, ev) {
116
+ log.info("tool.invoked", { tool: ev.tool, method: ev.method, outcome: ev.outcome, durationMs: ev.durationMs });
117
+ recorder.record(ev);
118
+ }
119
+ var NOOP_RECORDER = {
120
+ record() {
121
+ },
122
+ async flush() {
123
+ }
124
+ };
125
+ function createNoopRecorder() {
126
+ return NOOP_RECORDER;
127
+ }
128
+ function readRuntimeEnv(name) {
129
+ try {
130
+ const denoEnv = globalThis.Deno?.env;
131
+ const value = denoEnv?.get?.(name);
132
+ if (value)
133
+ return value;
134
+ } catch {
135
+ }
136
+ try {
137
+ if (typeof process !== "undefined") {
138
+ const value = process.env?.[name];
139
+ if (value)
140
+ return value;
141
+ }
142
+ } catch {
143
+ }
144
+ return void 0;
145
+ }
146
+ function probeWaitUntil() {
147
+ const fn = globalThis.EdgeRuntime?.waitUntil;
148
+ return typeof fn === "function" ? fn.bind(globalThis.EdgeRuntime) : void 0;
149
+ }
150
+ function createMetricsRecorder(ctx, deps = {}) {
151
+ const { config, server } = ctx;
152
+ if (!config.enabled)
153
+ return NOOP_RECORDER;
154
+ const doFetch = deps.fetch ?? globalThis.fetch;
155
+ if (!doFetch) {
156
+ log.warn("metrics.disabled_no_fetch", { endpoint: config.endpoint });
157
+ return NOOP_RECORDER;
158
+ }
159
+ const usesLovableEndpoint = config.endpoint === DEFAULT_METRICS_ENDPOINT;
160
+ const apiKey = usesLovableEndpoint ? (deps.getApiKey ?? (() => readRuntimeEnv(config.apiKeyEnvVar)))() : void 0;
161
+ if (usesLovableEndpoint && !apiKey) {
162
+ log.warn("metrics.disabled_no_api_key", { envVar: config.apiKeyEnvVar, endpoint: config.endpoint });
163
+ return NOOP_RECORDER;
164
+ }
165
+ const waitUntil = deps.waitUntil ?? probeWaitUntil();
166
+ const buffer = [];
167
+ let timer;
168
+ const schedule = (p) => {
169
+ if (waitUntil) {
170
+ try {
171
+ waitUntil(p);
172
+ return;
173
+ } catch (err) {
174
+ log.warn("metrics.wait_until_failed", describeError(err));
175
+ }
176
+ }
177
+ void p.catch(() => {
178
+ });
179
+ };
180
+ const headers = {};
181
+ if (!usesLovableEndpoint) {
182
+ for (const [key, value] of Object.entries(config.headers)) {
183
+ if (key.toLowerCase() !== "content-type")
184
+ headers[key] = value;
185
+ }
186
+ }
187
+ headers["content-type"] = "application/json";
188
+ if (apiKey)
189
+ headers.authorization = `Bearer ${apiKey}`;
190
+ const flush = async () => {
191
+ if (buffer.length === 0)
192
+ return;
193
+ const records = buffer.splice(0, buffer.length);
194
+ const dropped = records.length;
195
+ try {
196
+ const res = await doFetch(config.endpoint, {
197
+ method: "POST",
198
+ headers,
199
+ body: buildLogsPayload(records, server),
200
+ keepalive: true
201
+ });
202
+ if (!res.ok) {
203
+ log.warn("metrics.flush_rejected", { status: res.status, dropped, endpoint: config.endpoint });
204
+ }
205
+ } catch (err) {
206
+ log.warn("metrics.flush_failed", { ...describeError(err), dropped, endpoint: config.endpoint });
207
+ }
208
+ };
209
+ const ensureTimer = () => {
210
+ if (timer !== void 0)
211
+ return;
212
+ timer = setInterval(() => schedule(flush()), config.flushIntervalMs);
213
+ timer.unref?.();
214
+ };
215
+ return {
216
+ record(ev) {
217
+ if (config.sampleRate < 1 && Math.random() >= config.sampleRate)
218
+ return;
219
+ buffer.push({ ...ev, timeUnixNano: nowUnixNano() });
220
+ ensureTimer();
221
+ if (buffer.length >= config.maxBatchSize)
222
+ schedule(flush());
223
+ },
224
+ flush
225
+ };
226
+ }
227
+ function createRecorderForRuntime(mcp) {
228
+ const config = resolveMetricsConfig(mcp.metrics);
229
+ if (!config.enabled)
230
+ return createNoopRecorder();
231
+ return createMetricsRecorder({ config, server: { name: mcp.name, version: mcp.version } });
232
+ }
233
+
47
234
  // src/core/promise.ts
48
235
  function cachedPromise(load, label) {
49
236
  let settled = false;
@@ -409,12 +596,13 @@ function assertRequiredScopes(auth, context) {
409
596
  throw new OAuthTokenError(403, "insufficient_scope", "Additional OAuth scope is required");
410
597
  }
411
598
  }
412
- function createRequestAuthorizer(mcp, options = {}) {
599
+ function createRequestAuthorizer(mcp, options = {}, recorder) {
413
600
  const runtime = getOAuthRuntime(mcp, options);
414
601
  return {
415
602
  async authorize(request) {
416
603
  if (runtime.kind === "unconfigured")
417
604
  return { ok: true };
605
+ const startedAt = nowMs();
418
606
  const token = parseBearerToken(request);
419
607
  if (!token) {
420
608
  log.info("auth.no_bearer_token", { outcome: "401" });
@@ -427,6 +615,12 @@ function createRequestAuthorizer(mcp, options = {}) {
427
615
  } catch (err) {
428
616
  if (err instanceof OAuthConfigurationError) {
429
617
  log.error("auth.config_error", { ...describeError(err), outcome: "500" });
618
+ recorder?.record({
619
+ tool: null,
620
+ method: "authorize",
621
+ outcome: "auth_config_error",
622
+ durationMs: nowMs() - startedAt
623
+ });
430
624
  return { ok: false, response: oauthConfigurationErrorResponse() };
431
625
  }
432
626
  if (err instanceof OAuthTokenError) {
@@ -484,6 +678,9 @@ export {
484
678
  JSON_HEADERS,
485
679
  headResponse,
486
680
  methodNotAllowed,
681
+ nowMs,
682
+ reportInvocation,
683
+ createRecorderForRuntime,
487
684
  resolveProtectedResource,
488
685
  assertResourcePathShape,
489
686
  oauthConfigurationErrorResponse,
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  assertRestResourceBinding,
3
3
  corsPreflightResponse,
4
+ createRecorderForRuntime,
4
5
  createRequestAuthorizer,
5
6
  headResponse,
6
7
  methodNotAllowed,
7
8
  withCors
8
- } from "./chunk-G4XA7IJM.js";
9
+ } from "./chunk-H6BKTNJY.js";
9
10
 
10
11
  // src/protocols/rest/list-tools.ts
11
12
  import { objectFromShape } from "@modelcontextprotocol/sdk/server/zod-compat.js";
@@ -32,9 +33,9 @@ function buildMcpListing(mcp) {
32
33
  }))
33
34
  };
34
35
  }
35
- function createListToolsHandler(mcp, options = {}) {
36
+ function createListToolsHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
36
37
  assertRestResourceBinding(mcp, options);
37
- const authorizer = createRequestAuthorizer(mcp, options);
38
+ const authorizer = createRequestAuthorizer(mcp, options, recorder);
38
39
  const handle = async (request) => {
39
40
  const authResult = await authorizer.authorize(request);
40
41
  if (!authResult.ok)
@@ -41,40 +41,6 @@ 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
-
76
44
  export {
77
- DEFAULT_METRICS_ENDPOINT,
78
- resolveMetricsConfig,
79
45
  ToolContext
80
46
  };
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "0.15.0";
2
+ var version = "0.16.0";
3
3
 
4
4
  export {
5
5
  version
@@ -7,11 +7,11 @@ import {
7
7
  oauthConfigurationErrorResponse,
8
8
  resolveProtectedResource,
9
9
  withCors
10
- } from "./chunk-G4XA7IJM.js";
10
+ } from "./chunk-H6BKTNJY.js";
11
11
  import {
12
12
  describeError,
13
13
  log
14
- } from "./chunk-QC3DXQTH.js";
14
+ } from "./chunk-DCWYK4CA.js";
15
15
 
16
16
  // src/protocols/oauth-metadata.ts
17
17
  function notFound() {
@@ -1,18 +1,17 @@
1
- import {
2
- createRecorderForRuntime,
3
- nowMs
4
- } from "./chunk-XQSTN54Y.js";
5
1
  import {
6
2
  ToolContext
7
- } from "./chunk-P3F324UC.js";
3
+ } from "./chunk-MA5H6PSF.js";
8
4
  import {
9
5
  JSON_HEADERS,
10
6
  assertRestResourceBinding,
11
7
  corsPreflightResponse,
8
+ createRecorderForRuntime,
12
9
  createRequestAuthorizer,
13
10
  methodNotAllowed,
11
+ nowMs,
12
+ reportInvocation,
14
13
  withCors
15
- } from "./chunk-G4XA7IJM.js";
14
+ } from "./chunk-H6BKTNJY.js";
16
15
 
17
16
  // src/protocols/rest/invoke-tool.ts
18
17
  import { getParseErrorMessage, objectFromShape, safeParseAsync } from "@modelcontextprotocol/sdk/server/zod-compat.js";
@@ -30,7 +29,7 @@ function isEmptyArgs(value) {
30
29
  }
31
30
  function createInvokeToolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
32
31
  assertRestResourceBinding(mcp, options);
33
- const authorizer = createRequestAuthorizer(mcp, options);
32
+ const authorizer = createRequestAuthorizer(mcp, options, recorder);
34
33
  const handle = async (request, toolName) => {
35
34
  const authResult = await authorizer.authorize(request);
36
35
  if (!authResult.ok)
@@ -88,20 +87,30 @@ function createInvokeToolHandler(mcp, options = {}, recorder = createRecorderFor
88
87
  try {
89
88
  result = await tool.handler(args, new ToolContext(authResult.auth));
90
89
  } catch {
91
- recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
90
+ reportInvocation(recorder, {
91
+ tool: tool.name,
92
+ method: "tools/call",
93
+ outcome: "handler_error",
94
+ durationMs: nowMs() - start
95
+ });
92
96
  return new Response(JSON.stringify({ error: "handler threw", tool: toolName }), {
93
97
  status: 500,
94
98
  headers: JSON_HEADERS
95
99
  });
96
100
  }
97
101
  if (result == null) {
98
- recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
102
+ reportInvocation(recorder, {
103
+ tool: tool.name,
104
+ method: "tools/call",
105
+ outcome: "handler_error",
106
+ durationMs: nowMs() - start
107
+ });
99
108
  return new Response(JSON.stringify({ error: `tool "${toolName}" returned no result` }), {
100
109
  status: 500,
101
110
  headers: JSON_HEADERS
102
111
  });
103
112
  }
104
- recorder.record({
113
+ reportInvocation(recorder, {
105
114
  tool: tool.name,
106
115
  method: "tools/call",
107
116
  outcome: result.isError ? "tool_error" : "ok",
@@ -13,7 +13,7 @@ function isFileMissing(err) {
13
13
  }
14
14
 
15
15
  // package.json
16
- var version = "0.15.0";
16
+ var version = "0.16.0";
17
17
 
18
18
  // src/protocols/rest/list-tools.ts
19
19
  var import_zod_compat = require("@modelcontextprotocol/sdk/server/zod-compat.js");
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  buildMcpListing
4
- } from "../chunk-VNRQPA4K.js";
5
- import "../chunk-G4XA7IJM.js";
6
- import "../chunk-QC3DXQTH.js";
4
+ } from "../chunk-I5CAQUQI.js";
5
+ import "../chunk-H6BKTNJY.js";
6
+ import "../chunk-DCWYK4CA.js";
7
7
  import "../chunk-6DXGZZA4.js";
8
8
  import {
9
9
  isFileMissing
10
10
  } from "../chunk-Y3ZFPEQH.js";
11
11
  import {
12
12
  version
13
- } from "../chunk-LWHVXXKQ.js";
13
+ } from "../chunk-SKSKC747.js";
14
14
 
15
15
  // src/manifest/io.ts
16
16
  import { randomUUID } from "crypto";
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
- ToolContext,
3
- resolveMetricsConfig
4
- } from "./chunk-P3F324UC.js";
2
+ ToolContext
3
+ } from "./chunk-MA5H6PSF.js";
5
4
  import {
6
5
  parseSafeUrl,
6
+ resolveMetricsConfig,
7
7
  setLogLevel
8
- } from "./chunk-QC3DXQTH.js";
8
+ } from "./chunk-DCWYK4CA.js";
9
9
 
10
10
  // src/core/define.ts
11
11
  function assertUniqueNames(mcp) {