@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
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
version
|
|
3
|
-
} from "../../chunk-NOFPRSSI.js";
|
|
4
1
|
import {
|
|
5
2
|
isFileMissing
|
|
6
3
|
} from "../../chunk-Y3ZFPEQH.js";
|
|
@@ -8,9 +5,13 @@ import {
|
|
|
8
5
|
FUNCTIONS_MOUNT_PREFIX,
|
|
9
6
|
assertFunctionName
|
|
10
7
|
} from "../../chunk-XQWJN6DC.js";
|
|
8
|
+
import {
|
|
9
|
+
version
|
|
10
|
+
} from "../../chunk-GIHCQT6L.js";
|
|
11
11
|
|
|
12
12
|
// src/stacks/supabase/vite.ts
|
|
13
13
|
import { resolve as resolve2, sep as sep2 } from "path";
|
|
14
|
+
import { loadEnv } from "vite";
|
|
14
15
|
|
|
15
16
|
// src/stacks/supabase/emit.ts
|
|
16
17
|
import { build } from "esbuild";
|
|
@@ -95,7 +96,16 @@ function externalizeBareAsNpm(versions) {
|
|
|
95
96
|
}
|
|
96
97
|
};
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
+
function importMetaEnvDefines(inlineEnv) {
|
|
100
|
+
const env = inlineEnv ?? {};
|
|
101
|
+
const defines = { "import.meta.env": JSON.stringify(env) };
|
|
102
|
+
for (const [key, value] of Object.entries(env)) {
|
|
103
|
+
if (/^[A-Za-z_$][\w$]*$/.test(key))
|
|
104
|
+
defines[`import.meta.env.${key}`] = JSON.stringify(value);
|
|
105
|
+
}
|
|
106
|
+
return defines;
|
|
107
|
+
}
|
|
108
|
+
async function bundleFunctionEntry(projectRoot, mcpEntryAbs, functionName, inlineEnv) {
|
|
99
109
|
const versions = readProjectDependencyVersions(projectRoot);
|
|
100
110
|
const wrapper = `import mcp from ${JSON.stringify(mcpEntryAbs)};
|
|
101
111
|
import { createSupabaseHandler } from "@lovable.dev/mcp-js/stacks/supabase";
|
|
@@ -111,6 +121,7 @@ Deno.serve(createSupabaseHandler(mcp, { functionName: ${JSON.stringify(functionN
|
|
|
111
121
|
target: "esnext",
|
|
112
122
|
absWorkingDir: projectRoot,
|
|
113
123
|
logLevel: "silent",
|
|
124
|
+
define: importMetaEnvDefines(inlineEnv),
|
|
114
125
|
plugins: [externalizeBareAsNpm(versions)]
|
|
115
126
|
});
|
|
116
127
|
const out = result.outputFiles[0]?.text ?? "";
|
|
@@ -149,7 +160,7 @@ async function syncSupabaseFunction(options) {
|
|
|
149
160
|
cleanupOrphans(paths.functionDir, /* @__PURE__ */ new Set(), removed);
|
|
150
161
|
return { written, removed, inputs: [] };
|
|
151
162
|
}
|
|
152
|
-
const { code, inputs } = await bundleFunctionEntry(projectRoot, paths.mcpEntryAbs, functionName);
|
|
163
|
+
const { code, inputs } = await bundleFunctionEntry(projectRoot, paths.mcpEntryAbs, functionName, options.inlineEnv);
|
|
153
164
|
if (writeIfChanged(paths.functionEntry, code, hasGeneratedBanner)) {
|
|
154
165
|
written.push(paths.functionEntry);
|
|
155
166
|
}
|
|
@@ -193,6 +204,16 @@ function cleanupOrphans(functionDir, expected, removed) {
|
|
|
193
204
|
function normalizePath(p) {
|
|
194
205
|
return p.split(sep2).join("/");
|
|
195
206
|
}
|
|
207
|
+
function productionImportMetaEnv(config) {
|
|
208
|
+
return {
|
|
209
|
+
...loadEnv("production", config.envDir, config.envPrefix),
|
|
210
|
+
MODE: "production",
|
|
211
|
+
BASE_URL: config.base ?? "/",
|
|
212
|
+
DEV: false,
|
|
213
|
+
PROD: true,
|
|
214
|
+
SSR: false
|
|
215
|
+
};
|
|
216
|
+
}
|
|
196
217
|
function mcpPlugin(options = {}) {
|
|
197
218
|
const mcpEntryOption = options.mcpEntry ?? DEFAULT_MCP_ENTRY;
|
|
198
219
|
const functionsDirOption = options.functionsDir ?? DEFAULT_FUNCTIONS_DIR;
|
|
@@ -200,13 +221,15 @@ function mcpPlugin(options = {}) {
|
|
|
200
221
|
const urlPath = `${FUNCTIONS_MOUNT_PREFIX}${functionName}`;
|
|
201
222
|
let projectRoot = process.cwd();
|
|
202
223
|
let mcpEntryAbs = resolve2(projectRoot, mcpEntryOption);
|
|
224
|
+
let inlineEnv = {};
|
|
203
225
|
let watchedInputs = /* @__PURE__ */ new Set();
|
|
204
226
|
const regenerate = async () => {
|
|
205
227
|
const { inputs } = await syncSupabaseFunction({
|
|
206
228
|
projectRoot,
|
|
207
229
|
mcpEntry: mcpEntryOption,
|
|
208
230
|
functionsDir: functionsDirOption,
|
|
209
|
-
functionName
|
|
231
|
+
functionName,
|
|
232
|
+
inlineEnv
|
|
210
233
|
});
|
|
211
234
|
watchedInputs = new Set(inputs.map(normalizePath));
|
|
212
235
|
};
|
|
@@ -224,6 +247,7 @@ function mcpPlugin(options = {}) {
|
|
|
224
247
|
async configResolved(config) {
|
|
225
248
|
projectRoot = config.root;
|
|
226
249
|
mcpEntryAbs = resolve2(projectRoot, mcpEntryOption);
|
|
250
|
+
inlineEnv = productionImportMetaEnv(config);
|
|
227
251
|
await regenerate();
|
|
228
252
|
},
|
|
229
253
|
configureServer(server) {
|
|
@@ -107,6 +107,214 @@ function describeError(err) {
|
|
|
107
107
|
return { value: String(err) };
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// src/metrics/config.ts
|
|
111
|
+
var DEFAULT_METRICS_ENDPOINT = "https://api.lovable.dev/v1/app-mcp-usage";
|
|
112
|
+
var METRICS_API_KEY_ENV_VAR = "LOVABLE_API_KEY";
|
|
113
|
+
var METRICS_SAMPLE_RATE = 1;
|
|
114
|
+
var METRICS_FLUSH_INTERVAL_MS = 5e3;
|
|
115
|
+
var METRICS_MAX_BATCH_SIZE = 50;
|
|
116
|
+
function assertMetricsEndpoint(endpoint) {
|
|
117
|
+
let url;
|
|
118
|
+
try {
|
|
119
|
+
url = new URL(endpoint);
|
|
120
|
+
} catch {
|
|
121
|
+
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}`);
|
|
122
|
+
}
|
|
123
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
124
|
+
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be http(s), got ${JSON.stringify(endpoint)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function resolveMetricsConfig(config = true) {
|
|
128
|
+
const options = typeof config === "boolean" ? { enabled: config } : config;
|
|
129
|
+
const endpoint = options.endpoint ?? DEFAULT_METRICS_ENDPOINT;
|
|
130
|
+
assertMetricsEndpoint(endpoint);
|
|
131
|
+
return Object.freeze({
|
|
132
|
+
enabled: options.enabled ?? true,
|
|
133
|
+
endpoint,
|
|
134
|
+
headers: options.headers ?? {},
|
|
135
|
+
apiKeyEnvVar: METRICS_API_KEY_ENV_VAR,
|
|
136
|
+
sampleRate: METRICS_SAMPLE_RATE,
|
|
137
|
+
flushIntervalMs: METRICS_FLUSH_INTERVAL_MS,
|
|
138
|
+
maxBatchSize: METRICS_MAX_BATCH_SIZE
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// package.json
|
|
143
|
+
var version = "0.15.1";
|
|
144
|
+
|
|
145
|
+
// src/metrics/otlp.ts
|
|
146
|
+
var SCOPE_NAME = "@lovable.dev/mcp-js";
|
|
147
|
+
var EVENT_NAME = "mcp.tool.invocation";
|
|
148
|
+
var SEVERITY_INFO = 9;
|
|
149
|
+
function strAttr(key, value) {
|
|
150
|
+
return { key, value: { stringValue: value } };
|
|
151
|
+
}
|
|
152
|
+
function intAttr(key, value) {
|
|
153
|
+
return { key, value: { intValue: String(Math.round(value)) } };
|
|
154
|
+
}
|
|
155
|
+
function toLogRecord(rec) {
|
|
156
|
+
const attributes = [
|
|
157
|
+
strAttr("event.name", EVENT_NAME),
|
|
158
|
+
strAttr("mcp.method", rec.method),
|
|
159
|
+
strAttr("mcp.outcome", rec.outcome),
|
|
160
|
+
intAttr("mcp.duration_ms", rec.durationMs)
|
|
161
|
+
];
|
|
162
|
+
if (rec.tool !== null)
|
|
163
|
+
attributes.push(strAttr("mcp.tool", rec.tool));
|
|
164
|
+
if (rec.reqBytes !== void 0)
|
|
165
|
+
attributes.push(intAttr("mcp.request_size_bytes", rec.reqBytes));
|
|
166
|
+
if (rec.resBytes !== void 0)
|
|
167
|
+
attributes.push(intAttr("mcp.response_size_bytes", rec.resBytes));
|
|
168
|
+
return {
|
|
169
|
+
timeUnixNano: rec.timeUnixNano,
|
|
170
|
+
observedTimeUnixNano: rec.timeUnixNano,
|
|
171
|
+
severityNumber: SEVERITY_INFO,
|
|
172
|
+
severityText: "INFO",
|
|
173
|
+
body: { stringValue: EVENT_NAME },
|
|
174
|
+
attributes
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function buildLogsPayload(records, server) {
|
|
178
|
+
return JSON.stringify({
|
|
179
|
+
resourceLogs: [
|
|
180
|
+
{
|
|
181
|
+
resource: {
|
|
182
|
+
attributes: [
|
|
183
|
+
strAttr("service.name", server.name),
|
|
184
|
+
strAttr("service.version", server.version),
|
|
185
|
+
strAttr("telemetry.sdk.name", SCOPE_NAME),
|
|
186
|
+
strAttr("telemetry.sdk.version", version),
|
|
187
|
+
strAttr("telemetry.sdk.language", "webjs")
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
scopeLogs: [
|
|
191
|
+
{
|
|
192
|
+
scope: { name: SCOPE_NAME, version },
|
|
193
|
+
logRecords: records.map(toLogRecord)
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function nowUnixNano() {
|
|
201
|
+
return `${Date.now()}000000`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/metrics/recorder.ts
|
|
205
|
+
function nowMs() {
|
|
206
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
207
|
+
}
|
|
208
|
+
var NOOP_RECORDER = {
|
|
209
|
+
record() {
|
|
210
|
+
},
|
|
211
|
+
async flush() {
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
function createNoopRecorder() {
|
|
215
|
+
return NOOP_RECORDER;
|
|
216
|
+
}
|
|
217
|
+
function readRuntimeEnv(name) {
|
|
218
|
+
try {
|
|
219
|
+
const denoEnv = globalThis.Deno?.env;
|
|
220
|
+
const value = denoEnv?.get?.(name);
|
|
221
|
+
if (value)
|
|
222
|
+
return value;
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
if (typeof process !== "undefined") {
|
|
227
|
+
const value = process.env?.[name];
|
|
228
|
+
if (value)
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
return void 0;
|
|
234
|
+
}
|
|
235
|
+
function probeWaitUntil() {
|
|
236
|
+
const fn = globalThis.EdgeRuntime?.waitUntil;
|
|
237
|
+
return typeof fn === "function" ? fn.bind(globalThis.EdgeRuntime) : void 0;
|
|
238
|
+
}
|
|
239
|
+
function createMetricsRecorder(ctx, deps = {}) {
|
|
240
|
+
const { config, server } = ctx;
|
|
241
|
+
if (!config.enabled)
|
|
242
|
+
return NOOP_RECORDER;
|
|
243
|
+
const doFetch = deps.fetch ?? globalThis.fetch;
|
|
244
|
+
if (!doFetch)
|
|
245
|
+
return NOOP_RECORDER;
|
|
246
|
+
const usesLovableEndpoint = config.endpoint === DEFAULT_METRICS_ENDPOINT;
|
|
247
|
+
const apiKey = usesLovableEndpoint ? (deps.getApiKey ?? (() => readRuntimeEnv(config.apiKeyEnvVar)))() : void 0;
|
|
248
|
+
if (usesLovableEndpoint && !apiKey) {
|
|
249
|
+
log.debug("metrics.disabled_no_api_key", { envVar: config.apiKeyEnvVar });
|
|
250
|
+
return NOOP_RECORDER;
|
|
251
|
+
}
|
|
252
|
+
const waitUntil = deps.waitUntil ?? probeWaitUntil();
|
|
253
|
+
const buffer = [];
|
|
254
|
+
let timer;
|
|
255
|
+
const schedule = (p) => {
|
|
256
|
+
if (waitUntil) {
|
|
257
|
+
try {
|
|
258
|
+
waitUntil(p);
|
|
259
|
+
return;
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
void p.catch(() => {
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
const headers = {};
|
|
267
|
+
if (!usesLovableEndpoint) {
|
|
268
|
+
for (const [key, value] of Object.entries(config.headers)) {
|
|
269
|
+
if (key.toLowerCase() !== "content-type")
|
|
270
|
+
headers[key] = value;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
headers["content-type"] = "application/json";
|
|
274
|
+
if (apiKey)
|
|
275
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
276
|
+
const flush = async () => {
|
|
277
|
+
if (buffer.length === 0)
|
|
278
|
+
return;
|
|
279
|
+
const records = buffer.splice(0, buffer.length);
|
|
280
|
+
try {
|
|
281
|
+
const res = await doFetch(config.endpoint, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers,
|
|
284
|
+
body: buildLogsPayload(records, server),
|
|
285
|
+
keepalive: true
|
|
286
|
+
});
|
|
287
|
+
if (!res.ok)
|
|
288
|
+
log.debug("metrics.flush_rejected", { status: res.status });
|
|
289
|
+
} catch (err) {
|
|
290
|
+
log.debug("metrics.flush_failed", describeError(err));
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const ensureTimer = () => {
|
|
294
|
+
if (timer !== void 0)
|
|
295
|
+
return;
|
|
296
|
+
timer = setInterval(() => schedule(flush()), config.flushIntervalMs);
|
|
297
|
+
timer.unref?.();
|
|
298
|
+
};
|
|
299
|
+
return {
|
|
300
|
+
record(ev) {
|
|
301
|
+
if (config.sampleRate < 1 && Math.random() >= config.sampleRate)
|
|
302
|
+
return;
|
|
303
|
+
buffer.push({ ...ev, timeUnixNano: nowUnixNano() });
|
|
304
|
+
ensureTimer();
|
|
305
|
+
if (buffer.length >= config.maxBatchSize)
|
|
306
|
+
schedule(flush());
|
|
307
|
+
},
|
|
308
|
+
flush
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function createRecorderForRuntime(mcp) {
|
|
312
|
+
const config = resolveMetricsConfig(mcp.metrics);
|
|
313
|
+
if (!config.enabled)
|
|
314
|
+
return createNoopRecorder();
|
|
315
|
+
return createMetricsRecorder({ config, server: { name: mcp.name, version: mcp.version } });
|
|
316
|
+
}
|
|
317
|
+
|
|
110
318
|
// src/core/promise.ts
|
|
111
319
|
function cachedPromise(load, label) {
|
|
112
320
|
let settled = false;
|
|
@@ -535,12 +743,13 @@ function assertRequiredScopes(auth, context) {
|
|
|
535
743
|
throw new OAuthTokenError(403, "insufficient_scope", "Additional OAuth scope is required");
|
|
536
744
|
}
|
|
537
745
|
}
|
|
538
|
-
function createRequestAuthorizer(mcp, options = {}) {
|
|
746
|
+
function createRequestAuthorizer(mcp, options = {}, recorder) {
|
|
539
747
|
const runtime = getOAuthRuntime(mcp, options);
|
|
540
748
|
return {
|
|
541
749
|
async authorize(request) {
|
|
542
750
|
if (runtime.kind === "unconfigured")
|
|
543
751
|
return { ok: true };
|
|
752
|
+
const startedAt = nowMs();
|
|
544
753
|
const token = parseBearerToken(request);
|
|
545
754
|
if (!token) {
|
|
546
755
|
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
@@ -553,6 +762,12 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
553
762
|
} catch (err) {
|
|
554
763
|
if (err instanceof OAuthConfigurationError) {
|
|
555
764
|
log.error("auth.config_error", { ...describeError(err), outcome: "500" });
|
|
765
|
+
recorder?.record({
|
|
766
|
+
tool: null,
|
|
767
|
+
method: "authorize",
|
|
768
|
+
outcome: "auth_config_error",
|
|
769
|
+
durationMs: nowMs() - startedAt
|
|
770
|
+
});
|
|
556
771
|
return { ok: false, response: oauthConfigurationErrorResponse() };
|
|
557
772
|
}
|
|
558
773
|
if (err instanceof OAuthTokenError) {
|
|
@@ -650,23 +865,32 @@ function corsPreflightResponse(allowMethods) {
|
|
|
650
865
|
}
|
|
651
866
|
|
|
652
867
|
// src/protocols/mcp/protocol.ts
|
|
653
|
-
function adaptToolToSdkCallback(tool, auth) {
|
|
868
|
+
function adaptToolToSdkCallback(tool, auth, recorder) {
|
|
654
869
|
return async (first) => {
|
|
655
870
|
const args = tool.inputSchema ? first ?? {} : {};
|
|
871
|
+
const start = nowMs();
|
|
656
872
|
let result;
|
|
657
873
|
try {
|
|
658
874
|
result = await tool.handler(args, new ToolContext(auth));
|
|
659
875
|
} catch {
|
|
876
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
660
877
|
return { content: [{ type: "text", text: "tool execution failed" }], isError: true };
|
|
661
878
|
}
|
|
662
879
|
if (result == null) {
|
|
880
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
663
881
|
return { content: [{ type: "text", text: `tool "${tool.name}" returned no result` }], isError: true };
|
|
664
882
|
}
|
|
883
|
+
recorder.record({
|
|
884
|
+
tool: tool.name,
|
|
885
|
+
method: "tools/call",
|
|
886
|
+
outcome: result.isError ? "tool_error" : "ok",
|
|
887
|
+
durationMs: nowMs() - start
|
|
888
|
+
});
|
|
665
889
|
return { content: result.content ?? [], structuredContent: result.structuredContent, isError: result.isError };
|
|
666
890
|
};
|
|
667
891
|
}
|
|
668
|
-
function createMcpProtocolHandler(mcp, options = {}) {
|
|
669
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
892
|
+
function createMcpProtocolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
|
|
893
|
+
const authorizer = createRequestAuthorizer(mcp, options, recorder);
|
|
670
894
|
const handle = async (request) => {
|
|
671
895
|
const authResult = await authorizer.authorize(request);
|
|
672
896
|
if (!authResult.ok)
|
|
@@ -686,7 +910,7 @@ function createMcpProtocolHandler(mcp, options = {}) {
|
|
|
686
910
|
outputSchema: tool.outputSchema,
|
|
687
911
|
annotations: tool.annotations
|
|
688
912
|
},
|
|
689
|
-
adaptToolToSdkCallback(tool, authResult.auth)
|
|
913
|
+
adaptToolToSdkCallback(tool, authResult.auth, recorder)
|
|
690
914
|
);
|
|
691
915
|
}
|
|
692
916
|
const transport = new import_webStandardStreamableHttp.WebStandardStreamableHTTPServerTransport({
|
|
@@ -695,6 +919,7 @@ function createMcpProtocolHandler(mcp, options = {}) {
|
|
|
695
919
|
await server.connect(transport);
|
|
696
920
|
return await transport.handleRequest(request);
|
|
697
921
|
} catch (err) {
|
|
922
|
+
recorder.record({ tool: null, method: "transport", outcome: "transport_error", durationMs: 0 });
|
|
698
923
|
log.error("mcp.transport_error", { ...describeError(err), outcome: "500 internal error" });
|
|
699
924
|
return Response.json(
|
|
700
925
|
{ jsonrpc: "2.0", id: null, error: { code: -32603, message: "internal error" } },
|
|
@@ -795,9 +1020,9 @@ function buildMcpListing(mcp) {
|
|
|
795
1020
|
}))
|
|
796
1021
|
};
|
|
797
1022
|
}
|
|
798
|
-
function createListToolsHandler(mcp, options = {}) {
|
|
1023
|
+
function createListToolsHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
|
|
799
1024
|
assertRestResourceBinding(mcp, options);
|
|
800
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
1025
|
+
const authorizer = createRequestAuthorizer(mcp, options, recorder);
|
|
801
1026
|
const handle = async (request) => {
|
|
802
1027
|
const authResult = await authorizer.authorize(request);
|
|
803
1028
|
if (!authResult.ok)
|
|
@@ -828,9 +1053,9 @@ function isEmptyArgs(value) {
|
|
|
828
1053
|
return false;
|
|
829
1054
|
return Object.keys(value).length === 0;
|
|
830
1055
|
}
|
|
831
|
-
function createInvokeToolHandler(mcp, options = {}) {
|
|
1056
|
+
function createInvokeToolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
|
|
832
1057
|
assertRestResourceBinding(mcp, options);
|
|
833
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
1058
|
+
const authorizer = createRequestAuthorizer(mcp, options, recorder);
|
|
834
1059
|
const handle = async (request, toolName) => {
|
|
835
1060
|
const authResult = await authorizer.authorize(request);
|
|
836
1061
|
if (!authResult.ok)
|
|
@@ -884,20 +1109,29 @@ function createInvokeToolHandler(mcp, options = {}) {
|
|
|
884
1109
|
});
|
|
885
1110
|
}
|
|
886
1111
|
let result;
|
|
1112
|
+
const start = nowMs();
|
|
887
1113
|
try {
|
|
888
1114
|
result = await tool.handler(args, new ToolContext(authResult.auth));
|
|
889
1115
|
} catch {
|
|
1116
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
890
1117
|
return new Response(JSON.stringify({ error: "handler threw", tool: toolName }), {
|
|
891
1118
|
status: 500,
|
|
892
1119
|
headers: JSON_HEADERS
|
|
893
1120
|
});
|
|
894
1121
|
}
|
|
895
1122
|
if (result == null) {
|
|
1123
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
896
1124
|
return new Response(JSON.stringify({ error: `tool "${toolName}" returned no result` }), {
|
|
897
1125
|
status: 500,
|
|
898
1126
|
headers: JSON_HEADERS
|
|
899
1127
|
});
|
|
900
1128
|
}
|
|
1129
|
+
recorder.record({
|
|
1130
|
+
tool: tool.name,
|
|
1131
|
+
method: "tools/call",
|
|
1132
|
+
outcome: result.isError ? "tool_error" : "ok",
|
|
1133
|
+
durationMs: nowMs() - start
|
|
1134
|
+
});
|
|
901
1135
|
return Response.json({
|
|
902
1136
|
content: result.content ?? [],
|
|
903
1137
|
structuredContent: result.structuredContent,
|
|
@@ -3,20 +3,21 @@ import {
|
|
|
3
3
|
} from "../../chunk-UQK5UO6C.js";
|
|
4
4
|
import {
|
|
5
5
|
createMcpProtocolHandler
|
|
6
|
-
} from "../../chunk-
|
|
6
|
+
} from "../../chunk-UCPYLSNN.js";
|
|
7
7
|
import {
|
|
8
8
|
createOAuthProtectedResourceMetadataHandler
|
|
9
|
-
} from "../../chunk-
|
|
9
|
+
} from "../../chunk-ILYTJXDR.js";
|
|
10
10
|
import {
|
|
11
11
|
createInvokeToolHandler
|
|
12
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-CEDD57G2.js";
|
|
13
13
|
import {
|
|
14
14
|
createListToolsHandler
|
|
15
|
-
} from "../../chunk-
|
|
15
|
+
} from "../../chunk-QT5L4CS3.js";
|
|
16
16
|
import "../../chunk-MA5H6PSF.js";
|
|
17
|
-
import "../../chunk-
|
|
18
|
-
import "../../chunk-
|
|
17
|
+
import "../../chunk-YTOMGJV5.js";
|
|
18
|
+
import "../../chunk-DCWYK4CA.js";
|
|
19
19
|
import "../../chunk-6DXGZZA4.js";
|
|
20
|
+
import "../../chunk-GIHCQT6L.js";
|
|
20
21
|
|
|
21
22
|
// src/stacks/tanstack/handlers.ts
|
|
22
23
|
function forwarded(request, options) {
|
|
@@ -105,6 +105,29 @@ declare class ToolContext {
|
|
|
105
105
|
getClaims(): JwtClaims | undefined;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Usage-metrics options. Pass a boolean to the Vite plugins for the common
|
|
110
|
+
* on/off case, or this object to override the ingest endpoint (e.g. to
|
|
111
|
+
* self-host the collector outside Lovable).
|
|
112
|
+
*/
|
|
113
|
+
interface MetricsOptions {
|
|
114
|
+
/** Master switch. Default `true`; metrics still self-disable at runtime when
|
|
115
|
+
* the API key env var is absent. */
|
|
116
|
+
enabled?: boolean;
|
|
117
|
+
/** OTLP/HTTP (JSON) logs URL to POST batches to — the default Lovable route or
|
|
118
|
+
* any OTLP collector. Must be an absolute http(s) URL.
|
|
119
|
+
* @default "https://api.lovable.dev/v1/app-mcp-usage" */
|
|
120
|
+
endpoint?: string;
|
|
121
|
+
/** Extra request headers — e.g. the auth token for a private OTLP collector.
|
|
122
|
+
* Sent verbatim with every batch (and baked into the build output, so source
|
|
123
|
+
* your own build-time env for secrets). Ignored for the default Lovable
|
|
124
|
+
* endpoint, which authenticates with `LOVABLE_API_KEY` instead. `content-type`
|
|
125
|
+
* cannot be overridden (OTLP/HTTP JSON requires `application/json`). */
|
|
126
|
+
headers?: Record<string, string>;
|
|
127
|
+
}
|
|
128
|
+
/** Either the bare on/off toggle or the full options object. */
|
|
129
|
+
type MetricsConfig = boolean | MetricsOptions;
|
|
130
|
+
|
|
108
131
|
/**
|
|
109
132
|
* Any zod schema. Aliased to zod's `ZodType` (no generics) — non-deprecated
|
|
110
133
|
* in both v3 and v4. Use this when you need to type an individual schema;
|
|
@@ -273,10 +296,16 @@ interface McpDefinitionInput {
|
|
|
273
296
|
*/
|
|
274
297
|
readonly instructions: string;
|
|
275
298
|
readonly tools: readonly AnyToolDefinition[];
|
|
299
|
+
/**
|
|
300
|
+
* Usage-metrics emission (runtime telemetry), on by default. `false` opts out;
|
|
301
|
+
* `{ endpoint, headers }` sends to your own OTLP collector. Self-disables at
|
|
302
|
+
* runtime without `LOVABLE_API_KEY` on the default Lovable endpoint.
|
|
303
|
+
*/
|
|
304
|
+
readonly metrics?: MetricsConfig;
|
|
276
305
|
}
|
|
277
306
|
declare const McpDefinitionBrand: unique symbol;
|
|
278
307
|
type McpDefinition = McpDefinitionInput & {
|
|
279
308
|
readonly [McpDefinitionBrand]: never;
|
|
280
309
|
};
|
|
281
310
|
|
|
282
|
-
export { type AudioContent as A, type ContentAnnotations as C, type EmbeddedBlobResource as E, type ImageContent as I, type JwtClaims as J, type McpAuthConfig as M, type ResourceLink as R, type TextContent as T, type ZodRawShape as Z, type ContentBlock as a, type EmbeddedResource as b, type EmbeddedTextResource as c, type McpDefinition as d, type McpDefinitionInput as e, type
|
|
311
|
+
export { type AudioContent as A, type ContentAnnotations as C, type EmbeddedBlobResource as E, type ImageContent as I, type JwtClaims as J, type McpAuthConfig as M, type ResourceLink as R, type TextContent as T, type ZodRawShape as Z, type ContentBlock as a, type EmbeddedResource as b, type EmbeddedTextResource as c, type McpDefinition as d, type McpDefinitionInput as e, type MetricsConfig as f, type MetricsOptions as g, type ResourceLinkIcon as h, type ToolAnnotations as i, type ToolContent as j, ToolContext as k, type ToolDefinition as l, type ToolHandlerResult as m, type ZodSchema as n };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovable.dev/mcp-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "Author MCP servers for Lovable apps. Declare tools with defineTool, register them in defineMcp, and a framework adapter (TanStack or Supabase Edge Functions) emits the route(s) at build time.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|