@lovable.dev/mcp-js 0.13.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 +31 -1
- package/dist/{chunk-SIITHGUD.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/stacks/tanstack/vite.cjs +36 -22
- package/dist/stacks/tanstack/vite.d.cts +9 -7
- package/dist/stacks/tanstack/vite.d.ts +9 -7
- package/dist/stacks/tanstack/vite.js +36 -22
- package/dist/{types-BanzncFh.d.ts → types-COY42xux.d.ts} +30 -1
- package/package.json +1 -1
|
@@ -649,23 +649,240 @@ function corsPreflightResponse(allowMethods) {
|
|
|
649
649
|
});
|
|
650
650
|
}
|
|
651
651
|
|
|
652
|
+
// src/metrics/config.ts
|
|
653
|
+
var DEFAULT_METRICS_ENDPOINT = "https://api.lovable.dev/v1/app-mcp-usage";
|
|
654
|
+
var METRICS_API_KEY_ENV_VAR = "LOVABLE_API_KEY";
|
|
655
|
+
var METRICS_SAMPLE_RATE = 1;
|
|
656
|
+
var METRICS_FLUSH_INTERVAL_MS = 5e3;
|
|
657
|
+
var METRICS_MAX_BATCH_SIZE = 50;
|
|
658
|
+
function assertMetricsEndpoint(endpoint) {
|
|
659
|
+
let url;
|
|
660
|
+
try {
|
|
661
|
+
url = new URL(endpoint);
|
|
662
|
+
} catch {
|
|
663
|
+
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}`);
|
|
664
|
+
}
|
|
665
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
666
|
+
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be http(s), got ${JSON.stringify(endpoint)}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function resolveMetricsConfig(config = true) {
|
|
670
|
+
const options = typeof config === "boolean" ? { enabled: config } : config;
|
|
671
|
+
const endpoint = options.endpoint ?? DEFAULT_METRICS_ENDPOINT;
|
|
672
|
+
assertMetricsEndpoint(endpoint);
|
|
673
|
+
return Object.freeze({
|
|
674
|
+
enabled: options.enabled ?? true,
|
|
675
|
+
endpoint,
|
|
676
|
+
headers: options.headers ?? {},
|
|
677
|
+
apiKeyEnvVar: METRICS_API_KEY_ENV_VAR,
|
|
678
|
+
sampleRate: METRICS_SAMPLE_RATE,
|
|
679
|
+
flushIntervalMs: METRICS_FLUSH_INTERVAL_MS,
|
|
680
|
+
maxBatchSize: METRICS_MAX_BATCH_SIZE
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// package.json
|
|
685
|
+
var version = "0.15.0";
|
|
686
|
+
|
|
687
|
+
// src/metrics/otlp.ts
|
|
688
|
+
var SCOPE_NAME = "@lovable.dev/mcp-js";
|
|
689
|
+
var EVENT_NAME = "mcp.tool.invocation";
|
|
690
|
+
var SEVERITY_INFO = 9;
|
|
691
|
+
function strAttr(key, value) {
|
|
692
|
+
return { key, value: { stringValue: value } };
|
|
693
|
+
}
|
|
694
|
+
function intAttr(key, value) {
|
|
695
|
+
return { key, value: { intValue: String(Math.round(value)) } };
|
|
696
|
+
}
|
|
697
|
+
function toLogRecord(rec) {
|
|
698
|
+
const attributes = [
|
|
699
|
+
strAttr("event.name", EVENT_NAME),
|
|
700
|
+
strAttr("mcp.method", rec.method),
|
|
701
|
+
strAttr("mcp.outcome", rec.outcome),
|
|
702
|
+
intAttr("mcp.duration_ms", rec.durationMs)
|
|
703
|
+
];
|
|
704
|
+
if (rec.tool !== null)
|
|
705
|
+
attributes.push(strAttr("mcp.tool", rec.tool));
|
|
706
|
+
if (rec.reqBytes !== void 0)
|
|
707
|
+
attributes.push(intAttr("mcp.request_size_bytes", rec.reqBytes));
|
|
708
|
+
if (rec.resBytes !== void 0)
|
|
709
|
+
attributes.push(intAttr("mcp.response_size_bytes", rec.resBytes));
|
|
710
|
+
return {
|
|
711
|
+
timeUnixNano: rec.timeUnixNano,
|
|
712
|
+
observedTimeUnixNano: rec.timeUnixNano,
|
|
713
|
+
severityNumber: SEVERITY_INFO,
|
|
714
|
+
severityText: "INFO",
|
|
715
|
+
body: { stringValue: EVENT_NAME },
|
|
716
|
+
attributes
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function buildLogsPayload(records, server) {
|
|
720
|
+
return JSON.stringify({
|
|
721
|
+
resourceLogs: [
|
|
722
|
+
{
|
|
723
|
+
resource: {
|
|
724
|
+
attributes: [
|
|
725
|
+
strAttr("service.name", server.name),
|
|
726
|
+
strAttr("service.version", server.version),
|
|
727
|
+
strAttr("telemetry.sdk.name", SCOPE_NAME),
|
|
728
|
+
strAttr("telemetry.sdk.version", version),
|
|
729
|
+
strAttr("telemetry.sdk.language", "webjs")
|
|
730
|
+
]
|
|
731
|
+
},
|
|
732
|
+
scopeLogs: [
|
|
733
|
+
{
|
|
734
|
+
scope: { name: SCOPE_NAME, version },
|
|
735
|
+
logRecords: records.map(toLogRecord)
|
|
736
|
+
}
|
|
737
|
+
]
|
|
738
|
+
}
|
|
739
|
+
]
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
function nowUnixNano() {
|
|
743
|
+
return `${Date.now()}000000`;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/metrics/recorder.ts
|
|
747
|
+
function nowMs() {
|
|
748
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
749
|
+
}
|
|
750
|
+
var NOOP_RECORDER = {
|
|
751
|
+
record() {
|
|
752
|
+
},
|
|
753
|
+
async flush() {
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
function createNoopRecorder() {
|
|
757
|
+
return NOOP_RECORDER;
|
|
758
|
+
}
|
|
759
|
+
function readRuntimeEnv(name) {
|
|
760
|
+
try {
|
|
761
|
+
const denoEnv = globalThis.Deno?.env;
|
|
762
|
+
const value = denoEnv?.get?.(name);
|
|
763
|
+
if (value)
|
|
764
|
+
return value;
|
|
765
|
+
} catch {
|
|
766
|
+
}
|
|
767
|
+
try {
|
|
768
|
+
if (typeof process !== "undefined") {
|
|
769
|
+
const value = process.env?.[name];
|
|
770
|
+
if (value)
|
|
771
|
+
return value;
|
|
772
|
+
}
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
775
|
+
return void 0;
|
|
776
|
+
}
|
|
777
|
+
function probeWaitUntil() {
|
|
778
|
+
const fn = globalThis.EdgeRuntime?.waitUntil;
|
|
779
|
+
return typeof fn === "function" ? fn.bind(globalThis.EdgeRuntime) : void 0;
|
|
780
|
+
}
|
|
781
|
+
function createMetricsRecorder(ctx, deps = {}) {
|
|
782
|
+
const { config, server } = ctx;
|
|
783
|
+
if (!config.enabled)
|
|
784
|
+
return NOOP_RECORDER;
|
|
785
|
+
const doFetch = deps.fetch ?? globalThis.fetch;
|
|
786
|
+
if (!doFetch)
|
|
787
|
+
return NOOP_RECORDER;
|
|
788
|
+
const usesLovableEndpoint = config.endpoint === DEFAULT_METRICS_ENDPOINT;
|
|
789
|
+
const apiKey = usesLovableEndpoint ? (deps.getApiKey ?? (() => readRuntimeEnv(config.apiKeyEnvVar)))() : void 0;
|
|
790
|
+
if (usesLovableEndpoint && !apiKey) {
|
|
791
|
+
log.debug("metrics.disabled_no_api_key", { envVar: config.apiKeyEnvVar });
|
|
792
|
+
return NOOP_RECORDER;
|
|
793
|
+
}
|
|
794
|
+
const waitUntil = deps.waitUntil ?? probeWaitUntil();
|
|
795
|
+
const buffer = [];
|
|
796
|
+
let timer;
|
|
797
|
+
const schedule = (p) => {
|
|
798
|
+
if (waitUntil) {
|
|
799
|
+
try {
|
|
800
|
+
waitUntil(p);
|
|
801
|
+
return;
|
|
802
|
+
} catch {
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
void p.catch(() => {
|
|
806
|
+
});
|
|
807
|
+
};
|
|
808
|
+
const headers = {};
|
|
809
|
+
if (!usesLovableEndpoint) {
|
|
810
|
+
for (const [key, value] of Object.entries(config.headers)) {
|
|
811
|
+
if (key.toLowerCase() !== "content-type")
|
|
812
|
+
headers[key] = value;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
headers["content-type"] = "application/json";
|
|
816
|
+
if (apiKey)
|
|
817
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
818
|
+
const flush = async () => {
|
|
819
|
+
if (buffer.length === 0)
|
|
820
|
+
return;
|
|
821
|
+
const records = buffer.splice(0, buffer.length);
|
|
822
|
+
try {
|
|
823
|
+
const res = await doFetch(config.endpoint, {
|
|
824
|
+
method: "POST",
|
|
825
|
+
headers,
|
|
826
|
+
body: buildLogsPayload(records, server),
|
|
827
|
+
keepalive: true
|
|
828
|
+
});
|
|
829
|
+
if (!res.ok)
|
|
830
|
+
log.debug("metrics.flush_rejected", { status: res.status });
|
|
831
|
+
} catch (err) {
|
|
832
|
+
log.debug("metrics.flush_failed", describeError(err));
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
const ensureTimer = () => {
|
|
836
|
+
if (timer !== void 0)
|
|
837
|
+
return;
|
|
838
|
+
timer = setInterval(() => schedule(flush()), config.flushIntervalMs);
|
|
839
|
+
timer.unref?.();
|
|
840
|
+
};
|
|
841
|
+
return {
|
|
842
|
+
record(ev) {
|
|
843
|
+
if (config.sampleRate < 1 && Math.random() >= config.sampleRate)
|
|
844
|
+
return;
|
|
845
|
+
buffer.push({ ...ev, timeUnixNano: nowUnixNano() });
|
|
846
|
+
ensureTimer();
|
|
847
|
+
if (buffer.length >= config.maxBatchSize)
|
|
848
|
+
schedule(flush());
|
|
849
|
+
},
|
|
850
|
+
flush
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
function createRecorderForRuntime(mcp) {
|
|
854
|
+
const config = resolveMetricsConfig(mcp.metrics);
|
|
855
|
+
if (!config.enabled)
|
|
856
|
+
return createNoopRecorder();
|
|
857
|
+
return createMetricsRecorder({ config, server: { name: mcp.name, version: mcp.version } });
|
|
858
|
+
}
|
|
859
|
+
|
|
652
860
|
// src/protocols/mcp/protocol.ts
|
|
653
|
-
function adaptToolToSdkCallback(tool, auth) {
|
|
861
|
+
function adaptToolToSdkCallback(tool, auth, recorder) {
|
|
654
862
|
return async (first) => {
|
|
655
863
|
const args = tool.inputSchema ? first ?? {} : {};
|
|
864
|
+
const start = nowMs();
|
|
656
865
|
let result;
|
|
657
866
|
try {
|
|
658
867
|
result = await tool.handler(args, new ToolContext(auth));
|
|
659
868
|
} catch {
|
|
869
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
660
870
|
return { content: [{ type: "text", text: "tool execution failed" }], isError: true };
|
|
661
871
|
}
|
|
662
872
|
if (result == null) {
|
|
873
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
663
874
|
return { content: [{ type: "text", text: `tool "${tool.name}" returned no result` }], isError: true };
|
|
664
875
|
}
|
|
876
|
+
recorder.record({
|
|
877
|
+
tool: tool.name,
|
|
878
|
+
method: "tools/call",
|
|
879
|
+
outcome: result.isError ? "tool_error" : "ok",
|
|
880
|
+
durationMs: nowMs() - start
|
|
881
|
+
});
|
|
665
882
|
return { content: result.content ?? [], structuredContent: result.structuredContent, isError: result.isError };
|
|
666
883
|
};
|
|
667
884
|
}
|
|
668
|
-
function createMcpProtocolHandler(mcp, options = {}) {
|
|
885
|
+
function createMcpProtocolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
|
|
669
886
|
const authorizer = createRequestAuthorizer(mcp, options);
|
|
670
887
|
const handle = async (request) => {
|
|
671
888
|
const authResult = await authorizer.authorize(request);
|
|
@@ -686,7 +903,7 @@ function createMcpProtocolHandler(mcp, options = {}) {
|
|
|
686
903
|
outputSchema: tool.outputSchema,
|
|
687
904
|
annotations: tool.annotations
|
|
688
905
|
},
|
|
689
|
-
adaptToolToSdkCallback(tool, authResult.auth)
|
|
906
|
+
adaptToolToSdkCallback(tool, authResult.auth, recorder)
|
|
690
907
|
);
|
|
691
908
|
}
|
|
692
909
|
const transport = new import_webStandardStreamableHttp.WebStandardStreamableHTTPServerTransport({
|
|
@@ -695,6 +912,7 @@ function createMcpProtocolHandler(mcp, options = {}) {
|
|
|
695
912
|
await server.connect(transport);
|
|
696
913
|
return await transport.handleRequest(request);
|
|
697
914
|
} catch (err) {
|
|
915
|
+
recorder.record({ tool: null, method: "transport", outcome: "transport_error", durationMs: 0 });
|
|
698
916
|
log.error("mcp.transport_error", { ...describeError(err), outcome: "500 internal error" });
|
|
699
917
|
return Response.json(
|
|
700
918
|
{ jsonrpc: "2.0", id: null, error: { code: -32603, message: "internal error" } },
|
|
@@ -828,7 +1046,7 @@ function isEmptyArgs(value) {
|
|
|
828
1046
|
return false;
|
|
829
1047
|
return Object.keys(value).length === 0;
|
|
830
1048
|
}
|
|
831
|
-
function createInvokeToolHandler(mcp, options = {}) {
|
|
1049
|
+
function createInvokeToolHandler(mcp, options = {}, recorder = createRecorderForRuntime(mcp)) {
|
|
832
1050
|
assertRestResourceBinding(mcp, options);
|
|
833
1051
|
const authorizer = createRequestAuthorizer(mcp, options);
|
|
834
1052
|
const handle = async (request, toolName) => {
|
|
@@ -884,20 +1102,29 @@ function createInvokeToolHandler(mcp, options = {}) {
|
|
|
884
1102
|
});
|
|
885
1103
|
}
|
|
886
1104
|
let result;
|
|
1105
|
+
const start = nowMs();
|
|
887
1106
|
try {
|
|
888
1107
|
result = await tool.handler(args, new ToolContext(authResult.auth));
|
|
889
1108
|
} catch {
|
|
1109
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
890
1110
|
return new Response(JSON.stringify({ error: "handler threw", tool: toolName }), {
|
|
891
1111
|
status: 500,
|
|
892
1112
|
headers: JSON_HEADERS
|
|
893
1113
|
});
|
|
894
1114
|
}
|
|
895
1115
|
if (result == null) {
|
|
1116
|
+
recorder.record({ tool: tool.name, method: "tools/call", outcome: "handler_error", durationMs: nowMs() - start });
|
|
896
1117
|
return new Response(JSON.stringify({ error: `tool "${toolName}" returned no result` }), {
|
|
897
1118
|
status: 500,
|
|
898
1119
|
headers: JSON_HEADERS
|
|
899
1120
|
});
|
|
900
1121
|
}
|
|
1122
|
+
recorder.record({
|
|
1123
|
+
tool: tool.name,
|
|
1124
|
+
method: "tools/call",
|
|
1125
|
+
outcome: result.isError ? "tool_error" : "ok",
|
|
1126
|
+
durationMs: nowMs() - start
|
|
1127
|
+
});
|
|
901
1128
|
return Response.json({
|
|
902
1129
|
content: result.content ?? [],
|
|
903
1130
|
structuredContent: result.structuredContent,
|
|
@@ -3,20 +3,22 @@ import {
|
|
|
3
3
|
} from "../../chunk-UQK5UO6C.js";
|
|
4
4
|
import {
|
|
5
5
|
createMcpProtocolHandler
|
|
6
|
-
} from "../../chunk-
|
|
6
|
+
} from "../../chunk-RHDEW56Y.js";
|
|
7
7
|
import {
|
|
8
8
|
createOAuthProtectedResourceMetadataHandler
|
|
9
9
|
} from "../../chunk-QDKOF4UF.js";
|
|
10
10
|
import {
|
|
11
11
|
createInvokeToolHandler
|
|
12
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-T2RED2TG.js";
|
|
13
13
|
import {
|
|
14
14
|
createListToolsHandler
|
|
15
15
|
} from "../../chunk-VNRQPA4K.js";
|
|
16
|
-
import "../../chunk-
|
|
16
|
+
import "../../chunk-XQSTN54Y.js";
|
|
17
|
+
import "../../chunk-P3F324UC.js";
|
|
17
18
|
import "../../chunk-G4XA7IJM.js";
|
|
18
19
|
import "../../chunk-QC3DXQTH.js";
|
|
19
20
|
import "../../chunk-6DXGZZA4.js";
|
|
21
|
+
import "../../chunk-LWHVXXKQ.js";
|
|
20
22
|
|
|
21
23
|
// src/stacks/tanstack/handlers.ts
|
|
22
24
|
function forwarded(request, options) {
|
|
@@ -56,20 +56,28 @@ function wellKnownRouteFile(routesDir, urlPath) {
|
|
|
56
56
|
});
|
|
57
57
|
return (0, import_node_path.resolve)(routesDir, ...fileSegments);
|
|
58
58
|
}
|
|
59
|
-
function
|
|
59
|
+
function resolveRestRoutes(routesDir, mcpUrlPath) {
|
|
60
|
+
const parentSegments = mcpUrlPath.replace(/^\/+/, "").replace(/\/+$/, "").split("/").slice(0, -1);
|
|
61
|
+
const restDir = (0, import_node_path.resolve)(routesDir, ...parentSegments, "[.mcp]");
|
|
62
|
+
const restUrlBase = `/${[...parentSegments, ".mcp"].join("/")}`;
|
|
63
|
+
return {
|
|
64
|
+
listToolsRouteFile: (0, import_node_path.resolve)(restDir, "list-tools.ts"),
|
|
65
|
+
invokeToolRouteFile: (0, import_node_path.resolve)(restDir, "invoke-tool", "$tool.ts"),
|
|
66
|
+
listToolsLiteral: `${restUrlBase}/list-tools`,
|
|
67
|
+
invokeToolLiteral: `${restUrlBase}/invoke-tool/$tool`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function resolveAllRoutes(projectRoot, routesDirOption, routeFileName, mcpUrlPath, metadataUrlPath) {
|
|
60
71
|
const routesDir = (0, import_node_path.resolve)(projectRoot, routesDirOption);
|
|
61
72
|
assertContains(projectRoot, routesDir, `routesDir "${routesDirOption}"`);
|
|
62
73
|
const mcpRouteFile = (0, import_node_path.resolve)(routesDir, routeFileName);
|
|
63
74
|
assertContains(routesDir, mcpRouteFile, `routeFileName "${routeFileName}"`);
|
|
64
75
|
const metadataRouteFile = wellKnownRouteFile(routesDir, metadataUrlPath);
|
|
65
76
|
assertContains(routesDir, metadataRouteFile, `metadataPath "${metadataUrlPath}"`);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
listToolsRouteFile: (0, import_node_path.resolve)(routesDir, "[.mcp]", "list-tools.ts"),
|
|
71
|
-
invokeToolRouteFile: (0, import_node_path.resolve)(routesDir, "[.mcp]", "invoke-tool", "$tool.ts")
|
|
72
|
-
};
|
|
77
|
+
const rest = resolveRestRoutes(routesDir, mcpUrlPath);
|
|
78
|
+
assertContains(routesDir, rest.listToolsRouteFile, `REST routes for path "${mcpUrlPath}"`);
|
|
79
|
+
assertContains(routesDir, rest.invokeToolRouteFile, `REST routes for path "${mcpUrlPath}"`);
|
|
80
|
+
return { routesDir, mcpRouteFile, metadataRouteFile, ...rest };
|
|
73
81
|
}
|
|
74
82
|
function assertUrlPathShape(urlPath) {
|
|
75
83
|
if (!urlPath.startsWith("/")) {
|
|
@@ -231,12 +239,15 @@ function mcpPlugin(options = {}) {
|
|
|
231
239
|
const trustForwardedHost = options.trustForwardedHost !== false;
|
|
232
240
|
let projectRoot = process.cwd();
|
|
233
241
|
let mcpEntry = (0, import_node_path.resolve)(projectRoot, mcpEntryOption);
|
|
234
|
-
let {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
242
|
+
let {
|
|
243
|
+
routesDir,
|
|
244
|
+
mcpRouteFile,
|
|
245
|
+
metadataRouteFile,
|
|
246
|
+
listToolsRouteFile,
|
|
247
|
+
invokeToolRouteFile,
|
|
248
|
+
listToolsLiteral,
|
|
249
|
+
invokeToolLiteral
|
|
250
|
+
} = resolveAllRoutes(projectRoot, routesDirOption, routeFileName, urlPath, metadataUrlPath);
|
|
240
251
|
const regenerate = () => {
|
|
241
252
|
let mcpEntryExists = true;
|
|
242
253
|
try {
|
|
@@ -262,13 +273,13 @@ function mcpPlugin(options = {}) {
|
|
|
262
273
|
routes.push(
|
|
263
274
|
{
|
|
264
275
|
file: listToolsRouteFile,
|
|
265
|
-
routeLiteral:
|
|
276
|
+
routeLiteral: listToolsLiteral,
|
|
266
277
|
handlerFactory: "createTanStackListToolsHandler",
|
|
267
278
|
spaFallbackNote: true
|
|
268
279
|
},
|
|
269
280
|
{
|
|
270
281
|
file: invokeToolRouteFile,
|
|
271
|
-
routeLiteral:
|
|
282
|
+
routeLiteral: invokeToolLiteral,
|
|
272
283
|
handlerFactory: "createTanStackInvokeToolHandler",
|
|
273
284
|
spaFallbackNote: true
|
|
274
285
|
}
|
|
@@ -310,12 +321,15 @@ function mcpPlugin(options = {}) {
|
|
|
310
321
|
configResolved(config) {
|
|
311
322
|
projectRoot = config.root;
|
|
312
323
|
mcpEntry = (0, import_node_path.resolve)(projectRoot, mcpEntryOption);
|
|
313
|
-
({
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
324
|
+
({
|
|
325
|
+
routesDir,
|
|
326
|
+
mcpRouteFile,
|
|
327
|
+
metadataRouteFile,
|
|
328
|
+
listToolsRouteFile,
|
|
329
|
+
invokeToolRouteFile,
|
|
330
|
+
listToolsLiteral,
|
|
331
|
+
invokeToolLiteral
|
|
332
|
+
} = resolveAllRoutes(projectRoot, routesDirOption, routeFileName, urlPath, metadataUrlPath));
|
|
319
333
|
regenerate();
|
|
320
334
|
},
|
|
321
335
|
configureServer(server) {
|
|
@@ -16,13 +16,15 @@ interface McpPluginOptions {
|
|
|
16
16
|
*/
|
|
17
17
|
routesDir?: string;
|
|
18
18
|
/**
|
|
19
|
-
* Public URL path for the MCP-protocol route
|
|
19
|
+
* Public URL path for the MCP-protocol route.
|
|
20
20
|
*
|
|
21
|
-
* The REST companion routes
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* The REST companion routes co-locate with this path under a `.mcp`
|
|
22
|
+
* sub-segment of the same parent prefix: `/mcp` keeps them at
|
|
23
|
+
* `/.mcp/list-tools` and `/.mcp/invoke-tool/$tool`, while
|
|
24
|
+
* `/api/public/mcp` moves them to `/api/public/.mcp/*`. The `.mcp`
|
|
25
|
+
* namespace keeps them from colliding with user-defined URLs without
|
|
26
|
+
* anchoring them at the root. Set `restRoutes: false` to drop the
|
|
27
|
+
* companions entirely.
|
|
26
28
|
*
|
|
27
29
|
* Must start with `/` and use only `/mcp`-style segments; anything more
|
|
28
30
|
* exotic doesn't map cleanly to a TanStack file name.
|
|
@@ -37,7 +39,7 @@ interface McpPluginOptions {
|
|
|
37
39
|
routeFileName?: string;
|
|
38
40
|
/**
|
|
39
41
|
* Set to `false` to skip emitting the REST companion routes at
|
|
40
|
-
*
|
|
42
|
+
* `<path-parent>/.mcp/list-tools` and `<path-parent>/.mcp/<tool>`. Set
|
|
41
43
|
* `protectedResourceMetadataRoute: false` as well if you want the MCP
|
|
42
44
|
* protocol route to be the only emitted surface.
|
|
43
45
|
* @default true
|
|
@@ -16,13 +16,15 @@ interface McpPluginOptions {
|
|
|
16
16
|
*/
|
|
17
17
|
routesDir?: string;
|
|
18
18
|
/**
|
|
19
|
-
* Public URL path for the MCP-protocol route
|
|
19
|
+
* Public URL path for the MCP-protocol route.
|
|
20
20
|
*
|
|
21
|
-
* The REST companion routes
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* The REST companion routes co-locate with this path under a `.mcp`
|
|
22
|
+
* sub-segment of the same parent prefix: `/mcp` keeps them at
|
|
23
|
+
* `/.mcp/list-tools` and `/.mcp/invoke-tool/$tool`, while
|
|
24
|
+
* `/api/public/mcp` moves them to `/api/public/.mcp/*`. The `.mcp`
|
|
25
|
+
* namespace keeps them from colliding with user-defined URLs without
|
|
26
|
+
* anchoring them at the root. Set `restRoutes: false` to drop the
|
|
27
|
+
* companions entirely.
|
|
26
28
|
*
|
|
27
29
|
* Must start with `/` and use only `/mcp`-style segments; anything more
|
|
28
30
|
* exotic doesn't map cleanly to a TanStack file name.
|
|
@@ -37,7 +39,7 @@ interface McpPluginOptions {
|
|
|
37
39
|
routeFileName?: string;
|
|
38
40
|
/**
|
|
39
41
|
* Set to `false` to skip emitting the REST companion routes at
|
|
40
|
-
*
|
|
42
|
+
* `<path-parent>/.mcp/list-tools` and `<path-parent>/.mcp/<tool>`. Set
|
|
41
43
|
* `protectedResourceMetadataRoute: false` as well if you want the MCP
|
|
42
44
|
* protocol route to be the only emitted surface.
|
|
43
45
|
* @default true
|
|
@@ -25,20 +25,28 @@ function wellKnownRouteFile(routesDir, urlPath) {
|
|
|
25
25
|
});
|
|
26
26
|
return resolve(routesDir, ...fileSegments);
|
|
27
27
|
}
|
|
28
|
-
function
|
|
28
|
+
function resolveRestRoutes(routesDir, mcpUrlPath) {
|
|
29
|
+
const parentSegments = mcpUrlPath.replace(/^\/+/, "").replace(/\/+$/, "").split("/").slice(0, -1);
|
|
30
|
+
const restDir = resolve(routesDir, ...parentSegments, "[.mcp]");
|
|
31
|
+
const restUrlBase = `/${[...parentSegments, ".mcp"].join("/")}`;
|
|
32
|
+
return {
|
|
33
|
+
listToolsRouteFile: resolve(restDir, "list-tools.ts"),
|
|
34
|
+
invokeToolRouteFile: resolve(restDir, "invoke-tool", "$tool.ts"),
|
|
35
|
+
listToolsLiteral: `${restUrlBase}/list-tools`,
|
|
36
|
+
invokeToolLiteral: `${restUrlBase}/invoke-tool/$tool`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function resolveAllRoutes(projectRoot, routesDirOption, routeFileName, mcpUrlPath, metadataUrlPath) {
|
|
29
40
|
const routesDir = resolve(projectRoot, routesDirOption);
|
|
30
41
|
assertContains(projectRoot, routesDir, `routesDir "${routesDirOption}"`);
|
|
31
42
|
const mcpRouteFile = resolve(routesDir, routeFileName);
|
|
32
43
|
assertContains(routesDir, mcpRouteFile, `routeFileName "${routeFileName}"`);
|
|
33
44
|
const metadataRouteFile = wellKnownRouteFile(routesDir, metadataUrlPath);
|
|
34
45
|
assertContains(routesDir, metadataRouteFile, `metadataPath "${metadataUrlPath}"`);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
listToolsRouteFile: resolve(routesDir, "[.mcp]", "list-tools.ts"),
|
|
40
|
-
invokeToolRouteFile: resolve(routesDir, "[.mcp]", "invoke-tool", "$tool.ts")
|
|
41
|
-
};
|
|
46
|
+
const rest = resolveRestRoutes(routesDir, mcpUrlPath);
|
|
47
|
+
assertContains(routesDir, rest.listToolsRouteFile, `REST routes for path "${mcpUrlPath}"`);
|
|
48
|
+
assertContains(routesDir, rest.invokeToolRouteFile, `REST routes for path "${mcpUrlPath}"`);
|
|
49
|
+
return { routesDir, mcpRouteFile, metadataRouteFile, ...rest };
|
|
42
50
|
}
|
|
43
51
|
function assertUrlPathShape(urlPath) {
|
|
44
52
|
if (!urlPath.startsWith("/")) {
|
|
@@ -200,12 +208,15 @@ function mcpPlugin(options = {}) {
|
|
|
200
208
|
const trustForwardedHost = options.trustForwardedHost !== false;
|
|
201
209
|
let projectRoot = process.cwd();
|
|
202
210
|
let mcpEntry = resolve(projectRoot, mcpEntryOption);
|
|
203
|
-
let {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
211
|
+
let {
|
|
212
|
+
routesDir,
|
|
213
|
+
mcpRouteFile,
|
|
214
|
+
metadataRouteFile,
|
|
215
|
+
listToolsRouteFile,
|
|
216
|
+
invokeToolRouteFile,
|
|
217
|
+
listToolsLiteral,
|
|
218
|
+
invokeToolLiteral
|
|
219
|
+
} = resolveAllRoutes(projectRoot, routesDirOption, routeFileName, urlPath, metadataUrlPath);
|
|
209
220
|
const regenerate = () => {
|
|
210
221
|
let mcpEntryExists = true;
|
|
211
222
|
try {
|
|
@@ -231,13 +242,13 @@ function mcpPlugin(options = {}) {
|
|
|
231
242
|
routes.push(
|
|
232
243
|
{
|
|
233
244
|
file: listToolsRouteFile,
|
|
234
|
-
routeLiteral:
|
|
245
|
+
routeLiteral: listToolsLiteral,
|
|
235
246
|
handlerFactory: "createTanStackListToolsHandler",
|
|
236
247
|
spaFallbackNote: true
|
|
237
248
|
},
|
|
238
249
|
{
|
|
239
250
|
file: invokeToolRouteFile,
|
|
240
|
-
routeLiteral:
|
|
251
|
+
routeLiteral: invokeToolLiteral,
|
|
241
252
|
handlerFactory: "createTanStackInvokeToolHandler",
|
|
242
253
|
spaFallbackNote: true
|
|
243
254
|
}
|
|
@@ -279,12 +290,15 @@ function mcpPlugin(options = {}) {
|
|
|
279
290
|
configResolved(config) {
|
|
280
291
|
projectRoot = config.root;
|
|
281
292
|
mcpEntry = resolve(projectRoot, mcpEntryOption);
|
|
282
|
-
({
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
293
|
+
({
|
|
294
|
+
routesDir,
|
|
295
|
+
mcpRouteFile,
|
|
296
|
+
metadataRouteFile,
|
|
297
|
+
listToolsRouteFile,
|
|
298
|
+
invokeToolRouteFile,
|
|
299
|
+
listToolsLiteral,
|
|
300
|
+
invokeToolLiteral
|
|
301
|
+
} = resolveAllRoutes(projectRoot, routesDirOption, routeFileName, urlPath, metadataUrlPath));
|
|
288
302
|
regenerate();
|
|
289
303
|
},
|
|
290
304
|
configureServer(server) {
|
|
@@ -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.0",
|
|
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": {
|