@lovable.dev/mcp-js 0.22.1 → 0.22.2-rc.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/dist/authorize-CumVyk7p.d.cts +27 -0
- package/dist/authorize-D6p4-9_D.d.ts +27 -0
- package/dist/base-DCe0kP79.d.cts +29 -0
- package/dist/base-DCe0kP79.d.ts +29 -0
- package/dist/cli/extract-manifest.cjs +116 -201
- package/dist/cli/extract-manifest.d.cts +1 -1
- package/dist/cli/extract-manifest.d.ts +1 -1
- package/dist/cli/extract-manifest.js +117 -156
- package/dist/content-D5LJAHyM.js +12 -0
- package/dist/content-NCYIGgOb.cjs +17 -0
- package/dist/context-BweeEcoA.cjs +56 -0
- package/dist/context-CIpPzN7P.js +51 -0
- package/dist/cors-C2n2LxrC.js +830 -0
- package/dist/cors-Cga2PWEg.cjs +913 -0
- package/dist/forwarded--h4efJy-.js +43 -0
- package/dist/forwarded-Pi8Hs5Ps.cjs +48 -0
- package/dist/fs-errors-CWNzOV75.cjs +11 -0
- package/dist/fs-errors-PA2t1TIp.js +6 -0
- package/dist/index.cjs +129 -329
- package/dist/index.d.cts +26 -26
- package/dist/index.d.ts +26 -26
- package/dist/index.js +118 -177
- package/dist/{io-D4srzRKc.d.ts → io-CfSPxeBN.d.ts} +6 -4
- package/dist/io-DPYvBqET.d.cts +15 -0
- package/dist/list-tools-07OV14rK.cjs +57 -0
- package/dist/list-tools-B4TTPEl1.d.cts +8 -0
- package/dist/list-tools-I8Hvub9P.js +46 -0
- package/dist/list-tools-bh6-eqZM.d.ts +8 -0
- package/dist/logger-2OXtfiIW.js +141 -0
- package/dist/logger-BqPOa_E1.cjs +194 -0
- package/dist/mcp-B6z5Givq.js +123 -0
- package/dist/mcp-DLv_c1O5.cjs +128 -0
- package/dist/metadata-path-CAkNcfyY.js +12 -0
- package/dist/metadata-path-zd7NSNoa.cjs +17 -0
- package/dist/package-CxQcGWdU.cjs +9 -0
- package/dist/package-xS6mk-b_.js +4 -0
- package/dist/paths-IS65L6TA.js +7 -0
- package/dist/paths-OP318a6c.cjs +18 -0
- package/dist/protocols/mcp/index.cjs +3 -750
- package/dist/protocols/mcp/index.d.cts +6 -7
- package/dist/protocols/mcp/index.d.ts +6 -7
- package/dist/protocols/mcp/index.js +2 -12
- package/dist/protocols/oauth-metadata.cjs +48 -539
- package/dist/protocols/oauth-metadata.d.cts +5 -6
- package/dist/protocols/oauth-metadata.d.ts +5 -6
- package/dist/protocols/oauth-metadata.js +52 -10
- package/dist/protocols/rest/index.cjs +5 -843
- package/dist/protocols/rest/index.d.cts +7 -10
- package/dist/protocols/rest/index.d.ts +7 -10
- package/dist/protocols/rest/index.js +3 -16
- package/dist/rest-CggWx00c.js +117 -0
- package/dist/rest-DxhK7-3b.cjs +122 -0
- package/dist/stacks/supabase/index.cjs +76 -1318
- package/dist/stacks/supabase/index.d.cts +29 -30
- package/dist/stacks/supabase/index.d.ts +29 -30
- package/dist/stacks/supabase/index.js +73 -89
- package/dist/stacks/supabase/vite.cjs +258 -287
- package/dist/stacks/supabase/vite.d.cts +24 -24
- package/dist/stacks/supabase/vite.d.ts +24 -24
- package/dist/stacks/supabase/vite.js +252 -252
- package/dist/stacks/tanstack/index.cjs +42 -1272
- package/dist/stacks/tanstack/index.d.cts +8 -9
- package/dist/stacks/tanstack/index.d.ts +8 -9
- package/dist/stacks/tanstack/index.js +36 -41
- package/dist/stacks/tanstack/vite.cjs +223 -312
- package/dist/stacks/tanstack/vite.d.cts +80 -81
- package/dist/stacks/tanstack/vite.d.ts +80 -81
- package/dist/stacks/tanstack/vite.js +216 -282
- package/dist/types-BYdYYTjk.d.cts +321 -0
- package/dist/types-BYdYYTjk.d.ts +321 -0
- package/package.json +4 -4
- package/dist/authorize-BMeXd_Sh.d.ts +0 -25
- package/dist/base-CM-KCpp6.d.ts +0 -28
- package/dist/chunk-5HVSJCKE.js +0 -114
- package/dist/chunk-6DXGZZA4.js +0 -6
- package/dist/chunk-6QFZYYUV.js +0 -11
- package/dist/chunk-6SZOFKGH.js +0 -58
- package/dist/chunk-H37EB22A.js +0 -125
- package/dist/chunk-IFDWUM7L.js +0 -741
- package/dist/chunk-IXZIFYO6.js +0 -79
- package/dist/chunk-MA5H6PSF.js +0 -46
- package/dist/chunk-TJN33K5U.js +0 -141
- package/dist/chunk-UQK5UO6C.js +0 -26
- package/dist/chunk-XQWJN6DC.js +0 -14
- package/dist/chunk-Y3ZFPEQH.js +0 -8
- package/dist/chunk-ZQXSWTPU.js +0 -6
- package/dist/types-qqrmJ736.d.ts +0 -321
|
@@ -1,1322 +1,80 @@
|
|
|
1
|
-
"
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/stacks/supabase/index.ts
|
|
21
|
-
var supabase_exports = {};
|
|
22
|
-
__export(supabase_exports, {
|
|
23
|
-
createSupabaseHandler: () => createSupabaseHandler
|
|
24
|
-
});
|
|
25
|
-
module.exports = __toCommonJS(supabase_exports);
|
|
26
|
-
|
|
27
|
-
// src/auth/metadata-path.ts
|
|
28
|
-
var OAUTH_PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
29
|
-
|
|
30
|
-
// src/core/url.ts
|
|
31
|
-
function trimTrailingSlash(value) {
|
|
32
|
-
return value.replace(/\/+$/, "");
|
|
33
|
-
}
|
|
34
|
-
function isLocalHTTPHost(hostname) {
|
|
35
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
36
|
-
}
|
|
37
|
-
function urlSafetyProblem(url) {
|
|
38
|
-
const isAllowedHTTP = url.protocol === "http:" && isLocalHTTPHost(url.hostname);
|
|
39
|
-
if (url.protocol !== "https:" && !isAllowedHTTP) {
|
|
40
|
-
return "must use https://, except localhost development URLs";
|
|
41
|
-
}
|
|
42
|
-
if (url.username || url.password) {
|
|
43
|
-
return "must not include credentials";
|
|
44
|
-
}
|
|
45
|
-
if (url.search || url.hash) {
|
|
46
|
-
return "must not include query or fragment";
|
|
47
|
-
}
|
|
48
|
-
return void 0;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// src/auth/resource.ts
|
|
52
|
-
function resolveProtectedResource(auth, request, options) {
|
|
53
|
-
if (auth.resource)
|
|
54
|
-
return trimTrailingSlash(auth.resource);
|
|
55
|
-
const path = resolveResourcePath(options.resourcePath, request);
|
|
56
|
-
const resourceURL = new URL(path, request.url);
|
|
57
|
-
resourceURL.search = "";
|
|
58
|
-
resourceURL.hash = "";
|
|
59
|
-
return trimTrailingSlash(resourceURL.toString());
|
|
60
|
-
}
|
|
61
|
-
function assertResourcePathShape(resourcePath, label = "resourcePath") {
|
|
62
|
-
if (!resourcePath.startsWith("/") || resourcePath.startsWith("//") || resourcePath.includes("\\") || /(^|\/)\.\.(\/|$)/.test(resourcePath)) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
`@lovable.dev/mcp-js: ${label} must be an absolute path beginning with "/" without ".." segments or backslashes (got ${JSON.stringify(resourcePath)})`
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function resolveResourcePath(resourcePath, request) {
|
|
69
|
-
if (resourcePath === void 0)
|
|
70
|
-
return new URL(request.url).pathname;
|
|
71
|
-
assertResourcePathShape(resourcePath);
|
|
72
|
-
return resourcePath;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// src/core/forwarded.ts
|
|
76
|
-
function applyForwardedOrigin(request, options) {
|
|
77
|
-
const proto = options.trustForwardedProto ? firstForwardedValue(request, "x-forwarded-proto") : void 0;
|
|
78
|
-
const host = options.trustForwardedHost ? firstForwardedValue(request, "x-forwarded-host") : void 0;
|
|
79
|
-
if (proto === void 0 && host === void 0)
|
|
80
|
-
return request;
|
|
81
|
-
const url = new URL(request.url);
|
|
82
|
-
let changed = false;
|
|
83
|
-
if (proto !== void 0 && `${proto}:` !== url.protocol) {
|
|
84
|
-
url.protocol = `${proto}:`;
|
|
85
|
-
changed = true;
|
|
86
|
-
}
|
|
87
|
-
if (host !== void 0 && host !== url.host) {
|
|
88
|
-
url.host = host;
|
|
89
|
-
changed = true;
|
|
90
|
-
}
|
|
91
|
-
return changed ? new Request(url.href, request) : request;
|
|
92
|
-
}
|
|
93
|
-
function firstForwardedValue(request, header) {
|
|
94
|
-
const value = request.headers.get(header)?.split(",")[0]?.trim();
|
|
95
|
-
return value ? value : void 0;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// src/metrics/config.ts
|
|
99
|
-
var DEFAULT_METRICS_ENDPOINT = "https://api.lovable.dev/v1/app-mcp-usage";
|
|
100
|
-
var METRICS_API_KEY_ENV_VAR = "LOVABLE_API_KEY";
|
|
101
|
-
function assertMetricsEndpoint(endpoint) {
|
|
102
|
-
let url;
|
|
103
|
-
try {
|
|
104
|
-
url = new URL(endpoint);
|
|
105
|
-
} catch {
|
|
106
|
-
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}`);
|
|
107
|
-
}
|
|
108
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
109
|
-
throw new Error(`@lovable.dev/mcp-js: metrics.endpoint must be http(s), got ${JSON.stringify(endpoint)}`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function resolveMetricsConfig(config = true) {
|
|
113
|
-
const options = typeof config === "boolean" ? { enabled: config } : config;
|
|
114
|
-
const endpoint = options.endpoint ?? DEFAULT_METRICS_ENDPOINT;
|
|
115
|
-
assertMetricsEndpoint(endpoint);
|
|
116
|
-
return Object.freeze({
|
|
117
|
-
enabled: options.enabled ?? true,
|
|
118
|
-
endpoint,
|
|
119
|
-
headers: options.headers ?? {},
|
|
120
|
-
apiKeyEnvVar: METRICS_API_KEY_ENV_VAR
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// src/core/logger.ts
|
|
125
|
-
var LEVEL_RANK = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 };
|
|
126
|
-
function isLogLevel(value) {
|
|
127
|
-
return typeof value === "string" && value in LEVEL_RANK;
|
|
128
|
-
}
|
|
129
|
-
var LOG_LEVEL_ENV_VAR = "LOVABLE_MCP_LOG_LEVEL";
|
|
130
|
-
function readEnvLevel() {
|
|
131
|
-
try {
|
|
132
|
-
const raw = typeof process !== "undefined" ? process.env?.[LOG_LEVEL_ENV_VAR] : void 0;
|
|
133
|
-
const normalized = raw?.trim().toLowerCase();
|
|
134
|
-
return isLogLevel(normalized) ? normalized : void 0;
|
|
135
|
-
} catch {
|
|
136
|
-
return void 0;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
var currentLevel = readEnvLevel() ?? "debug";
|
|
140
|
-
function setLogLevel(level) {
|
|
141
|
-
currentLevel = level;
|
|
142
|
-
}
|
|
143
|
-
function parseLogLevel(raw) {
|
|
144
|
-
const normalized = raw?.trim().toLowerCase();
|
|
145
|
-
return isLogLevel(normalized) ? normalized : void 0;
|
|
146
|
-
}
|
|
147
|
-
function applyLogLevelFromEnv(raw) {
|
|
148
|
-
setLogLevel(parseLogLevel(raw) ?? "debug");
|
|
149
|
-
}
|
|
150
|
-
function enabled(level) {
|
|
151
|
-
return LEVEL_RANK[level] <= LEVEL_RANK[currentLevel];
|
|
152
|
-
}
|
|
153
|
-
function emit(level, method, event, fields) {
|
|
154
|
-
if (!enabled(level))
|
|
155
|
-
return;
|
|
156
|
-
const message = `[mcp-js] ${event}`;
|
|
157
|
-
if (fields)
|
|
158
|
-
console[method](message, fields);
|
|
159
|
-
else
|
|
160
|
-
console[method](message);
|
|
161
|
-
}
|
|
162
|
-
var log = {
|
|
163
|
-
error: (event, fields) => emit("error", "error", event, fields),
|
|
164
|
-
warn: (event, fields) => emit("warn", "warn", event, fields),
|
|
165
|
-
info: (event, fields) => emit("info", "info", event, fields),
|
|
166
|
-
debug: (event, fields) => emit("debug", "debug", event, fields)
|
|
167
|
-
};
|
|
168
|
-
function describeError(err) {
|
|
169
|
-
if (err instanceof Error) {
|
|
170
|
-
const code = err.code;
|
|
171
|
-
return { name: err.name, message: err.message, ...typeof code === "string" ? { code } : {} };
|
|
172
|
-
}
|
|
173
|
-
return { value: String(err) };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// package.json
|
|
177
|
-
var version = "0.22.1";
|
|
178
|
-
|
|
179
|
-
// src/metrics/otlp.ts
|
|
180
|
-
var SCOPE_NAME = "@lovable.dev/mcp-js";
|
|
181
|
-
var EVENT_NAME = "mcp.tool.invocation";
|
|
182
|
-
var SEVERITY_INFO = 9;
|
|
183
|
-
function strAttr(key, value) {
|
|
184
|
-
return { key, value: { stringValue: value } };
|
|
185
|
-
}
|
|
186
|
-
function intAttr(key, value) {
|
|
187
|
-
return { key, value: { intValue: String(Math.round(value)) } };
|
|
188
|
-
}
|
|
189
|
-
function toLogRecord(rec) {
|
|
190
|
-
const attributes = [
|
|
191
|
-
strAttr("event.name", EVENT_NAME),
|
|
192
|
-
strAttr("mcp.method", rec.method),
|
|
193
|
-
strAttr("mcp.outcome", rec.outcome),
|
|
194
|
-
intAttr("mcp.duration_ms", rec.durationMs)
|
|
195
|
-
];
|
|
196
|
-
if (rec.tool !== null)
|
|
197
|
-
attributes.push(strAttr("mcp.tool", rec.tool));
|
|
198
|
-
if (rec.reqBytes !== void 0)
|
|
199
|
-
attributes.push(intAttr("mcp.request_size_bytes", rec.reqBytes));
|
|
200
|
-
if (rec.resBytes !== void 0)
|
|
201
|
-
attributes.push(intAttr("mcp.response_size_bytes", rec.resBytes));
|
|
202
|
-
attributes.push(strAttr("mcp.stack", rec.stack));
|
|
203
|
-
if (rec.endUserId !== void 0)
|
|
204
|
-
attributes.push(strAttr("mcp.end_user_id", rec.endUserId));
|
|
205
|
-
return {
|
|
206
|
-
timeUnixNano: rec.timeUnixNano,
|
|
207
|
-
observedTimeUnixNano: rec.timeUnixNano,
|
|
208
|
-
severityNumber: SEVERITY_INFO,
|
|
209
|
-
severityText: "INFO",
|
|
210
|
-
body: { stringValue: EVENT_NAME },
|
|
211
|
-
attributes
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
function buildLogsPayload(records, server) {
|
|
215
|
-
return JSON.stringify({
|
|
216
|
-
resourceLogs: [
|
|
217
|
-
{
|
|
218
|
-
resource: {
|
|
219
|
-
attributes: [
|
|
220
|
-
strAttr("service.name", server.name),
|
|
221
|
-
strAttr("service.version", server.version),
|
|
222
|
-
strAttr("telemetry.sdk.name", SCOPE_NAME),
|
|
223
|
-
strAttr("telemetry.sdk.version", version),
|
|
224
|
-
strAttr("telemetry.sdk.language", "webjs")
|
|
225
|
-
]
|
|
226
|
-
},
|
|
227
|
-
scopeLogs: [
|
|
228
|
-
{
|
|
229
|
-
scope: { name: SCOPE_NAME, version },
|
|
230
|
-
logRecords: records.map(toLogRecord)
|
|
231
|
-
}
|
|
232
|
-
]
|
|
233
|
-
}
|
|
234
|
-
]
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
function nowUnixNano() {
|
|
238
|
-
return `${Date.now()}000000`;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// src/metrics/impl/base.ts
|
|
242
|
-
function nowMs() {
|
|
243
|
-
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
244
|
-
}
|
|
245
|
-
var NOOP_RECORDER = { async emit() {
|
|
246
|
-
} };
|
|
247
|
-
function createNoopRecorder() {
|
|
248
|
-
return NOOP_RECORDER;
|
|
249
|
-
}
|
|
250
|
-
function readProcessEnv(name) {
|
|
251
|
-
try {
|
|
252
|
-
return typeof process !== "undefined" ? process.env?.[name] || void 0 : void 0;
|
|
253
|
-
} catch {
|
|
254
|
-
return void 0;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
var BaseMetricRecorder = class {
|
|
258
|
-
constructor(config, server, deps = {}) {
|
|
259
|
-
this.config = config;
|
|
260
|
-
this.server = server;
|
|
261
|
-
this.deps = deps;
|
|
262
|
-
this.doFetch = deps.fetch ?? (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : void 0);
|
|
263
|
-
this.usesLovableEndpoint = config.endpoint === DEFAULT_METRICS_ENDPOINT;
|
|
264
|
-
const headers = {};
|
|
265
|
-
if (!this.usesLovableEndpoint) {
|
|
266
|
-
for (const [key, value] of Object.entries(config.headers)) {
|
|
267
|
-
if (key.toLowerCase() !== "content-type")
|
|
268
|
-
headers[key] = value;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
headers["content-type"] = "application/json";
|
|
272
|
-
this.baseHeaders = headers;
|
|
273
|
-
}
|
|
274
|
-
doFetch;
|
|
275
|
-
usesLovableEndpoint;
|
|
276
|
-
baseHeaders;
|
|
277
|
-
apiKey;
|
|
278
|
-
lazyLogLevelApplied = false;
|
|
279
|
-
async emit(ev) {
|
|
280
|
-
await this.applyLazyLogLevel();
|
|
281
|
-
log.info("tool.invoked", {
|
|
282
|
-
tool: ev.tool,
|
|
283
|
-
method: ev.method,
|
|
284
|
-
outcome: ev.outcome,
|
|
285
|
-
durationMs: ev.durationMs,
|
|
286
|
-
stack: this.stack,
|
|
287
|
-
...ev.errorText !== void 0 && { errorText: ev.errorText }
|
|
288
|
-
});
|
|
289
|
-
if (!this.config.enabled)
|
|
290
|
-
return;
|
|
291
|
-
if (!this.doFetch) {
|
|
292
|
-
log.warn("metrics.disabled_no_fetch", { endpoint: this.config.endpoint });
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
const headers = { ...this.baseHeaders };
|
|
296
|
-
if (this.usesLovableEndpoint) {
|
|
297
|
-
const key = await this.resolveApiKey();
|
|
298
|
-
if (!key) {
|
|
299
|
-
log.warn("metrics.disabled_no_api_key", { envVar: this.config.apiKeyEnvVar, endpoint: this.config.endpoint });
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
headers.authorization = `Bearer ${key}`;
|
|
303
|
-
}
|
|
304
|
-
const body = buildLogsPayload([{ ...ev, timeUnixNano: nowUnixNano(), stack: this.stack }], this.server);
|
|
305
|
-
try {
|
|
306
|
-
const res = await this.doFetch(this.config.endpoint, { method: "POST", headers, body, keepalive: true });
|
|
307
|
-
if (!res.ok)
|
|
308
|
-
log.warn("metrics.rejected", { status: res.status, endpoint: this.config.endpoint });
|
|
309
|
-
} catch (err) {
|
|
310
|
-
log.warn("metrics.failed", { ...describeError(err), endpoint: this.config.endpoint });
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
// Cache only a successful resolution so a miss is retried on the next emit (the
|
|
314
|
-
// Cloudflare binding may not be readable on the very first attempt in an isolate).
|
|
315
|
-
async resolveApiKey() {
|
|
316
|
-
if (this.apiKey)
|
|
317
|
-
return this.apiKey;
|
|
318
|
-
this.apiKey = this.deps.getApiKey ? this.deps.getApiKey() : await this.readEnv(this.config.apiKeyEnvVar);
|
|
319
|
-
return this.apiKey;
|
|
320
|
-
}
|
|
321
|
-
async applyLazyLogLevel() {
|
|
322
|
-
if (this.lazyLogLevelApplied)
|
|
323
|
-
return;
|
|
324
|
-
this.lazyLogLevelApplied = true;
|
|
325
|
-
applyLogLevelFromEnv(await this.readEnv(LOG_LEVEL_ENV_VAR));
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
// src/metrics/impl/cloudflare.ts
|
|
330
|
-
var cloudflareEnvPromise;
|
|
331
|
-
async function readCloudflareEnv(name) {
|
|
332
|
-
try {
|
|
333
|
-
const moduleSpecifier = "cloudflare:workers";
|
|
334
|
-
cloudflareEnvPromise ??= import(
|
|
335
|
-
/* @vite-ignore */
|
|
336
|
-
moduleSpecifier
|
|
337
|
-
).then((m) => m.env).catch((err) => {
|
|
338
|
-
log.debug("metrics.cloudflare_env_import_failed", describeError(err));
|
|
339
|
-
return void 0;
|
|
340
|
-
});
|
|
341
|
-
const env = await cloudflareEnvPromise;
|
|
342
|
-
const raw = env?.[name];
|
|
343
|
-
const value = typeof raw === "string" && raw ? raw : void 0;
|
|
344
|
-
log.debug("metrics.read_cloudflare_env", { name, hasEnvBinding: !!env, found: value !== void 0 });
|
|
345
|
-
return value;
|
|
346
|
-
} catch (err) {
|
|
347
|
-
log.debug("metrics.read_cloudflare_env_error", { name, ...describeError(err) });
|
|
348
|
-
return void 0;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
var TanStackMetricRecorder = class extends BaseMetricRecorder {
|
|
352
|
-
stack = "tanstack";
|
|
353
|
-
async readEnv(name) {
|
|
354
|
-
return readProcessEnv(name) ?? await readCloudflareEnv(name);
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
// src/metrics/impl/supabase.ts
|
|
359
|
-
function readDenoEnv(name) {
|
|
360
|
-
try {
|
|
361
|
-
const denoEnv = globalThis.Deno?.env;
|
|
362
|
-
const value = denoEnv?.get?.(name) || void 0;
|
|
363
|
-
log.debug("metrics.read_deno_env", { name, hasDenoEnv: !!denoEnv, found: !!value });
|
|
364
|
-
return value;
|
|
365
|
-
} catch (err) {
|
|
366
|
-
log.debug("metrics.read_deno_env_error", { name, ...describeError(err) });
|
|
367
|
-
return void 0;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
var SupabaseMetricRecorder = class extends BaseMetricRecorder {
|
|
371
|
-
stack = "supabase";
|
|
372
|
-
async readEnv(name) {
|
|
373
|
-
return readDenoEnv(name) ?? readProcessEnv(name);
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
// src/metrics/recorder.ts
|
|
378
|
-
function createRecorderForRuntime(mcp, opts) {
|
|
379
|
-
const config = resolveMetricsConfig(mcp.metrics);
|
|
380
|
-
const server = { name: mcp.name, version: mcp.version };
|
|
381
|
-
if (!config.enabled)
|
|
382
|
-
return createNoopRecorder();
|
|
383
|
-
return opts.stack === "tanstack" ? new TanStackMetricRecorder(config, server) : new SupabaseMetricRecorder(config, server);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// src/protocols/mcp/protocol.ts
|
|
387
|
-
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
388
|
-
var import_webStandardStreamableHttp = require("@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js");
|
|
389
|
-
|
|
390
|
-
// src/core/http.ts
|
|
391
|
-
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
392
|
-
function headResponse(response) {
|
|
393
|
-
return new Response(null, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
394
|
-
}
|
|
395
|
-
function methodNotAllowed(allow) {
|
|
396
|
-
return new Response(JSON.stringify({ error: "method not allowed" }), {
|
|
397
|
-
status: 405,
|
|
398
|
-
headers: { ...JSON_HEADERS, Allow: allow }
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// src/core/promise.ts
|
|
403
|
-
function cachedPromise(load, label) {
|
|
404
|
-
let settled = false;
|
|
405
|
-
let value;
|
|
406
|
-
return async () => {
|
|
407
|
-
if (settled) {
|
|
408
|
-
if (label)
|
|
409
|
-
log.debug(`${label}.cache_hit`);
|
|
410
|
-
return value;
|
|
411
|
-
}
|
|
412
|
-
if (label)
|
|
413
|
-
log.debug(`${label}.load_start`);
|
|
414
|
-
try {
|
|
415
|
-
const loaded = await load();
|
|
416
|
-
settled = true;
|
|
417
|
-
value = loaded;
|
|
418
|
-
if (label)
|
|
419
|
-
log.debug(`${label}.settled`);
|
|
420
|
-
return loaded;
|
|
421
|
-
} catch (err) {
|
|
422
|
-
if (label)
|
|
423
|
-
log.debug(`${label}.load_failed`, describeError(err));
|
|
424
|
-
throw err;
|
|
425
|
-
}
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// src/core/validation.ts
|
|
430
|
-
function parseSafeUrl(subject, raw, ErrorClass = Error) {
|
|
431
|
-
let url;
|
|
432
|
-
try {
|
|
433
|
-
url = new URL(raw);
|
|
434
|
-
} catch {
|
|
435
|
-
throw new ErrorClass(`${subject} must be an absolute URL`);
|
|
436
|
-
}
|
|
437
|
-
const problem = urlSafetyProblem(url);
|
|
438
|
-
if (problem) {
|
|
439
|
-
throw new ErrorClass(`${subject} ${problem}`);
|
|
440
|
-
}
|
|
441
|
-
return url;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// src/auth/discovery.ts
|
|
445
|
-
var OAuthConfigurationError = class extends Error {
|
|
446
|
-
constructor(message) {
|
|
447
|
-
super(message);
|
|
448
|
-
this.name = "OAuthConfigurationError";
|
|
449
|
-
}
|
|
450
|
-
};
|
|
451
|
-
var METADATA_FETCH_TIMEOUT_MS = 5e3;
|
|
452
|
-
function issuerPath(url) {
|
|
453
|
-
const path = trimTrailingSlash(url.pathname);
|
|
454
|
-
return path === "" ? void 0 : path;
|
|
455
|
-
}
|
|
456
|
-
function pathInsertedOAuthMetadataUrls(url, path) {
|
|
457
|
-
return [
|
|
458
|
-
`${url.origin}/.well-known/oauth-authorization-server${path}`,
|
|
459
|
-
`${url.origin}/.well-known/openid-configuration${path}`
|
|
460
|
-
];
|
|
461
|
-
}
|
|
462
|
-
function oauthMetadataUrlsForIssuer(issuer) {
|
|
463
|
-
const normalizedIssuer = trimTrailingSlash(issuer);
|
|
464
|
-
const url = new URL(normalizedIssuer);
|
|
465
|
-
const path = issuerPath(url);
|
|
466
|
-
if (!path) {
|
|
467
|
-
return [
|
|
468
|
-
`${normalizedIssuer}/.well-known/oauth-authorization-server`,
|
|
469
|
-
`${normalizedIssuer}/.well-known/openid-configuration`
|
|
470
|
-
];
|
|
471
|
-
}
|
|
472
|
-
return [...pathInsertedOAuthMetadataUrls(url, path), `${normalizedIssuer}/.well-known/openid-configuration`];
|
|
473
|
-
}
|
|
474
|
-
async function fetchFirstValidOAuthServerMetadata(metadataUrls, expectedIssuer) {
|
|
475
|
-
const errors = [];
|
|
476
|
-
for (const url of metadataUrls) {
|
|
477
|
-
try {
|
|
478
|
-
return await fetchOAuthServerMetadata(url, expectedIssuer);
|
|
479
|
-
} catch (err) {
|
|
480
|
-
log.debug("oauth.discovery.attempt_failed", { url, ...describeError(err) });
|
|
481
|
-
errors.push(`${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
log.error("oauth.discovery.exhausted", { expectedIssuer, urlsTried: metadataUrls, errors });
|
|
485
|
-
throw new Error(`failed to discover OAuth server metadata (${errors.join("; ")})`);
|
|
486
|
-
}
|
|
487
|
-
async function fetchOAuthServerMetadata(url, expectedIssuer) {
|
|
488
|
-
log.debug("oauth.discovery.fetch", { url });
|
|
489
|
-
const response = await fetch(url, { signal: AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS), redirect: "manual" });
|
|
490
|
-
if (!response.ok) {
|
|
491
|
-
throw new Error(String(response.status));
|
|
492
|
-
}
|
|
493
|
-
const json = await response.json();
|
|
494
|
-
if (typeof json.issuer !== "string") {
|
|
495
|
-
throw new Error("missing issuer");
|
|
496
|
-
}
|
|
497
|
-
parseSafeUrl("discovered issuer", json.issuer);
|
|
498
|
-
if (trimTrailingSlash(json.issuer) !== expectedIssuer) {
|
|
499
|
-
log.warn("oauth.discovery.issuer_mismatch", { url, expectedIssuer, published: json.issuer });
|
|
500
|
-
throw new Error("issuer mismatch");
|
|
501
|
-
}
|
|
502
|
-
if (typeof json.jwks_uri !== "string") {
|
|
503
|
-
throw new Error("missing jwks_uri");
|
|
504
|
-
}
|
|
505
|
-
parseSafeUrl("discovered jwks_uri", json.jwks_uri);
|
|
506
|
-
return { issuer: json.issuer, jwks_uri: json.jwks_uri };
|
|
507
|
-
}
|
|
508
|
-
async function fetchIssuerOAuthServerMetadata(issuer) {
|
|
509
|
-
try {
|
|
510
|
-
return await fetchFirstValidOAuthServerMetadata(oauthMetadataUrlsForIssuer(issuer), issuer);
|
|
511
|
-
} catch (err) {
|
|
512
|
-
log.error("oauth.discovery.config_error", {
|
|
513
|
-
issuer,
|
|
514
|
-
...describeError(err),
|
|
515
|
-
outcome: "500 oauth configuration error"
|
|
516
|
-
});
|
|
517
|
-
throw new OAuthConfigurationError(
|
|
518
|
-
`OAuth issuer discovery failed: ${err instanceof Error ? err.message : String(err)}`
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
function createOAuthDiscoveryResolver(auth) {
|
|
523
|
-
const configuredIssuer = trimTrailingSlash(auth.issuer);
|
|
524
|
-
const oauthServerMetadata = cachedPromise(
|
|
525
|
-
() => fetchIssuerOAuthServerMetadata(configuredIssuer),
|
|
526
|
-
"oauth.discovery.metadata"
|
|
527
|
-
);
|
|
528
|
-
return {
|
|
529
|
-
resolveIssuer: async () => configuredIssuer,
|
|
530
|
-
resolveJwksUri: async () => {
|
|
531
|
-
if (auth.jwksUri) {
|
|
532
|
-
log.debug("oauth.jwks.resolved", { jwksUri: auth.jwksUri, source: "configured" });
|
|
533
|
-
return auth.jwksUri;
|
|
534
|
-
}
|
|
535
|
-
const jwksUri = (await oauthServerMetadata()).jwks_uri;
|
|
536
|
-
log.debug("oauth.jwks.resolved", { jwksUri, source: "discovered" });
|
|
537
|
-
return jwksUri;
|
|
538
|
-
}
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// src/auth/verifier.ts
|
|
543
|
-
var import_jose = require("jose");
|
|
544
|
-
|
|
545
|
-
// src/auth/claims.ts
|
|
546
|
-
function readString(value) {
|
|
547
|
-
return typeof value === "string" ? value : void 0;
|
|
548
|
-
}
|
|
549
|
-
function splitScopes(value) {
|
|
550
|
-
if (typeof value === "string")
|
|
551
|
-
return value.split(/\s+/).filter(Boolean);
|
|
552
|
-
return stringClaimList(value);
|
|
553
|
-
}
|
|
554
|
-
function stringClaim(claims, name) {
|
|
555
|
-
return readString(claims[name]);
|
|
556
|
-
}
|
|
557
|
-
function stringClaimList(value) {
|
|
558
|
-
if (typeof value === "string")
|
|
559
|
-
return value === "" ? [] : [value];
|
|
560
|
-
if (Array.isArray(value))
|
|
561
|
-
return value.filter((entry) => typeof entry === "string" && entry !== "");
|
|
562
|
-
return [];
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// src/auth/verifier.ts
|
|
566
|
-
var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
|
|
567
|
-
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
568
|
-
var DEFAULT_ACCESS_TOKEN_TYPS = ["at+jwt", "JWT"];
|
|
569
|
-
var JWKS_FETCH_TIMEOUT_MS = 5e3;
|
|
570
|
-
var OAuthTokenError = class extends Error {
|
|
571
|
-
constructor(status, oauthError, message) {
|
|
572
|
-
super(message);
|
|
573
|
-
this.status = status;
|
|
574
|
-
this.oauthError = oauthError;
|
|
575
|
-
this.name = "OAuthTokenError";
|
|
576
|
-
}
|
|
577
|
-
};
|
|
578
|
-
function resolveAcceptedAudiences(auth, resource) {
|
|
579
|
-
return auth.acceptedAudiences ?? [resource];
|
|
580
|
-
}
|
|
581
|
-
function tokenHeaderFields(token) {
|
|
582
|
-
try {
|
|
583
|
-
const header = (0, import_jose.decodeProtectedHeader)(token);
|
|
584
|
-
return { jwtAlg: header.alg, jwtKid: header.kid, tokenLength: token.length };
|
|
585
|
-
} catch {
|
|
586
|
-
return { tokenLength: token.length };
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
async function fetchVerificationKeySet(jwksUri) {
|
|
590
|
-
try {
|
|
591
|
-
const response = await fetch(jwksUri, { signal: AbortSignal.timeout(JWKS_FETCH_TIMEOUT_MS), redirect: "manual" });
|
|
592
|
-
if (!response.ok)
|
|
593
|
-
throw new Error(`JWKS endpoint returned ${response.status}`);
|
|
594
|
-
const json = await response.json();
|
|
595
|
-
return (0, import_jose.createLocalJWKSet)(json);
|
|
596
|
-
} catch (err) {
|
|
597
|
-
log.error("oauth.jwks.fetch_failed", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
598
|
-
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
function assertAccessTokenTyp(token, allowed) {
|
|
602
|
-
let header;
|
|
603
|
-
try {
|
|
604
|
-
header = (0, import_jose.decodeProtectedHeader)(token);
|
|
605
|
-
} catch (err) {
|
|
606
|
-
log.debug("oauth.verify.bad_header", { ...describeError(err), outcome: "401 invalid_token" });
|
|
607
|
-
throw new OAuthTokenError(401, "invalid_token", "Malformed JWT header");
|
|
608
|
-
}
|
|
609
|
-
const typ = header.typ;
|
|
610
|
-
if (typeof typ !== "string" || !allowed.includes(typ)) {
|
|
611
|
-
log.debug("oauth.verify.bad_typ", { jwtTyp: typ, allowed, outcome: "401 invalid_token" });
|
|
612
|
-
throw new OAuthTokenError(401, "invalid_token", "Access token typ header is not accepted");
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
async function verifyJwtClaims(token, keySet, issuer, auth) {
|
|
616
|
-
try {
|
|
617
|
-
const { payload } = await (0, import_jose.jwtVerify)(token, keySet, {
|
|
618
|
-
// `issuer` is trimmed of any trailing slash; accept both forms so a token whose
|
|
619
|
-
// `iss` carries the slash the AS publishes still verifies.
|
|
620
|
-
issuer: [issuer, `${issuer}/`],
|
|
621
|
-
algorithms: auth.algorithms ? [...auth.algorithms] : DEFAULT_JWT_ALGORITHMS,
|
|
622
|
-
requiredClaims: ["sub", "exp"],
|
|
623
|
-
clockTolerance: auth.clockToleranceSeconds ?? DEFAULT_CLOCK_TOLERANCE_SECONDS
|
|
624
|
-
});
|
|
625
|
-
return payload;
|
|
626
|
-
} catch (err) {
|
|
627
|
-
log.debug("oauth.verify.rejected", { ...describeError(err), outcome: "401 invalid_token" });
|
|
628
|
-
throw err;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
function checkAcceptedAudience(claims, accepted, auth) {
|
|
632
|
-
const audPopulated = claims.aud !== void 0 && !(Array.isArray(claims.aud) && claims.aud.length === 0);
|
|
633
|
-
if (audPopulated) {
|
|
634
|
-
if (stringClaimList(claims.aud).some((audience) => accepted.includes(audience)))
|
|
635
|
-
return "aud";
|
|
636
|
-
log.debug("oauth.verify.bad_audience", { tokenAud: claims.aud, accepted, outcome: "401 invalid_token" });
|
|
637
|
-
} else if (auth.acceptResourceClaim !== false) {
|
|
638
|
-
const acceptedTrimmed = accepted.map(trimTrailingSlash);
|
|
639
|
-
if (stringClaimList(claims.resource).some((resource) => acceptedTrimmed.includes(trimTrailingSlash(resource))))
|
|
640
|
-
return "resource";
|
|
641
|
-
log.debug("oauth.verify.bad_resource", { tokenResource: claims.resource, accepted, outcome: "401 invalid_token" });
|
|
642
|
-
} else {
|
|
643
|
-
log.debug("oauth.verify.empty_audience", { accepted, outcome: "401 invalid_token" });
|
|
644
|
-
}
|
|
645
|
-
throw new OAuthTokenError(401, "invalid_token", "token audience is not accepted");
|
|
646
|
-
}
|
|
647
|
-
function assertNonEmptySubject(claims) {
|
|
648
|
-
const sub = claims["sub"];
|
|
649
|
-
if (typeof sub !== "string" || sub.trim() === "") {
|
|
650
|
-
log.debug("oauth.verify.bad_subject", { subType: typeof sub });
|
|
651
|
-
throw new OAuthTokenError(401, "invalid_token", "token subject claim must be a non-empty string");
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
function assertOAuthClientClaim(auth, clientId) {
|
|
655
|
-
if (auth.requireOAuthClientClaim !== false && !clientId) {
|
|
656
|
-
log.debug("oauth.verify.missing_client_claim", { outcome: "401 invalid_token" });
|
|
657
|
-
throw new OAuthTokenError(401, "invalid_token", "OAuth client claim is required");
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
function makeBearer(token) {
|
|
661
|
-
return Object.defineProperty({}, "token", { value: token, enumerable: false });
|
|
662
|
-
}
|
|
663
|
-
function buildMcpAuthContext(args) {
|
|
664
|
-
const { claims } = args;
|
|
665
|
-
return {
|
|
666
|
-
type: "oauth",
|
|
667
|
-
principal: {
|
|
668
|
-
claims,
|
|
669
|
-
issuer: args.issuer,
|
|
670
|
-
resource: args.resource,
|
|
671
|
-
acceptedAudiences: args.acceptedAudiences,
|
|
672
|
-
scopes: splitScopes(claims.scope),
|
|
673
|
-
sub: stringClaim(claims, "sub"),
|
|
674
|
-
email: stringClaim(claims, "email"),
|
|
675
|
-
clientId: stringClaim(claims, "client_id") ?? stringClaim(claims, "azp")
|
|
676
|
-
},
|
|
677
|
-
bearer: makeBearer(args.token)
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
function createOAuthTokenVerifier(auth, discovery) {
|
|
681
|
-
const allowedTyps = auth.accessTokenTyp ?? DEFAULT_ACCESS_TOKEN_TYPS;
|
|
682
|
-
return async (token, request, options) => {
|
|
683
|
-
const resource = resolveProtectedResource(auth, request, options);
|
|
684
|
-
const issuer = await discovery.resolveIssuer();
|
|
685
|
-
const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
|
|
686
|
-
log.debug("oauth.verify.start", { issuer, acceptedAudiences, resource, ...tokenHeaderFields(token) });
|
|
687
|
-
const jwksUri = await discovery.resolveJwksUri();
|
|
688
|
-
log.debug("oauth.jwks.fetch", { jwksUri });
|
|
689
|
-
const keySet = await fetchVerificationKeySet(jwksUri);
|
|
690
|
-
assertAccessTokenTyp(token, allowedTyps);
|
|
691
|
-
const claims = await verifyJwtClaims(token, keySet, issuer, auth);
|
|
692
|
-
const audienceVia = checkAcceptedAudience(claims, acceptedAudiences, auth);
|
|
693
|
-
assertNonEmptySubject(claims);
|
|
694
|
-
const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
|
|
695
|
-
assertOAuthClientClaim(auth, context.principal.clientId);
|
|
696
|
-
log.info("oauth.verify.ok", {
|
|
697
|
-
sub: context.principal.sub,
|
|
698
|
-
clientId: context.principal.clientId,
|
|
699
|
-
scopes: context.principal.scopes,
|
|
700
|
-
audienceVia
|
|
701
|
-
});
|
|
702
|
-
return context;
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// src/auth/authorize.ts
|
|
707
|
-
function quoteAuthenticateParam(value) {
|
|
708
|
-
const sanitized = value.replace(/[\u0000-\u001F\u007F]/g, "");
|
|
709
|
-
return `"${sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
710
|
-
}
|
|
711
|
-
function resolveProtectedResourceMetadataUrl(auth, request, options) {
|
|
712
|
-
if (auth.protectedResourceMetadataUrl !== void 0)
|
|
713
|
-
return auth.protectedResourceMetadataUrl;
|
|
714
|
-
const base = auth.resource ?? request.url;
|
|
715
|
-
return new URL(options.metadataPath ?? OAUTH_PROTECTED_RESOURCE_METADATA_PATH, base).toString();
|
|
716
|
-
}
|
|
717
|
-
function wwwAuthenticateHeader(auth, request, options, params = {}) {
|
|
718
|
-
const values = [
|
|
719
|
-
`realm=${quoteAuthenticateParam("mcp")}`,
|
|
720
|
-
`resource_metadata=${quoteAuthenticateParam(resolveProtectedResourceMetadataUrl(auth, request, options))}`
|
|
721
|
-
];
|
|
722
|
-
if (auth.requiredScopes && auth.requiredScopes.length > 0) {
|
|
723
|
-
values.push(`scope=${quoteAuthenticateParam(auth.requiredScopes.join(" "))}`);
|
|
724
|
-
}
|
|
725
|
-
if (params.error)
|
|
726
|
-
values.push(`error=${quoteAuthenticateParam(params.error)}`);
|
|
727
|
-
if (params.errorDescription)
|
|
728
|
-
values.push(`error_description=${quoteAuthenticateParam(params.errorDescription)}`);
|
|
729
|
-
return `Bearer ${values.join(", ")}`;
|
|
730
|
-
}
|
|
731
|
-
function challengeResponse(auth, request, options, status, oauthError, errorDescription) {
|
|
732
|
-
const headers = new Headers(JSON_HEADERS);
|
|
733
|
-
headers.set(
|
|
734
|
-
"WWW-Authenticate",
|
|
735
|
-
wwwAuthenticateHeader(auth, request, options, { error: oauthError, errorDescription })
|
|
736
|
-
);
|
|
737
|
-
headers.set("Cache-Control", "no-store");
|
|
738
|
-
return new Response(JSON.stringify({ error: status === 403 ? "forbidden" : "unauthorized" }), {
|
|
739
|
-
status,
|
|
740
|
-
headers
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
function oauthConfigurationErrorResponse() {
|
|
744
|
-
return new Response(JSON.stringify({ error: "oauth configuration error" }), {
|
|
745
|
-
status: 500,
|
|
746
|
-
headers: { ...JSON_HEADERS, "Cache-Control": "no-store" }
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
function parseBearerToken(request) {
|
|
750
|
-
const header = request.headers.get("Authorization");
|
|
751
|
-
if (!header)
|
|
752
|
-
return void 0;
|
|
753
|
-
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
754
|
-
if (!match)
|
|
755
|
-
return void 0;
|
|
756
|
-
const token = match[1].trim();
|
|
757
|
-
if (token === "" || /\s/.test(token))
|
|
758
|
-
return void 0;
|
|
759
|
-
return token;
|
|
760
|
-
}
|
|
761
|
-
function getOAuthRuntime(mcp, options = {}) {
|
|
762
|
-
if (options.resourcePath !== void 0)
|
|
763
|
-
assertResourcePathShape(options.resourcePath);
|
|
764
|
-
if (options.metadataPath !== void 0)
|
|
765
|
-
assertResourcePathShape(options.metadataPath, "metadataPath");
|
|
766
|
-
const stableOptions = { resourcePath: options.resourcePath, metadataPath: options.metadataPath };
|
|
767
|
-
const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
|
|
768
|
-
if (!auth)
|
|
769
|
-
return { kind: "unconfigured", options: stableOptions };
|
|
770
|
-
if (options.metadataPath !== void 0 && auth.protectedResourceMetadataUrl !== void 0) {
|
|
771
|
-
throw new Error(
|
|
772
|
-
`@lovable.dev/mcp-js: the Vite plugin generates protected-resource metadata (metadataPath set), so auth.protectedResourceMetadataUrl must not also be set. Drop protectedResourceMetadataUrl, or set protectedResourceMetadataRoute: false in mcpPlugin(...) to host the document yourself.`
|
|
773
|
-
);
|
|
774
|
-
}
|
|
775
|
-
const discovery = createOAuthDiscoveryResolver(auth);
|
|
776
|
-
return {
|
|
777
|
-
kind: "configured",
|
|
778
|
-
auth,
|
|
779
|
-
discovery,
|
|
780
|
-
verify: createOAuthTokenVerifier(auth, discovery),
|
|
781
|
-
options: stableOptions
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
function assertRestResourceBinding(mcp, options = {}) {
|
|
785
|
-
const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
|
|
786
|
-
if (auth && auth.resource === void 0 && options.resourcePath === void 0) {
|
|
787
|
-
throw new Error(
|
|
788
|
-
`@lovable.dev/mcp-js: REST companion handlers require auth.resource or a resourcePath so principal.resource binds to the public MCP route, not the internal /.mcp/* request path`
|
|
789
|
-
);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
function missingRequiredScopes(auth, scopes) {
|
|
793
|
-
const requiredScopes = auth.requiredScopes ?? [];
|
|
794
|
-
if (requiredScopes.length === 0)
|
|
795
|
-
return [];
|
|
796
|
-
const granted = new Set(scopes);
|
|
797
|
-
return requiredScopes.filter((scope) => !granted.has(scope));
|
|
798
|
-
}
|
|
799
|
-
function assertRequiredScopes(auth, context) {
|
|
800
|
-
if (missingRequiredScopes(auth, context.principal.scopes).length > 0) {
|
|
801
|
-
throw new OAuthTokenError(403, "insufficient_scope", "Additional OAuth scope is required");
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
function createRequestAuthorizer(mcp, options = {}) {
|
|
805
|
-
const runtime = getOAuthRuntime(mcp, options);
|
|
806
|
-
return {
|
|
807
|
-
async authorize(request, recorder) {
|
|
808
|
-
if (runtime.kind === "unconfigured")
|
|
809
|
-
return { ok: true };
|
|
810
|
-
const startedAt = nowMs();
|
|
811
|
-
const token = parseBearerToken(request);
|
|
812
|
-
if (!token) {
|
|
813
|
-
log.info("auth.no_bearer_token", { outcome: "401" });
|
|
814
|
-
return { ok: false, response: challengeResponse(runtime.auth, request, runtime.options, 401) };
|
|
815
|
-
}
|
|
816
|
-
try {
|
|
817
|
-
const auth = await runtime.verify(token, request, runtime.options);
|
|
818
|
-
assertRequiredScopes(runtime.auth, auth);
|
|
819
|
-
return { ok: true, auth };
|
|
820
|
-
} catch (err) {
|
|
821
|
-
if (err instanceof OAuthConfigurationError) {
|
|
822
|
-
log.error("auth.config_error", { ...describeError(err), outcome: "500" });
|
|
823
|
-
await recorder?.emit({
|
|
824
|
-
tool: null,
|
|
825
|
-
method: "authorize",
|
|
826
|
-
outcome: "auth_config_error",
|
|
827
|
-
durationMs: nowMs() - startedAt
|
|
828
|
-
});
|
|
829
|
-
return { ok: false, response: oauthConfigurationErrorResponse() };
|
|
830
|
-
}
|
|
831
|
-
if (err instanceof OAuthTokenError) {
|
|
832
|
-
log.info("auth.token_rejected", { status: err.status, oauthError: err.oauthError });
|
|
833
|
-
return {
|
|
834
|
-
ok: false,
|
|
835
|
-
response: challengeResponse(
|
|
836
|
-
runtime.auth,
|
|
837
|
-
request,
|
|
838
|
-
runtime.options,
|
|
839
|
-
err.status,
|
|
840
|
-
err.oauthError,
|
|
841
|
-
err.message
|
|
842
|
-
)
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
log.error("auth.unexpected_error", { ...describeError(err), outcome: "401" });
|
|
846
|
-
return {
|
|
847
|
-
ok: false,
|
|
848
|
-
response: challengeResponse(
|
|
849
|
-
runtime.auth,
|
|
850
|
-
request,
|
|
851
|
-
runtime.options,
|
|
852
|
-
401,
|
|
853
|
-
"invalid_token",
|
|
854
|
-
"Invalid access token"
|
|
855
|
-
)
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// src/auth/context.ts
|
|
863
|
-
var ToolContext = class {
|
|
864
|
-
#auth;
|
|
865
|
-
constructor(auth) {
|
|
866
|
-
this.#auth = auth;
|
|
867
|
-
}
|
|
868
|
-
/** Whether the in-flight tool call carries a verified auth context. */
|
|
869
|
-
isAuthenticated() {
|
|
870
|
-
return this.#auth !== void 0;
|
|
871
|
-
}
|
|
872
|
-
/** The verified bearer token, or `undefined` when unauthenticated. Pass it to downstream APIs; never return or log it. */
|
|
873
|
-
getToken() {
|
|
874
|
-
return this.#auth?.bearer.token;
|
|
875
|
-
}
|
|
876
|
-
/** The verified user id (the token `sub`), or `undefined`. */
|
|
877
|
-
getUserId() {
|
|
878
|
-
return this.#auth?.principal.sub;
|
|
879
|
-
}
|
|
880
|
-
/** The verified user email, or `undefined` when absent. */
|
|
881
|
-
getUserEmail() {
|
|
882
|
-
return this.#auth?.principal.email;
|
|
883
|
-
}
|
|
884
|
-
/** The verified OAuth `client_id`, or `undefined`. */
|
|
885
|
-
getClientId() {
|
|
886
|
-
return this.#auth?.principal.clientId;
|
|
887
|
-
}
|
|
888
|
-
/** The verified OAuth scopes, or `undefined` when unauthenticated. */
|
|
889
|
-
getScopes() {
|
|
890
|
-
return this.#auth?.principal.scopes;
|
|
891
|
-
}
|
|
892
|
-
/** The verified token issuer, or `undefined`. */
|
|
893
|
-
getIssuer() {
|
|
894
|
-
return this.#auth?.principal.issuer;
|
|
895
|
-
}
|
|
896
|
-
/**
|
|
897
|
-
* The full verified JWT claims, or `undefined`. Use this for app/business
|
|
898
|
-
* authorization on issuer-specific claims that have no dedicated accessor.
|
|
899
|
-
*/
|
|
900
|
-
getClaims() {
|
|
901
|
-
return this.#auth?.principal.claims;
|
|
902
|
-
}
|
|
903
|
-
};
|
|
904
|
-
|
|
905
|
-
// src/core/content.ts
|
|
906
|
-
function extractTextContent(content) {
|
|
907
|
-
if (!content)
|
|
908
|
-
return void 0;
|
|
909
|
-
const text = content.filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
|
|
910
|
-
return text.length > 0 ? text : void 0;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// src/core/cors.ts
|
|
914
|
-
var EXPOSE_HEADERS = "WWW-Authenticate, Mcp-Session-Id, Mcp-Protocol-Version";
|
|
915
|
-
var ALLOW_HEADERS = "Authorization, Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID";
|
|
916
|
-
function withCors(response) {
|
|
917
|
-
response.headers.set("Access-Control-Allow-Origin", "*");
|
|
918
|
-
response.headers.set("Access-Control-Expose-Headers", EXPOSE_HEADERS);
|
|
919
|
-
return response;
|
|
920
|
-
}
|
|
921
|
-
function corsPreflightResponse(allowMethods) {
|
|
922
|
-
return new Response(null, {
|
|
923
|
-
status: 204,
|
|
924
|
-
headers: {
|
|
925
|
-
"Access-Control-Allow-Origin": "*",
|
|
926
|
-
"Access-Control-Allow-Methods": allowMethods,
|
|
927
|
-
"Access-Control-Allow-Headers": ALLOW_HEADERS,
|
|
928
|
-
"Access-Control-Max-Age": "86400"
|
|
929
|
-
}
|
|
930
|
-
});
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
// src/protocols/mcp/protocol.ts
|
|
934
|
-
function adaptToolToSdkCallback(tool, auth, recorder) {
|
|
935
|
-
const endUserId = auth?.principal.sub;
|
|
936
|
-
return async (first) => {
|
|
937
|
-
const args = tool.inputSchema ? first ?? {} : {};
|
|
938
|
-
const start = nowMs();
|
|
939
|
-
let result;
|
|
940
|
-
try {
|
|
941
|
-
result = await tool.handler(args, new ToolContext(auth));
|
|
942
|
-
} catch {
|
|
943
|
-
await recorder.emit({
|
|
944
|
-
tool: tool.name,
|
|
945
|
-
method: "tools/call",
|
|
946
|
-
outcome: "handler_error",
|
|
947
|
-
durationMs: nowMs() - start,
|
|
948
|
-
endUserId
|
|
949
|
-
});
|
|
950
|
-
return { content: [{ type: "text", text: "tool execution failed" }], isError: true };
|
|
951
|
-
}
|
|
952
|
-
if (result == null) {
|
|
953
|
-
await recorder.emit({
|
|
954
|
-
tool: tool.name,
|
|
955
|
-
method: "tools/call",
|
|
956
|
-
outcome: "handler_error",
|
|
957
|
-
durationMs: nowMs() - start,
|
|
958
|
-
endUserId
|
|
959
|
-
});
|
|
960
|
-
return { content: [{ type: "text", text: `tool "${tool.name}" returned no result` }], isError: true };
|
|
961
|
-
}
|
|
962
|
-
await recorder.emit({
|
|
963
|
-
tool: tool.name,
|
|
964
|
-
method: "tools/call",
|
|
965
|
-
outcome: result.isError ? "tool_error" : "ok",
|
|
966
|
-
durationMs: nowMs() - start,
|
|
967
|
-
errorText: result.isError ? extractTextContent(result.content) : void 0,
|
|
968
|
-
endUserId
|
|
969
|
-
});
|
|
970
|
-
return { content: result.content ?? [], structuredContent: result.structuredContent, isError: result.isError };
|
|
971
|
-
};
|
|
972
|
-
}
|
|
973
|
-
function createMcpProtocolHandler(mcp, options = {}) {
|
|
974
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
975
|
-
const handle = async (request, recorder) => {
|
|
976
|
-
const authResult = await authorizer.authorize(request, recorder);
|
|
977
|
-
if (!authResult.ok)
|
|
978
|
-
return authResult.response;
|
|
979
|
-
try {
|
|
980
|
-
const server = new import_mcp.McpServer(
|
|
981
|
-
{ name: mcp.name, version: mcp.version, title: mcp.title },
|
|
982
|
-
{ instructions: mcp.instructions }
|
|
983
|
-
);
|
|
984
|
-
for (const tool of mcp.tools) {
|
|
985
|
-
server.registerTool(
|
|
986
|
-
tool.name,
|
|
987
|
-
{
|
|
988
|
-
title: tool.title,
|
|
989
|
-
description: tool.description,
|
|
990
|
-
inputSchema: tool.inputSchema,
|
|
991
|
-
outputSchema: tool.outputSchema,
|
|
992
|
-
annotations: tool.annotations
|
|
993
|
-
},
|
|
994
|
-
adaptToolToSdkCallback(tool, authResult.auth, recorder)
|
|
995
|
-
);
|
|
996
|
-
}
|
|
997
|
-
const transport = new import_webStandardStreamableHttp.WebStandardStreamableHTTPServerTransport({
|
|
998
|
-
sessionIdGenerator: void 0
|
|
999
|
-
});
|
|
1000
|
-
await server.connect(transport);
|
|
1001
|
-
return await transport.handleRequest(request);
|
|
1002
|
-
} catch (err) {
|
|
1003
|
-
await recorder.emit({
|
|
1004
|
-
tool: null,
|
|
1005
|
-
method: "transport",
|
|
1006
|
-
outcome: "transport_error",
|
|
1007
|
-
durationMs: 0,
|
|
1008
|
-
endUserId: authResult.auth?.principal.sub
|
|
1009
|
-
});
|
|
1010
|
-
log.error("mcp.transport_error", { ...describeError(err), outcome: "500 internal error" });
|
|
1011
|
-
return Response.json(
|
|
1012
|
-
{ jsonrpc: "2.0", id: null, error: { code: -32603, message: "internal error" } },
|
|
1013
|
-
{ status: 500 }
|
|
1014
|
-
);
|
|
1015
|
-
}
|
|
1016
|
-
};
|
|
1017
|
-
return async (request, recorder = createNoopRecorder()) => {
|
|
1018
|
-
if (request.method === "OPTIONS")
|
|
1019
|
-
return corsPreflightResponse("GET, POST, DELETE, OPTIONS");
|
|
1020
|
-
return withCors(await handle(request, recorder));
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// src/protocols/oauth-metadata.ts
|
|
1025
|
-
function notFound() {
|
|
1026
|
-
return withCors(
|
|
1027
|
-
new Response(JSON.stringify({ error: "not found" }), {
|
|
1028
|
-
status: 404,
|
|
1029
|
-
// `no-store` so a 404 (OAuth unconfigured) isn't heuristically cached and
|
|
1030
|
-
// then served past a later deploy that enables OAuth and starts returning 200.
|
|
1031
|
-
headers: { ...JSON_HEADERS, "Cache-Control": "no-store" }
|
|
1032
|
-
})
|
|
1033
|
-
);
|
|
1034
|
-
}
|
|
1035
|
-
async function buildProtectedResourceMetadata(mcp, auth, request, options, discovery) {
|
|
1036
|
-
const issuer = await discovery.resolveIssuer();
|
|
1037
|
-
const body = {
|
|
1038
|
-
resource: resolveProtectedResource(auth, request, options),
|
|
1039
|
-
authorization_servers: [issuer],
|
|
1040
|
-
bearer_methods_supported: ["header"],
|
|
1041
|
-
resource_name: auth.resourceName ?? mcp.title,
|
|
1042
|
-
resource_documentation: auth.resourceDocumentation
|
|
1043
|
-
};
|
|
1044
|
-
if (auth.requiredScopes && auth.requiredScopes.length > 0)
|
|
1045
|
-
body.scopes_supported = auth.requiredScopes;
|
|
1046
|
-
return body;
|
|
1047
|
-
}
|
|
1048
|
-
function createOAuthProtectedResourceMetadataHandler(mcp, options = {}) {
|
|
1049
|
-
const runtime = getOAuthRuntime(mcp, options);
|
|
1050
|
-
if (runtime.kind === "configured" && runtime.auth.protectedResourceMetadataUrl === void 0 && runtime.auth.resource === void 0 && runtime.options.resourcePath === void 0) {
|
|
1051
|
-
throw new Error(
|
|
1052
|
-
`@lovable.dev/mcp-js: auth.resource or a resourcePath is required so the protected-resource metadata doesn't advertise the well-known URL as the resource`
|
|
1053
|
-
);
|
|
1054
|
-
}
|
|
1055
|
-
return async (request) => {
|
|
1056
|
-
if (runtime.kind !== "configured" || runtime.auth.protectedResourceMetadataUrl !== void 0)
|
|
1057
|
-
return notFound();
|
|
1058
|
-
if (request.method === "OPTIONS")
|
|
1059
|
-
return corsPreflightResponse("GET, HEAD, OPTIONS");
|
|
1060
|
-
if (request.method !== "GET" && request.method !== "HEAD")
|
|
1061
|
-
return withCors(methodNotAllowed("GET, HEAD, OPTIONS"));
|
|
1062
|
-
const headers = {
|
|
1063
|
-
...JSON_HEADERS,
|
|
1064
|
-
"Cache-Control": "public, max-age=300",
|
|
1065
|
-
Vary: "Host"
|
|
1066
|
-
};
|
|
1067
|
-
try {
|
|
1068
|
-
const metadata = await buildProtectedResourceMetadata(
|
|
1069
|
-
mcp,
|
|
1070
|
-
runtime.auth,
|
|
1071
|
-
request,
|
|
1072
|
-
runtime.options,
|
|
1073
|
-
runtime.discovery
|
|
1074
|
-
);
|
|
1075
|
-
const response = withCors(Response.json(metadata, { headers }));
|
|
1076
|
-
return request.method === "HEAD" ? headResponse(response) : response;
|
|
1077
|
-
} catch (err) {
|
|
1078
|
-
log.error("oauth.metadata.config_error", { ...describeError(err), outcome: "500 oauth configuration error" });
|
|
1079
|
-
const response = withCors(oauthConfigurationErrorResponse());
|
|
1080
|
-
return request.method === "HEAD" ? headResponse(response) : response;
|
|
1081
|
-
}
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
// src/protocols/rest/list-tools.ts
|
|
1086
|
-
var import_zod_compat = require("@modelcontextprotocol/sdk/server/zod-compat.js");
|
|
1087
|
-
var import_zod_json_schema_compat = require("@modelcontextprotocol/sdk/server/zod-json-schema-compat.js");
|
|
1088
|
-
function shapeToJsonSchema(shape) {
|
|
1089
|
-
if (!shape)
|
|
1090
|
-
return null;
|
|
1091
|
-
try {
|
|
1092
|
-
return (0, import_zod_json_schema_compat.toJsonSchemaCompat)((0, import_zod_compat.objectFromShape)(shape));
|
|
1093
|
-
} catch {
|
|
1094
|
-
return null;
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
function buildMcpListing(mcp) {
|
|
1098
|
-
return {
|
|
1099
|
-
server: { name: mcp.name, version: mcp.version, title: mcp.title },
|
|
1100
|
-
tools: mcp.tools.map((tool) => ({
|
|
1101
|
-
name: tool.name,
|
|
1102
|
-
title: tool.title,
|
|
1103
|
-
description: tool.description,
|
|
1104
|
-
annotations: tool.annotations,
|
|
1105
|
-
inputSchema: shapeToJsonSchema(tool.inputSchema),
|
|
1106
|
-
outputSchema: shapeToJsonSchema(tool.outputSchema)
|
|
1107
|
-
}))
|
|
1108
|
-
};
|
|
1109
|
-
}
|
|
1110
|
-
function createListToolsHandler(mcp, options = {}) {
|
|
1111
|
-
assertRestResourceBinding(mcp, options);
|
|
1112
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
1113
|
-
const handle = async (request, recorder) => {
|
|
1114
|
-
const authResult = await authorizer.authorize(request, recorder);
|
|
1115
|
-
if (!authResult.ok)
|
|
1116
|
-
return authResult.response;
|
|
1117
|
-
if (request.method !== "GET" && request.method !== "HEAD")
|
|
1118
|
-
return methodNotAllowed("GET, HEAD, OPTIONS");
|
|
1119
|
-
const response = Response.json(buildMcpListing(mcp));
|
|
1120
|
-
return request.method === "HEAD" ? headResponse(response) : response;
|
|
1121
|
-
};
|
|
1122
|
-
return async (request, recorder = createNoopRecorder()) => {
|
|
1123
|
-
if (request.method === "OPTIONS")
|
|
1124
|
-
return corsPreflightResponse("GET, HEAD, OPTIONS");
|
|
1125
|
-
return withCors(await handle(request, recorder));
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// src/protocols/rest/invoke-tool.ts
|
|
1130
|
-
var import_zod_compat2 = require("@modelcontextprotocol/sdk/server/zod-compat.js");
|
|
1131
|
-
var MAX_REFLECTED_TOOL_NAME = 256;
|
|
1132
|
-
function safeReflectName(name) {
|
|
1133
|
-
const text = String(name);
|
|
1134
|
-
return text.length > MAX_REFLECTED_TOOL_NAME ? `${text.slice(0, MAX_REFLECTED_TOOL_NAME)}\u2026` : text;
|
|
1135
|
-
}
|
|
1136
|
-
function isEmptyArgs(value) {
|
|
1137
|
-
if (value == null)
|
|
1138
|
-
return true;
|
|
1139
|
-
if (typeof value !== "object" || Array.isArray(value))
|
|
1140
|
-
return false;
|
|
1141
|
-
return Object.keys(value).length === 0;
|
|
1142
|
-
}
|
|
1143
|
-
function createInvokeToolHandler(mcp, options = {}) {
|
|
1144
|
-
assertRestResourceBinding(mcp, options);
|
|
1145
|
-
const authorizer = createRequestAuthorizer(mcp, options);
|
|
1146
|
-
const handle = async (request, toolName, recorder) => {
|
|
1147
|
-
const authResult = await authorizer.authorize(request, recorder);
|
|
1148
|
-
if (!authResult.ok)
|
|
1149
|
-
return authResult.response;
|
|
1150
|
-
const endUserId = authResult.auth?.principal.sub;
|
|
1151
|
-
if (request.method !== "POST")
|
|
1152
|
-
return methodNotAllowed("POST, OPTIONS");
|
|
1153
|
-
const tool = mcp.tools.find((t) => t.name === toolName);
|
|
1154
|
-
if (!tool) {
|
|
1155
|
-
return new Response(JSON.stringify({ error: `unknown tool: ${safeReflectName(toolName)}` }), {
|
|
1156
|
-
status: 404,
|
|
1157
|
-
headers: JSON_HEADERS
|
|
1158
|
-
});
|
|
1159
|
-
}
|
|
1160
|
-
let rawArgs = {};
|
|
1161
|
-
const text = await request.text();
|
|
1162
|
-
if (text) {
|
|
1163
|
-
try {
|
|
1164
|
-
rawArgs = JSON.parse(text);
|
|
1165
|
-
} catch {
|
|
1166
|
-
return new Response(JSON.stringify({ error: "invalid JSON body" }), {
|
|
1167
|
-
status: 400,
|
|
1168
|
-
headers: JSON_HEADERS
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
let args = rawArgs;
|
|
1173
|
-
if (tool.inputSchema) {
|
|
1174
|
-
try {
|
|
1175
|
-
const schema = (0, import_zod_compat2.objectFromShape)(tool.inputSchema);
|
|
1176
|
-
const parsed = await (0, import_zod_compat2.safeParseAsync)(schema, rawArgs);
|
|
1177
|
-
if (!parsed.success) {
|
|
1178
|
-
return new Response(
|
|
1179
|
-
JSON.stringify({
|
|
1180
|
-
error: "validation failed",
|
|
1181
|
-
details: (0, import_zod_compat2.getParseErrorMessage)(parsed.error)
|
|
1182
|
-
}),
|
|
1183
|
-
{ status: 400, headers: JSON_HEADERS }
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
1186
|
-
args = parsed.data;
|
|
1187
|
-
} catch {
|
|
1188
|
-
return new Response(JSON.stringify({ error: "schema error", tool: toolName }), {
|
|
1189
|
-
status: 500,
|
|
1190
|
-
headers: JSON_HEADERS
|
|
1191
|
-
});
|
|
1192
|
-
}
|
|
1193
|
-
} else if (!isEmptyArgs(rawArgs)) {
|
|
1194
|
-
return new Response(JSON.stringify({ error: "tool has no inputSchema; expected empty body" }), {
|
|
1195
|
-
status: 400,
|
|
1196
|
-
headers: JSON_HEADERS
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
let result;
|
|
1200
|
-
const start = nowMs();
|
|
1201
|
-
try {
|
|
1202
|
-
result = await tool.handler(args, new ToolContext(authResult.auth));
|
|
1203
|
-
} catch {
|
|
1204
|
-
await recorder.emit({
|
|
1205
|
-
tool: tool.name,
|
|
1206
|
-
method: "tools/call",
|
|
1207
|
-
outcome: "handler_error",
|
|
1208
|
-
durationMs: nowMs() - start,
|
|
1209
|
-
endUserId
|
|
1210
|
-
});
|
|
1211
|
-
return new Response(JSON.stringify({ error: "handler threw", tool: toolName }), {
|
|
1212
|
-
status: 500,
|
|
1213
|
-
headers: JSON_HEADERS
|
|
1214
|
-
});
|
|
1215
|
-
}
|
|
1216
|
-
if (result == null) {
|
|
1217
|
-
await recorder.emit({
|
|
1218
|
-
tool: tool.name,
|
|
1219
|
-
method: "tools/call",
|
|
1220
|
-
outcome: "handler_error",
|
|
1221
|
-
durationMs: nowMs() - start,
|
|
1222
|
-
endUserId
|
|
1223
|
-
});
|
|
1224
|
-
return new Response(JSON.stringify({ error: `tool "${toolName}" returned no result` }), {
|
|
1225
|
-
status: 500,
|
|
1226
|
-
headers: JSON_HEADERS
|
|
1227
|
-
});
|
|
1228
|
-
}
|
|
1229
|
-
await recorder.emit({
|
|
1230
|
-
tool: tool.name,
|
|
1231
|
-
method: "tools/call",
|
|
1232
|
-
outcome: result.isError ? "tool_error" : "ok",
|
|
1233
|
-
durationMs: nowMs() - start,
|
|
1234
|
-
errorText: result.isError ? extractTextContent(result.content) : void 0,
|
|
1235
|
-
endUserId
|
|
1236
|
-
});
|
|
1237
|
-
return Response.json({
|
|
1238
|
-
content: result.content ?? [],
|
|
1239
|
-
structuredContent: result.structuredContent,
|
|
1240
|
-
isError: result.isError
|
|
1241
|
-
});
|
|
1242
|
-
};
|
|
1243
|
-
return async (request, toolName, recorder = createNoopRecorder()) => {
|
|
1244
|
-
if (request.method === "OPTIONS")
|
|
1245
|
-
return corsPreflightResponse("POST, OPTIONS");
|
|
1246
|
-
return withCors(await handle(request, toolName, recorder));
|
|
1247
|
-
};
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
// src/stacks/supabase/paths.ts
|
|
1251
|
-
var FUNCTIONS_MOUNT_PREFIX = "/functions/v1/";
|
|
1252
|
-
function assertFunctionName(value, label = "functionName") {
|
|
1253
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
|
|
1254
|
-
throw new Error(
|
|
1255
|
-
`@lovable.dev/mcp-js: ${label} must be a single path segment matching [A-Za-z0-9_-], got ${JSON.stringify(value)}`
|
|
1256
|
-
);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
// src/stacks/supabase/handler.ts
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_logger = require("../../logger-BqPOa_E1.cjs");
|
|
3
|
+
const require_cors = require("../../cors-Cga2PWEg.cjs");
|
|
4
|
+
const require_metadata_path = require("../../metadata-path-zd7NSNoa.cjs");
|
|
5
|
+
const require_mcp = require("../../mcp-DLv_c1O5.cjs");
|
|
6
|
+
const require_protocols_oauth_metadata = require("../../protocols/oauth-metadata.cjs");
|
|
7
|
+
const require_list_tools = require("../../list-tools-07OV14rK.cjs");
|
|
8
|
+
const require_rest = require("../../rest-DxhK7-3b.cjs");
|
|
9
|
+
const require_forwarded = require("../../forwarded-Pi8Hs5Ps.cjs");
|
|
10
|
+
const require_paths = require("../../paths-OP318a6c.cjs");
|
|
11
|
+
//#region src/stacks/supabase/handler.ts
|
|
1261
12
|
function deriveResourcePath(options) {
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
assertFunctionName(options.functionName);
|
|
1267
|
-
return `${FUNCTIONS_MOUNT_PREFIX}${options.functionName}`;
|
|
13
|
+
if (options.resourcePath !== void 0) return options.resourcePath;
|
|
14
|
+
if (options.functionName === void 0) return void 0;
|
|
15
|
+
require_paths.assertFunctionName(options.functionName);
|
|
16
|
+
return `${require_paths.FUNCTIONS_MOUNT_PREFIX}${options.functionName}`;
|
|
1268
17
|
}
|
|
1269
18
|
function dispatchFor(pathname) {
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
19
|
+
const path = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
|
|
20
|
+
if (path.endsWith("/.well-known/oauth-protected-resource")) return { kind: "metadata" };
|
|
21
|
+
if (path.endsWith("/.mcp/list-tools")) return { kind: "list-tools" };
|
|
22
|
+
const invokeMatch = /\/\.mcp\/invoke-tool\/([^/]+)$/.exec(path);
|
|
23
|
+
if (invokeMatch) {
|
|
24
|
+
let toolName;
|
|
25
|
+
try {
|
|
26
|
+
toolName = decodeURIComponent(invokeMatch[1]);
|
|
27
|
+
} catch {
|
|
28
|
+
return { kind: "mcp" };
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
kind: "invoke-tool",
|
|
32
|
+
toolName
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return { kind: "mcp" };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build a single Web-Standard handler that serves the full MCP surface from
|
|
39
|
+
* one Supabase Edge Function. The returned handler dispatches by URL suffix:
|
|
40
|
+
*
|
|
41
|
+
* | Suffix | Target |
|
|
42
|
+
* | --- | --- |
|
|
43
|
+
* | `/.well-known/oauth-protected-resource` | RFC 9728 metadata |
|
|
44
|
+
* | `/.mcp/list-tools` | REST tool catalog |
|
|
45
|
+
* | `/.mcp/invoke-tool/<tool>` | REST tool dispatcher |
|
|
46
|
+
* | (anything else) | MCP streamable-HTTP protocol |
|
|
47
|
+
*
|
|
48
|
+
* Pass the handler to `Deno.serve(...)` in the emitted function entry.
|
|
49
|
+
* The same OAuth bearer gate protects every dispatch target.
|
|
50
|
+
*/
|
|
1289
51
|
function createSupabaseHandler(mcp, options = {}) {
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
1320
|
-
0 && (module.exports = {
|
|
1321
|
-
createSupabaseHandler
|
|
1322
|
-
});
|
|
52
|
+
const resourcePath = deriveResourcePath(options);
|
|
53
|
+
if (resourcePath !== void 0) require_cors.assertResourcePathShape(resourcePath);
|
|
54
|
+
const servesOwnPrm = !(mcp.auth?.type === "oauth" && mcp.auth.protectedResourceMetadataUrl !== void 0);
|
|
55
|
+
const metadataPath = resourcePath === void 0 || !servesOwnPrm ? void 0 : `${require_logger.trimTrailingSlash(resourcePath)}${require_metadata_path.OAUTH_PROTECTED_RESOURCE_METADATA_PATH}`;
|
|
56
|
+
const runtimeOptions = resourcePath === void 0 ? {} : {
|
|
57
|
+
resourcePath,
|
|
58
|
+
...metadataPath ? { metadataPath } : {}
|
|
59
|
+
};
|
|
60
|
+
const mcpHandler = require_mcp.createMcpProtocolHandler(mcp, runtimeOptions);
|
|
61
|
+
const listToolsHandler = require_list_tools.createListToolsHandler(mcp, runtimeOptions);
|
|
62
|
+
const invokeToolHandler = require_rest.createInvokeToolHandler(mcp, runtimeOptions);
|
|
63
|
+
const metadataHandler = require_protocols_oauth_metadata.createOAuthProtectedResourceMetadataHandler(mcp, runtimeOptions);
|
|
64
|
+
return async (request) => {
|
|
65
|
+
const req = require_forwarded.applyForwardedOrigin(request, {
|
|
66
|
+
trustForwardedProto: true,
|
|
67
|
+
trustForwardedHost: options.trustForwardedHost
|
|
68
|
+
});
|
|
69
|
+
const recorder = require_cors.createRecorderForRuntime(mcp, { stack: "supabase" });
|
|
70
|
+
const target = dispatchFor(new URL(req.url).pathname);
|
|
71
|
+
switch (target.kind) {
|
|
72
|
+
case "metadata": return metadataHandler(req);
|
|
73
|
+
case "list-tools": return listToolsHandler(req, recorder);
|
|
74
|
+
case "invoke-tool": return invokeToolHandler(req, target.toolName, recorder);
|
|
75
|
+
case "mcp": return mcpHandler(req, recorder);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
exports.createSupabaseHandler = createSupabaseHandler;
|