@lovable.dev/mcp-js 0.14.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 (35) hide show
  1. package/README.md +28 -0
  2. package/dist/{chunk-NOFPRSSI.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/{types-BanzncFh.d.ts → types-COY42xux.d.ts} +30 -1
  35. package/package.json +1 -1
@@ -649,23 +649,240 @@ function corsPreflightResponse(allowMethods) {
649
649
  });
650
650
  }
651
651
 
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
+
652
860
  // src/protocols/mcp/protocol.ts
653
- function adaptToolToSdkCallback(tool, auth) {
861
+ function adaptToolToSdkCallback(tool, auth, recorder) {
654
862
  return async (first) => {
655
863
  const args = tool.inputSchema ? first ?? {} : {};
864
+ const start = nowMs();
656
865
  let result;
657
866
  try {
658
867
  result = await tool.handler(args, new ToolContext(auth));
659
868
  } catch {
869
+ recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
660
870
  return { content: [{ type: "text", text: "tool execution failed" }], isError: true };
661
871
  }
662
872
  if (result == null) {
873
+ recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
663
874
  return { content: [{ type: "text", text: `tool "${tool.name}" returned no result` }], isError: true };
664
875
  }
876
+ recorder.record({
877
+ tool: tool.name,
878
+ method: "tools/call",
879
+ outcome: result.isError ? "tool_error" : "ok",
880
+ durationMs: nowMs() - start
881
+ });
665
882
  return { content: result.content ?? [], structuredContent: result.structuredContent, isError: result.isError };
666
883
  };
667
884
  }
668
- function createMcpProtocolHandler(mcp, options = {}) {
885
+ function createMcpProtocolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
669
886
  const authorizer = createRequestAuthorizer(mcp, options);
670
887
  const handle = async (request) => {
671
888
  const authResult = await authorizer.authorize(request);
@@ -686,7 +903,7 @@ function createMcpProtocolHandler(mcp, options = {}) {
686
903
  outputSchema: tool.outputSchema,
687
904
  annotations: tool.annotations
688
905
  },
689
- adaptToolToSdkCallback(tool, authResult.auth)
906
+ adaptToolToSdkCallback(tool, authResult.auth, recorder)
690
907
  );
691
908
  }
692
909
  const transport = new import_webStandardStreamableHttp.WebStandardStreamableHTTPServerTransport({
@@ -695,6 +912,7 @@ function createMcpProtocolHandler(mcp, options = {}) {
695
912
  await server.connect(transport);
696
913
  return await transport.handleRequest(request);
697
914
  } catch (err) {
915
+ recorder.record({ tool: null, method: "transport", outcome: "transport_error", durationMs: 0 });
698
916
  log.error("mcp.transport_error", { ...describeError(err), outcome: "500 internal error" });
699
917
  return Response.json(
700
918
  { jsonrpc: "2.0", id: null, error: { code: -32603, message: "internal error" } },
@@ -828,7 +1046,7 @@ function isEmptyArgs(value) {
828
1046
  return false;
829
1047
  return Object.keys(value).length === 0;
830
1048
  }
831
- function createInvokeToolHandler(mcp, options = {}) {
1049
+ function createInvokeToolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
832
1050
  assertRestResourceBinding(mcp, options);
833
1051
  const authorizer = createRequestAuthorizer(mcp, options);
834
1052
  const handle = async (request, toolName) => {
@@ -884,20 +1102,29 @@ function createInvokeToolHandler(mcp, options = {}) {
884
1102
  });
885
1103
  }
886
1104
  let result;
1105
+ const start = nowMs();
887
1106
  try {
888
1107
  result = await tool.handler(args, new ToolContext(authResult.auth));
889
1108
  } catch {
1109
+ recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
890
1110
  return new Response(JSON.stringify({ error: "handler threw", tool: toolName }), {
891
1111
  status: 500,
892
1112
  headers: JSON_HEADERS
893
1113
  });
894
1114
  }
895
1115
  if (result == null) {
1116
+ recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
896
1117
  return new Response(JSON.stringify({ error: `tool "${toolName}" returned no result` }), {
897
1118
  status: 500,
898
1119
  headers: JSON_HEADERS
899
1120
  });
900
1121
  }
1122
+ recorder.record({
1123
+ tool: tool.name,
1124
+ method: "tools/call",
1125
+ outcome: result.isError ? "tool_error" : "ok",
1126
+ durationMs: nowMs() - start
1127
+ });
901
1128
  return Response.json({
902
1129
  content: result.content ?? [],
903
1130
  structuredContent: result.structuredContent,
@@ -1,5 +1,5 @@
1
1
  import { M as McpRuntimeOptions } from '../../authorize-BMeXd_Sh.js';
2
- import { d as McpDefinition } from '../../types-BanzncFh.js';
2
+ import { d as McpDefinition } from '../../types-COY42xux.js';
3
3
  import 'zod';
4
4
 
5
5
  interface TanStackRouteCtx {
@@ -1,5 +1,5 @@
1
1
  import { M as McpRuntimeOptions } from '../../authorize-BMeXd_Sh.js';
2
- import { d as McpDefinition } from '../../types-BanzncFh.js';
2
+ import { d as McpDefinition } from '../../types-COY42xux.js';
3
3
  import 'zod';
4
4
 
5
5
  interface TanStackRouteCtx {
@@ -3,20 +3,22 @@ import {
3
3
  } from "../../chunk-UQK5UO6C.js";
4
4
  import {
5
5
  createMcpProtocolHandler
6
- } from "../../chunk-WLNT2FX7.js";
6
+ } from "../../chunk-RHDEW56Y.js";
7
7
  import {
8
8
  createOAuthProtectedResourceMetadataHandler
9
9
  } from "../../chunk-QDKOF4UF.js";
10
10
  import {
11
11
  createInvokeToolHandler
12
- } from "../../chunk-YWLMMDET.js";
12
+ } from "../../chunk-T2RED2TG.js";
13
13
  import {
14
14
  createListToolsHandler
15
15
  } from "../../chunk-VNRQPA4K.js";
16
- import "../../chunk-MA5H6PSF.js";
16
+ import "../../chunk-XQSTN54Y.js";
17
+ import "../../chunk-P3F324UC.js";
17
18
  import "../../chunk-G4XA7IJM.js";
18
19
  import "../../chunk-QC3DXQTH.js";
19
20
  import "../../chunk-6DXGZZA4.js";
21
+ import "../../chunk-LWHVXXKQ.js";
20
22
 
21
23
  // src/stacks/tanstack/handlers.ts
22
24
  function forwarded(request, options) {
@@ -105,6 +105,29 @@ declare class ToolContext {
105
105
  getClaims(): JwtClaims | undefined;
106
106
  }
107
107
 
108
+ /**
109
+ * Usage-metrics options. Pass a boolean to the Vite plugins for the common
110
+ * on/off case, or this object to override the ingest endpoint (e.g. to
111
+ * self-host the collector outside Lovable).
112
+ */
113
+ interface MetricsOptions {
114
+ /** Master switch. Default `true`; metrics still self-disable at runtime when
115
+ * the API key env var is absent. */
116
+ enabled?: boolean;
117
+ /** OTLP/HTTP (JSON) logs URL to POST batches to — the default Lovable route or
118
+ * any OTLP collector. Must be an absolute http(s) URL.
119
+ * @default "https://api.lovable.dev/v1/app-mcp-usage" */
120
+ endpoint?: string;
121
+ /** Extra request headers — e.g. the auth token for a private OTLP collector.
122
+ * Sent verbatim with every batch (and baked into the build output, so source
123
+ * your own build-time env for secrets). Ignored for the default Lovable
124
+ * endpoint, which authenticates with `LOVABLE_API_KEY` instead. `content-type`
125
+ * cannot be overridden (OTLP/HTTP JSON requires `application/json`). */
126
+ headers?: Record<string, string>;
127
+ }
128
+ /** Either the bare on/off toggle or the full options object. */
129
+ type MetricsConfig = boolean | MetricsOptions;
130
+
108
131
  /**
109
132
  * Any zod schema. Aliased to zod's `ZodType` (no generics) — non-deprecated
110
133
  * in both v3 and v4. Use this when you need to type an individual schema;
@@ -273,10 +296,16 @@ interface McpDefinitionInput {
273
296
  */
274
297
  readonly instructions: string;
275
298
  readonly tools: readonly AnyToolDefinition[];
299
+ /**
300
+ * Usage-metrics emission (runtime telemetry), on by default. `false` opts out;
301
+ * `{ endpoint, headers }` sends to your own OTLP collector. Self-disables at
302
+ * runtime without `LOVABLE_API_KEY` on the default Lovable endpoint.
303
+ */
304
+ readonly metrics?: MetricsConfig;
276
305
  }
277
306
  declare const McpDefinitionBrand: unique symbol;
278
307
  type McpDefinition = McpDefinitionInput & {
279
308
  readonly [McpDefinitionBrand]: never;
280
309
  };
281
310
 
282
- export { type AudioContent as A, type ContentAnnotations as C, type EmbeddedBlobResource as E, type ImageContent as I, type JwtClaims as J, type McpAuthConfig as M, type ResourceLink as R, type TextContent as T, type ZodRawShape as Z, type ContentBlock as a, type EmbeddedResource as b, type EmbeddedTextResource as c, type McpDefinition as d, type McpDefinitionInput as e, type ResourceLinkIcon as f, type ToolAnnotations as g, type ToolContent as h, ToolContext as i, type ToolDefinition as j, type ToolHandlerResult as k, type ZodSchema as l };
311
+ export { type AudioContent as A, type ContentAnnotations as C, type EmbeddedBlobResource as E, type ImageContent as I, type JwtClaims as J, type McpAuthConfig as M, type ResourceLink as R, type TextContent as T, type ZodRawShape as Z, type ContentBlock as a, type EmbeddedResource as b, type EmbeddedTextResource as c, type McpDefinition as d, type McpDefinitionInput as e, type MetricsConfig as f, type MetricsOptions as g, type ResourceLinkIcon as h, type ToolAnnotations as i, type ToolContent as j, ToolContext as k, type ToolDefinition as l, type ToolHandlerResult as m, type ZodSchema as n };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovable.dev/mcp-js",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Author MCP servers for Lovable apps. Declare tools with defineTool, register them in defineMcp, and a framework adapter (TanStack or Supabase Edge Functions) emits the route(s) at build time.",
5
5
  "type": "module",
6
6
  "repository": {