@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.
Files changed (71) hide show
  1. package/README.md +13 -7
  2. package/dist/{authorize-Cs1yyLrq.d.ts → authorize-BjNE3LPr.d.ts} +1 -1
  3. package/dist/{authorize-BcNuNPtP.d.cts → authorize-BzZFS0zi.d.cts} +1 -1
  4. package/dist/{base-CReh7d3E.d.cts → base-DSH8NnPj.d.cts} +10 -3
  5. package/dist/{base-CReh7d3E.d.ts → base-DSH8NnPj.d.ts} +10 -3
  6. package/dist/cli/extract-manifest.cjs +2 -2
  7. package/dist/cli/extract-manifest.js +2 -2
  8. package/dist/{authorize-CB35c3Wg.cjs → cors-CmKzDN7k.cjs} +154 -59
  9. package/dist/{authorize-FOlzhxrM.js → cors-D4YbODs-.js} +150 -55
  10. package/dist/{forwarded-BlLF3dfc.js → forwarded-B3EBjHnA.js} +3 -3
  11. package/dist/{forwarded-Bhwcj22u.cjs → forwarded-BC_RHnn5.cjs} +3 -3
  12. package/dist/{gate-B9RSzvoI.cjs → gate-n_HX6eQ5.cjs} +6 -4
  13. package/dist/{gate-BeHmyGxk.js → gate-pGsVlY_4.js} +5 -3
  14. package/dist/index.cjs +38 -1
  15. package/dist/index.d.cts +3 -3
  16. package/dist/index.d.ts +3 -3
  17. package/dist/index.js +38 -1
  18. package/dist/{io-BzTGjG_c.d.cts → io-BEwvgN8s.d.cts} +1 -1
  19. package/dist/{io--MeYrWG7.d.ts → io-CvALf9NE.d.ts} +1 -1
  20. package/dist/list-tools-Dkyyje7j.cjs +48 -0
  21. package/dist/{list-tools-Dt-mhh52.js → list-tools-ePP9tXND.js} +9 -13
  22. package/dist/{list-tools-Bku6XkVc.d.ts → list-tools-qIvVJeWZ.d.ts} +3 -3
  23. package/dist/{list-tools-Cn7tpE0V.d.cts → list-tools-zagoI477.d.cts} +3 -3
  24. package/dist/{logger-BkMMCFTv.cjs → logger-CN1q-KNn.cjs} +6 -6
  25. package/dist/{logger-rGakBvJ1.js → logger-Ctv5xUSD.js} +6 -6
  26. package/dist/mcp-Cs4zfvSi.js +336 -0
  27. package/dist/mcp-DQ1Vdv4v.cjs +341 -0
  28. package/dist/{package-BdaLbzEh.cjs → package-C3YVYgxp.cjs} +1 -1
  29. package/dist/{package-dtiombMt.js → package-DzB_5Hur.js} +1 -1
  30. package/dist/protocols/mcp/index.cjs +1 -1
  31. package/dist/protocols/mcp/index.d.cts +3 -3
  32. package/dist/protocols/mcp/index.d.ts +3 -3
  33. package/dist/protocols/mcp/index.js +1 -1
  34. package/dist/protocols/oauth-metadata.cjs +12 -10
  35. package/dist/protocols/oauth-metadata.d.cts +2 -2
  36. package/dist/protocols/oauth-metadata.d.ts +2 -2
  37. package/dist/protocols/oauth-metadata.js +6 -4
  38. package/dist/protocols/rest/index.cjs +2 -2
  39. package/dist/protocols/rest/index.d.cts +4 -4
  40. package/dist/protocols/rest/index.d.ts +4 -4
  41. package/dist/protocols/rest/index.js +2 -2
  42. package/dist/{rest-Cit2862S.js → rest-DSclWpIt.js} +13 -12
  43. package/dist/{rest-1Pf9sYNU.cjs → rest-P6XZWP42.cjs} +28 -27
  44. package/dist/schema-CW1jKvio.js +65 -0
  45. package/dist/schema-D3vClxb7.cjs +76 -0
  46. package/dist/stacks/supabase/index.cjs +8 -8
  47. package/dist/stacks/supabase/index.d.cts +1 -1
  48. package/dist/stacks/supabase/index.d.ts +1 -1
  49. package/dist/stacks/supabase/index.js +6 -6
  50. package/dist/stacks/supabase/vite.cjs +1 -1
  51. package/dist/stacks/supabase/vite.d.cts +1 -1
  52. package/dist/stacks/supabase/vite.d.ts +1 -1
  53. package/dist/stacks/supabase/vite.js +1 -1
  54. package/dist/stacks/tanstack/index.cjs +8 -8
  55. package/dist/stacks/tanstack/index.d.cts +2 -2
  56. package/dist/stacks/tanstack/index.d.ts +2 -2
  57. package/dist/stacks/tanstack/index.js +5 -5
  58. package/dist/stacks/tanstack/vite.cjs +8 -4
  59. package/dist/stacks/tanstack/vite.d.cts +11 -1
  60. package/dist/stacks/tanstack/vite.d.ts +11 -1
  61. package/dist/stacks/tanstack/vite.js +8 -4
  62. package/dist/tool-result-CK6hXO43.cjs +89 -0
  63. package/dist/tool-result-DfCk445b.js +78 -0
  64. package/dist/{types-C0Wgm9zp.d.cts → types-D1cgsnPu.d.cts} +15 -7
  65. package/dist/{types-C0Wgm9zp.d.ts → types-D1cgsnPu.d.ts} +15 -7
  66. package/package.json +2 -1
  67. package/dist/content-D5LJAHyM.js +0 -12
  68. package/dist/content-NCYIGgOb.cjs +0 -17
  69. package/dist/list-tools-DiMCRunS.cjs +0 -52
  70. package/dist/mcp-DiVQhHIw.js +0 -148
  71. 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 policy protects 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>`. CORS and rate limiting still belong at the app host or edge.
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 is silent by default. Raise the log level 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 `sub`/`client_id`/`scopes`, and `jose` error codes.
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, read once at startup. Node/Deno only: it reads `process.env`,
226
- # which is absent on a deployed Cloudflare Worker (env arrives via bindings), so
227
- # the var is silently ignored there use `setLogLevel()` on Workers.
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 itprefer `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 `401`/`403` as `auth.token_rejected` (with the `jose` reason) or `auth.no_bearer_token` — so a request rejected for insufficient scope shows both `oauth.verify.ok` and the `auth.token_rejected` that follows it.
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 (`ok` / `handler_error` / `transport_error`), and latency and POSTs it in fire-and-forget batches as **OTLP/HTTP JSON logs**. No tool arguments or response payloads are ever captured. Emission is **on by default**, self-disables at runtime when `LOVABLE_API_KEY` is absent (so a local `vite dev` stays silent), and degrades silentlya failed flush never blocks or fails a tool call.
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,4 +1,4 @@
1
- import "./types-C0Wgm9zp.js";
1
+ import "./types-D1cgsnPu.js";
2
2
  //#region src/auth/authorize.d.ts
3
3
  interface McpRuntimeOptions {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import "./types-C0Wgm9zp.cjs";
1
+ import "./types-D1cgsnPu.cjs";
2
2
  //#region src/auth/authorize.d.ts
3
3
  interface McpRuntimeOptions {
4
4
  /**
@@ -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 auth-config failures. */
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 auth-config failures. */
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 auth-config failures. */
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 auth-config failures. */
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-BdaLbzEh.cjs");
3
- const require_list_tools = require("../list-tools-DiMCRunS.cjs");
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-dtiombMt.js";
3
- import { t as buildMcpListing } from "../list-tools-Dt-mhh52.js";
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-BkMMCFTv.cjs");
2
- const require_package = require("./package-BdaLbzEh.cjs");
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 withCors(request.method === "HEAD" ? headResponse(response) : response);
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
- * which still logs `tool.invoked` even if the key or endpoint is unavailable. */
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 keySet = await fetchVerificationKeySet(jwksUri);
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
- require_logger.log.info("auth.no_bearer_token", { outcome: "401" });
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.error("auth.unexpected_error", {
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() {