@lovable.dev/mcp-js 0.27.1 → 0.29.0-rc.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 +13 -7
- package/dist/{authorize-Cs1yyLrq.d.ts → authorize-BjNE3LPr.d.ts} +1 -1
- package/dist/{authorize-BcNuNPtP.d.cts → authorize-BzZFS0zi.d.cts} +1 -1
- package/dist/{base-CReh7d3E.d.cts → base-DSH8NnPj.d.cts} +10 -3
- package/dist/{base-CReh7d3E.d.ts → base-DSH8NnPj.d.ts} +10 -3
- package/dist/cli/extract-manifest.cjs +2 -2
- package/dist/cli/extract-manifest.js +2 -2
- package/dist/{authorize-CB35c3Wg.cjs → cors-CmKzDN7k.cjs} +154 -59
- package/dist/{authorize-FOlzhxrM.js → cors-D4YbODs-.js} +150 -55
- package/dist/{forwarded-BlLF3dfc.js → forwarded-B3EBjHnA.js} +3 -3
- package/dist/{forwarded-Bhwcj22u.cjs → forwarded-BC_RHnn5.cjs} +3 -3
- package/dist/{gate-B9RSzvoI.cjs → gate-n_HX6eQ5.cjs} +6 -4
- package/dist/{gate-BeHmyGxk.js → gate-pGsVlY_4.js} +5 -3
- package/dist/index.cjs +38 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +38 -1
- package/dist/{io-BzTGjG_c.d.cts → io-BEwvgN8s.d.cts} +1 -1
- package/dist/{io--MeYrWG7.d.ts → io-CvALf9NE.d.ts} +1 -1
- package/dist/list-tools-Dkyyje7j.cjs +48 -0
- package/dist/{list-tools-Dt-mhh52.js → list-tools-ePP9tXND.js} +9 -13
- package/dist/{list-tools-Bku6XkVc.d.ts → list-tools-qIvVJeWZ.d.ts} +3 -3
- package/dist/{list-tools-Cn7tpE0V.d.cts → list-tools-zagoI477.d.cts} +3 -3
- package/dist/{logger-BkMMCFTv.cjs → logger-CN1q-KNn.cjs} +6 -6
- package/dist/{logger-rGakBvJ1.js → logger-Ctv5xUSD.js} +6 -6
- package/dist/mcp-Cs4zfvSi.js +336 -0
- package/dist/mcp-DQ1Vdv4v.cjs +341 -0
- package/dist/{package-BdaLbzEh.cjs → package-C3YVYgxp.cjs} +1 -1
- package/dist/{package-dtiombMt.js → package-DzB_5Hur.js} +1 -1
- package/dist/protocols/mcp/index.cjs +1 -1
- package/dist/protocols/mcp/index.d.cts +3 -3
- package/dist/protocols/mcp/index.d.ts +3 -3
- package/dist/protocols/mcp/index.js +1 -1
- package/dist/protocols/oauth-metadata.cjs +12 -10
- package/dist/protocols/oauth-metadata.d.cts +2 -2
- package/dist/protocols/oauth-metadata.d.ts +2 -2
- package/dist/protocols/oauth-metadata.js +6 -4
- package/dist/protocols/rest/index.cjs +2 -2
- package/dist/protocols/rest/index.d.cts +4 -4
- package/dist/protocols/rest/index.d.ts +4 -4
- package/dist/protocols/rest/index.js +2 -2
- package/dist/{rest-Cit2862S.js → rest-DSclWpIt.js} +13 -12
- package/dist/{rest-1Pf9sYNU.cjs → rest-P6XZWP42.cjs} +28 -27
- package/dist/schema-CW1jKvio.js +65 -0
- package/dist/schema-D3vClxb7.cjs +76 -0
- package/dist/stacks/supabase/index.cjs +8 -8
- package/dist/stacks/supabase/index.d.cts +1 -1
- package/dist/stacks/supabase/index.d.ts +1 -1
- package/dist/stacks/supabase/index.js +6 -6
- package/dist/stacks/supabase/vite.cjs +1 -1
- package/dist/stacks/supabase/vite.d.cts +1 -1
- package/dist/stacks/supabase/vite.d.ts +1 -1
- package/dist/stacks/supabase/vite.js +1 -1
- package/dist/stacks/tanstack/index.cjs +8 -8
- package/dist/stacks/tanstack/index.d.cts +2 -2
- package/dist/stacks/tanstack/index.d.ts +2 -2
- package/dist/stacks/tanstack/index.js +5 -5
- package/dist/stacks/tanstack/vite.cjs +8 -4
- package/dist/stacks/tanstack/vite.d.cts +11 -1
- package/dist/stacks/tanstack/vite.d.ts +11 -1
- package/dist/stacks/tanstack/vite.js +8 -4
- package/dist/tool-result-CK6hXO43.cjs +89 -0
- package/dist/tool-result-DfCk445b.js +78 -0
- package/dist/{types-C0Wgm9zp.d.cts → types-D1cgsnPu.d.cts} +15 -7
- package/dist/{types-C0Wgm9zp.d.ts → types-D1cgsnPu.d.ts} +15 -7
- package/package.json +2 -1
- package/dist/content-D5LJAHyM.js +0 -12
- package/dist/content-NCYIGgOb.cjs +0 -17
- package/dist/list-tools-DiMCRunS.cjs +0 -52
- package/dist/mcp-DiVQhHIw.js +0 -148
- package/dist/mcp-ECo1Ixk8.cjs +0 -153
package/README.md
CHANGED
|
@@ -51,7 +51,13 @@ That's the whole authoring surface. No imperative server construction, no route
|
|
|
51
51
|
|
|
52
52
|
The table shows the default `path: "/mcp"`. The REST companions co-locate under the parent of the configured `path`, so `mcpPlugin({ path: "/api/public/mcp" })` emits them at `/api/public/.mcp/list-tools` and `/api/public/.mcp/invoke-tool/<tool>` (file: `src/routes/api/public/[.mcp]/...`). Pass `restRoutes: false` to drop them.
|
|
53
53
|
|
|
54
|
-
MCP (`POST <path>`, `/mcp` by default) is the public wire format clients speak directly. REST (the `.mcp/*` companions, co-located under the `path` parent) is internal RPC — only an upstream MCP proxy calls it; never a browser or a hand-rolled client. All runtime routes import the same `defineMcp` result, so they stay in sync on which tools exist and which auth
|
|
54
|
+
MCP (`POST <path>`, `/mcp` by default) is the public wire format clients speak directly. REST (the `.mcp/*` companions, co-located under the `path` parent) is internal RPC — only an upstream MCP proxy calls it; never a browser or a hand-rolled client. All runtime routes import the same `defineMcp` result, so they stay in sync on which tools exist and which auth and browser-origin policies protect them. If `defineMcp({ auth: ... })` is omitted, the handlers stay unauthenticated; if OAuth auth is configured, MCP and REST both require the proxy/client to pass `Authorization: Bearer <token>`. Rate limiting belongs at the app host or edge.
|
|
55
|
+
|
|
56
|
+
The MCP endpoint supports both protocol eras on one URL. Finalized `2026-07-28` clients send the version, client capabilities, and mirrored HTTP headers on every request; the server returns typed results plus cache metadata on discovery and list responses without initialization or protocol sessions. Initialization-era clients continue through a stateless compatibility path: `initialize`, `tools/list`, and `tools/call` work as independent requests, and the endpoint never mints `Mcp-Session-Id`. `GET` and `DELETE` return `405` because there is no standalone stream or session to manage.
|
|
57
|
+
|
|
58
|
+
`outputSchema` accepts either the existing object-shape shorthand (`{ value: z.string() }`) or a JSON-Schema-representable Zod schema such as `z.array(z.string())`; `structuredContent` may be any JSON value. The server returns the schema's parsed output, so coercions are reflected in the result; transforms with no truthful JSON Schema are rejected when `defineMcp` runs. Modern clients receive the natural schema and value. The compatibility path wraps non-object schemas and values under `result` for initialization-era clients whose wire format requires an object root.
|
|
59
|
+
|
|
60
|
+
Browser-origin checks apply to the MCP, REST companion, and OAuth metadata routes before authentication. Omit `allowedOrigins` to allow requests with no `Origin` header (Go, desktop, CLI, and server clients) plus same-origin browser requests. Set `allowedOrigins: ["https://client.example.com"]` for exact additional origins, or `allowedOrigins: "any"` to intentionally restore wildcard CORS. Exact and same origins are reflected with `Vary: Origin`; other browser origins receive `403` without an OAuth challenge. Reverse-proxy adapters compare against the effective public origin after applying only their configured trusted forwarded headers.
|
|
55
61
|
|
|
56
62
|
The OAuth metadata route is emitted by default and returns `404` until OAuth auth is configured. Disable it with `mcpPlugin({ protectedResourceMetadataRoute: false })` only if the app owns `/.well-known/oauth-protected-resource` itself.
|
|
57
63
|
|
|
@@ -211,7 +217,7 @@ Use `auth.oauth.issuer(...)`, set `resource` or `acceptedAudiences` to anchor th
|
|
|
211
217
|
|
|
212
218
|
## Debug logging
|
|
213
219
|
|
|
214
|
-
The runtime
|
|
220
|
+
The runtime logs operational events (tool invocations, auth outcomes, warnings, errors) at `info` by default. Raise the level to `debug` to trace OAuth discovery, JWKS resolution, and token verification when a deployed MCP server returns `401`/`500` and you need to see why. Logs go to `console` (so they land in your platform's function logs) and **never include the bearer token, full claims, or PII** — only non-secret fields such as the JWT header `alg`/`kid`, `issuer`, `jwks_uri`, the token `scopes`, and `jose` error codes. End-user identifiers (the token `sub`/`client_id`) appear only at `debug`, as the `oauth.verify.principal` event — the default `info` level never collects them.
|
|
215
221
|
|
|
216
222
|
Enable it either way:
|
|
217
223
|
|
|
@@ -222,17 +228,17 @@ setLogLevel("debug"); // "silent" | "error" | "warn" | "info" | "debug"
|
|
|
222
228
|
```
|
|
223
229
|
|
|
224
230
|
```bash
|
|
225
|
-
# Or via env var
|
|
226
|
-
#
|
|
227
|
-
#
|
|
231
|
+
# Or via env var. Node reads it at startup; on Deno and Cloudflare Workers the
|
|
232
|
+
# recorder applies it at the first request's telemetry emit, so logs before that
|
|
233
|
+
# point may not honor it — prefer `setLogLevel()` there.
|
|
228
234
|
LOVABLE_MCP_LOG_LEVEL=debug
|
|
229
235
|
```
|
|
230
236
|
|
|
231
|
-
A `500` on an OAuth-protected route is always one of two causes, logged at `error`: an `OAuthConfigurationError` from issuer-metadata discovery or a JWKS *fetch* failure (`oauth.discovery.config_error` / `oauth.jwks.fetch_failed` → `auth.config_error`), or a transport-level fault in the MCP handler (`mcp.transport_error`). Auth outcomes are logged at `info`: a granted request as `oauth.verify.ok`, and a
|
|
237
|
+
A `500` on an OAuth-protected route is always one of two causes, logged at `error`: an `OAuthConfigurationError` from issuer-metadata discovery or a JWKS *fetch* failure (`oauth.discovery.config_error` / `oauth.jwks.fetch_failed` → `auth.config_error`), or a transport-level fault in the MCP handler (`mcp.transport_error`). Auth outcomes are logged at `info`: a granted request as `oauth.verify.ok`, a request with no parseable bearer as `auth.no_bearer_token`, and a rejected token as `auth.token_rejected` (a `jose` rejection such as an expired token carries the `jose` error name and code; the full reason appears at `debug` as `oauth.verify.rejected`) — so a request rejected for insufficient scope shows both `oauth.verify.ok` and the `auth.token_rejected` that follows it. A sustained flood of `auth.token_rejected` whose `code` is `ERR_JOSE_NOT_SUPPORTED` or `ERR_JOSE_ALG_NOT_ALLOWED` (rather than `ERR_JWT_EXPIRED`) points at server-side key or algorithm configuration, not client error.
|
|
232
238
|
|
|
233
239
|
## Usage metrics
|
|
234
240
|
|
|
235
|
-
Each MCP server records per-invocation telemetry — tool name, JSON-RPC method, outcome (`
|
|
241
|
+
Each MCP server records per-invocation telemetry — tool name, JSON-RPC method, outcome, and latency — as **OTLP/HTTP JSON logs**, one awaited POST per record, bounded by a 3-second timeout; a failed or slow POST is logged (`metrics.failed`) and swallowed, never failing the request. Outcomes cover tool results (`ok`, `tool_error`, `handler_error`, `transport_error`) and the authorization gate: `auth_challenge` for an unauthenticated request answered with the `401` challenge (so probe traffic produces records), and the `auth_*` discovery/JWKS failure outcomes. No tool arguments or response payloads are ever captured. Emission is **on by default** and self-disables at runtime when `LOVABLE_API_KEY` is absent: records are dropped and one `metrics.disabled_no_api_key` warning is logged per process, so a local `vite dev` stays quiet. With `metrics: false` the recorder never runs — on Deno and Cloudflare Workers that also means `LOVABLE_MCP_LOG_LEVEL` is never applied (use `setLogLevel()` instead).
|
|
236
242
|
|
|
237
243
|
Configure it on the MCP definition:
|
|
238
244
|
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
//#region src/metrics/impl/base.d.ts
|
|
2
|
-
type InvocationOutcome = "ok" | "tool_error" | "handler_error" | "transport_error" | "auth_discovery_config_error" | "auth_discovery_unreachable" | "auth_jwks_config_error" | "auth_jwks_unreachable";
|
|
2
|
+
type InvocationOutcome = "ok" | "tool_error" | "handler_error" | "transport_error" | "auth_challenge" | "auth_discovery_config_error" | "auth_discovery_unreachable" | "auth_jwks_config_error" | "auth_jwks_unreachable";
|
|
3
3
|
/** One recorded MCP call. Carries no arguments or response payloads — only the
|
|
4
4
|
* tool name, the JSON-RPC method, the result class, timing, and byte sizes.
|
|
5
5
|
* `errorText` is the sole exception: it is logged locally for debugging and
|
|
6
6
|
* deliberately never included in the OTLP payload (see `emit` / `buildLogsPayload`). */
|
|
7
7
|
interface InvocationRecord {
|
|
8
|
-
/** Tool name for `tools/call`; `null` for list/initialize, transport faults, and
|
|
8
|
+
/** Tool name for `tools/call`; `null` for list/initialize, transport faults, and `authorize` records. */
|
|
9
9
|
tool: string | null;
|
|
10
|
-
/** JSON-RPC method, e.g. `tools/call`; `authorize` for
|
|
10
|
+
/** JSON-RPC method, e.g. `tools/call`; `authorize` for records from the authorization gate. */
|
|
11
11
|
method: string;
|
|
12
12
|
outcome: InvocationOutcome;
|
|
13
13
|
durationMs: number;
|
|
14
14
|
reqBytes?: number;
|
|
15
15
|
resBytes?: number;
|
|
16
|
+
/** HTTP request method. Set on `auth_challenge` so probe traffic (GET vs
|
|
17
|
+
* POST discovery) is distinguishable; absent on tool-call records. */
|
|
18
|
+
httpMethod?: string;
|
|
19
|
+
/** Wire surface that served the call: the modern stateless revision, the
|
|
20
|
+
* initialization-era path, or the REST companion. Absent on records emitted
|
|
21
|
+
* before protocol classification (auth gate, transport faults). */
|
|
22
|
+
protocol?: "2026-07-28" | "legacy" | "rest";
|
|
16
23
|
/** The app end-user that made the call (the access token `sub`), or
|
|
17
24
|
* `undefined` when the request is unauthenticated. Not a Lovable identity. */
|
|
18
25
|
endUserId?: string;
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
//#region src/metrics/impl/base.d.ts
|
|
2
|
-
type InvocationOutcome = "ok" | "tool_error" | "handler_error" | "transport_error" | "auth_discovery_config_error" | "auth_discovery_unreachable" | "auth_jwks_config_error" | "auth_jwks_unreachable";
|
|
2
|
+
type InvocationOutcome = "ok" | "tool_error" | "handler_error" | "transport_error" | "auth_challenge" | "auth_discovery_config_error" | "auth_discovery_unreachable" | "auth_jwks_config_error" | "auth_jwks_unreachable";
|
|
3
3
|
/** One recorded MCP call. Carries no arguments or response payloads — only the
|
|
4
4
|
* tool name, the JSON-RPC method, the result class, timing, and byte sizes.
|
|
5
5
|
* `errorText` is the sole exception: it is logged locally for debugging and
|
|
6
6
|
* deliberately never included in the OTLP payload (see `emit` / `buildLogsPayload`). */
|
|
7
7
|
interface InvocationRecord {
|
|
8
|
-
/** Tool name for `tools/call`; `null` for list/initialize, transport faults, and
|
|
8
|
+
/** Tool name for `tools/call`; `null` for list/initialize, transport faults, and `authorize` records. */
|
|
9
9
|
tool: string | null;
|
|
10
|
-
/** JSON-RPC method, e.g. `tools/call`; `authorize` for
|
|
10
|
+
/** JSON-RPC method, e.g. `tools/call`; `authorize` for records from the authorization gate. */
|
|
11
11
|
method: string;
|
|
12
12
|
outcome: InvocationOutcome;
|
|
13
13
|
durationMs: number;
|
|
14
14
|
reqBytes?: number;
|
|
15
15
|
resBytes?: number;
|
|
16
|
+
/** HTTP request method. Set on `auth_challenge` so probe traffic (GET vs
|
|
17
|
+
* POST discovery) is distinguishable; absent on tool-call records. */
|
|
18
|
+
httpMethod?: string;
|
|
19
|
+
/** Wire surface that served the call: the modern stateless revision, the
|
|
20
|
+
* initialization-era path, or the REST companion. Absent on records emitted
|
|
21
|
+
* before protocol classification (auth gate, transport faults). */
|
|
22
|
+
protocol?: "2026-07-28" | "legacy" | "rest";
|
|
16
23
|
/** The app end-user that made the call (the access token `sub`), or
|
|
17
24
|
* `undefined` when the request is unauthenticated. Not a Lovable identity. */
|
|
18
25
|
endUserId?: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_package = require("../package-
|
|
3
|
-
const require_list_tools = require("../list-tools-
|
|
2
|
+
const require_package = require("../package-C3YVYgxp.cjs");
|
|
3
|
+
const require_list_tools = require("../list-tools-Dkyyje7j.cjs");
|
|
4
4
|
const require_fs_errors = require("../fs-errors-CWNzOV75.cjs");
|
|
5
5
|
let node_fs = require("node:fs");
|
|
6
6
|
let node_path = require("node:path");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as version } from "../package-
|
|
3
|
-
import { t as buildMcpListing } from "../list-tools-
|
|
2
|
+
import { t as version } from "../package-DzB_5Hur.js";
|
|
3
|
+
import { t as buildMcpListing } from "../list-tools-ePP9tXND.js";
|
|
4
4
|
import { t as isFileMissing } from "../fs-errors-PA2t1TIp.js";
|
|
5
5
|
import { lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, resolve } from "node:path";
|
|
@@ -1,28 +1,7 @@
|
|
|
1
|
-
const require_logger = require("./logger-
|
|
2
|
-
const require_package = require("./package-
|
|
1
|
+
const require_logger = require("./logger-CN1q-KNn.cjs");
|
|
2
|
+
const require_package = require("./package-C3YVYgxp.cjs");
|
|
3
3
|
require("./metadata-path-zd7NSNoa.cjs");
|
|
4
4
|
let jose = require("jose");
|
|
5
|
-
//#region src/core/cors.ts
|
|
6
|
-
const EXPOSE_HEADERS = "WWW-Authenticate, Mcp-Session-Id, Mcp-Protocol-Version";
|
|
7
|
-
const ALLOW_HEADERS = "Authorization, Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID";
|
|
8
|
-
/** Add permissive CORS to a built response, preserving its other headers. */
|
|
9
|
-
function withCors(response) {
|
|
10
|
-
response.headers.set("Access-Control-Allow-Origin", "*");
|
|
11
|
-
response.headers.set("Access-Control-Expose-Headers", EXPOSE_HEADERS);
|
|
12
|
-
return response;
|
|
13
|
-
}
|
|
14
|
-
function corsPreflightResponse(allowMethods) {
|
|
15
|
-
return new Response(null, {
|
|
16
|
-
status: 204,
|
|
17
|
-
headers: {
|
|
18
|
-
"Access-Control-Allow-Origin": "*",
|
|
19
|
-
"Access-Control-Allow-Methods": allowMethods,
|
|
20
|
-
"Access-Control-Allow-Headers": ALLOW_HEADERS,
|
|
21
|
-
"Access-Control-Max-Age": "86400"
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
//#endregion
|
|
26
5
|
//#region src/core/http.ts
|
|
27
6
|
const JSON_HEADERS = { "Content-Type": "application/json" };
|
|
28
7
|
/** Strip the body from a built response, preserving status and headers, for HEAD. */
|
|
@@ -34,7 +13,7 @@ function headResponse(response) {
|
|
|
34
13
|
});
|
|
35
14
|
}
|
|
36
15
|
function finalizeWireResponse(request, response) {
|
|
37
|
-
return
|
|
16
|
+
return request.method === "HEAD" ? headResponse(response) : response;
|
|
38
17
|
}
|
|
39
18
|
function methodNotAllowed(allow) {
|
|
40
19
|
return new Response(JSON.stringify({ error: "method not allowed" }), {
|
|
@@ -73,6 +52,8 @@ function toLogRecord(rec) {
|
|
|
73
52
|
if (rec.tool !== null) attributes.push(strAttr("mcp.tool", rec.tool));
|
|
74
53
|
if (rec.reqBytes !== void 0) attributes.push(intAttr("mcp.request_size_bytes", rec.reqBytes));
|
|
75
54
|
if (rec.resBytes !== void 0) attributes.push(intAttr("mcp.response_size_bytes", rec.resBytes));
|
|
55
|
+
if (rec.httpMethod !== void 0) attributes.push(strAttr("http.request.method", rec.httpMethod));
|
|
56
|
+
if (rec.protocol !== void 0) attributes.push(strAttr("mcp.protocol", rec.protocol));
|
|
76
57
|
attributes.push(strAttr("mcp.stack", rec.stack));
|
|
77
58
|
if (rec.endUserId !== void 0) attributes.push(strAttr("mcp.end_user_id", rec.endUserId));
|
|
78
59
|
return {
|
|
@@ -120,6 +101,8 @@ function nowUnixNano() {
|
|
|
120
101
|
function nowMs() {
|
|
121
102
|
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
122
103
|
}
|
|
104
|
+
const METRICS_POST_TIMEOUT_MS = 3e3;
|
|
105
|
+
let warnedNoApiKey = false;
|
|
123
106
|
const NOOP_RECORDER = { async emit() {} };
|
|
124
107
|
function createNoopRecorder() {
|
|
125
108
|
return NOOP_RECORDER;
|
|
@@ -168,12 +151,13 @@ var BaseMetricRecorder = class {
|
|
|
168
151
|
}
|
|
169
152
|
async emit(ev) {
|
|
170
153
|
await this.applyLazyLogLevel();
|
|
171
|
-
require_logger.log.info("tool.invoked", {
|
|
154
|
+
if (ev.outcome !== "auth_challenge") require_logger.log.info("tool.invoked", {
|
|
172
155
|
tool: ev.tool,
|
|
173
156
|
method: ev.method,
|
|
174
157
|
outcome: ev.outcome,
|
|
175
158
|
durationMs: ev.durationMs,
|
|
176
159
|
stack: this.stack,
|
|
160
|
+
...ev.protocol !== void 0 && { protocol: ev.protocol },
|
|
177
161
|
...ev.errorText !== void 0 && { errorText: ev.errorText }
|
|
178
162
|
});
|
|
179
163
|
if (!this.config.enabled) return;
|
|
@@ -181,30 +165,34 @@ var BaseMetricRecorder = class {
|
|
|
181
165
|
require_logger.log.warn("metrics.disabled_no_fetch", { endpoint: this.config.endpoint });
|
|
182
166
|
return;
|
|
183
167
|
}
|
|
184
|
-
const headers = { ...this.baseHeaders };
|
|
185
|
-
if (this.usesLovableEndpoint) {
|
|
186
|
-
const key = await this.resolveApiKey();
|
|
187
|
-
if (!key) {
|
|
188
|
-
require_logger.log.warn("metrics.disabled_no_api_key", {
|
|
189
|
-
envVar: this.config.apiKeyEnvVar,
|
|
190
|
-
endpoint: this.config.endpoint
|
|
191
|
-
});
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
headers.authorization = `Bearer ${key}`;
|
|
195
|
-
}
|
|
196
|
-
const { errorText: _localOnly, ...wireEvent } = ev;
|
|
197
|
-
const body = buildLogsPayload([{
|
|
198
|
-
...wireEvent,
|
|
199
|
-
timeUnixNano: nowUnixNano(),
|
|
200
|
-
stack: this.stack
|
|
201
|
-
}], this.server);
|
|
202
168
|
try {
|
|
169
|
+
const headers = { ...this.baseHeaders };
|
|
170
|
+
if (this.usesLovableEndpoint) {
|
|
171
|
+
const key = await this.resolveApiKey();
|
|
172
|
+
if (!key) {
|
|
173
|
+
if (!warnedNoApiKey) {
|
|
174
|
+
warnedNoApiKey = true;
|
|
175
|
+
require_logger.log.warn("metrics.disabled_no_api_key", {
|
|
176
|
+
envVar: this.config.apiKeyEnvVar,
|
|
177
|
+
endpoint: this.config.endpoint
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
headers.authorization = `Bearer ${key}`;
|
|
183
|
+
}
|
|
184
|
+
const { errorText: _localOnly, ...wireEvent } = ev;
|
|
185
|
+
const body = buildLogsPayload([{
|
|
186
|
+
...wireEvent,
|
|
187
|
+
timeUnixNano: nowUnixNano(),
|
|
188
|
+
stack: this.stack
|
|
189
|
+
}], this.server);
|
|
203
190
|
const res = await this.doFetch(this.config.endpoint, {
|
|
204
191
|
method: "POST",
|
|
205
192
|
headers,
|
|
206
193
|
body,
|
|
207
|
-
keepalive: true
|
|
194
|
+
keepalive: true,
|
|
195
|
+
signal: AbortSignal.timeout(METRICS_POST_TIMEOUT_MS)
|
|
208
196
|
});
|
|
209
197
|
if (!res.ok) require_logger.log.warn("metrics.rejected", {
|
|
210
198
|
status: res.status,
|
|
@@ -295,8 +283,8 @@ var SupabaseMetricRecorder = class extends BaseMetricRecorder {
|
|
|
295
283
|
//#endregion
|
|
296
284
|
//#region src/metrics/recorder.ts
|
|
297
285
|
/** Vend the recorder for a stack, in-flight per request. Returns a no-op when
|
|
298
|
-
* metrics are disabled (`metrics: false`); otherwise the stack-specific class,
|
|
299
|
-
*
|
|
286
|
+
* metrics are disabled (`metrics: false`); otherwise the stack-specific class, which
|
|
287
|
+
* logs `tool.invoked` for tool invocations even if the key or endpoint is unavailable. */
|
|
300
288
|
function createRecorderForRuntime(mcp, opts) {
|
|
301
289
|
const config = require_logger.resolveMetricsConfig(mcp.metrics);
|
|
302
290
|
const server = {
|
|
@@ -593,6 +581,16 @@ async function fetchVerificationKeySet(jwksUri) {
|
|
|
593
581
|
throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`, "jwks");
|
|
594
582
|
}
|
|
595
583
|
}
|
|
584
|
+
function assertCompactJwsShape(token) {
|
|
585
|
+
const parts = token.split(".");
|
|
586
|
+
if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
|
|
587
|
+
require_logger.log.debug("oauth.verify.bad_shape", {
|
|
588
|
+
tokenLength: token.length,
|
|
589
|
+
outcome: "401 invalid_token"
|
|
590
|
+
});
|
|
591
|
+
throw new OAuthTokenError(401, "invalid_token", "Access token is not a JWT");
|
|
592
|
+
}
|
|
593
|
+
}
|
|
596
594
|
function assertAccessTokenTyp(token, allowed) {
|
|
597
595
|
let header;
|
|
598
596
|
try {
|
|
@@ -694,6 +692,8 @@ function buildMcpAuthContext(args) {
|
|
|
694
692
|
function createOAuthTokenVerifier(auth, discovery) {
|
|
695
693
|
const allowedTyps = auth.accessTokenTyp ?? DEFAULT_ACCESS_TOKEN_TYPS;
|
|
696
694
|
return async (token, request, options) => {
|
|
695
|
+
assertCompactJwsShape(token);
|
|
696
|
+
assertAccessTokenTyp(token, allowedTyps);
|
|
697
697
|
const resource = resolveProtectedResource(auth, request, options);
|
|
698
698
|
const issuer = await discovery.resolveIssuer();
|
|
699
699
|
const acceptedAudiences = resolveAcceptedAudiences(auth, resource);
|
|
@@ -705,9 +705,7 @@ function createOAuthTokenVerifier(auth, discovery) {
|
|
|
705
705
|
});
|
|
706
706
|
const jwksUri = await discovery.resolveJwksUri();
|
|
707
707
|
require_logger.log.debug("oauth.jwks.fetch", { jwksUri });
|
|
708
|
-
const
|
|
709
|
-
assertAccessTokenTyp(token, allowedTyps);
|
|
710
|
-
const claims = await verifyJwtClaims(token, keySet, issuer, auth);
|
|
708
|
+
const claims = await verifyJwtClaims(token, await fetchVerificationKeySet(jwksUri), issuer, auth);
|
|
711
709
|
const audienceVia = checkAcceptedAudience(claims, acceptedAudiences, auth);
|
|
712
710
|
assertNonEmptySubject(claims);
|
|
713
711
|
const context = buildMcpAuthContext({
|
|
@@ -719,11 +717,13 @@ function createOAuthTokenVerifier(auth, discovery) {
|
|
|
719
717
|
});
|
|
720
718
|
assertOAuthClientClaim(auth, context.principal.clientId);
|
|
721
719
|
require_logger.log.info("oauth.verify.ok", {
|
|
722
|
-
sub: context.principal.sub,
|
|
723
|
-
clientId: context.principal.clientId,
|
|
724
720
|
scopes: context.principal.scopes,
|
|
725
721
|
audienceVia
|
|
726
722
|
});
|
|
723
|
+
require_logger.log.debug("oauth.verify.principal", {
|
|
724
|
+
sub: context.principal.sub,
|
|
725
|
+
clientId: context.principal.clientId
|
|
726
|
+
});
|
|
727
727
|
return context;
|
|
728
728
|
};
|
|
729
729
|
}
|
|
@@ -776,6 +776,22 @@ function oauthUpstreamUnreachableResponse() {
|
|
|
776
776
|
}
|
|
777
777
|
});
|
|
778
778
|
}
|
|
779
|
+
const KNOWN_HTTP_METHODS = /* @__PURE__ */ new Set([
|
|
780
|
+
"GET",
|
|
781
|
+
"HEAD",
|
|
782
|
+
"POST",
|
|
783
|
+
"PUT",
|
|
784
|
+
"DELETE",
|
|
785
|
+
"PATCH",
|
|
786
|
+
"OPTIONS",
|
|
787
|
+
"TRACE",
|
|
788
|
+
"CONNECT",
|
|
789
|
+
"QUERY"
|
|
790
|
+
]);
|
|
791
|
+
function normalizeHttpMethod(method) {
|
|
792
|
+
const upper = method.toUpperCase();
|
|
793
|
+
return KNOWN_HTTP_METHODS.has(upper) ? upper : "_OTHER";
|
|
794
|
+
}
|
|
779
795
|
function parseBearerToken(request) {
|
|
780
796
|
const header = request.headers.get("Authorization");
|
|
781
797
|
if (!header) return void 0;
|
|
@@ -838,7 +854,18 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
838
854
|
const startedAt = nowMs();
|
|
839
855
|
const token = parseBearerToken(request);
|
|
840
856
|
if (!token) {
|
|
841
|
-
|
|
857
|
+
const httpMethod = normalizeHttpMethod(request.method);
|
|
858
|
+
require_logger.log.info("auth.no_bearer_token", {
|
|
859
|
+
httpMethod,
|
|
860
|
+
outcome: "401"
|
|
861
|
+
});
|
|
862
|
+
await recorder?.emit({
|
|
863
|
+
tool: null,
|
|
864
|
+
method: "authorize",
|
|
865
|
+
outcome: "auth_challenge",
|
|
866
|
+
httpMethod,
|
|
867
|
+
durationMs: nowMs() - startedAt
|
|
868
|
+
});
|
|
842
869
|
return {
|
|
843
870
|
ok: false,
|
|
844
871
|
response: challengeResponse(runtime.auth, request, runtime.options, 401)
|
|
@@ -896,7 +923,12 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
896
923
|
response: challengeResponse(runtime.auth, request, runtime.options, err.status, err.oauthError, err.message)
|
|
897
924
|
};
|
|
898
925
|
}
|
|
899
|
-
require_logger.log.
|
|
926
|
+
if (err instanceof jose.errors.JOSEError) require_logger.log.info("auth.token_rejected", {
|
|
927
|
+
...require_logger.describeError(err),
|
|
928
|
+
status: 401,
|
|
929
|
+
oauthError: "invalid_token"
|
|
930
|
+
});
|
|
931
|
+
else require_logger.log.error("auth.unexpected_error", {
|
|
900
932
|
...require_logger.describeError(err),
|
|
901
933
|
outcome: "401"
|
|
902
934
|
});
|
|
@@ -908,6 +940,69 @@ function createRequestAuthorizer(mcp, options = {}) {
|
|
|
908
940
|
} };
|
|
909
941
|
}
|
|
910
942
|
//#endregion
|
|
943
|
+
//#region src/core/cors.ts
|
|
944
|
+
const EXPOSE_HEADERS = "WWW-Authenticate, MCP-Protocol-Version, Mcp-Session-Id";
|
|
945
|
+
const ALLOW_HEADERS = "Authorization, Content-Type, MCP-Protocol-Version, Mcp-Method, Mcp-Name, Mcp-Session-Id, Last-Event-ID";
|
|
946
|
+
const MCP_PARAM_HEADER = /^mcp-param-[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
|
|
947
|
+
function appendVary(headers, value) {
|
|
948
|
+
const values = new Set((headers.get("Vary") ?? "").split(",").map((entry) => entry.trim()).filter(Boolean));
|
|
949
|
+
values.add(value);
|
|
950
|
+
headers.set("Vary", [...values].join(", "));
|
|
951
|
+
}
|
|
952
|
+
function addCorsHeaders(response, allowedOrigin, variesByOrigin) {
|
|
953
|
+
if (allowedOrigin !== void 0) {
|
|
954
|
+
response.headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
955
|
+
response.headers.set("Access-Control-Expose-Headers", EXPOSE_HEADERS);
|
|
956
|
+
}
|
|
957
|
+
if (variesByOrigin) appendVary(response.headers, "Origin");
|
|
958
|
+
return response;
|
|
959
|
+
}
|
|
960
|
+
function allowedRequestHeaders(request) {
|
|
961
|
+
const allowed = new Map(ALLOW_HEADERS.split(", ").map((header) => [header.toLowerCase(), header]));
|
|
962
|
+
for (const header of (request.headers.get("Access-Control-Request-Headers") ?? "").split(",")) {
|
|
963
|
+
const normalized = header.trim();
|
|
964
|
+
if (MCP_PARAM_HEADER.test(normalized)) allowed.set(normalized.toLowerCase(), normalized);
|
|
965
|
+
}
|
|
966
|
+
return [...allowed.values()].join(", ");
|
|
967
|
+
}
|
|
968
|
+
function evaluateCors(request, allowedOrigins) {
|
|
969
|
+
const origin = request.headers.get("Origin");
|
|
970
|
+
const allowAny = allowedOrigins === "any";
|
|
971
|
+
let allowedOrigin;
|
|
972
|
+
if (allowAny) allowedOrigin = "*";
|
|
973
|
+
else if (origin === null) allowedOrigin = void 0;
|
|
974
|
+
else if (origin === new URL(request.url).origin || allowedOrigins?.includes(origin)) allowedOrigin = origin;
|
|
975
|
+
else allowedOrigin = null;
|
|
976
|
+
if (allowedOrigin === null) {
|
|
977
|
+
const response = Response.json({ error: "origin not allowed" }, {
|
|
978
|
+
status: 403,
|
|
979
|
+
headers: { "Cache-Control": "no-store" }
|
|
980
|
+
});
|
|
981
|
+
appendVary(response.headers, "Origin");
|
|
982
|
+
return {
|
|
983
|
+
ok: false,
|
|
984
|
+
response
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
ok: true,
|
|
989
|
+
withHeaders: (response) => addCorsHeaders(response, allowedOrigin, !allowAny),
|
|
990
|
+
preflight: (allowMethods) => {
|
|
991
|
+
const requestedHeaders = request.headers.get("Access-Control-Request-Headers");
|
|
992
|
+
const response = new Response(null, {
|
|
993
|
+
status: 204,
|
|
994
|
+
headers: {
|
|
995
|
+
"Access-Control-Allow-Methods": allowMethods,
|
|
996
|
+
"Access-Control-Allow-Headers": allowedRequestHeaders(request),
|
|
997
|
+
"Access-Control-Max-Age": "86400"
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
if (requestedHeaders) appendVary(response.headers, "Access-Control-Request-Headers");
|
|
1001
|
+
return addCorsHeaders(response, allowedOrigin, !allowAny);
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
//#endregion
|
|
911
1006
|
Object.defineProperty(exports, "JSON_HEADERS", {
|
|
912
1007
|
enumerable: true,
|
|
913
1008
|
get: function() {
|
|
@@ -926,12 +1021,6 @@ Object.defineProperty(exports, "assertRestResourceBinding", {
|
|
|
926
1021
|
return assertRestResourceBinding;
|
|
927
1022
|
}
|
|
928
1023
|
});
|
|
929
|
-
Object.defineProperty(exports, "corsPreflightResponse", {
|
|
930
|
-
enumerable: true,
|
|
931
|
-
get: function() {
|
|
932
|
-
return corsPreflightResponse;
|
|
933
|
-
}
|
|
934
|
-
});
|
|
935
1024
|
Object.defineProperty(exports, "createNoopRecorder", {
|
|
936
1025
|
enumerable: true,
|
|
937
1026
|
get: function() {
|
|
@@ -950,6 +1039,12 @@ Object.defineProperty(exports, "createRequestAuthorizer", {
|
|
|
950
1039
|
return createRequestAuthorizer;
|
|
951
1040
|
}
|
|
952
1041
|
});
|
|
1042
|
+
Object.defineProperty(exports, "evaluateCors", {
|
|
1043
|
+
enumerable: true,
|
|
1044
|
+
get: function() {
|
|
1045
|
+
return evaluateCors;
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
953
1048
|
Object.defineProperty(exports, "finalizeWireResponse", {
|
|
954
1049
|
enumerable: true,
|
|
955
1050
|
get: function() {
|