@lovable.dev/mcp-js 0.14.0 → 0.15.1
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-YWLMMDET.js → chunk-CEDD57G2.js} +14 -3
- package/dist/{chunk-QC3DXQTH.js → chunk-DCWYK4CA.js} +34 -0
- package/dist/{chunk-NOFPRSSI.js → chunk-GIHCQT6L.js} +1 -1
- package/dist/{chunk-QDKOF4UF.js → chunk-ILYTJXDR.js} +2 -2
- package/dist/{chunk-VNRQPA4K.js → chunk-QT5L4CS3.js} +4 -3
- package/dist/{chunk-WLNT2FX7.js → chunk-UCPYLSNN.js} +18 -6
- package/dist/{chunk-G4XA7IJM.js → chunk-YTOMGJV5.js} +189 -2
- package/dist/cli/extract-manifest.cjs +1 -1
- package/dist/cli/extract-manifest.js +6 -6
- package/dist/index.cjs +33 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -1
- package/dist/protocols/mcp/index.cjs +230 -5
- 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 -3
- package/dist/protocols/oauth-metadata.d.cts +1 -1
- package/dist/protocols/oauth-metadata.d.ts +1 -1
- package/dist/protocols/oauth-metadata.js +4 -3
- package/dist/protocols/rest/index.cjs +229 -5
- package/dist/protocols/rest/index.d.cts +4 -3
- package/dist/protocols/rest/index.d.ts +4 -3
- package/dist/protocols/rest/index.js +5 -4
- package/dist/recorder-qlzUKFxG.d.ts +21 -0
- package/dist/stacks/supabase/index.cjs +263 -28
- package/dist/stacks/supabase/index.d.cts +1 -1
- package/dist/stacks/supabase/index.d.ts +1 -1
- package/dist/stacks/supabase/index.js +13 -10
- package/dist/stacks/supabase/vite.cjs +28 -4
- package/dist/stacks/supabase/vite.js +30 -6
- package/dist/stacks/tanstack/index.cjs +243 -9
- package/dist/stacks/tanstack/index.d.cts +1 -1
- package/dist/stacks/tanstack/index.d.ts +1 -1
- package/dist/stacks/tanstack/index.js +7 -6
- package/dist/{types-BanzncFh.d.ts → types-COY42xux.d.ts} +30 -1
- package/package.json +1 -1
|
@@ -72,6 +72,214 @@ function describeError(err) {
|
|
|
72
72
|
return { value: String(err) };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// src/metrics/config.ts
|
|
76
|
+
var DEFAULT_METRICS_ENDPOINT = "https://api.lovable.dev/v1/app-mcp-usage";
|
|
77
|
+
var METRICS_API_KEY_ENV_VAR = "LOVABLE_API_KEY";
|
|
78
|
+
var METRICS_SAMPLE_RATE = 1;
|
|
79
|
+
var METRICS_FLUSH_INTERVAL_MS = 5e3;
|
|
80
|
+
var METRICS_MAX_BATCH_SIZE = 50;
|
|
81
|
+
function assertMetricsEndpoint(endpoint) {
|
|
82
|
+
let url;
|
|
83
|
+
try {
|
|
84
|
+
url = new URL(endpoint);
|
|
85
|
+
} catch {
|
|
86
|
+
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}`);
|
|
87
|
+
}
|
|
88
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
89
|
+
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be http(s), got ${JSON.stringify(endpoint)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function resolveMetricsConfig(config = true) {
|
|
93
|
+
const options = typeof config === "boolean" ? { enabled: config } : config;
|
|
94
|
+
const endpoint = options.endpoint ?? DEFAULT_METRICS_ENDPOINT;
|
|
95
|
+
assertMetricsEndpoint(endpoint);
|
|
96
|
+
return Object.freeze({
|
|
97
|
+
enabled: options.enabled ?? true,
|
|
98
|
+
endpoint,
|
|
99
|
+
headers: options.headers ?? {},
|
|
100
|
+
apiKeyEnvVar: METRICS_API_KEY_ENV_VAR,
|
|
101
|
+
sampleRate: METRICS_SAMPLE_RATE,
|
|
102
|
+
flushIntervalMs: METRICS_FLUSH_INTERVAL_MS,
|
|
103
|
+
maxBatchSize: METRICS_MAX_BATCH_SIZE
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// package.json
|
|
108
|
+
var version = "0.15.1";
|
|
109
|
+
|
|
110
|
+
// src/metrics/otlp.ts
|
|
111
|
+
var SCOPE_NAME = "@lovable.dev/mcp-js";
|
|
112
|
+
var EVENT_NAME = "mcp.tool.invocation";
|
|
113
|
+
var SEVERITY_INFO = 9;
|
|
114
|
+
function strAttr(key, value) {
|
|
115
|
+
return { key, value: { stringValue: value } };
|
|
116
|
+
}
|
|
117
|
+
function intAttr(key, value) {
|
|
118
|
+
return { key, value: { intValue: String(Math.round(value)) } };
|
|
119
|
+
}
|
|
120
|
+
function toLogRecord(rec) {
|
|
121
|
+
const attributes = [
|
|
122
|
+
strAttr("event.name", EVENT_NAME),
|
|
123
|
+
strAttr("mcp.method", rec.method),
|
|
124
|
+
strAttr("mcp.outcome", rec.outcome),
|
|
125
|
+
intAttr("mcp.duration_ms", rec.durationMs)
|
|
126
|
+
];
|
|
127
|
+
if (rec.tool !== null)
|
|
128
|
+
attributes.push(strAttr("mcp.tool", rec.tool));
|
|
129
|
+
if (rec.reqBytes !== void 0)
|
|
130
|
+
attributes.push(intAttr("mcp.request_size_bytes", rec.reqBytes));
|
|
131
|
+
if (rec.resBytes !== void 0)
|
|
132
|
+
attributes.push(intAttr("mcp.response_size_bytes", rec.resBytes));
|
|
133
|
+
return {
|
|
134
|
+
timeUnixNano: rec.timeUnixNano,
|
|
135
|
+
observedTimeUnixNano: rec.timeUnixNano,
|
|
136
|
+
severityNumber: SEVERITY_INFO,
|
|
137
|
+
severityText: "INFO",
|
|
138
|
+
body: { stringValue: EVENT_NAME },
|
|
139
|
+
attributes
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function buildLogsPayload(records, server) {
|
|
143
|
+
return JSON.stringify({
|
|
144
|
+
resourceLogs: [
|
|
145
|
+
{
|
|
146
|
+
resource: {
|
|
147
|
+
attributes: [
|
|
148
|
+
strAttr("service.name", server.name),
|
|
149
|
+
strAttr("service.version", server.version),
|
|
150
|
+
strAttr("telemetry.sdk.name", SCOPE_NAME),
|
|
151
|
+
strAttr("telemetry.sdk.version", version),
|
|
152
|
+
strAttr("telemetry.sdk.language", "webjs")
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
scopeLogs: [
|
|
156
|
+
{
|
|
157
|
+
scope: { name: SCOPE_NAME, version },
|
|
158
|
+
logRecords: records.map(toLogRecord)
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function nowUnixNano() {
|
|
166
|
+
return `${Date.now()}000000`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/metrics/recorder.ts
|
|
170
|
+
function nowMs() {
|
|
171
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
172
|
+
}
|
|
173
|
+
var NOOP_RECORDER = {
|
|
174
|
+
record() {
|
|
175
|
+
},
|
|
176
|
+
async flush() {
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
function createNoopRecorder() {
|
|
180
|
+
return NOOP_RECORDER;
|
|
181
|
+
}
|
|
182
|
+
function readRuntimeEnv(name) {
|
|
183
|
+
try {
|
|
184
|
+
const denoEnv = globalThis.Deno?.env;
|
|
185
|
+
const value = denoEnv?.get?.(name);
|
|
186
|
+
if (value)
|
|
187
|
+
return value;
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
if (typeof process !== "undefined") {
|
|
192
|
+
const value = process.env?.[name];
|
|
193
|
+
if (value)
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
return void 0;
|
|
199
|
+
}
|
|
200
|
+
function probeWaitUntil() {
|
|
201
|
+
const fn = globalThis.EdgeRuntime?.waitUntil;
|
|
202
|
+
return typeof fn === "function" ? fn.bind(globalThis.EdgeRuntime) : void 0;
|
|
203
|
+
}
|
|
204
|
+
function createMetricsRecorder(ctx, deps = {}) {
|
|
205
|
+
const { config, server } = ctx;
|
|
206
|
+
if (!config.enabled)
|
|
207
|
+
return NOOP_RECORDER;
|
|
208
|
+
const doFetch = deps.fetch ?? globalThis.fetch;
|
|
209
|
+
if (!doFetch)
|
|
210
|
+
return NOOP_RECORDER;
|
|
211
|
+
const usesLovableEndpoint = config.endpoint === DEFAULT_METRICS_ENDPOINT;
|
|
212
|
+
const apiKey = usesLovableEndpoint ? (deps.getApiKey ?? (() => readRuntimeEnv(config.apiKeyEnvVar)))() : void 0;
|
|
213
|
+
if (usesLovableEndpoint && !apiKey) {
|
|
214
|
+
log.debug("metrics.disabled_no_api_key", { envVar: config.apiKeyEnvVar });
|
|
215
|
+
return NOOP_RECORDER;
|
|
216
|
+
}
|
|
217
|
+
const waitUntil = deps.waitUntil ?? probeWaitUntil();
|
|
218
|
+
const buffer = [];
|
|
219
|
+
let timer;
|
|
220
|
+
const schedule = (p) => {
|
|
221
|
+
if (waitUntil) {
|
|
222
|
+
try {
|
|
223
|
+
waitUntil(p);
|
|
224
|
+
return;
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
void p.catch(() => {
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
const headers = {};
|
|
232
|
+
if (!usesLovableEndpoint) {
|
|
233
|
+
for (const [key, value] of Object.entries(config.headers)) {
|
|
234
|
+
if (key.toLowerCase() !== "content-type")
|
|
235
|
+
headers[key] = value;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
headers["content-type"] = "application/json";
|
|
239
|
+
if (apiKey)
|
|
240
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
241
|
+
const flush = async () => {
|
|
242
|
+
if (buffer.length === 0)
|
|
243
|
+
return;
|
|
244
|
+
const records = buffer.splice(0, buffer.length);
|
|
245
|
+
try {
|
|
246
|
+
const res = await doFetch(config.endpoint, {
|
|
247
|
+
method: "POST",
|
|
248
|
+
headers,
|
|
249
|
+
body: buildLogsPayload(records, server),
|
|
250
|
+
keepalive: true
|
|
251
|
+
});
|
|
252
|
+
if (!res.ok)
|
|
253
|
+
log.debug("metrics.flush_rejected", { status: res.status });
|
|
254
|
+
} catch (err) {
|
|
255
|
+
log.debug("metrics.flush_failed", describeError(err));
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
const ensureTimer = () => {
|
|
259
|
+
if (timer !== void 0)
|
|
260
|
+
return;
|
|
261
|
+
timer = setInterval(() => schedule(flush()), config.flushIntervalMs);
|
|
262
|
+
timer.unref?.();
|
|
263
|
+
};
|
|
264
|
+
return {
|
|
265
|
+
record(ev) {
|
|
266
|
+
if (config.sampleRate < 1 && Math.random() >= config.sampleRate)
|
|
267
|
+
return;
|
|
268
|
+
buffer.push({ ...ev, timeUnixNano: nowUnixNano() });
|
|
269
|
+
ensureTimer();
|
|
270
|
+
if (buffer.length >= config.maxBatchSize)
|
|
271
|
+
schedule(flush());
|
|
272
|
+
},
|
|
273
|
+
flush
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function createRecorderForRuntime(mcp) {
|
|
277
|
+
const config = resolveMetricsConfig(mcp.metrics);
|
|
278
|
+
if (!config.enabled)
|
|
279
|
+
return createNoopRecorder();
|
|
280
|
+
return createMetricsRecorder({ config, server: { name: mcp.name, version: mcp.version } });
|
|
281
|
+
}
|
|
282
|
+
|
|
75
283
|
// src/core/promise.ts
|
|
76
284
|
function cachedPromise(load, label) {
|
|
77
285
|
let settled = false;
|
|
@@ -492,12 +700,13 @@ function assertRequiredScopes(auth, context) {
|
|
|
492
700
|
throw new OAuthTokenError(403, "insufficient_scope", "Additional OAuth scope is required");
|
|
493
701
|
}
|
|
494
702
|
}
|
|
495
|
-
function createRequestAuthorizer(mcp, options = {}) {
|
|
703
|
+
function createRequestAuthorizer(mcp, options = {}, recorder) {
|
|
496
704
|
const runtime = getOAuthRuntime(mcp, options);
|
|
497
705
|
return {
|
|
498
706
|
async authorize(request) {
|
|
499
707
|
if (runtime.kind === "unconfigured")
|
|
500
708
|
return { ok: true };
|
|
709
|
+
const startedAt = nowMs();
|
|
501
710
|
const token = parseBearerToken(request);
|
|
502
711
|
if (!token) {
|
|
503
712
|
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
@@ -510,6 +719,12 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
510
719
|
} catch (err) {
|
|
511
720
|
if (err instanceof OAuthConfigurationError) {
|
|
512
721
|
log.error("auth.config_error", { ...describeError(err), outcome: "500" });
|
|
722
|
+
recorder?.record({
|
|
723
|
+
tool: null,
|
|
724
|
+
method: "authorize",
|
|
725
|
+
outcome: "auth_config_error",
|
|
726
|
+
durationMs: nowMs() - startedAt
|
|
727
|
+
});
|
|
513
728
|
return { ok: false, response: oauthConfigurationErrorResponse() };
|
|
514
729
|
}
|
|
515
730
|
if (err instanceof OAuthTokenError) {
|
|
@@ -607,23 +822,32 @@ function corsPreflightResponse(allowMethods) {
|
|
|
607
822
|
}
|
|
608
823
|
|
|
609
824
|
// src/protocols/mcp/protocol.ts
|
|
610
|
-
function adaptToolToSdkCallback(tool, auth) {
|
|
825
|
+
function adaptToolToSdkCallback(tool, auth, recorder) {
|
|
611
826
|
return async (first) => {
|
|
612
827
|
const args = tool.inputSchema ? first ?? {} : {};
|
|
828
|
+
const start = nowMs();
|
|
613
829
|
let result;
|
|
614
830
|
try {
|
|
615
831
|
result = await tool.handler(args, new ToolContext(auth));
|
|
616
832
|
} catch {
|
|
833
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
617
834
|
return { content: [{ type: "text", text: "tool execution failed" }], isError: true };
|
|
618
835
|
}
|
|
619
836
|
if (result == null) {
|
|
837
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
620
838
|
return { content: [{ type: "text", text: `tool "${tool.name}" returned no result` }], isError: true };
|
|
621
839
|
}
|
|
840
|
+
recorder.record({
|
|
841
|
+
tool: tool.name,
|
|
842
|
+
method: "tools/call",
|
|
843
|
+
outcome: result.isError ? "tool_error" : "ok",
|
|
844
|
+
durationMs: nowMs() - start
|
|
845
|
+
});
|
|
622
846
|
return { content: result.content ?? [], structuredContent: result.structuredContent, isError: result.isError };
|
|
623
847
|
};
|
|
624
848
|
}
|
|
625
|
-
function createMcpProtocolHandler(mcp, options = {}) {
|
|
626
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
849
|
+
function createMcpProtocolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
|
|
850
|
+
const authorizer = createRequestAuthorizer(mcp, options, recorder);
|
|
627
851
|
const handle = async (request) => {
|
|
628
852
|
const authResult = await authorizer.authorize(request);
|
|
629
853
|
if (!authResult.ok)
|
|
@@ -643,7 +867,7 @@ function createMcpProtocolHandler(mcp, options = {}) {
|
|
|
643
867
|
outputSchema: tool.outputSchema,
|
|
644
868
|
annotations: tool.annotations
|
|
645
869
|
},
|
|
646
|
-
adaptToolToSdkCallback(tool, authResult.auth)
|
|
870
|
+
adaptToolToSdkCallback(tool, authResult.auth, recorder)
|
|
647
871
|
);
|
|
648
872
|
}
|
|
649
873
|
const transport = new import_webStandardStreamableHttp.WebStandardStreamableHTTPServerTransport({
|
|
@@ -652,6 +876,7 @@ function createMcpProtocolHandler(mcp, options = {}) {
|
|
|
652
876
|
await server.connect(transport);
|
|
653
877
|
return await transport.handleRequest(request);
|
|
654
878
|
} catch (err) {
|
|
879
|
+
recorder.record({ tool: null, method: "transport", outcome: "transport_error", durationMs: 0 });
|
|
655
880
|
log.error("mcp.transport_error", { ...describeError(err), outcome: "500 internal error" });
|
|
656
881
|
return Response.json(
|
|
657
882
|
{ 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-qlzUKFxG.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-qlzUKFxG.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,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpProtocolHandler
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-UCPYLSNN.js";
|
|
4
4
|
import "../../chunk-MA5H6PSF.js";
|
|
5
|
-
import "../../chunk-
|
|
6
|
-
import "../../chunk-
|
|
5
|
+
import "../../chunk-YTOMGJV5.js";
|
|
6
|
+
import "../../chunk-DCWYK4CA.js";
|
|
7
7
|
import "../../chunk-6DXGZZA4.js";
|
|
8
|
+
import "../../chunk-GIHCQT6L.js";
|
|
8
9
|
export {
|
|
9
10
|
createMcpProtocolHandler
|
|
10
11
|
};
|
|
@@ -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>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createOAuthProtectedResourceMetadataHandler
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
5
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-ILYTJXDR.js";
|
|
4
|
+
import "../chunk-YTOMGJV5.js";
|
|
5
|
+
import "../chunk-DCWYK4CA.js";
|
|
6
6
|
import "../chunk-6DXGZZA4.js";
|
|
7
|
+
import "../chunk-GIHCQT6L.js";
|
|
7
8
|
export {
|
|
8
9
|
createOAuthProtectedResourceMetadataHandler
|
|
9
10
|
};
|
|
@@ -82,6 +82,214 @@ function describeError(err) {
|
|
|
82
82
|
return { value: String(err) };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// src/metrics/config.ts
|
|
86
|
+
var DEFAULT_METRICS_ENDPOINT = "https://api.lovable.dev/v1/app-mcp-usage";
|
|
87
|
+
var METRICS_API_KEY_ENV_VAR = "LOVABLE_API_KEY";
|
|
88
|
+
var METRICS_SAMPLE_RATE = 1;
|
|
89
|
+
var METRICS_FLUSH_INTERVAL_MS = 5e3;
|
|
90
|
+
var METRICS_MAX_BATCH_SIZE = 50;
|
|
91
|
+
function assertMetricsEndpoint(endpoint) {
|
|
92
|
+
let url;
|
|
93
|
+
try {
|
|
94
|
+
url = new URL(endpoint);
|
|
95
|
+
} catch {
|
|
96
|
+
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}`);
|
|
97
|
+
}
|
|
98
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
99
|
+
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be http(s), got ${JSON.stringify(endpoint)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function resolveMetricsConfig(config = true) {
|
|
103
|
+
const options = typeof config === "boolean" ? { enabled: config } : config;
|
|
104
|
+
const endpoint = options.endpoint ?? DEFAULT_METRICS_ENDPOINT;
|
|
105
|
+
assertMetricsEndpoint(endpoint);
|
|
106
|
+
return Object.freeze({
|
|
107
|
+
enabled: options.enabled ?? true,
|
|
108
|
+
endpoint,
|
|
109
|
+
headers: options.headers ?? {},
|
|
110
|
+
apiKeyEnvVar: METRICS_API_KEY_ENV_VAR,
|
|
111
|
+
sampleRate: METRICS_SAMPLE_RATE,
|
|
112
|
+
flushIntervalMs: METRICS_FLUSH_INTERVAL_MS,
|
|
113
|
+
maxBatchSize: METRICS_MAX_BATCH_SIZE
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// package.json
|
|
118
|
+
var version = "0.15.1";
|
|
119
|
+
|
|
120
|
+
// src/metrics/otlp.ts
|
|
121
|
+
var SCOPE_NAME = "@lovable.dev/mcp-js";
|
|
122
|
+
var EVENT_NAME = "mcp.tool.invocation";
|
|
123
|
+
var SEVERITY_INFO = 9;
|
|
124
|
+
function strAttr(key, value) {
|
|
125
|
+
return { key, value: { stringValue: value } };
|
|
126
|
+
}
|
|
127
|
+
function intAttr(key, value) {
|
|
128
|
+
return { key, value: { intValue: String(Math.round(value)) } };
|
|
129
|
+
}
|
|
130
|
+
function toLogRecord(rec) {
|
|
131
|
+
const attributes = [
|
|
132
|
+
strAttr("event.name", EVENT_NAME),
|
|
133
|
+
strAttr("mcp.method", rec.method),
|
|
134
|
+
strAttr("mcp.outcome", rec.outcome),
|
|
135
|
+
intAttr("mcp.duration_ms", rec.durationMs)
|
|
136
|
+
];
|
|
137
|
+
if (rec.tool !== null)
|
|
138
|
+
attributes.push(strAttr("mcp.tool", rec.tool));
|
|
139
|
+
if (rec.reqBytes !== void 0)
|
|
140
|
+
attributes.push(intAttr("mcp.request_size_bytes", rec.reqBytes));
|
|
141
|
+
if (rec.resBytes !== void 0)
|
|
142
|
+
attributes.push(intAttr("mcp.response_size_bytes", rec.resBytes));
|
|
143
|
+
return {
|
|
144
|
+
timeUnixNano: rec.timeUnixNano,
|
|
145
|
+
observedTimeUnixNano: rec.timeUnixNano,
|
|
146
|
+
severityNumber: SEVERITY_INFO,
|
|
147
|
+
severityText: "INFO",
|
|
148
|
+
body: { stringValue: EVENT_NAME },
|
|
149
|
+
attributes
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function buildLogsPayload(records, server) {
|
|
153
|
+
return JSON.stringify({
|
|
154
|
+
resourceLogs: [
|
|
155
|
+
{
|
|
156
|
+
resource: {
|
|
157
|
+
attributes: [
|
|
158
|
+
strAttr("service.name", server.name),
|
|
159
|
+
strAttr("service.version", server.version),
|
|
160
|
+
strAttr("telemetry.sdk.name", SCOPE_NAME),
|
|
161
|
+
strAttr("telemetry.sdk.version", version),
|
|
162
|
+
strAttr("telemetry.sdk.language", "webjs")
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
scopeLogs: [
|
|
166
|
+
{
|
|
167
|
+
scope: { name: SCOPE_NAME, version },
|
|
168
|
+
logRecords: records.map(toLogRecord)
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function nowUnixNano() {
|
|
176
|
+
return `${Date.now()}000000`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/metrics/recorder.ts
|
|
180
|
+
function nowMs() {
|
|
181
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
182
|
+
}
|
|
183
|
+
var NOOP_RECORDER = {
|
|
184
|
+
record() {
|
|
185
|
+
},
|
|
186
|
+
async flush() {
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
function createNoopRecorder() {
|
|
190
|
+
return NOOP_RECORDER;
|
|
191
|
+
}
|
|
192
|
+
function readRuntimeEnv(name) {
|
|
193
|
+
try {
|
|
194
|
+
const denoEnv = globalThis.Deno?.env;
|
|
195
|
+
const value = denoEnv?.get?.(name);
|
|
196
|
+
if (value)
|
|
197
|
+
return value;
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
if (typeof process !== "undefined") {
|
|
202
|
+
const value = process.env?.[name];
|
|
203
|
+
if (value)
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
return void 0;
|
|
209
|
+
}
|
|
210
|
+
function probeWaitUntil() {
|
|
211
|
+
const fn = globalThis.EdgeRuntime?.waitUntil;
|
|
212
|
+
return typeof fn === "function" ? fn.bind(globalThis.EdgeRuntime) : void 0;
|
|
213
|
+
}
|
|
214
|
+
function createMetricsRecorder(ctx, deps = {}) {
|
|
215
|
+
const { config, server } = ctx;
|
|
216
|
+
if (!config.enabled)
|
|
217
|
+
return NOOP_RECORDER;
|
|
218
|
+
const doFetch = deps.fetch ?? globalThis.fetch;
|
|
219
|
+
if (!doFetch)
|
|
220
|
+
return NOOP_RECORDER;
|
|
221
|
+
const usesLovableEndpoint = config.endpoint === DEFAULT_METRICS_ENDPOINT;
|
|
222
|
+
const apiKey = usesLovableEndpoint ? (deps.getApiKey ?? (() => readRuntimeEnv(config.apiKeyEnvVar)))() : void 0;
|
|
223
|
+
if (usesLovableEndpoint && !apiKey) {
|
|
224
|
+
log.debug("metrics.disabled_no_api_key", { envVar: config.apiKeyEnvVar });
|
|
225
|
+
return NOOP_RECORDER;
|
|
226
|
+
}
|
|
227
|
+
const waitUntil = deps.waitUntil ?? probeWaitUntil();
|
|
228
|
+
const buffer = [];
|
|
229
|
+
let timer;
|
|
230
|
+
const schedule = (p) => {
|
|
231
|
+
if (waitUntil) {
|
|
232
|
+
try {
|
|
233
|
+
waitUntil(p);
|
|
234
|
+
return;
|
|
235
|
+
} catch {
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
void p.catch(() => {
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
const headers = {};
|
|
242
|
+
if (!usesLovableEndpoint) {
|
|
243
|
+
for (const [key, value] of Object.entries(config.headers)) {
|
|
244
|
+
if (key.toLowerCase() !== "content-type")
|
|
245
|
+
headers[key] = value;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
headers["content-type"] = "application/json";
|
|
249
|
+
if (apiKey)
|
|
250
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
251
|
+
const flush = async () => {
|
|
252
|
+
if (buffer.length === 0)
|
|
253
|
+
return;
|
|
254
|
+
const records = buffer.splice(0, buffer.length);
|
|
255
|
+
try {
|
|
256
|
+
const res = await doFetch(config.endpoint, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers,
|
|
259
|
+
body: buildLogsPayload(records, server),
|
|
260
|
+
keepalive: true
|
|
261
|
+
});
|
|
262
|
+
if (!res.ok)
|
|
263
|
+
log.debug("metrics.flush_rejected", { status: res.status });
|
|
264
|
+
} catch (err) {
|
|
265
|
+
log.debug("metrics.flush_failed", describeError(err));
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
const ensureTimer = () => {
|
|
269
|
+
if (timer !== void 0)
|
|
270
|
+
return;
|
|
271
|
+
timer = setInterval(() => schedule(flush()), config.flushIntervalMs);
|
|
272
|
+
timer.unref?.();
|
|
273
|
+
};
|
|
274
|
+
return {
|
|
275
|
+
record(ev) {
|
|
276
|
+
if (config.sampleRate < 1 && Math.random() >= config.sampleRate)
|
|
277
|
+
return;
|
|
278
|
+
buffer.push({ ...ev, timeUnixNano: nowUnixNano() });
|
|
279
|
+
ensureTimer();
|
|
280
|
+
if (buffer.length >= config.maxBatchSize)
|
|
281
|
+
schedule(flush());
|
|
282
|
+
},
|
|
283
|
+
flush
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function createRecorderForRuntime(mcp) {
|
|
287
|
+
const config = resolveMetricsConfig(mcp.metrics);
|
|
288
|
+
if (!config.enabled)
|
|
289
|
+
return createNoopRecorder();
|
|
290
|
+
return createMetricsRecorder({ config, server: { name: mcp.name, version: mcp.version } });
|
|
291
|
+
}
|
|
292
|
+
|
|
85
293
|
// src/core/promise.ts
|
|
86
294
|
function cachedPromise(load, label) {
|
|
87
295
|
let settled = false;
|
|
@@ -510,12 +718,13 @@ function assertRequiredScopes(auth, context) {
|
|
|
510
718
|
throw new OAuthTokenError(403, "insufficient_scope", "Additional OAuth scope is required");
|
|
511
719
|
}
|
|
512
720
|
}
|
|
513
|
-
function createRequestAuthorizer(mcp, options = {}) {
|
|
721
|
+
function createRequestAuthorizer(mcp, options = {}, recorder) {
|
|
514
722
|
const runtime = getOAuthRuntime(mcp, options);
|
|
515
723
|
return {
|
|
516
724
|
async authorize(request) {
|
|
517
725
|
if (runtime.kind === "unconfigured")
|
|
518
726
|
return { ok: true };
|
|
727
|
+
const startedAt = nowMs();
|
|
519
728
|
const token = parseBearerToken(request);
|
|
520
729
|
if (!token) {
|
|
521
730
|
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
@@ -528,6 +737,12 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
528
737
|
} catch (err) {
|
|
529
738
|
if (err instanceof OAuthConfigurationError) {
|
|
530
739
|
log.error("auth.config_error", { ...describeError(err), outcome: "500" });
|
|
740
|
+
recorder?.record({
|
|
741
|
+
tool: null,
|
|
742
|
+
method: "authorize",
|
|
743
|
+
outcome: "auth_config_error",
|
|
744
|
+
durationMs: nowMs() - startedAt
|
|
745
|
+
});
|
|
531
746
|
return { ok: false, response: oauthConfigurationErrorResponse() };
|
|
532
747
|
}
|
|
533
748
|
if (err instanceof OAuthTokenError) {
|
|
@@ -604,9 +819,9 @@ function buildMcpListing(mcp) {
|
|
|
604
819
|
}))
|
|
605
820
|
};
|
|
606
821
|
}
|
|
607
|
-
function createListToolsHandler(mcp, options = {}) {
|
|
822
|
+
function createListToolsHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
|
|
608
823
|
assertRestResourceBinding(mcp, options);
|
|
609
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
824
|
+
const authorizer = createRequestAuthorizer(mcp, options, recorder);
|
|
610
825
|
const handle = async (request) => {
|
|
611
826
|
const authResult = await authorizer.authorize(request);
|
|
612
827
|
if (!authResult.ok)
|
|
@@ -682,9 +897,9 @@ function isEmptyArgs(value) {
|
|
|
682
897
|
return false;
|
|
683
898
|
return Object.keys(value).length === 0;
|
|
684
899
|
}
|
|
685
|
-
function createInvokeToolHandler(mcp, options = {}) {
|
|
900
|
+
function createInvokeToolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
|
|
686
901
|
assertRestResourceBinding(mcp, options);
|
|
687
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
902
|
+
const authorizer = createRequestAuthorizer(mcp, options, recorder);
|
|
688
903
|
const handle = async (request, toolName) => {
|
|
689
904
|
const authResult = await authorizer.authorize(request);
|
|
690
905
|
if (!authResult.ok)
|
|
@@ -738,20 +953,29 @@ function createInvokeToolHandler(mcp, options = {}) {
|
|
|
738
953
|
});
|
|
739
954
|
}
|
|
740
955
|
let result;
|
|
956
|
+
const start = nowMs();
|
|
741
957
|
try {
|
|
742
958
|
result = await tool.handler(args, new ToolContext(authResult.auth));
|
|
743
959
|
} catch {
|
|
960
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
744
961
|
return new Response(JSON.stringify({ error: "handler threw", tool: toolName }), {
|
|
745
962
|
status: 500,
|
|
746
963
|
headers: JSON_HEADERS
|
|
747
964
|
});
|
|
748
965
|
}
|
|
749
966
|
if (result == null) {
|
|
967
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
750
968
|
return new Response(JSON.stringify({ error: `tool "${toolName}" returned no result` }), {
|
|
751
969
|
status: 500,
|
|
752
970
|
headers: JSON_HEADERS
|
|
753
971
|
});
|
|
754
972
|
}
|
|
973
|
+
recorder.record({
|
|
974
|
+
tool: tool.name,
|
|
975
|
+
method: "tools/call",
|
|
976
|
+
outcome: result.isError ? "tool_error" : "ok",
|
|
977
|
+
durationMs: nowMs() - start
|
|
978
|
+
});
|
|
755
979
|
return Response.json({
|
|
756
980
|
content: result.content ?? [],
|
|
757
981
|
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-qlzUKFxG.js';
|
|
3
4
|
import 'zod';
|
|
4
5
|
|
|
5
6
|
type RestListToolsHandler = (request: Request) => Promise<Response>;
|
|
6
|
-
declare function createListToolsHandler(mcp: McpDefinition, options?: McpRuntimeOptions): RestListToolsHandler;
|
|
7
|
+
declare function createListToolsHandler(mcp: McpDefinition, options?: McpRuntimeOptions, recorder?: MetricsRecorder): 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 };
|