@lovable.dev/mcp-js 0.29.0-rc.1 → 0.29.0-rc.2

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 (65) hide show
  1. package/README.internal.md +22 -31
  2. package/README.md +23 -13
  3. package/dist/{authorize-BjNE3LPr.d.ts → authorize-Bpz8oxjF.d.ts} +2 -5
  4. package/dist/{authorize-BzZFS0zi.d.cts → authorize-Cqr7Jps0.d.cts} +2 -5
  5. package/dist/cli/extract-manifest.cjs +25 -3
  6. package/dist/cli/extract-manifest.js +24 -2
  7. package/dist/{cors-BDTQQJE2.js → cors-C_VffY86.js} +15 -21
  8. package/dist/{cors-BRv5KXKQ.cjs → cors-DNzLB6qa.cjs} +14 -26
  9. package/dist/{errors-DSe96Ffd.js → errors-Bwd1L0Rf.js} +23 -8
  10. package/dist/{errors-CN6Da8iz.cjs → errors-Cr95rSkB.cjs} +23 -8
  11. package/dist/index.cjs +1 -1
  12. package/dist/index.d.cts +2 -2
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.js +1 -1
  15. package/dist/{io-BYUyNFdn.d.ts → io-Bh_WpbxU.d.cts} +1 -1
  16. package/dist/{io-B_YLNWDL.d.cts → io-DrHivYt0.d.ts} +1 -1
  17. package/dist/mcp-Bl7pQYdv.js +346 -0
  18. package/dist/mcp-CRdT2Fai.cjs +351 -0
  19. package/dist/{package-BI5r4uGM.js → package-CYA-5C24.js} +1 -1
  20. package/dist/{package-ClCIqol1.cjs → package-DNSwCau1.cjs} +1 -1
  21. package/dist/protocols/mcp/index.cjs +1 -1
  22. package/dist/protocols/mcp/index.d.cts +36 -3
  23. package/dist/protocols/mcp/index.d.ts +36 -3
  24. package/dist/protocols/mcp/index.js +1 -1
  25. package/dist/protocols/oauth-metadata.cjs +1 -1
  26. package/dist/protocols/oauth-metadata.d.cts +2 -2
  27. package/dist/protocols/oauth-metadata.d.ts +2 -2
  28. package/dist/protocols/oauth-metadata.js +1 -1
  29. package/dist/stacks/supabase/index.cjs +6 -35
  30. package/dist/stacks/supabase/index.d.cts +1 -4
  31. package/dist/stacks/supabase/index.d.ts +1 -4
  32. package/dist/stacks/supabase/index.js +6 -35
  33. package/dist/stacks/supabase/vite.cjs +1 -1
  34. package/dist/stacks/supabase/vite.d.cts +1 -1
  35. package/dist/stacks/supabase/vite.d.ts +1 -1
  36. package/dist/stacks/supabase/vite.js +1 -1
  37. package/dist/stacks/tanstack/index.cjs +2 -23
  38. package/dist/stacks/tanstack/index.d.cts +3 -20
  39. package/dist/stacks/tanstack/index.d.ts +3 -20
  40. package/dist/stacks/tanstack/index.js +3 -22
  41. package/dist/stacks/tanstack/vite.cjs +5 -33
  42. package/dist/stacks/tanstack/vite.d.cts +5 -28
  43. package/dist/stacks/tanstack/vite.d.ts +5 -28
  44. package/dist/stacks/tanstack/vite.js +5 -33
  45. package/dist/{types-D1cgsnPu.d.cts → types-DnGEKnvI.d.cts} +43 -20
  46. package/dist/{types-D1cgsnPu.d.ts → types-DnGEKnvI.d.ts} +43 -20
  47. package/package.json +4 -11
  48. package/dist/base-C8osWdMU.d.cts +0 -35
  49. package/dist/base-C8osWdMU.d.ts +0 -35
  50. package/dist/gate-CAJSRt_t.cjs +0 -26
  51. package/dist/gate-uCVrbDBb.js +0 -21
  52. package/dist/list-tools-C1OiTDR9.js +0 -37
  53. package/dist/list-tools-C3AoI2Zw.d.cts +0 -8
  54. package/dist/list-tools-DES10avA.cjs +0 -48
  55. package/dist/list-tools-DXqSPvNY.d.ts +0 -8
  56. package/dist/mcp-C9B7o75m.cjs +0 -345
  57. package/dist/mcp-HDkUr85V.js +0 -340
  58. package/dist/protocols/rest/index.cjs +0 -5
  59. package/dist/protocols/rest/index.d.cts +0 -9
  60. package/dist/protocols/rest/index.d.ts +0 -9
  61. package/dist/protocols/rest/index.js +0 -3
  62. package/dist/rest-BBdfHMGh.cjs +0 -134
  63. package/dist/rest-Cr7NhxL3.js +0 -129
  64. package/dist/tool-result-CK6hXO43.cjs +0 -89
  65. package/dist/tool-result-DfCk445b.js +0 -78
@@ -4,10 +4,10 @@ Architecture rationale, source layout, and dev scripts for contributors. The use
4
4
 
5
5
  ## Folder meaning
6
6
 
7
- - **`protocols/`** is the wire layer — one folder per externally observable HTTP surface. `protocols/mcp` is the public MCP-over-HTTP surface; `protocols/oauth-metadata` serves the RFC 9728 protected-resource metadata required to bootstrap protected MCP clients; `protocols/rest` is internal RPC for the upstream MCP proxy. Every backend wires both MCP and REST, plus the metadata endpoint when OAuth is configured.
8
- - **`auth/`** (unpublished) is the cross-cutting OAuth middleware — bearer verification, issuer/JWKS discovery, the `auth.oauth.*` config namespace. It depends only on `core/`; both `protocols/mcp` and `protocols/rest` depend on it for the shared bearer gate, so it isn't a wire-format peer of `mcp`/`rest` and doesn't live under `protocols/`.
9
- - **`stacks/`** is the framework integration. Two are published: `stacks/tanstack/` (file-router routes emitted by a Vite plugin) and `stacks/supabase/` (a single Deno-served function with URL-suffix dispatch, emitted by the Supabase Vite plugin). Future entries (classic Vite, Cloudflare Workers, …) live alongside them. Each stack forwards to `protocols/{mcp,oauth-metadata,rest}` directly — protocol logic stays shared, only ctx unwrapping and route emission differ.
10
- - **`manifest/`** is the stack-agnostic snapshot writer. `lovable-mcp-extract-manifest` is one CLI for every stack: it asks Vite for the resolved config, finds any plugin whose name matches `@lovable.dev/mcp-js*`, and reads `mcpEntry` + `urlPath` off its `api`. TanStack publishes its configured route path (`/mcp`); Supabase publishes `/functions/v1/<functionName>`. The projection (`extract.ts`) is pure and shares `buildMcpListing` with the live `GET /.mcp/list-tools` route so the committed manifest and the served catalog can't drift.
7
+ - **`protocols/`** is the wire layer — one folder per externally observable HTTP surface. `protocols/mcp` is the public MCP-over-HTTP surface; `protocols/oauth-metadata` serves the RFC 9728 protected-resource metadata required to bootstrap protected MCP clients. `protocols/gate.ts` is the shared CORS auth method gate both surfaces run behind.
8
+ - **`auth/`** (unpublished) is the cross-cutting OAuth middleware — bearer verification, issuer/JWKS discovery, the `auth.oauth.*` config namespace. It depends only on `core/`; `protocols/mcp` depends on it for the bearer gate, so it isn't a wire-format peer of `mcp` and doesn't live under `protocols/`.
9
+ - **`stacks/`** is the framework integration. Two are published: `stacks/tanstack/` (file-router routes emitted by a Vite plugin) and `stacks/supabase/` (a single Deno-served function with URL-suffix dispatch, emitted by the Supabase Vite plugin). Future entries (classic Vite, Cloudflare Workers, …) live alongside them. Each stack forwards to `protocols/{mcp,oauth-metadata}` directly — protocol logic stays shared, only ctx unwrapping and route emission differ.
10
+ - **`manifest/`** is the stack-agnostic snapshot writer. `lovable-mcp-extract-manifest` is one CLI for every stack: it asks Vite for the resolved config, finds any plugin whose name matches `@lovable.dev/mcp-js*`, and reads `mcpEntry` + `urlPath` off its `api`. TanStack publishes its configured route path (`/mcp`); Supabase publishes `/functions/v1/<functionName>`. The projection (`extract.ts` + `listing.ts`) is pure and derives from the same `defineMcp` definition the live `tools/list` serves, so the committed manifest and the served catalog can't drift.
11
11
 
12
12
  ## Design decisions
13
13
 
@@ -27,29 +27,22 @@ TanStack's file-based router needs a route file on disk to register a URL. If we
27
27
 
28
28
  This is the same insight that drives shipping an SDK instead of having the agent author the MCP runtime: bug fixes ship to all apps on the next build, not per-app.
29
29
 
30
- ### 3. The `[.mcp]` URL prefix uses bracket escaping
30
+ ### 3. Dot-prefixed URL paths use bracket escaping
31
31
 
32
- TanStack's file routing maps filenames to URLs and filters dot-prefixed entries as hidden — both directories (`src/routes/.mcp/...`) and flat files (`.mcp.list-tools.ts`). The escape is bracket-quoted literal segments: `[.mcp]/list-tools.ts` maps to `/.mcp/list-tools` and shows up in the generated route tree. The plugin emits at `src/routes/[.mcp]/list-tools.ts` and `src/routes/[.mcp]/invoke-tool/$tool.ts`.
32
+ TanStack's file routing maps filenames to URLs and filters dot-prefixed entries as hidden. The escape is bracket-quoted literal segments: the OAuth metadata route is emitted at `src/routes/[.well-known]/oauth-protected-resource.ts`, which maps to `/.well-known/oauth-protected-resource` and shows up in the generated route tree.
33
33
 
34
- ### 4. `invoke-tool/<tool>` instead of `<tool>` directly
34
+ ### 4. Dynamic tool resolution at runtime
35
35
 
36
- The `invoke-tool/` segment scopes the user-defined tool namespace, so future endpoints under `/.mcp/` (`list-tools`, `health`, `audit-log`, anything else added) can't collide with a user-defined tool named the same thing. Cheap upfront, breaking to add later.
37
-
38
- ### 5. Dynamic tool resolution at runtime
39
-
40
- The dispatcher resolves the tool name against the live `mcp.tools` array on every request. From the caller's perspective `POST /.mcp/invoke-tool/echo` and `POST /.mcp/invoke-tool/add` look like separate endpoints; from the author's perspective the source of truth is one array. **Tool resolution is a per-request operation, not a per-build operation.**
36
+ Every request builds a fresh `McpServer` from the live `mcp.tools` array; the source of truth is one array. **Tool registration is a per-request operation, not a per-build operation.**
41
37
 
42
38
  This is architectural foundation, not convenience. Resolution stays inside app code so it can grow:
43
39
 
44
- - **Per-user / per-scope tool visibility.** The lookup that maps `params.tool` to a handler can run *inside* app code, with access to the request's authenticated identity. An admin user might see `purge_workspace`; a regular user wouldn't see it in `list-tools` and would get a 404 from `invoke-tool`. Plan-tier gating, beta cohorts, feature flags, workspace roles — all want runtime context to decide what to expose.
45
- - **The build doesn't have user context.** Emitting per-tool files at build time would lock the tool catalog to the deploy. Dynamic resolution leaves that decision to the request lifecycle.
46
- - **The MCP and REST surfaces stay in sync automatically.** `tools/list` (over MCP) and `GET /.mcp/list-tools` (over REST) both read the same array. When per-user resolution lands, both surfaces filter through the same hook.
47
-
48
- `list-tools` stays a separate handler (not auto-injected into the dispatcher) because it's naturally a `GET` — distinct HTTP semantics from `invoke-tool`'s `POST`. The per-user filter will hook into both, but the HTTP shape stays clean.
40
+ - **Per-user / per-scope tool visibility.** Registration runs *inside* app code, with access to the request's authenticated identity. An admin user might see `purge_workspace`; a regular user wouldn't see it in `tools/list` and couldn't call it. Plan-tier gating, beta cohorts, feature flags, workspace roles — all want runtime context to decide what to expose.
41
+ - **The build doesn't have user context.** Baking the catalog in at build time would lock it to the deploy. Per-request registration leaves that decision to the request lifecycle.
49
42
 
50
43
  **Planned:** delegate the per-request tool-resolution step to app code via a hook (something like `resolveTools(ctx)` returning the subset of `mcp.tools` the caller can see). Until then, every authenticated caller sees the full array.
51
44
 
52
- ### 6. Type-erased `AnyToolDefinition` at the array boundary
45
+ ### 5. Type-erased `AnyToolDefinition` at the array boundary
53
46
 
54
47
  `ToolDefinition<TInput>` is generic — the handler's args are inferred from `inputSchema`:
55
48
 
@@ -63,7 +56,7 @@ This works per-tool at the `defineTool` call site. It breaks at the `defineMcp({
63
56
 
64
57
  `AnyToolDefinition` is the non-generic version used only at the array boundary, with `handler: (args: any) => ...`. `any` is bivariant in TypeScript, so any concrete handler assigns. Per-arg typing still happens inside `defineTool` — the only place it materially matters.
65
58
 
66
- ### 7. Title, description, and instructions are required
59
+ ### 6. Title, description, and instructions are required
67
60
 
68
61
  The MCP spec promoted `title` to a top-level field on `Tool` and `Server` in 2025-06-18+; we require it. We also require `description` on tools and `instructions` on servers because:
69
62
 
@@ -73,7 +66,7 @@ The MCP spec promoted `title` to a top-level field on `Tool` and `Server` in 202
73
66
 
74
67
  The MCP spec's legacy `annotations.title` is omitted from our `ToolAnnotations` type — use the top-level `title` instead. Two locations for the same field encourage drift.
75
68
 
76
- ### 8. Decoupled type surface from `@modelcontextprotocol/sdk`
69
+ ### 7. Decoupled type surface from `@modelcontextprotocol/sdk`
77
70
 
78
71
  Two reasons:
79
72
 
@@ -86,9 +79,9 @@ The published `.d.ts` files import only from `zod` and `vite`. Three SDK concern
86
79
  - **`ZodRawShape`, `ZodType`, `infer`** — sourced directly from `zod` (a peer dep). zod v3 and v4 both export all three at the top level; `ZodType` is non-deprecated in both versions with safe generic defaults. We re-export them from `core/types.ts` as `ZodRawShape` / `ZodSchema` / `ShapeOutput` (`ZodSchema` is a no-generic alias for `ZodType`). No dependency on `@modelcontextprotocol/sdk/server/zod-compat.js` types.
87
80
  - **`ContentBlock` union** — `TextContent | ImageContent | AudioContent | EmbeddedResource | ResourceLink`, structurally matching MCP's wire format. Consumers can type custom content (`return { content: [<ImageContent>, <TextContent>] }`) without referencing the SDK.
88
81
 
89
- The **runtime** still hands off to the MCP SDKwe still `import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"` and call `registerTool`, and we still use `objectFromShape` + `safeParseAsync` + `toJsonSchemaCompat` from the SDK's zod-compat module for REST input validation and JSON-Schema serialization. That's a runtime dependency, not a type-surface dependency.
82
+ The **runtime** still hands off to the MCP SDKs`createMcpHandler` + `McpServer` come from `@modelcontextprotocol/server` (v2), and `objectFromShape` + `safeParseAsync` + `toJsonSchemaCompat` come from `@modelcontextprotocol/sdk`'s zod-compat modules for input validation and JSON-Schema serialization. Those are runtime dependencies, not type-surface dependencies.
90
83
 
91
- ### 9. Stale-route cleanup on every regen
84
+ ### 8. Stale-route cleanup on every regen
92
85
 
93
86
  The `GENERATED_BANNER` is the SDK's ownership token. Cleanup walks the whole `routesDir`, and any `.ts` file that carries the banner but is no longer in the current emit set gets unlinked. Catches two real cases:
94
87
 
@@ -123,17 +116,16 @@ src/
123
116
  resource.ts # protected-resource URL resolution
124
117
  metadata-path.ts # shared RFC 9728 metadata path constant
125
118
  types.ts # auth config + context types
119
+ metrics/ # OTLP metrics recorder (unpublished); config, recorder, exporters under impl/
126
120
  protocols/
121
+ gate.ts # shared CORS → auth → method gate
127
122
  oauth-metadata.ts # createOAuthProtectedResourceMetadataHandler (RFC 9728 wire surface)
128
123
  mcp/
129
124
  protocol.ts # createMcpProtocolHandler (Web-Standard)
130
125
  index.ts # barrel
131
- rest/
132
- list-tools.ts # createListToolsHandler (Web-Standard, GET)
133
- invoke-tool.ts # createInvokeToolHandler (Web-Standard, POST)
134
- index.ts # barrel
135
126
  manifest/ # stack-agnostic manifest extraction (TanStack + Supabase both feed it)
136
127
  extract.ts # project a defineMcp result to the manifest (types + manifestFromDefinition)
128
+ listing.ts # buildMcpListing: the tool-catalog projection embedded in the manifest
137
129
  io.ts # runExtract/syncManifest: read any @lovable.dev/mcp-js* plugin's api + write .lovable/mcp/manifest.json
138
130
  ssr-loader.ts # produceViaSsr: load the entry via a throwaway Vite SSR server
139
131
  cli/
@@ -149,12 +141,11 @@ src/
149
141
  vite.ts # mcpPlugin (Vite-lifecycle trigger; exposes McpPluginApi with urlPath=/functions/v1/<functionName>)
150
142
  index.ts # barrel
151
143
  tests/
152
- core/{define,promise,url}.test.ts
153
- auth/{claims,config,context,oauth}.test.ts # OAuth config + bearer verification
144
+ core/{content,cors,define,errors,forwarded,logger,promise,supabase-config,url}.test.ts
145
+ auth/{claims,config,context,oauth,oauth-fallback,oauth-logging,oauth-metrics}.test.ts # OAuth config + bearer verification
146
+ metrics/{config,dispatch,otlp,recorder}.test.ts
154
147
  protocols/
155
148
  mcp/protocol.test.ts
156
- rest/{list-tools,invoke-tool}.test.ts
157
- parity.test.ts # REST↔MCP equivalence contract
158
149
  manifest/{extract,io,ssr-loader,run-extract}.test.ts # stack-agnostic extractor (run-extract covers TanStack + Supabase)
159
150
  stacks/
160
151
  tanstack/{handlers,vite}.test.ts
@@ -191,7 +182,7 @@ pnpm example:supabase:install # install example/supabase deps
191
182
 
192
183
  **Integration tests** live under `tests/integration/` and run against a live HTTP server. `pnpm test:integration:tanstack` builds the SDK, installs `example/tanstack`, boots its dev server on port 8080 via a Vitest `globalSetup`, runs the suite, and tears the server down. If the server is already running at `MCP_BASE_URL` (default `http://localhost:8080`), the setup reuses it — useful when iterating on the suite. See `CHANGELOG.md` for version history.
193
184
 
194
- **OAuth integration test** (`tests/integration/oauth.test.ts`) is hermetic: `pnpm test:integration:oauth` builds + installs `example/tanstack-supabase-oauth`, then a self-contained `beforeAll` starts a mock RS256 issuer (JWKS + Supabase userinfo/PostgREST stubs), boots the example pointed at it on port 8081, signs a local test JWT, and asserts metadata, bearer challenges, and authorized REST + MCP calls. It runs in its own `integration-oauth` Vitest project (no shared `globalSetup`).
185
+ **OAuth integration test** (`tests/integration/oauth.test.ts`) is hermetic: `pnpm test:integration:oauth` builds + installs `example/tanstack-supabase-oauth`, then a self-contained `beforeAll` starts a mock RS256 issuer (JWKS + Supabase userinfo/PostgREST stubs), boots the example pointed at it on port 8081, signs a local test JWT, and asserts metadata, bearer challenges, and authorized MCP calls. It runs in its own `integration-oauth` Vitest project (no shared `globalSetup`).
195
186
 
196
187
  **OAuth local loop** lives under `example/tanstack-supabase-oauth`. `pnpm example:oauth:dev` uses that example's checked-in `.env`, which points at the shared Lovable/Supabase test project; `pnpm example:oauth:smoke` verifies metadata/challenge behavior against a running example and, with `MCP_ACCESS_TOKEN`, authenticated calls.
197
188
 
package/README.md CHANGED
@@ -46,18 +46,16 @@ That's the whole authoring surface. No imperative server construction, no route
46
46
  | -------------------------------- | ------------------------------------------ | ---------------------------------------- |
47
47
  | `POST /mcp` | `src/routes/mcp.ts` | Full MCP streamable-HTTP protocol |
48
48
  | `GET /.well-known/oauth-protected-resource` | `src/routes/[.well-known]/oauth-protected-resource.ts` | OAuth protected-resource metadata |
49
- | `GET /.mcp/list-tools` | `src/routes/[.mcp]/list-tools.ts` | Tool catalog with JSON Schemas |
50
- | `POST /.mcp/invoke-tool/<tool>` | `src/routes/[.mcp]/invoke-tool/$tool.ts` | REST dispatcher; one handler call per tool |
51
49
 
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.
50
+ The table shows the default `path: "/mcp"`; `mcpPlugin({ path: "/api/public/mcp" })` moves the MCP route accordingly.
53
51
 
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.
52
+ MCP (`POST <path>`, `/mcp` by default) is the wire format clients speak directly. 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, clients must pass `Authorization: Bearer <token>`. Rate limiting belongs at the app host or edge.
55
53
 
56
54
  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
55
 
58
56
  `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
57
 
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.
58
+ Browser-origin checks apply to the MCP 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.
61
59
 
62
60
  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.
63
61
 
@@ -65,7 +63,7 @@ To serve the metadata from a different path — e.g. a workspace-private project
65
63
 
66
64
  ### `.lovable/mcp/manifest.json`
67
65
 
68
- `.lovable/mcp/manifest.json` is a snapshot the Lovable platform reads to register the MCP server. Envelope fields: `version` (manifest schema version), `sdk_version` (the `@lovable.dev/mcp-js` release that wrote the snapshot), `path`, and `auth`. `auth` is the server's auth configuration, lifted into the envelope (not into `mcp`): `{ "type": "none" }`, or `{ "type": "oauth", ... }` mirroring the `defineMcp({ auth })` config (snake_case — `issuer`, `accepted_audiences`, `required_scopes`, `resource`, …). The `mcp` field is **exactly the `GET /.mcp/list-tools` body** — `server` plus the tool catalog (`name`/`title`/`description`/`annotations` and JSON-Schema `inputSchema`/`outputSchema`) — so the committed snapshot can't drift from what the live route serves. It's produced by loading the entry and reading the catalog off the `defineMcp` result, so the manifest reflects exactly what the server exposes, including tools built programmatically (spreads, computed names, tools from npm). **The `lovable-mcp-extract-manifest` CLI writes it** (loading the entry through Vite's SSR module loader, so it works under Node/Bun), and removes it when the entry is deleted. The Lovable platform runs the CLI in its commit pipeline; the Vite plugin only generates routes. Commit it: the platform reads the committed file.
66
+ `.lovable/mcp/manifest.json` is a snapshot the Lovable platform reads to register the MCP server. Envelope fields: `version` (manifest schema version), `sdk_version` (the `@lovable.dev/mcp-js` release that wrote the snapshot), `path`, and `auth`. `auth` is the server's auth configuration, lifted into the envelope (not into `mcp`): `{ "type": "none" }`, or `{ "type": "oauth", ... }` mirroring the `defineMcp({ auth })` config (snake_case — `issuer`, `accepted_audiences`, `required_scopes`, `resource`, …). The `mcp` field is the server listing — `server` plus the tool catalog (`name`/`title`/`description`/`annotations` and JSON-Schema `inputSchema`/`outputSchema`) — the same projection the live MCP endpoint serves from `tools/list`. It's produced by loading the entry and reading the catalog off the `defineMcp` result, so the manifest reflects exactly what the server exposes, including tools built programmatically (spreads, computed names, tools from npm). **The `lovable-mcp-extract-manifest` CLI writes it** (loading the entry through Vite's SSR module loader, so it works under Node/Bun), and removes it when the entry is deleted. The Lovable platform runs the CLI in its commit pipeline; the Vite plugin only generates routes. Commit it: the platform reads the committed file.
69
67
 
70
68
  Three caveats:
71
69
 
@@ -168,6 +166,23 @@ and no `node:async_hooks` dependency, so it works on any runtime and is safe
168
166
  under concurrency by construction. A handler that hands `ctx` to deferred work (a
169
167
  detached timer, an un-awaited promise) keeps full access to it.
170
168
 
169
+ Beyond auth, `ToolContext` carries the per-request runtime surface:
170
+
171
+ | Member | Purpose |
172
+ | --- | --- |
173
+ | `signal` | An `AbortSignal` that aborts when the client cancels the request or disconnects. Pass it to `fetch` and other cancelable APIs, or check `signal.aborted` between steps of long work. |
174
+ | `client` | What the calling client declared about itself (`McpClientInfo`): `protocol` (`"2026-07-28"` or `"legacy"`), and for modern requests the envelope's `info`, `capabilities`, and `extensions`. Unverified client input — use it to adapt output, never to authorize. |
175
+ | `progress(update)` | Sends a `notifications/progress` frame (`{ progress, total?, message? }`) for long-running tools. A no-op unless the client requested progress by sending `_meta.progressToken`; safe to call unconditionally. |
176
+
177
+ ```ts
178
+ handler: async ({ url }, ctx) => {
179
+ await ctx.progress({ progress: 0, total: 2, message: "fetching" });
180
+ const res = await fetch(url, { signal: ctx.signal });
181
+ await ctx.progress({ progress: 1, total: 2, message: "processing" });
182
+ return { content: [{ type: "text", text: await res.text() }] };
183
+ };
184
+ ```
185
+
171
186
  **Security contract for the bearer token.** `getToken()` is the only way to read
172
187
  the raw token. `ToolContext` holds the verified context in a private field, so the
173
188
  credential can't be read off the instance via `JSON.stringify`, object spread, or
@@ -177,7 +192,7 @@ construction (e.g. calling Supabase on the user's behalf); never return it from
177
192
  tool or write it to logs. A leaked token stays valid at the authorization server
178
193
  until it expires.
179
194
 
180
- **Caller-visible errors.** Throwing `ToolError` from a handler returns an `isError` result carrying exactly its message, on both the MCP and REST surfaces — the same shape as returning `{ isError: true, content: [...] }`. Any other exception is redacted to a generic failure on the wire; its name and message appear only in the server's local `tool.invoked` log line (`errorText`), never in telemetry.
195
+ **Caller-visible errors.** Throwing `ToolError` from a handler returns an `isError` result carrying exactly its message — the same shape as returning `{ isError: true, content: [...] }`. Any other exception is redacted to a generic failure on the wire; its name and message appear only in the server's local `tool.invoked` log line (`errorText`), never in telemetry.
181
196
 
182
197
  ```ts
183
198
  import { ToolError } from "@lovable.dev/mcp-js";
@@ -209,8 +224,6 @@ Use an OAuth access token from the configured authorization server for MCP calls
209
224
 
210
225
  Protected-resource metadata is derived from the same config. When `resource` is omitted, the published resource is the incoming request origin plus the MCP route path the stack adapter supplies as `resourcePath` (for example, `https://my-app.lovable.app/mcp`); `authorization_servers` is the configured issuer, and `resource_name` is `defineMcp({ title })` unless `resourceName` overrides it. Because the metadata endpoint is served from `/.well-known/...`, it can't infer the resource from its own path — the handler throws at construction when neither `resource` nor `resourcePath` is set (the generated TanStack routes always pass `resourcePath`). For Supabase auth, this metadata still names the MCP resource even though Supabase JWTs use the project audience `"authenticated"`.
211
226
 
212
- Stack adapters must pass the public MCP route as `resourcePath` when they also expose REST companion routes. The REST companion handlers (`createInvokeToolHandler`, `createListToolsHandler`) throw at construction unless `resource` or `resourcePath` is set, so `principal.resource` binds to `/mcp` rather than leaking the internal `/.mcp/*` RPC path it was reached through. The generated TanStack routes already pass it.
213
-
214
227
  The request-derived `resource` default trusts the incoming `Host` to name this server's origin. Because a config always sets `acceptedAudiences` (Supabase: `"authenticated"`) or `resource`, this default only names the advertised protected-resource metadata, never the accepted JWT audience. A platform that terminates TLS and sets `Host` from the verified domain (Lovable Cloud) makes this safe. Deployments fronted by a proxy that forwards an attacker-controlled `Host` should pin `resource` to the canonical URL so a spoofed host cannot shift the advertised metadata URL.
215
228
 
216
229
  Use `auth.oauth.issuer(...)`, set `resource` or `acceptedAudiences` to anchor the accepted audience, and optionally set `jwksUri` and `requiredScopes`. For Supabase project auth, set `acceptedAudiences: "authenticated"`, keep app/business checks in app code, and forward `ctx.getToken()` to Supabase for RLS-backed data access.
@@ -238,7 +251,7 @@ A `500` on an OAuth-protected route is always one of two causes, logged at `erro
238
251
 
239
252
  ## Usage metrics
240
253
 
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).
254
+ 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`), 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 — and `origin_rejected` for a request refused `403` by the origin policy. Tool-call records also carry the protocol era (`mcp.protocol`: `2026-07-28` or `legacy`); records emitted before protocol classification (auth challenges, transport errors, origin rejections) have no protocol attribute. 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).
242
255
 
243
256
  Configure it on the MCP definition:
244
257
 
@@ -303,8 +316,6 @@ The plugin bundles `src/lib/mcp/index.ts` (and the tool files it imports) into `
303
316
  | URL suffix | Target |
304
317
  | --- | --- |
305
318
  | (root) | MCP streamable-HTTP protocol |
306
- | `/.mcp/list-tools` | REST tool catalog |
307
- | `/.mcp/invoke-tool/<tool>` | REST tool dispatcher |
308
319
  | `/.well-known/oauth-protected-resource` | RFC 9728 metadata |
309
320
 
310
321
  In production the function is mounted at `https://<project-ref>.supabase.co/functions/v1/mcp` and the dispatcher binds the OAuth `resource` to that URL via `functionName`. Locally, run `supabase functions serve mcp --no-verify-jwt` so Supabase's edge-runtime doesn't 401 before the SDK's OAuth verifier sees the bearer.
@@ -318,7 +329,6 @@ Ownership: the emitted `index.ts` carries a `// AUTO-GENERATED by @lovable.dev/m
318
329
  | `@lovable.dev/mcp-js` | `defineTool`, `defineMcp`, public types |
319
330
  | `@lovable.dev/mcp-js/protocols/mcp` | `createMcpProtocolHandler` — Web-Standard MCP-over-HTTP |
320
331
  | `@lovable.dev/mcp-js/protocols/oauth-metadata` | `createOAuthProtectedResourceMetadataHandler` (the `auth` namespace lives at the root) |
321
- | `@lovable.dev/mcp-js/protocols/rest` | `createListToolsHandler`, `createInvokeToolHandler` — Web-Standard |
322
332
  | `@lovable.dev/mcp-js/stacks/tanstack` | TanStack-route-ctx adapters (`createTanStack*Handler`) |
323
333
  | `@lovable.dev/mcp-js/stacks/tanstack/vite` | The Vite plugin |
324
334
  | `@lovable.dev/mcp-js/stacks/supabase` | `createSupabaseHandler` — Web-Standard Supabase Edge Function dispatcher |
@@ -1,10 +1,7 @@
1
- import "./types-D1cgsnPu.js";
1
+ import "./types-DnGEKnvI.js";
2
2
  //#region src/auth/authorize.d.ts
3
3
  interface McpRuntimeOptions {
4
- /**
5
- * Public MCP protocol path. REST companion handlers pass this so OAuth
6
- * resource/audience checks bind to `/mcp`, not internal `/.mcp/*` RPC URLs.
7
- */
4
+ /** Public MCP path OAuth resource/audience checks bind to when it differs from the request path. */
8
5
  resourcePath?: string;
9
6
  /**
10
7
  * Host-relative path of the SDK-served protected-resource metadata document.
@@ -1,10 +1,7 @@
1
- import "./types-D1cgsnPu.cjs";
1
+ import "./types-DnGEKnvI.cjs";
2
2
  //#region src/auth/authorize.d.ts
3
3
  interface McpRuntimeOptions {
4
- /**
5
- * Public MCP protocol path. REST companion handlers pass this so OAuth
6
- * resource/audience checks bind to `/mcp`, not internal `/.mcp/*` RPC URLs.
7
- */
4
+ /** Public MCP path OAuth resource/audience checks bind to when it differs from the request path. */
8
5
  resourcePath?: string;
9
6
  /**
10
7
  * Host-relative path of the SDK-served protected-resource metadata document.
@@ -1,11 +1,33 @@
1
1
  #!/usr/bin/env node
2
- const require_package = require("../package-ClCIqol1.cjs");
3
- const require_list_tools = require("../list-tools-DES10avA.cjs");
2
+ const require_schema = require("../schema-D3vClxb7.cjs");
3
+ const require_package = require("../package-DNSwCau1.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");
7
7
  let vite = require("vite");
8
8
  let node_crypto = require("node:crypto");
9
+ //#region src/manifest/listing.ts
10
+ function schemaToJsonSchema(definition, pipeStrategy) {
11
+ if (!definition) return null;
12
+ return require_schema.jsonSchemaFromDefinition(definition, pipeStrategy);
13
+ }
14
+ function buildMcpListing(mcp) {
15
+ return {
16
+ server: {
17
+ name: mcp.name,
18
+ version: mcp.version,
19
+ title: mcp.title
20
+ },
21
+ tools: mcp.tools.map((tool) => ({
22
+ name: tool.name,
23
+ title: tool.title,
24
+ description: tool.description,
25
+ annotations: tool.annotations,
26
+ inputSchema: schemaToJsonSchema(tool.inputSchema, "input"),
27
+ outputSchema: schemaToJsonSchema(tool.outputSchema, "output")
28
+ }))
29
+ };
30
+ }
9
31
  const MANIFEST_RELATIVE_PATH = ".lovable/mcp/manifest.json";
10
32
  function manifestAuth(auth) {
11
33
  if (!auth) return { type: "none" };
@@ -45,7 +67,7 @@ function manifestFromDefinition(definition, urlPath, source = "the MCP entry") {
45
67
  sdk_version: require_package.version,
46
68
  path: urlPath,
47
69
  auth: manifestAuth(def.auth),
48
- mcp: require_list_tools.buildMcpListing(def)
70
+ mcp: buildMcpListing(def)
49
71
  };
50
72
  }
51
73
  //#endregion
@@ -1,11 +1,33 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "../package-BI5r4uGM.js";
3
- import { t as buildMcpListing } from "../list-tools-C1OiTDR9.js";
2
+ import { t as jsonSchemaFromDefinition } from "../schema-CW1jKvio.js";
3
+ import { t as version } from "../package-CYA-5C24.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";
7
7
  import { createServer, resolveConfig } from "vite";
8
8
  import { randomUUID } from "node:crypto";
9
+ //#region src/manifest/listing.ts
10
+ function schemaToJsonSchema(definition, pipeStrategy) {
11
+ if (!definition) return null;
12
+ return jsonSchemaFromDefinition(definition, pipeStrategy);
13
+ }
14
+ function buildMcpListing(mcp) {
15
+ return {
16
+ server: {
17
+ name: mcp.name,
18
+ version: mcp.version,
19
+ title: mcp.title
20
+ },
21
+ tools: mcp.tools.map((tool) => ({
22
+ name: tool.name,
23
+ title: tool.title,
24
+ description: tool.description,
25
+ annotations: tool.annotations,
26
+ inputSchema: schemaToJsonSchema(tool.inputSchema, "input"),
27
+ outputSchema: schemaToJsonSchema(tool.outputSchema, "output")
28
+ }))
29
+ };
30
+ }
9
31
  const MANIFEST_RELATIVE_PATH = ".lovable/mcp/manifest.json";
10
32
  function manifestAuth(auth) {
11
33
  if (!auth) return { type: "none" };
@@ -1,5 +1,5 @@
1
1
  import { i as log, n as applyLogLevelFromEnv, o as parseSafeUrl, r as describeError, s as trimTrailingSlash, t as LOG_LEVEL_ENV_VAR, u as resolveMetricsConfig } from "./logger-Ctv5xUSD.js";
2
- import { t as version } from "./package-BI5r4uGM.js";
2
+ import { t as version } from "./package-CYA-5C24.js";
3
3
  import "./metadata-path-CAkNcfyY.js";
4
4
  import { createLocalJWKSet, decodeProtectedHeader, errors, jwtVerify } from "jose";
5
5
  //#region src/core/http.ts
@@ -827,17 +827,6 @@ function getOAuthRuntime(mcp, options = {}) {
827
827
  options: stableOptions
828
828
  };
829
829
  }
830
- /**
831
- * REST companion routes (`/.mcp/list-tools`, `/.mcp/invoke-tool/*`) are internal
832
- * RPC the main MCP server calls, so their own request path must never become the
833
- * advertised resource. Require `auth.resource` or a `resourcePath` so the bound
834
- * `principal.resource` is the public MCP route, not the internal `/.mcp/*` path.
835
- * The MCP protocol handler is exempt: its own path is the public resource.
836
- */
837
- function assertRestResourceBinding(mcp, options = {}) {
838
- const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
839
- if (auth && auth.resource === void 0 && options.resourcePath === void 0) throw new Error(`@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`);
840
- }
841
830
  function missingRequiredScopes(auth, scopes) {
842
831
  const requiredScopes = auth.requiredScopes ?? [];
843
832
  if (requiredScopes.length === 0) return [];
@@ -942,7 +931,15 @@ function createRequestAuthorizer(mcp, options = {}) {
942
931
  //#endregion
943
932
  //#region src/core/cors.ts
944
933
  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";
934
+ const ALLOW_HEADERS = [
935
+ "Authorization",
936
+ "Content-Type",
937
+ "MCP-Protocol-Version",
938
+ "Mcp-Method",
939
+ "Mcp-Name",
940
+ "Mcp-Session-Id",
941
+ "Last-Event-ID"
942
+ ];
946
943
  const MCP_PARAM_HEADER = /^mcp-param-[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
947
944
  function appendVary(headers, value) {
948
945
  const values = new Set((headers.get("Vary") ?? "").split(",").map((entry) => entry.trim()).filter(Boolean));
@@ -958,7 +955,7 @@ function addCorsHeaders(response, allowedOrigin, variesByOrigin) {
958
955
  return response;
959
956
  }
960
957
  function allowedRequestHeaders(request) {
961
- const allowed = new Map(ALLOW_HEADERS.split(", ").map((header) => [header.toLowerCase(), header]));
958
+ const allowed = new Map(ALLOW_HEADERS.map((header) => [header.toLowerCase(), header]));
962
959
  for (const header of (request.headers.get("Access-Control-Request-Headers") ?? "").split(",")) {
963
960
  const normalized = header.trim();
964
961
  if (MCP_PARAM_HEADER.test(normalized)) allowed.set(normalized.toLowerCase(), normalized);
@@ -968,12 +965,8 @@ function allowedRequestHeaders(request) {
968
965
  function evaluateCors(request, allowedOrigins) {
969
966
  const origin = request.headers.get("Origin");
970
967
  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) {
968
+ const originAllowed = origin === null || origin === new URL(request.url).origin || allowedOrigins?.includes(origin) === true;
969
+ if (!allowAny && !originAllowed) {
977
970
  const response = Response.json({ error: "origin not allowed" }, {
978
971
  status: 403,
979
972
  headers: { "Cache-Control": "no-store" }
@@ -984,6 +977,7 @@ function evaluateCors(request, allowedOrigins) {
984
977
  response
985
978
  };
986
979
  }
980
+ const allowedOrigin = allowAny ? "*" : origin ?? void 0;
987
981
  return {
988
982
  ok: true,
989
983
  withHeaders: (response) => addCorsHeaders(response, allowedOrigin, !allowAny),
@@ -1003,4 +997,4 @@ function evaluateCors(request, allowedOrigins) {
1003
997
  };
1004
998
  }
1005
999
  //#endregion
1006
- export { oauthConfigurationErrorResponse as a, createRecorderForRuntime as c, JSON_HEADERS as d, finalizeWireResponse as f, getOAuthRuntime as i, createNoopRecorder as l, assertRestResourceBinding as n, assertResourcePathShape as o, methodNotAllowed as p, createRequestAuthorizer as r, resolveProtectedResource as s, evaluateCors as t, nowMs as u };
1000
+ export { assertResourcePathShape as a, createNoopRecorder as c, finalizeWireResponse as d, methodNotAllowed as f, oauthConfigurationErrorResponse as i, nowMs as l, createRequestAuthorizer as n, resolveProtectedResource as o, getOAuthRuntime as r, createRecorderForRuntime as s, evaluateCors as t, JSON_HEADERS as u };
@@ -1,5 +1,5 @@
1
1
  const require_logger = require("./logger-CN1q-KNn.cjs");
2
- const require_package = require("./package-ClCIqol1.cjs");
2
+ const require_package = require("./package-DNSwCau1.cjs");
3
3
  require("./metadata-path-zd7NSNoa.cjs");
4
4
  let jose = require("jose");
5
5
  //#region src/core/http.ts
@@ -827,17 +827,6 @@ function getOAuthRuntime(mcp, options = {}) {
827
827
  options: stableOptions
828
828
  };
829
829
  }
830
- /**
831
- * REST companion routes (`/.mcp/list-tools`, `/.mcp/invoke-tool/*`) are internal
832
- * RPC the main MCP server calls, so their own request path must never become the
833
- * advertised resource. Require `auth.resource` or a `resourcePath` so the bound
834
- * `principal.resource` is the public MCP route, not the internal `/.mcp/*` path.
835
- * The MCP protocol handler is exempt: its own path is the public resource.
836
- */
837
- function assertRestResourceBinding(mcp, options = {}) {
838
- const auth = mcp.auth?.type === "oauth" ? mcp.auth : void 0;
839
- if (auth && auth.resource === void 0 && options.resourcePath === void 0) throw new Error(`@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`);
840
- }
841
830
  function missingRequiredScopes(auth, scopes) {
842
831
  const requiredScopes = auth.requiredScopes ?? [];
843
832
  if (requiredScopes.length === 0) return [];
@@ -942,7 +931,15 @@ function createRequestAuthorizer(mcp, options = {}) {
942
931
  //#endregion
943
932
  //#region src/core/cors.ts
944
933
  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";
934
+ const ALLOW_HEADERS = [
935
+ "Authorization",
936
+ "Content-Type",
937
+ "MCP-Protocol-Version",
938
+ "Mcp-Method",
939
+ "Mcp-Name",
940
+ "Mcp-Session-Id",
941
+ "Last-Event-ID"
942
+ ];
946
943
  const MCP_PARAM_HEADER = /^mcp-param-[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
947
944
  function appendVary(headers, value) {
948
945
  const values = new Set((headers.get("Vary") ?? "").split(",").map((entry) => entry.trim()).filter(Boolean));
@@ -958,7 +955,7 @@ function addCorsHeaders(response, allowedOrigin, variesByOrigin) {
958
955
  return response;
959
956
  }
960
957
  function allowedRequestHeaders(request) {
961
- const allowed = new Map(ALLOW_HEADERS.split(", ").map((header) => [header.toLowerCase(), header]));
958
+ const allowed = new Map(ALLOW_HEADERS.map((header) => [header.toLowerCase(), header]));
962
959
  for (const header of (request.headers.get("Access-Control-Request-Headers") ?? "").split(",")) {
963
960
  const normalized = header.trim();
964
961
  if (MCP_PARAM_HEADER.test(normalized)) allowed.set(normalized.toLowerCase(), normalized);
@@ -968,12 +965,8 @@ function allowedRequestHeaders(request) {
968
965
  function evaluateCors(request, allowedOrigins) {
969
966
  const origin = request.headers.get("Origin");
970
967
  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) {
968
+ const originAllowed = origin === null || origin === new URL(request.url).origin || allowedOrigins?.includes(origin) === true;
969
+ if (!allowAny && !originAllowed) {
977
970
  const response = Response.json({ error: "origin not allowed" }, {
978
971
  status: 403,
979
972
  headers: { "Cache-Control": "no-store" }
@@ -984,6 +977,7 @@ function evaluateCors(request, allowedOrigins) {
984
977
  response
985
978
  };
986
979
  }
980
+ const allowedOrigin = allowAny ? "*" : origin ?? void 0;
987
981
  return {
988
982
  ok: true,
989
983
  withHeaders: (response) => addCorsHeaders(response, allowedOrigin, !allowAny),
@@ -1015,12 +1009,6 @@ Object.defineProperty(exports, "assertResourcePathShape", {
1015
1009
  return assertResourcePathShape;
1016
1010
  }
1017
1011
  });
1018
- Object.defineProperty(exports, "assertRestResourceBinding", {
1019
- enumerable: true,
1020
- get: function() {
1021
- return assertRestResourceBinding;
1022
- }
1023
- });
1024
1012
  Object.defineProperty(exports, "createNoopRecorder", {
1025
1013
  enumerable: true,
1026
1014
  get: function() {
@@ -1,15 +1,30 @@
1
1
  //#region src/auth/context.ts
2
- /**
3
- * The auth surface passed to every tool handler as its second argument. It wraps
4
- * the verified per-request auth context in a private field, so the context and
5
- * the bearer token inside it — can't be read, enumerated, spread, or
6
- * `JSON.stringify`'d off the instance; the accessors below are the only way out.
7
- * `getToken()` is the single intentional escape hatch for the raw bearer.
8
- */
2
+ const NEVER_ABORTED = new AbortController().signal;
3
+ /** The per-request context passed to every tool handler as its second argument. The
4
+ * auth context lives in a private field so the accessors are the only way out; `getToken()` is the sole bearer escape hatch. */
9
5
  var ToolContext = class {
10
6
  #auth;
11
- constructor(auth) {
7
+ #signal;
8
+ #client;
9
+ #sendProgress;
10
+ constructor(auth, request = {}) {
12
11
  this.#auth = auth;
12
+ this.#signal = request.signal ?? NEVER_ABORTED;
13
+ this.#client = request.client ?? { protocol: "legacy" };
14
+ this.#sendProgress = request.sendProgress;
15
+ }
16
+ /** Aborts when the client cancels the request or disconnects. */
17
+ get signal() {
18
+ return this.#signal;
19
+ }
20
+ /** What the client declared about itself on this request. Unverified input. */
21
+ get client() {
22
+ return this.#client;
23
+ }
24
+ /** Send a progress notification for this call. A no-op when the client
25
+ * didn't request progress (no `progressToken` on the request). */
26
+ async progress(update) {
27
+ await this.#sendProgress?.(update);
13
28
  }
14
29
  /** Whether the in-flight tool call carries a verified auth context. */
15
30
  isAuthenticated() {
@@ -1,15 +1,30 @@
1
1
  //#region src/auth/context.ts
2
- /**
3
- * The auth surface passed to every tool handler as its second argument. It wraps
4
- * the verified per-request auth context in a private field, so the context and
5
- * the bearer token inside it — can't be read, enumerated, spread, or
6
- * `JSON.stringify`'d off the instance; the accessors below are the only way out.
7
- * `getToken()` is the single intentional escape hatch for the raw bearer.
8
- */
2
+ const NEVER_ABORTED = new AbortController().signal;
3
+ /** The per-request context passed to every tool handler as its second argument. The
4
+ * auth context lives in a private field so the accessors are the only way out; `getToken()` is the sole bearer escape hatch. */
9
5
  var ToolContext = class {
10
6
  #auth;
11
- constructor(auth) {
7
+ #signal;
8
+ #client;
9
+ #sendProgress;
10
+ constructor(auth, request = {}) {
12
11
  this.#auth = auth;
12
+ this.#signal = request.signal ?? NEVER_ABORTED;
13
+ this.#client = request.client ?? { protocol: "legacy" };
14
+ this.#sendProgress = request.sendProgress;
15
+ }
16
+ /** Aborts when the client cancels the request or disconnects. */
17
+ get signal() {
18
+ return this.#signal;
19
+ }
20
+ /** What the client declared about itself on this request. Unverified input. */
21
+ get client() {
22
+ return this.#client;
23
+ }
24
+ /** Send a progress notification for this call. A no-op when the client
25
+ * didn't request progress (no `progressToken` on the request). */
26
+ async progress(update) {
27
+ await this.#sendProgress?.(update);
13
28
  }
14
29
  /** Whether the in-flight tool call carries a verified auth context. */
15
30
  isAuthenticated() {
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_logger = require("./logger-CN1q-KNn.cjs");
3
3
  const require_schema = require("./schema-D3vClxb7.cjs");
4
- const require_errors = require("./errors-CN6Da8iz.cjs");
4
+ const require_errors = require("./errors-Cr95rSkB.cjs");
5
5
  //#region src/core/define.ts
6
6
  function assertUniqueNames(mcp) {
7
7
  const seen = /* @__PURE__ */ new Set();