@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
@@ -606,23 +606,240 @@ function corsPreflightResponse(allowMethods) {
606
606
  });
607
607
  }
608
608
 
609
+ // src/metrics/config.ts
610
+ var DEFAULT_METRICS_ENDPOINT = "https://api.lovable.dev/v1/app-mcp-usage";
611
+ var METRICS_API_KEY_ENV_VAR = "LOVABLE_API_KEY";
612
+ var METRICS_SAMPLE_RATE = 1;
613
+ var METRICS_FLUSH_INTERVAL_MS = 5e3;
614
+ var METRICS_MAX_BATCH_SIZE = 50;
615
+ function assertMetricsEndpoint(endpoint) {
616
+ let url;
617
+ try {
618
+ url = new URL(endpoint);
619
+ } catch {
620
+ throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}`);
621
+ }
622
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
623
+ throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be http(s), got ${JSON.stringify(endpoint)}`);
624
+ }
625
+ }
626
+ function resolveMetricsConfig(config = true) {
627
+ const options = typeof config === "boolean" ? { enabled: config } : config;
628
+ const endpoint = options.endpoint ?? DEFAULT_METRICS_ENDPOINT;
629
+ assertMetricsEndpoint(endpoint);
630
+ return Object.freeze({
631
+ enabled: options.enabled ?? true,
632
+ endpoint,
633
+ headers: options.headers ?? {},
634
+ apiKeyEnvVar: METRICS_API_KEY_ENV_VAR,
635
+ sampleRate: METRICS_SAMPLE_RATE,
636
+ flushIntervalMs: METRICS_FLUSH_INTERVAL_MS,
637
+ maxBatchSize: METRICS_MAX_BATCH_SIZE
638
+ });
639
+ }
640
+
641
+ // package.json
642
+ var version = "0.15.0";
643
+
644
+ // src/metrics/otlp.ts
645
+ var SCOPE_NAME = "@lovable.dev/mcp-js";
646
+ var EVENT_NAME = "mcp.tool.invocation";
647
+ var SEVERITY_INFO = 9;
648
+ function strAttr(key, value) {
649
+ return { key, value: { stringValue: value } };
650
+ }
651
+ function intAttr(key, value) {
652
+ return { key, value: { intValue: String(Math.round(value)) } };
653
+ }
654
+ function toLogRecord(rec) {
655
+ const attributes = [
656
+ strAttr("event.name", EVENT_NAME),
657
+ strAttr("mcp.method", rec.method),
658
+ strAttr("mcp.outcome", rec.outcome),
659
+ intAttr("mcp.duration_ms", rec.durationMs)
660
+ ];
661
+ if (rec.tool !== null)
662
+ attributes.push(strAttr("mcp.tool", rec.tool));
663
+ if (rec.reqBytes !== void 0)
664
+ attributes.push(intAttr("mcp.request_size_bytes", rec.reqBytes));
665
+ if (rec.resBytes !== void 0)
666
+ attributes.push(intAttr("mcp.response_size_bytes", rec.resBytes));
667
+ return {
668
+ timeUnixNano: rec.timeUnixNano,
669
+ observedTimeUnixNano: rec.timeUnixNano,
670
+ severityNumber: SEVERITY_INFO,
671
+ severityText: "INFO",
672
+ body: { stringValue: EVENT_NAME },
673
+ attributes
674
+ };
675
+ }
676
+ function buildLogsPayload(records, server) {
677
+ return JSON.stringify({
678
+ resourceLogs: [
679
+ {
680
+ resource: {
681
+ attributes: [
682
+ strAttr("service.name", server.name),
683
+ strAttr("service.version", server.version),
684
+ strAttr("telemetry.sdk.name", SCOPE_NAME),
685
+ strAttr("telemetry.sdk.version", version),
686
+ strAttr("telemetry.sdk.language", "webjs")
687
+ ]
688
+ },
689
+ scopeLogs: [
690
+ {
691
+ scope: { name: SCOPE_NAME, version },
692
+ logRecords: records.map(toLogRecord)
693
+ }
694
+ ]
695
+ }
696
+ ]
697
+ });
698
+ }
699
+ function nowUnixNano() {
700
+ return `${Date.now()}000000`;
701
+ }
702
+
703
+ // src/metrics/recorder.ts
704
+ function nowMs() {
705
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
706
+ }
707
+ var NOOP_RECORDER = {
708
+ record() {
709
+ },
710
+ async flush() {
711
+ }
712
+ };
713
+ function createNoopRecorder() {
714
+ return NOOP_RECORDER;
715
+ }
716
+ function readRuntimeEnv(name) {
717
+ try {
718
+ const denoEnv = globalThis.Deno?.env;
719
+ const value = denoEnv?.get?.(name);
720
+ if (value)
721
+ return value;
722
+ } catch {
723
+ }
724
+ try {
725
+ if (typeof process !== "undefined") {
726
+ const value = process.env?.[name];
727
+ if (value)
728
+ return value;
729
+ }
730
+ } catch {
731
+ }
732
+ return void 0;
733
+ }
734
+ function probeWaitUntil() {
735
+ const fn = globalThis.EdgeRuntime?.waitUntil;
736
+ return typeof fn === "function" ? fn.bind(globalThis.EdgeRuntime) : void 0;
737
+ }
738
+ function createMetricsRecorder(ctx, deps = {}) {
739
+ const { config, server } = ctx;
740
+ if (!config.enabled)
741
+ return NOOP_RECORDER;
742
+ const doFetch = deps.fetch ?? globalThis.fetch;
743
+ if (!doFetch)
744
+ return NOOP_RECORDER;
745
+ const usesLovableEndpoint = config.endpoint === DEFAULT_METRICS_ENDPOINT;
746
+ const apiKey = usesLovableEndpoint ? (deps.getApiKey ?? (() => readRuntimeEnv(config.apiKeyEnvVar)))() : void 0;
747
+ if (usesLovableEndpoint && !apiKey) {
748
+ log.debug("metrics.disabled_no_api_key", { envVar: config.apiKeyEnvVar });
749
+ return NOOP_RECORDER;
750
+ }
751
+ const waitUntil = deps.waitUntil ?? probeWaitUntil();
752
+ const buffer = [];
753
+ let timer;
754
+ const schedule = (p) => {
755
+ if (waitUntil) {
756
+ try {
757
+ waitUntil(p);
758
+ return;
759
+ } catch {
760
+ }
761
+ }
762
+ void p.catch(() => {
763
+ });
764
+ };
765
+ const headers = {};
766
+ if (!usesLovableEndpoint) {
767
+ for (const [key, value] of Object.entries(config.headers)) {
768
+ if (key.toLowerCase() !== "content-type")
769
+ headers[key] = value;
770
+ }
771
+ }
772
+ headers["content-type"] = "application/json";
773
+ if (apiKey)
774
+ headers.authorization = `Bearer ${apiKey}`;
775
+ const flush = async () => {
776
+ if (buffer.length === 0)
777
+ return;
778
+ const records = buffer.splice(0, buffer.length);
779
+ try {
780
+ const res = await doFetch(config.endpoint, {
781
+ method: "POST",
782
+ headers,
783
+ body: buildLogsPayload(records, server),
784
+ keepalive: true
785
+ });
786
+ if (!res.ok)
787
+ log.debug("metrics.flush_rejected", { status: res.status });
788
+ } catch (err) {
789
+ log.debug("metrics.flush_failed", describeError(err));
790
+ }
791
+ };
792
+ const ensureTimer = () => {
793
+ if (timer !== void 0)
794
+ return;
795
+ timer = setInterval(() => schedule(flush()), config.flushIntervalMs);
796
+ timer.unref?.();
797
+ };
798
+ return {
799
+ record(ev) {
800
+ if (config.sampleRate < 1 && Math.random() >= config.sampleRate)
801
+ return;
802
+ buffer.push({ ...ev, timeUnixNano: nowUnixNano() });
803
+ ensureTimer();
804
+ if (buffer.length >= config.maxBatchSize)
805
+ schedule(flush());
806
+ },
807
+ flush
808
+ };
809
+ }
810
+ function createRecorderForRuntime(mcp) {
811
+ const config = resolveMetricsConfig(mcp.metrics);
812
+ if (!config.enabled)
813
+ return createNoopRecorder();
814
+ return createMetricsRecorder({ config, server: { name: mcp.name, version: mcp.version } });
815
+ }
816
+
609
817
  // src/protocols/mcp/protocol.ts
610
- function adaptToolToSdkCallback(tool, auth) {
818
+ function adaptToolToSdkCallback(tool, auth, recorder) {
611
819
  return async (first) => {
612
820
  const args = tool.inputSchema ? first ?? {} : {};
821
+ const start = nowMs();
613
822
  let result;
614
823
  try {
615
824
  result = await tool.handler(args, new ToolContext(auth));
616
825
  } catch {
826
+ recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
617
827
  return { content: [{ type: "text", text: "tool execution failed" }], isError: true };
618
828
  }
619
829
  if (result == null) {
830
+ recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
620
831
  return { content: [{ type: "text", text: `tool "${tool.name}" returned no result` }], isError: true };
621
832
  }
833
+ recorder.record({
834
+ tool: tool.name,
835
+ method: "tools/call",
836
+ outcome: result.isError ? "tool_error" : "ok",
837
+ durationMs: nowMs() - start
838
+ });
622
839
  return { content: result.content ?? [], structuredContent: result.structuredContent, isError: result.isError };
623
840
  };
624
841
  }
625
- function createMcpProtocolHandler(mcp, options = {}) {
842
+ function createMcpProtocolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
626
843
  const authorizer = createRequestAuthorizer(mcp, options);
627
844
  const handle = async (request) => {
628
845
  const authResult = await authorizer.authorize(request);
@@ -643,7 +860,7 @@ function createMcpProtocolHandler(mcp, options = {}) {
643
860
  outputSchema: tool.outputSchema,
644
861
  annotations: tool.annotations
645
862
  },
646
- adaptToolToSdkCallback(tool, authResult.auth)
863
+ adaptToolToSdkCallback(tool, authResult.auth, recorder)
647
864
  );
648
865
  }
649
866
  const transport = new import_webStandardStreamableHttp.WebStandardStreamableHTTPServerTransport({
@@ -652,6 +869,7 @@ function createMcpProtocolHandler(mcp, options = {}) {
652
869
  await server.connect(transport);
653
870
  return await transport.handleRequest(request);
654
871
  } catch (err) {
872
+ recorder.record({ tool: null, method: "transport", outcome: "transport_error", durationMs: 0 });
655
873
  log.error("mcp.transport_error", { ...describeError(err), outcome: "500 internal error" });
656
874
  return Response.json(
657
875
  { jsonrpc: "2.0", id: null, error: { code: -32603, message: "internal error" } },
@@ -1,5 +1,6 @@
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
+ import { M as MetricsRecorder } from '../../recorder-B-eJU4Qu.js';
3
4
  import 'zod';
4
5
 
5
6
  type McpProtocolHandler = (request: Request) => Promise<Response>;
@@ -9,6 +10,6 @@ type McpProtocolHandler = (request: Request) => Promise<Response>;
9
10
  * request — the transport is stateless, so this is cheap and there is no
10
11
  * cross-request state to leak.
11
12
  */
12
- declare function createMcpProtocolHandler(mcp: McpDefinition, options?: McpRuntimeOptions): McpProtocolHandler;
13
+ declare function createMcpProtocolHandler(mcp: McpDefinition, options?: McpRuntimeOptions, recorder?: MetricsRecorder): McpProtocolHandler;
13
14
 
14
15
  export { type McpProtocolHandler, createMcpProtocolHandler };
@@ -1,5 +1,6 @@
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
+ import { M as MetricsRecorder } from '../../recorder-B-eJU4Qu.js';
3
4
  import 'zod';
4
5
 
5
6
  type McpProtocolHandler = (request: Request) => Promise<Response>;
@@ -9,6 +10,6 @@ type McpProtocolHandler = (request: Request) => Promise<Response>;
9
10
  * request — the transport is stateless, so this is cheap and there is no
10
11
  * cross-request state to leak.
11
12
  */
12
- declare function createMcpProtocolHandler(mcp: McpDefinition, options?: McpRuntimeOptions): McpProtocolHandler;
13
+ declare function createMcpProtocolHandler(mcp: McpDefinition, options?: McpRuntimeOptions, recorder?: MetricsRecorder): McpProtocolHandler;
13
14
 
14
15
  export { type McpProtocolHandler, createMcpProtocolHandler };
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  createMcpProtocolHandler
3
- } from "../../chunk-WLNT2FX7.js";
4
- import "../../chunk-MA5H6PSF.js";
3
+ } from "../../chunk-RHDEW56Y.js";
4
+ import "../../chunk-XQSTN54Y.js";
5
+ import "../../chunk-P3F324UC.js";
5
6
  import "../../chunk-G4XA7IJM.js";
6
7
  import "../../chunk-QC3DXQTH.js";
7
8
  import "../../chunk-6DXGZZA4.js";
9
+ import "../../chunk-LWHVXXKQ.js";
8
10
  export {
9
11
  createMcpProtocolHandler
10
12
  };
@@ -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
  type OAuthProtectedResourceMetadataHandler = (request: Request) => Promise<Response>;
@@ -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
  type OAuthProtectedResourceMetadataHandler = (request: Request) => Promise<Response>;
@@ -669,6 +669,214 @@ var ToolContext = class {
669
669
  }
670
670
  };
671
671
 
672
+ // src/metrics/config.ts
673
+ var DEFAULT_METRICS_ENDPOINT = "https://api.lovable.dev/v1/app-mcp-usage";
674
+ var METRICS_API_KEY_ENV_VAR = "LOVABLE_API_KEY";
675
+ var METRICS_SAMPLE_RATE = 1;
676
+ var METRICS_FLUSH_INTERVAL_MS = 5e3;
677
+ var METRICS_MAX_BATCH_SIZE = 50;
678
+ function assertMetricsEndpoint(endpoint) {
679
+ let url;
680
+ try {
681
+ url = new URL(endpoint);
682
+ } catch {
683
+ throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}`);
684
+ }
685
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
686
+ throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be http(s), got ${JSON.stringify(endpoint)}`);
687
+ }
688
+ }
689
+ function resolveMetricsConfig(config = true) {
690
+ const options = typeof config === "boolean" ? { enabled: config } : config;
691
+ const endpoint = options.endpoint ?? DEFAULT_METRICS_ENDPOINT;
692
+ assertMetricsEndpoint(endpoint);
693
+ return Object.freeze({
694
+ enabled: options.enabled ?? true,
695
+ endpoint,
696
+ headers: options.headers ?? {},
697
+ apiKeyEnvVar: METRICS_API_KEY_ENV_VAR,
698
+ sampleRate: METRICS_SAMPLE_RATE,
699
+ flushIntervalMs: METRICS_FLUSH_INTERVAL_MS,
700
+ maxBatchSize: METRICS_MAX_BATCH_SIZE
701
+ });
702
+ }
703
+
704
+ // package.json
705
+ var version = "0.15.0";
706
+
707
+ // src/metrics/otlp.ts
708
+ var SCOPE_NAME = "@lovable.dev/mcp-js";
709
+ var EVENT_NAME = "mcp.tool.invocation";
710
+ var SEVERITY_INFO = 9;
711
+ function strAttr(key, value) {
712
+ return { key, value: { stringValue: value } };
713
+ }
714
+ function intAttr(key, value) {
715
+ return { key, value: { intValue: String(Math.round(value)) } };
716
+ }
717
+ function toLogRecord(rec) {
718
+ const attributes = [
719
+ strAttr("event.name", EVENT_NAME),
720
+ strAttr("mcp.method", rec.method),
721
+ strAttr("mcp.outcome", rec.outcome),
722
+ intAttr("mcp.duration_ms", rec.durationMs)
723
+ ];
724
+ if (rec.tool !== null)
725
+ attributes.push(strAttr("mcp.tool", rec.tool));
726
+ if (rec.reqBytes !== void 0)
727
+ attributes.push(intAttr("mcp.request_size_bytes", rec.reqBytes));
728
+ if (rec.resBytes !== void 0)
729
+ attributes.push(intAttr("mcp.response_size_bytes", rec.resBytes));
730
+ return {
731
+ timeUnixNano: rec.timeUnixNano,
732
+ observedTimeUnixNano: rec.timeUnixNano,
733
+ severityNumber: SEVERITY_INFO,
734
+ severityText: "INFO",
735
+ body: { stringValue: EVENT_NAME },
736
+ attributes
737
+ };
738
+ }
739
+ function buildLogsPayload(records, server) {
740
+ return JSON.stringify({
741
+ resourceLogs: [
742
+ {
743
+ resource: {
744
+ attributes: [
745
+ strAttr("service.name", server.name),
746
+ strAttr("service.version", server.version),
747
+ strAttr("telemetry.sdk.name", SCOPE_NAME),
748
+ strAttr("telemetry.sdk.version", version),
749
+ strAttr("telemetry.sdk.language", "webjs")
750
+ ]
751
+ },
752
+ scopeLogs: [
753
+ {
754
+ scope: { name: SCOPE_NAME, version },
755
+ logRecords: records.map(toLogRecord)
756
+ }
757
+ ]
758
+ }
759
+ ]
760
+ });
761
+ }
762
+ function nowUnixNano() {
763
+ return `${Date.now()}000000`;
764
+ }
765
+
766
+ // src/metrics/recorder.ts
767
+ function nowMs() {
768
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
769
+ }
770
+ var NOOP_RECORDER = {
771
+ record() {
772
+ },
773
+ async flush() {
774
+ }
775
+ };
776
+ function createNoopRecorder() {
777
+ return NOOP_RECORDER;
778
+ }
779
+ function readRuntimeEnv(name) {
780
+ try {
781
+ const denoEnv = globalThis.Deno?.env;
782
+ const value = denoEnv?.get?.(name);
783
+ if (value)
784
+ return value;
785
+ } catch {
786
+ }
787
+ try {
788
+ if (typeof process !== "undefined") {
789
+ const value = process.env?.[name];
790
+ if (value)
791
+ return value;
792
+ }
793
+ } catch {
794
+ }
795
+ return void 0;
796
+ }
797
+ function probeWaitUntil() {
798
+ const fn = globalThis.EdgeRuntime?.waitUntil;
799
+ return typeof fn === "function" ? fn.bind(globalThis.EdgeRuntime) : void 0;
800
+ }
801
+ function createMetricsRecorder(ctx, deps = {}) {
802
+ const { config, server } = ctx;
803
+ if (!config.enabled)
804
+ return NOOP_RECORDER;
805
+ const doFetch = deps.fetch ?? globalThis.fetch;
806
+ if (!doFetch)
807
+ return NOOP_RECORDER;
808
+ const usesLovableEndpoint = config.endpoint === DEFAULT_METRICS_ENDPOINT;
809
+ const apiKey = usesLovableEndpoint ? (deps.getApiKey ?? (() => readRuntimeEnv(config.apiKeyEnvVar)))() : void 0;
810
+ if (usesLovableEndpoint && !apiKey) {
811
+ log.debug("metrics.disabled_no_api_key", { envVar: config.apiKeyEnvVar });
812
+ return NOOP_RECORDER;
813
+ }
814
+ const waitUntil = deps.waitUntil ?? probeWaitUntil();
815
+ const buffer = [];
816
+ let timer;
817
+ const schedule = (p) => {
818
+ if (waitUntil) {
819
+ try {
820
+ waitUntil(p);
821
+ return;
822
+ } catch {
823
+ }
824
+ }
825
+ void p.catch(() => {
826
+ });
827
+ };
828
+ const headers = {};
829
+ if (!usesLovableEndpoint) {
830
+ for (const [key, value] of Object.entries(config.headers)) {
831
+ if (key.toLowerCase() !== "content-type")
832
+ headers[key] = value;
833
+ }
834
+ }
835
+ headers["content-type"] = "application/json";
836
+ if (apiKey)
837
+ headers.authorization = `Bearer ${apiKey}`;
838
+ const flush = async () => {
839
+ if (buffer.length === 0)
840
+ return;
841
+ const records = buffer.splice(0, buffer.length);
842
+ try {
843
+ const res = await doFetch(config.endpoint, {
844
+ method: "POST",
845
+ headers,
846
+ body: buildLogsPayload(records, server),
847
+ keepalive: true
848
+ });
849
+ if (!res.ok)
850
+ log.debug("metrics.flush_rejected", { status: res.status });
851
+ } catch (err) {
852
+ log.debug("metrics.flush_failed", describeError(err));
853
+ }
854
+ };
855
+ const ensureTimer = () => {
856
+ if (timer !== void 0)
857
+ return;
858
+ timer = setInterval(() => schedule(flush()), config.flushIntervalMs);
859
+ timer.unref?.();
860
+ };
861
+ return {
862
+ record(ev) {
863
+ if (config.sampleRate < 1 && Math.random() >= config.sampleRate)
864
+ return;
865
+ buffer.push({ ...ev, timeUnixNano: nowUnixNano() });
866
+ ensureTimer();
867
+ if (buffer.length >= config.maxBatchSize)
868
+ schedule(flush());
869
+ },
870
+ flush
871
+ };
872
+ }
873
+ function createRecorderForRuntime(mcp) {
874
+ const config = resolveMetricsConfig(mcp.metrics);
875
+ if (!config.enabled)
876
+ return createNoopRecorder();
877
+ return createMetricsRecorder({ config, server: { name: mcp.name, version: mcp.version } });
878
+ }
879
+
672
880
  // src/protocols/rest/invoke-tool.ts
673
881
  var MAX_REFLECTED_TOOL_NAME = 256;
674
882
  function safeReflectName(name) {
@@ -682,7 +890,7 @@ function isEmptyArgs(value) {
682
890
  return false;
683
891
  return Object.keys(value).length === 0;
684
892
  }
685
- function createInvokeToolHandler(mcp, options = {}) {
893
+ function createInvokeToolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
686
894
  assertRestResourceBinding(mcp, options);
687
895
  const authorizer = createRequestAuthorizer(mcp, options);
688
896
  const handle = async (request, toolName) => {
@@ -738,20 +946,29 @@ function createInvokeToolHandler(mcp, options = {}) {
738
946
  });
739
947
  }
740
948
  let result;
949
+ const start = nowMs();
741
950
  try {
742
951
  result = await tool.handler(args, new ToolContext(authResult.auth));
743
952
  } catch {
953
+ recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
744
954
  return new Response(JSON.stringify({ error: "handler threw", tool: toolName }), {
745
955
  status: 500,
746
956
  headers: JSON_HEADERS
747
957
  });
748
958
  }
749
959
  if (result == null) {
960
+ recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
750
961
  return new Response(JSON.stringify({ error: `tool "${toolName}" returned no result` }), {
751
962
  status: 500,
752
963
  headers: JSON_HEADERS
753
964
  });
754
965
  }
966
+ recorder.record({
967
+ tool: tool.name,
968
+ method: "tools/call",
969
+ outcome: result.isError ? "tool_error" : "ok",
970
+ durationMs: nowMs() - start
971
+ });
755
972
  return Response.json({
756
973
  content: result.content ?? [],
757
974
  structuredContent: result.structuredContent,
@@ -1,11 +1,12 @@
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
+ import { M as MetricsRecorder } from '../../recorder-B-eJU4Qu.js';
3
4
  import 'zod';
4
5
 
5
6
  type RestListToolsHandler = (request: Request) => Promise<Response>;
6
7
  declare function createListToolsHandler(mcp: McpDefinition, options?: McpRuntimeOptions): RestListToolsHandler;
7
8
 
8
9
  type RestInvokeToolHandler = (request: Request, toolName: string) => Promise<Response>;
9
- declare function createInvokeToolHandler(mcp: McpDefinition, options?: McpRuntimeOptions): RestInvokeToolHandler;
10
+ declare function createInvokeToolHandler(mcp: McpDefinition, options?: McpRuntimeOptions, recorder?: MetricsRecorder): RestInvokeToolHandler;
10
11
 
11
12
  export { type RestInvokeToolHandler, type RestListToolsHandler, createInvokeToolHandler, createListToolsHandler };
@@ -1,11 +1,12 @@
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
+ import { M as MetricsRecorder } from '../../recorder-B-eJU4Qu.js';
3
4
  import 'zod';
4
5
 
5
6
  type RestListToolsHandler = (request: Request) => Promise<Response>;
6
7
  declare function createListToolsHandler(mcp: McpDefinition, options?: McpRuntimeOptions): RestListToolsHandler;
7
8
 
8
9
  type RestInvokeToolHandler = (request: Request, toolName: string) => Promise<Response>;
9
- declare function createInvokeToolHandler(mcp: McpDefinition, options?: McpRuntimeOptions): RestInvokeToolHandler;
10
+ declare function createInvokeToolHandler(mcp: McpDefinition, options?: McpRuntimeOptions, recorder?: MetricsRecorder): RestInvokeToolHandler;
10
11
 
11
12
  export { type RestInvokeToolHandler, type RestListToolsHandler, createInvokeToolHandler, createListToolsHandler };
@@ -1,13 +1,15 @@
1
1
  import {
2
2
  createInvokeToolHandler
3
- } from "../../chunk-YWLMMDET.js";
3
+ } from "../../chunk-T2RED2TG.js";
4
4
  import {
5
5
  createListToolsHandler
6
6
  } from "../../chunk-VNRQPA4K.js";
7
- import "../../chunk-MA5H6PSF.js";
7
+ import "../../chunk-XQSTN54Y.js";
8
+ import "../../chunk-P3F324UC.js";
8
9
  import "../../chunk-G4XA7IJM.js";
9
10
  import "../../chunk-QC3DXQTH.js";
10
11
  import "../../chunk-6DXGZZA4.js";
12
+ import "../../chunk-LWHVXXKQ.js";
11
13
  export {
12
14
  createInvokeToolHandler,
13
15
  createListToolsHandler
@@ -0,0 +1,21 @@
1
+ type InvocationOutcome = "ok" | "tool_error" | "handler_error" | "transport_error";
2
+ /** One recorded MCP call. Carries no arguments or response payloads — only the
3
+ * tool name, the JSON-RPC method, the result class, timing, and byte sizes. */
4
+ interface InvocationRecord {
5
+ /** Tool name for `tools/call`; `null` for list/initialize and transport faults. */
6
+ tool: string | null;
7
+ /** JSON-RPC method, e.g. `tools/call`. */
8
+ method: string;
9
+ outcome: InvocationOutcome;
10
+ durationMs: number;
11
+ reqBytes?: number;
12
+ resBytes?: number;
13
+ }
14
+ interface MetricsRecorder {
15
+ /** Buffer one invocation. Synchronous, sampled, and never throws. */
16
+ record(ev: InvocationRecord): void;
17
+ /** Best-effort POST of the current buffer. Resolves even on network failure. */
18
+ flush(): Promise<void>;
19
+ }
20
+
21
+ export type { MetricsRecorder as M };