@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.
- package/README.md +28 -0
- package/dist/{chunk-NOFPRSSI.js → chunk-LWHVXXKQ.js} +1 -1
- package/dist/{chunk-MA5H6PSF.js → chunk-P3F324UC.js} +34 -0
- package/dist/{chunk-WLNT2FX7.js → chunk-RHDEW56Y.js} +18 -4
- package/dist/{chunk-YWLMMDET.js → chunk-T2RED2TG.js} +15 -2
- package/dist/chunk-XQSTN54Y.js +189 -0
- package/dist/cli/extract-manifest.cjs +1 -1
- package/dist/cli/extract-manifest.js +3 -3
- package/dist/index.cjs +33 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -2
- package/dist/protocols/mcp/index.cjs +221 -3
- package/dist/protocols/mcp/index.d.cts +3 -2
- package/dist/protocols/mcp/index.d.ts +3 -2
- package/dist/protocols/mcp/index.js +4 -2
- package/dist/protocols/oauth-metadata.d.cts +1 -1
- package/dist/protocols/oauth-metadata.d.ts +1 -1
- package/dist/protocols/rest/index.cjs +218 -1
- package/dist/protocols/rest/index.d.cts +3 -2
- package/dist/protocols/rest/index.d.ts +3 -2
- package/dist/protocols/rest/index.js +4 -2
- package/dist/recorder-B-eJU4Qu.d.ts +21 -0
- package/dist/stacks/supabase/index.cjs +250 -22
- package/dist/stacks/supabase/index.d.cts +1 -1
- package/dist/stacks/supabase/index.d.ts +1 -1
- package/dist/stacks/supabase/index.js +10 -5
- package/dist/stacks/supabase/vite.cjs +1 -1
- package/dist/stacks/supabase/vite.js +3 -3
- package/dist/stacks/tanstack/index.cjs +231 -4
- package/dist/stacks/tanstack/index.d.cts +1 -1
- package/dist/stacks/tanstack/index.d.ts +1 -1
- package/dist/stacks/tanstack/index.js +5 -3
- package/dist/{types-BanzncFh.d.ts → types-COY42xux.d.ts} +30 -1
- 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-
|
|
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-
|
|
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-
|
|
4
|
-
import "../../chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
3
|
+
} from "../../chunk-T2RED2TG.js";
|
|
4
4
|
import {
|
|
5
5
|
createListToolsHandler
|
|
6
6
|
} from "../../chunk-VNRQPA4K.js";
|
|
7
|
-
import "../../chunk-
|
|
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 };
|