@lovable.dev/mcp-js 0.29.0-rc.0 → 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.
- package/README.internal.md +22 -31
- package/README.md +23 -13
- package/dist/{authorize-BjNE3LPr.d.ts → authorize-Bpz8oxjF.d.ts} +2 -5
- package/dist/{authorize-BzZFS0zi.d.cts → authorize-Cqr7Jps0.d.cts} +2 -5
- package/dist/cli/extract-manifest.cjs +25 -3
- package/dist/cli/extract-manifest.js +24 -2
- package/dist/{cors-D4YbODs-.js → cors-C_VffY86.js} +15 -21
- package/dist/{cors-CmKzDN7k.cjs → cors-DNzLB6qa.cjs} +14 -26
- package/dist/{errors-DSe96Ffd.js → errors-Bwd1L0Rf.js} +23 -8
- package/dist/{errors-CN6Da8iz.cjs → errors-Cr95rSkB.cjs} +23 -8
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{io-CvALf9NE.d.ts → io-Bh_WpbxU.d.cts} +1 -1
- package/dist/{io-BEwvgN8s.d.cts → io-DrHivYt0.d.ts} +1 -1
- package/dist/mcp-Bl7pQYdv.js +346 -0
- package/dist/mcp-CRdT2Fai.cjs +351 -0
- package/dist/{package-DzB_5Hur.js → package-CYA-5C24.js} +1 -1
- package/dist/{package-C3YVYgxp.cjs → package-DNSwCau1.cjs} +1 -1
- package/dist/protocols/mcp/index.cjs +1 -1
- package/dist/protocols/mcp/index.d.cts +36 -3
- package/dist/protocols/mcp/index.d.ts +36 -3
- package/dist/protocols/mcp/index.js +1 -1
- package/dist/protocols/oauth-metadata.cjs +1 -1
- package/dist/protocols/oauth-metadata.d.cts +2 -2
- package/dist/protocols/oauth-metadata.d.ts +2 -2
- package/dist/protocols/oauth-metadata.js +1 -1
- package/dist/stacks/supabase/index.cjs +6 -35
- package/dist/stacks/supabase/index.d.cts +1 -4
- package/dist/stacks/supabase/index.d.ts +1 -4
- package/dist/stacks/supabase/index.js +6 -35
- 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 +2 -23
- package/dist/stacks/tanstack/index.d.cts +3 -20
- package/dist/stacks/tanstack/index.d.ts +3 -20
- package/dist/stacks/tanstack/index.js +3 -22
- package/dist/stacks/tanstack/vite.cjs +5 -33
- package/dist/stacks/tanstack/vite.d.cts +5 -32
- package/dist/stacks/tanstack/vite.d.ts +5 -32
- package/dist/stacks/tanstack/vite.js +5 -33
- package/dist/{types-D1cgsnPu.d.cts → types-DnGEKnvI.d.cts} +43 -20
- package/dist/{types-D1cgsnPu.d.ts → types-DnGEKnvI.d.ts} +43 -20
- package/package.json +4 -11
- package/dist/base-DSH8NnPj.d.cts +0 -36
- package/dist/base-DSH8NnPj.d.ts +0 -36
- package/dist/gate-n_HX6eQ5.cjs +0 -26
- package/dist/gate-pGsVlY_4.js +0 -21
- package/dist/list-tools-Dkyyje7j.cjs +0 -48
- package/dist/list-tools-ePP9tXND.js +0 -37
- package/dist/list-tools-qIvVJeWZ.d.ts +0 -8
- package/dist/list-tools-zagoI477.d.cts +0 -8
- package/dist/mcp-Cs4zfvSi.js +0 -336
- package/dist/mcp-DQ1Vdv4v.cjs +0 -341
- package/dist/protocols/rest/index.cjs +0 -5
- package/dist/protocols/rest/index.d.cts +0 -9
- package/dist/protocols/rest/index.d.ts +0 -9
- package/dist/protocols/rest/index.js +0 -3
- package/dist/rest-DSclWpIt.js +0 -129
- package/dist/rest-P6XZWP42.cjs +0 -134
- package/dist/tool-result-CK6hXO43.cjs +0 -89
- package/dist/tool-result-DfCk445b.js +0 -78
package/README.internal.md
CHANGED
|
@@ -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
|
|
8
|
-
- **`auth/`** (unpublished) is the cross-cutting OAuth middleware — bearer verification, issuer/JWKS discovery, the `auth.oauth.*` config namespace. It depends only on `core/`;
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
34
|
+
### 4. Dynamic tool resolution at runtime
|
|
35
35
|
|
|
36
|
-
|
|
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.**
|
|
45
|
-
- **The build doesn't have user context.**
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
|
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
|
-
###
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`)
|
|
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-
|
|
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-
|
|
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
|
|
3
|
-
const
|
|
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:
|
|
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
|
|
3
|
-
import { t as
|
|
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-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
972
|
-
if (allowAny
|
|
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 {
|
|
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-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
972
|
-
if (allowAny
|
|
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
|
-
|
|
4
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
*
|
|
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
|
-
|
|
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-
|
|
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();
|