@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
package/README.md
CHANGED
|
@@ -217,6 +217,34 @@ LOVABLE_MCP_LOG_LEVEL=debug
|
|
|
217
217
|
|
|
218
218
|
A `500` on an OAuth-protected route is always one of two causes, logged at `error`: an `OAuthConfigurationError` from issuer-metadata discovery or a JWKS *fetch* failure (`oauth.discovery.config_error` / `oauth.jwks.fetch_failed` → `auth.config_error`), or a transport-level fault in the MCP handler (`mcp.transport_error`). Auth outcomes are logged at `info`: a granted request as `oauth.verify.ok`, and a `401`/`403` as `auth.token_rejected` (with the `jose` reason) or `auth.no_bearer_token` — so a request rejected for insufficient scope shows both `oauth.verify.ok` and the `auth.token_rejected` that follows it.
|
|
219
219
|
|
|
220
|
+
## Usage metrics
|
|
221
|
+
|
|
222
|
+
Each MCP server records per-invocation telemetry — tool name, JSON-RPC method, outcome (`ok` / `handler_error` / `transport_error`), and latency — and POSTs it in fire-and-forget batches as **OTLP/HTTP JSON logs**. No tool arguments or response payloads are ever captured. Emission is **on by default**, self-disables at runtime when `LOVABLE_API_KEY` is absent (so a local `vite dev` stays silent), and degrades silently — a failed flush never blocks or fails a tool call.
|
|
223
|
+
|
|
224
|
+
Configure it on the MCP definition:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { defineMcp } from "@lovable.dev/mcp-js";
|
|
228
|
+
|
|
229
|
+
export default defineMcp({
|
|
230
|
+
name: "my-app-mcp",
|
|
231
|
+
// ...
|
|
232
|
+
tools: [],
|
|
233
|
+
metrics: {
|
|
234
|
+
endpoint: "https://your-collector.example.com/v1/logs",
|
|
235
|
+
headers: { authorization: "Bearer your-token" },
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
| Use case | Config |
|
|
241
|
+
| --- | --- |
|
|
242
|
+
| **Default (recommended)** | Omit `metrics`, or `metrics: true` — batches to Lovable, authenticated with `LOVABLE_API_KEY` |
|
|
243
|
+
| **Opt out entirely** | `metrics: false` |
|
|
244
|
+
| **Self-hosted collector** | `metrics: { endpoint, headers? }` — your own OTLP/HTTP (JSON) endpoint |
|
|
245
|
+
|
|
246
|
+
**Key scoping:** `LOVABLE_API_KEY` is read and sent (as the bearer) **only** for the default Lovable endpoint; a custom collector is reached with your own `headers` and never receives the workspace key. `headers` are baked into the build output, so source any token from your own build-time env. `content-type` is forced to `application/json` and can't be overridden.
|
|
247
|
+
|
|
220
248
|
## Supabase Edge Functions
|
|
221
249
|
|
|
222
250
|
Ship the same `defineMcp` server as a single Supabase Edge Function. Authoring is identical to the TanStack flow; only the build-time step differs — a different Vite plugin bundles your MCP entry and its local imports into one self-contained Deno function (with esbuild) instead of emitting a tree of file-router routes. Third-party dependencies are left as `npm:` specifiers that Deno resolves at runtime, so no import map / `deno.json` is needed.
|
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
JSON_HEADERS,
|
|
6
6
|
assertRestResourceBinding,
|
|
7
7
|
corsPreflightResponse,
|
|
8
|
+
createRecorderForRuntime,
|
|
8
9
|
createRequestAuthorizer,
|
|
9
10
|
methodNotAllowed,
|
|
11
|
+
nowMs,
|
|
10
12
|
withCors
|
|
11
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-YTOMGJV5.js";
|
|
12
14
|
|
|
13
15
|
// src/protocols/rest/invoke-tool.ts
|
|
14
16
|
import { getParseErrorMessage, objectFromShape, safeParseAsync } from "@modelcontextprotocol/sdk/server/zod-compat.js";
|
|
@@ -24,9 +26,9 @@ function isEmptyArgs(value) {
|
|
|
24
26
|
return false;
|
|
25
27
|
return Object.keys(value).length === 0;
|
|
26
28
|
}
|
|
27
|
-
function createInvokeToolHandler(mcp, options = {}) {
|
|
29
|
+
function createInvokeToolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
|
|
28
30
|
assertRestResourceBinding(mcp, options);
|
|
29
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
31
|
+
const authorizer = createRequestAuthorizer(mcp, options, recorder);
|
|
30
32
|
const handle = async (request, toolName) => {
|
|
31
33
|
const authResult = await authorizer.authorize(request);
|
|
32
34
|
if (!authResult.ok)
|
|
@@ -80,20 +82,29 @@ function createInvokeToolHandler(mcp, options = {}) {
|
|
|
80
82
|
});
|
|
81
83
|
}
|
|
82
84
|
let result;
|
|
85
|
+
const start = nowMs();
|
|
83
86
|
try {
|
|
84
87
|
result = await tool.handler(args, new ToolContext(authResult.auth));
|
|
85
88
|
} catch {
|
|
89
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
86
90
|
return new Response(JSON.stringify({ error: "handler threw", tool: toolName }), {
|
|
87
91
|
status: 500,
|
|
88
92
|
headers: JSON_HEADERS
|
|
89
93
|
});
|
|
90
94
|
}
|
|
91
95
|
if (result == null) {
|
|
96
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
92
97
|
return new Response(JSON.stringify({ error: `tool "${toolName}" returned no result` }), {
|
|
93
98
|
status: 500,
|
|
94
99
|
headers: JSON_HEADERS
|
|
95
100
|
});
|
|
96
101
|
}
|
|
102
|
+
recorder.record({
|
|
103
|
+
tool: tool.name,
|
|
104
|
+
method: "tools/call",
|
|
105
|
+
outcome: result.isError ? "tool_error" : "ok",
|
|
106
|
+
durationMs: nowMs() - start
|
|
107
|
+
});
|
|
97
108
|
return Response.json({
|
|
98
109
|
content: result.content ?? [],
|
|
99
110
|
structuredContent: result.structuredContent,
|
|
@@ -42,6 +42,38 @@ function describeError(err) {
|
|
|
42
42
|
return { value: String(err) };
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// src/metrics/config.ts
|
|
46
|
+
var DEFAULT_METRICS_ENDPOINT = "https://api.lovable.dev/v1/app-mcp-usage";
|
|
47
|
+
var METRICS_API_KEY_ENV_VAR = "LOVABLE_API_KEY";
|
|
48
|
+
var METRICS_SAMPLE_RATE = 1;
|
|
49
|
+
var METRICS_FLUSH_INTERVAL_MS = 5e3;
|
|
50
|
+
var METRICS_MAX_BATCH_SIZE = 50;
|
|
51
|
+
function assertMetricsEndpoint(endpoint) {
|
|
52
|
+
let url;
|
|
53
|
+
try {
|
|
54
|
+
url = new URL(endpoint);
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}`);
|
|
57
|
+
}
|
|
58
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
59
|
+
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be http(s), got ${JSON.stringify(endpoint)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function resolveMetricsConfig(config = true) {
|
|
63
|
+
const options = typeof config === "boolean" ? { enabled: config } : config;
|
|
64
|
+
const endpoint = options.endpoint ?? DEFAULT_METRICS_ENDPOINT;
|
|
65
|
+
assertMetricsEndpoint(endpoint);
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
enabled: options.enabled ?? true,
|
|
68
|
+
endpoint,
|
|
69
|
+
headers: options.headers ?? {},
|
|
70
|
+
apiKeyEnvVar: METRICS_API_KEY_ENV_VAR,
|
|
71
|
+
sampleRate: METRICS_SAMPLE_RATE,
|
|
72
|
+
flushIntervalMs: METRICS_FLUSH_INTERVAL_MS,
|
|
73
|
+
maxBatchSize: METRICS_MAX_BATCH_SIZE
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
45
77
|
// src/core/url.ts
|
|
46
78
|
function trimTrailingSlash(value) {
|
|
47
79
|
return value.replace(/\/+$/, "");
|
|
@@ -79,6 +111,8 @@ function parseSafeUrl(subject, raw, ErrorClass = Error) {
|
|
|
79
111
|
}
|
|
80
112
|
|
|
81
113
|
export {
|
|
114
|
+
DEFAULT_METRICS_ENDPOINT,
|
|
115
|
+
resolveMetricsConfig,
|
|
82
116
|
trimTrailingSlash,
|
|
83
117
|
parseSafeUrl,
|
|
84
118
|
setLogLevel,
|
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
oauthConfigurationErrorResponse,
|
|
8
8
|
resolveProtectedResource,
|
|
9
9
|
withCors
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-YTOMGJV5.js";
|
|
11
11
|
import {
|
|
12
12
|
describeError,
|
|
13
13
|
log
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-DCWYK4CA.js";
|
|
15
15
|
|
|
16
16
|
// src/protocols/oauth-metadata.ts
|
|
17
17
|
function notFound() {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
assertRestResourceBinding,
|
|
3
3
|
corsPreflightResponse,
|
|
4
|
+
createRecorderForRuntime,
|
|
4
5
|
createRequestAuthorizer,
|
|
5
6
|
headResponse,
|
|
6
7
|
methodNotAllowed,
|
|
7
8
|
withCors
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-YTOMGJV5.js";
|
|
9
10
|
|
|
10
11
|
// src/protocols/rest/list-tools.ts
|
|
11
12
|
import { objectFromShape } from "@modelcontextprotocol/sdk/server/zod-compat.js";
|
|
@@ -32,9 +33,9 @@ function buildMcpListing(mcp) {
|
|
|
32
33
|
}))
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
|
-
function createListToolsHandler(mcp, options = {}) {
|
|
36
|
+
function createListToolsHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
|
|
36
37
|
assertRestResourceBinding(mcp, options);
|
|
37
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
38
|
+
const authorizer = createRequestAuthorizer(mcp, options, recorder);
|
|
38
39
|
const handle = async (request) => {
|
|
39
40
|
const authResult = await authorizer.authorize(request);
|
|
40
41
|
if (!authResult.ok)
|
|
@@ -3,34 +3,45 @@ import {
|
|
|
3
3
|
} from "./chunk-MA5H6PSF.js";
|
|
4
4
|
import {
|
|
5
5
|
corsPreflightResponse,
|
|
6
|
+
createRecorderForRuntime,
|
|
6
7
|
createRequestAuthorizer,
|
|
8
|
+
nowMs,
|
|
7
9
|
withCors
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-YTOMGJV5.js";
|
|
9
11
|
import {
|
|
10
12
|
describeError,
|
|
11
13
|
log
|
|
12
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-DCWYK4CA.js";
|
|
13
15
|
|
|
14
16
|
// src/protocols/mcp/protocol.ts
|
|
15
17
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
18
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
17
|
-
function adaptToolToSdkCallback(tool, auth) {
|
|
19
|
+
function adaptToolToSdkCallback(tool, auth, recorder) {
|
|
18
20
|
return async (first) => {
|
|
19
21
|
const args = tool.inputSchema ? first ?? {} : {};
|
|
22
|
+
const start = nowMs();
|
|
20
23
|
let result;
|
|
21
24
|
try {
|
|
22
25
|
result = await tool.handler(args, new ToolContext(auth));
|
|
23
26
|
} catch {
|
|
27
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
24
28
|
return { content: [{ type: "text", text: "tool execution failed" }], isError: true };
|
|
25
29
|
}
|
|
26
30
|
if (result == null) {
|
|
31
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
27
32
|
return { content: [{ type: "text", text: `tool "${tool.name}" returned no result` }], isError: true };
|
|
28
33
|
}
|
|
34
|
+
recorder.record({
|
|
35
|
+
tool: tool.name,
|
|
36
|
+
method: "tools/call",
|
|
37
|
+
outcome: result.isError ? "tool_error" : "ok",
|
|
38
|
+
durationMs: nowMs() - start
|
|
39
|
+
});
|
|
29
40
|
return { content: result.content ?? [], structuredContent: result.structuredContent, isError: result.isError };
|
|
30
41
|
};
|
|
31
42
|
}
|
|
32
|
-
function createMcpProtocolHandler(mcp, options = {}) {
|
|
33
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
43
|
+
function createMcpProtocolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
|
|
44
|
+
const authorizer = createRequestAuthorizer(mcp, options, recorder);
|
|
34
45
|
const handle = async (request) => {
|
|
35
46
|
const authResult = await authorizer.authorize(request);
|
|
36
47
|
if (!authResult.ok)
|
|
@@ -50,7 +61,7 @@ function createMcpProtocolHandler(mcp, options = {}) {
|
|
|
50
61
|
outputSchema: tool.outputSchema,
|
|
51
62
|
annotations: tool.annotations
|
|
52
63
|
},
|
|
53
|
-
adaptToolToSdkCallback(tool, authResult.auth)
|
|
64
|
+
adaptToolToSdkCallback(tool, authResult.auth, recorder)
|
|
54
65
|
);
|
|
55
66
|
}
|
|
56
67
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
@@ -59,6 +70,7 @@ function createMcpProtocolHandler(mcp, options = {}) {
|
|
|
59
70
|
await server.connect(transport);
|
|
60
71
|
return await transport.handleRequest(request);
|
|
61
72
|
} catch (err) {
|
|
73
|
+
recorder.record({ tool: null, method: "transport", outcome: "transport_error", durationMs: 0 });
|
|
62
74
|
log.error("mcp.transport_error", { ...describeError(err), outcome: "500 internal error" });
|
|
63
75
|
return Response.json(
|
|
64
76
|
{ jsonrpc: "2.0", id: null, error: { code: -32603, message: "internal error" } },
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_METRICS_ENDPOINT,
|
|
2
3
|
describeError,
|
|
3
4
|
log,
|
|
4
5
|
parseSafeUrl,
|
|
6
|
+
resolveMetricsConfig,
|
|
5
7
|
trimTrailingSlash
|
|
6
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-DCWYK4CA.js";
|
|
7
9
|
import {
|
|
8
10
|
OAUTH_PROTECTED_RESOURCE_METADATA_PATH
|
|
9
11
|
} from "./chunk-6DXGZZA4.js";
|
|
12
|
+
import {
|
|
13
|
+
version
|
|
14
|
+
} from "./chunk-GIHCQT6L.js";
|
|
10
15
|
|
|
11
16
|
// src/core/http.ts
|
|
12
17
|
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
@@ -44,6 +49,179 @@ function resolveResourcePath(resourcePath, request) {
|
|
|
44
49
|
return resourcePath;
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
// src/metrics/otlp.ts
|
|
53
|
+
var SCOPE_NAME = "@lovable.dev/mcp-js";
|
|
54
|
+
var EVENT_NAME = "mcp.tool.invocation";
|
|
55
|
+
var SEVERITY_INFO = 9;
|
|
56
|
+
function strAttr(key, value) {
|
|
57
|
+
return { key, value: { stringValue: value } };
|
|
58
|
+
}
|
|
59
|
+
function intAttr(key, value) {
|
|
60
|
+
return { key, value: { intValue: String(Math.round(value)) } };
|
|
61
|
+
}
|
|
62
|
+
function toLogRecord(rec) {
|
|
63
|
+
const attributes = [
|
|
64
|
+
strAttr("event.name", EVENT_NAME),
|
|
65
|
+
strAttr("mcp.method", rec.method),
|
|
66
|
+
strAttr("mcp.outcome", rec.outcome),
|
|
67
|
+
intAttr("mcp.duration_ms", rec.durationMs)
|
|
68
|
+
];
|
|
69
|
+
if (rec.tool !== null)
|
|
70
|
+
attributes.push(strAttr("mcp.tool", rec.tool));
|
|
71
|
+
if (rec.reqBytes !== void 0)
|
|
72
|
+
attributes.push(intAttr("mcp.request_size_bytes", rec.reqBytes));
|
|
73
|
+
if (rec.resBytes !== void 0)
|
|
74
|
+
attributes.push(intAttr("mcp.response_size_bytes", rec.resBytes));
|
|
75
|
+
return {
|
|
76
|
+
timeUnixNano: rec.timeUnixNano,
|
|
77
|
+
observedTimeUnixNano: rec.timeUnixNano,
|
|
78
|
+
severityNumber: SEVERITY_INFO,
|
|
79
|
+
severityText: "INFO",
|
|
80
|
+
body: { stringValue: EVENT_NAME },
|
|
81
|
+
attributes
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function buildLogsPayload(records, server) {
|
|
85
|
+
return JSON.stringify({
|
|
86
|
+
resourceLogs: [
|
|
87
|
+
{
|
|
88
|
+
resource: {
|
|
89
|
+
attributes: [
|
|
90
|
+
strAttr("service.name", server.name),
|
|
91
|
+
strAttr("service.version", server.version),
|
|
92
|
+
strAttr("telemetry.sdk.name", SCOPE_NAME),
|
|
93
|
+
strAttr("telemetry.sdk.version", version),
|
|
94
|
+
strAttr("telemetry.sdk.language", "webjs")
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
scopeLogs: [
|
|
98
|
+
{
|
|
99
|
+
scope: { name: SCOPE_NAME, version },
|
|
100
|
+
logRecords: records.map(toLogRecord)
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function nowUnixNano() {
|
|
108
|
+
return `${Date.now()}000000`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/metrics/recorder.ts
|
|
112
|
+
function nowMs() {
|
|
113
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
114
|
+
}
|
|
115
|
+
var NOOP_RECORDER = {
|
|
116
|
+
record() {
|
|
117
|
+
},
|
|
118
|
+
async flush() {
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
function createNoopRecorder() {
|
|
122
|
+
return NOOP_RECORDER;
|
|
123
|
+
}
|
|
124
|
+
function readRuntimeEnv(name) {
|
|
125
|
+
try {
|
|
126
|
+
const denoEnv = globalThis.Deno?.env;
|
|
127
|
+
const value = denoEnv?.get?.(name);
|
|
128
|
+
if (value)
|
|
129
|
+
return value;
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
if (typeof process !== "undefined") {
|
|
134
|
+
const value = process.env?.[name];
|
|
135
|
+
if (value)
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
function probeWaitUntil() {
|
|
143
|
+
const fn = globalThis.EdgeRuntime?.waitUntil;
|
|
144
|
+
return typeof fn === "function" ? fn.bind(globalThis.EdgeRuntime) : void 0;
|
|
145
|
+
}
|
|
146
|
+
function createMetricsRecorder(ctx, deps = {}) {
|
|
147
|
+
const { config, server } = ctx;
|
|
148
|
+
if (!config.enabled)
|
|
149
|
+
return NOOP_RECORDER;
|
|
150
|
+
const doFetch = deps.fetch ?? globalThis.fetch;
|
|
151
|
+
if (!doFetch)
|
|
152
|
+
return NOOP_RECORDER;
|
|
153
|
+
const usesLovableEndpoint = config.endpoint === DEFAULT_METRICS_ENDPOINT;
|
|
154
|
+
const apiKey = usesLovableEndpoint ? (deps.getApiKey ?? (() => readRuntimeEnv(config.apiKeyEnvVar)))() : void 0;
|
|
155
|
+
if (usesLovableEndpoint && !apiKey) {
|
|
156
|
+
log.debug("metrics.disabled_no_api_key", { envVar: config.apiKeyEnvVar });
|
|
157
|
+
return NOOP_RECORDER;
|
|
158
|
+
}
|
|
159
|
+
const waitUntil = deps.waitUntil ?? probeWaitUntil();
|
|
160
|
+
const buffer = [];
|
|
161
|
+
let timer;
|
|
162
|
+
const schedule = (p) => {
|
|
163
|
+
if (waitUntil) {
|
|
164
|
+
try {
|
|
165
|
+
waitUntil(p);
|
|
166
|
+
return;
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
void p.catch(() => {
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
const headers = {};
|
|
174
|
+
if (!usesLovableEndpoint) {
|
|
175
|
+
for (const [key, value] of Object.entries(config.headers)) {
|
|
176
|
+
if (key.toLowerCase() !== "content-type")
|
|
177
|
+
headers[key] = value;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
headers["content-type"] = "application/json";
|
|
181
|
+
if (apiKey)
|
|
182
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
183
|
+
const flush = async () => {
|
|
184
|
+
if (buffer.length === 0)
|
|
185
|
+
return;
|
|
186
|
+
const records = buffer.splice(0, buffer.length);
|
|
187
|
+
try {
|
|
188
|
+
const res = await doFetch(config.endpoint, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers,
|
|
191
|
+
body: buildLogsPayload(records, server),
|
|
192
|
+
keepalive: true
|
|
193
|
+
});
|
|
194
|
+
if (!res.ok)
|
|
195
|
+
log.debug("metrics.flush_rejected", { status: res.status });
|
|
196
|
+
} catch (err) {
|
|
197
|
+
log.debug("metrics.flush_failed", describeError(err));
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const ensureTimer = () => {
|
|
201
|
+
if (timer !== void 0)
|
|
202
|
+
return;
|
|
203
|
+
timer = setInterval(() => schedule(flush()), config.flushIntervalMs);
|
|
204
|
+
timer.unref?.();
|
|
205
|
+
};
|
|
206
|
+
return {
|
|
207
|
+
record(ev) {
|
|
208
|
+
if (config.sampleRate < 1 && Math.random() >= config.sampleRate)
|
|
209
|
+
return;
|
|
210
|
+
buffer.push({ ...ev, timeUnixNano: nowUnixNano() });
|
|
211
|
+
ensureTimer();
|
|
212
|
+
if (buffer.length >= config.maxBatchSize)
|
|
213
|
+
schedule(flush());
|
|
214
|
+
},
|
|
215
|
+
flush
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function createRecorderForRuntime(mcp) {
|
|
219
|
+
const config = resolveMetricsConfig(mcp.metrics);
|
|
220
|
+
if (!config.enabled)
|
|
221
|
+
return createNoopRecorder();
|
|
222
|
+
return createMetricsRecorder({ config, server: { name: mcp.name, version: mcp.version } });
|
|
223
|
+
}
|
|
224
|
+
|
|
47
225
|
// src/core/promise.ts
|
|
48
226
|
function cachedPromise(load, label) {
|
|
49
227
|
let settled = false;
|
|
@@ -409,12 +587,13 @@ function assertRequiredScopes(auth, context) {
|
|
|
409
587
|
throw new OAuthTokenError(403, "insufficient_scope", "Additional OAuth scope is required");
|
|
410
588
|
}
|
|
411
589
|
}
|
|
412
|
-
function createRequestAuthorizer(mcp, options = {}) {
|
|
590
|
+
function createRequestAuthorizer(mcp, options = {}, recorder) {
|
|
413
591
|
const runtime = getOAuthRuntime(mcp, options);
|
|
414
592
|
return {
|
|
415
593
|
async authorize(request) {
|
|
416
594
|
if (runtime.kind === "unconfigured")
|
|
417
595
|
return { ok: true };
|
|
596
|
+
const startedAt = nowMs();
|
|
418
597
|
const token = parseBearerToken(request);
|
|
419
598
|
if (!token) {
|
|
420
599
|
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
@@ -427,6 +606,12 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
427
606
|
} catch (err) {
|
|
428
607
|
if (err instanceof OAuthConfigurationError) {
|
|
429
608
|
log.error("auth.config_error", { ...describeError(err), outcome: "500" });
|
|
609
|
+
recorder?.record({
|
|
610
|
+
tool: null,
|
|
611
|
+
method: "authorize",
|
|
612
|
+
outcome: "auth_config_error",
|
|
613
|
+
durationMs: nowMs() - startedAt
|
|
614
|
+
});
|
|
430
615
|
return { ok: false, response: oauthConfigurationErrorResponse() };
|
|
431
616
|
}
|
|
432
617
|
if (err instanceof OAuthTokenError) {
|
|
@@ -484,6 +669,8 @@ export {
|
|
|
484
669
|
JSON_HEADERS,
|
|
485
670
|
headResponse,
|
|
486
671
|
methodNotAllowed,
|
|
672
|
+
nowMs,
|
|
673
|
+
createRecorderForRuntime,
|
|
487
674
|
resolveProtectedResource,
|
|
488
675
|
assertResourcePathShape,
|
|
489
676
|
oauthConfigurationErrorResponse,
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
buildMcpListing
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
6
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-QT5L4CS3.js";
|
|
5
|
+
import "../chunk-YTOMGJV5.js";
|
|
6
|
+
import "../chunk-DCWYK4CA.js";
|
|
7
7
|
import "../chunk-6DXGZZA4.js";
|
|
8
|
-
import {
|
|
9
|
-
version
|
|
10
|
-
} from "../chunk-NOFPRSSI.js";
|
|
11
8
|
import {
|
|
12
9
|
isFileMissing
|
|
13
10
|
} from "../chunk-Y3ZFPEQH.js";
|
|
11
|
+
import {
|
|
12
|
+
version
|
|
13
|
+
} from "../chunk-GIHCQT6L.js";
|
|
14
14
|
|
|
15
15
|
// src/manifest/io.ts
|
|
16
16
|
import { randomUUID } from "crypto";
|
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,38 @@ __export(src_exports, {
|
|
|
28
28
|
});
|
|
29
29
|
module.exports = __toCommonJS(src_exports);
|
|
30
30
|
|
|
31
|
+
// src/metrics/config.ts
|
|
32
|
+
var DEFAULT_METRICS_ENDPOINT = "https://api.lovable.dev/v1/app-mcp-usage";
|
|
33
|
+
var METRICS_API_KEY_ENV_VAR = "LOVABLE_API_KEY";
|
|
34
|
+
var METRICS_SAMPLE_RATE = 1;
|
|
35
|
+
var METRICS_FLUSH_INTERVAL_MS = 5e3;
|
|
36
|
+
var METRICS_MAX_BATCH_SIZE = 50;
|
|
37
|
+
function assertMetricsEndpoint(endpoint) {
|
|
38
|
+
let url;
|
|
39
|
+
try {
|
|
40
|
+
url = new URL(endpoint);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}`);
|
|
43
|
+
}
|
|
44
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
45
|
+
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be http(s), got ${JSON.stringify(endpoint)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function resolveMetricsConfig(config = true) {
|
|
49
|
+
const options = typeof config === "boolean" ? { enabled: config } : config;
|
|
50
|
+
const endpoint = options.endpoint ?? DEFAULT_METRICS_ENDPOINT;
|
|
51
|
+
assertMetricsEndpoint(endpoint);
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
enabled: options.enabled ?? true,
|
|
54
|
+
endpoint,
|
|
55
|
+
headers: options.headers ?? {},
|
|
56
|
+
apiKeyEnvVar: METRICS_API_KEY_ENV_VAR,
|
|
57
|
+
sampleRate: METRICS_SAMPLE_RATE,
|
|
58
|
+
flushIntervalMs: METRICS_FLUSH_INTERVAL_MS,
|
|
59
|
+
maxBatchSize: METRICS_MAX_BATCH_SIZE
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
31
63
|
// src/core/url.ts
|
|
32
64
|
function isLocalHTTPHost(hostname) {
|
|
33
65
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
@@ -202,6 +234,7 @@ function defineMcp(def) {
|
|
|
202
234
|
assertUniqueNames(def);
|
|
203
235
|
if (def.auth)
|
|
204
236
|
assertOAuthConfig(def.auth);
|
|
237
|
+
resolveMetricsConfig(def.metrics);
|
|
205
238
|
freezeAuth(def.auth);
|
|
206
239
|
Object.freeze(def.tools);
|
|
207
240
|
for (const tool of def.tools)
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape,
|
|
2
|
-
export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink,
|
|
1
|
+
import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, l as ToolDefinition, M as McpAuthConfig } from './types-COY42xux.js';
|
|
2
|
+
export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, f as MetricsConfig, g as MetricsOptions, R as ResourceLink, h as ResourceLinkIcon, T as TextContent, i as ToolAnnotations, j as ToolContent, k as ToolContext, m as ToolHandlerResult, n as ZodSchema } from './types-COY42xux.js';
|
|
3
3
|
import 'zod';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape,
|
|
2
|
-
export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink,
|
|
1
|
+
import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, l as ToolDefinition, M as McpAuthConfig } from './types-COY42xux.js';
|
|
2
|
+
export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, f as MetricsConfig, g as MetricsOptions, R as ResourceLink, h as ResourceLinkIcon, T as TextContent, i as ToolAnnotations, j as ToolContent, k as ToolContext, m as ToolHandlerResult, n as ZodSchema } from './types-COY42xux.js';
|
|
3
3
|
import 'zod';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
} from "./chunk-MA5H6PSF.js";
|
|
4
4
|
import {
|
|
5
5
|
parseSafeUrl,
|
|
6
|
+
resolveMetricsConfig,
|
|
6
7
|
setLogLevel
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-DCWYK4CA.js";
|
|
8
9
|
|
|
9
10
|
// src/core/define.ts
|
|
10
11
|
function assertUniqueNames(mcp) {
|
|
@@ -147,6 +148,7 @@ function defineMcp(def) {
|
|
|
147
148
|
assertUniqueNames(def);
|
|
148
149
|
if (def.auth)
|
|
149
150
|
assertOAuthConfig(def.auth);
|
|
151
|
+
resolveMetricsConfig(def.metrics);
|
|
150
152
|
freezeAuth(def.auth);
|
|
151
153
|
Object.freeze(def.tools);
|
|
152
154
|
for (const tool of def.tools)
|