@lovable.dev/mcp-js 2.1.0 → 2.2.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -8
- package/dist/{authorize-CzyL7nmF.d.cts → authorize-BmYO5_-I.d.cts} +1 -1
- package/dist/{authorize-Drz_eJ7O.d.ts → authorize-d9-8n9u3.d.ts} +1 -1
- package/dist/{base-COwtdWE8.d.ts → base-BAxrhVjq.d.ts} +2 -2
- package/dist/{base-DD4MCXZ6.d.cts → base-Bmw6Wrb-.d.cts} +2 -2
- package/dist/cli/extract-manifest.cjs +1 -1
- package/dist/cli/extract-manifest.js +1 -1
- package/dist/{cors-BaRKDl4r.js → cors-BfacizN-.js} +2 -2
- package/dist/{cors-D-X0aP4O.cjs → cors-Dm9Yn8E5.cjs} +2 -2
- package/dist/errors-CpTdzogX.cjs +308 -0
- package/dist/errors-DGcJ8jPK.js +261 -0
- package/dist/index.cjs +5 -148
- package/dist/index.d.cts +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +3 -147
- package/dist/{io-pl5npWDw.d.ts → io-7U860POB.d.ts} +1 -1
- package/dist/{io-Ds5MRSiK.d.cts → io-CL1GeE3D.d.cts} +1 -1
- package/dist/{io-DG83CLKx.js → io-DD8wjGjd.js} +3 -2
- package/dist/{io-DZD6xmJQ.cjs → io-DaqblVaG.cjs} +3 -2
- package/dist/{logger-CN1q-KNn.cjs → logger-C-Hdu57K.cjs} +5 -1
- package/dist/{logger-Ctv5xUSD.js → logger-CpLgYbCG.js} +5 -1
- package/dist/{mcp-Bx3LUn5O.cjs → mcp-BGsPYWs1.cjs} +93 -16
- package/dist/{mcp-DcmfehoI.js → mcp-DdJJjYYZ.js} +93 -16
- package/dist/{package-BNFIhH0F.js → package-cPx-zYIb.js} +1 -1
- package/dist/{package-CFVl3oIe.cjs → package-hWM0Ru0w.cjs} +1 -1
- package/dist/protocols/mcp/index.cjs +1 -1
- package/dist/protocols/mcp/index.d.cts +3 -3
- package/dist/protocols/mcp/index.d.ts +3 -3
- package/dist/protocols/mcp/index.js +1 -1
- package/dist/protocols/oauth-metadata.cjs +2 -2
- package/dist/protocols/oauth-metadata.d.cts +3 -3
- package/dist/protocols/oauth-metadata.d.ts +3 -3
- package/dist/protocols/oauth-metadata.js +2 -2
- package/dist/stacks/supabase/index.cjs +3 -3
- package/dist/stacks/supabase/index.d.cts +1 -1
- package/dist/stacks/supabase/index.d.ts +1 -1
- package/dist/stacks/supabase/index.js +3 -3
- 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 -2
- package/dist/stacks/tanstack/index.d.cts +2 -2
- package/dist/stacks/tanstack/index.d.ts +2 -2
- package/dist/stacks/tanstack/index.js +2 -2
- package/dist/stacks/tanstack/vite.d.cts +1 -1
- package/dist/stacks/tanstack/vite.d.ts +1 -1
- package/dist/{types-B3_lX-L1.d.ts → types-BCKyT2wb.d.cts} +68 -20
- package/dist/{types-B3_lX-L1.d.cts → types-BCKyT2wb.d.ts} +68 -20
- package/package.json +2 -2
- package/dist/errors-Bwd1L0Rf.js +0 -100
- package/dist/errors-Cr95rSkB.cjs +0 -123
package/README.md
CHANGED
|
@@ -53,6 +53,87 @@ MCP (`POST <path>`, `/mcp` by default) is the wire format clients speak directly
|
|
|
53
53
|
|
|
54
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.
|
|
55
55
|
|
|
56
|
+
## Caller-dependent tool catalogs
|
|
57
|
+
|
|
58
|
+
Use a request context only when authenticated callers must see different tool
|
|
59
|
+
names. Bind its type once and export the resulting helper for tool files:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// src/lib/mcp/context.ts
|
|
63
|
+
import { defineTool } from "@lovable.dev/mcp-js";
|
|
64
|
+
|
|
65
|
+
export interface AppContext {
|
|
66
|
+
readonly plan: "free" | "pro";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const defineAppTool = defineTool.withAppContext<AppContext>();
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Create the app context from verified auth when an operation needs it, then
|
|
73
|
+
register every tool in the build-time union:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { auth, defineMcp } from "@lovable.dev/mcp-js";
|
|
77
|
+
import type { AppContext } from "./context";
|
|
78
|
+
import exportDataTool from "./tools/export-data";
|
|
79
|
+
|
|
80
|
+
export default defineMcp<AppContext>({
|
|
81
|
+
name: "my-app-mcp",
|
|
82
|
+
title: "My App MCP",
|
|
83
|
+
version: "0.1.0",
|
|
84
|
+
instructions: "Tools for interacting with My App.",
|
|
85
|
+
auth: auth.oauth.issuer({
|
|
86
|
+
issuer: "https://auth.example.com",
|
|
87
|
+
resource: "https://my-app.example.com/mcp",
|
|
88
|
+
}),
|
|
89
|
+
createAppContext: async ({ auth, signal }) => ({
|
|
90
|
+
// Unknown callers get the least-privilege catalog; only a failed lookup throws.
|
|
91
|
+
plan: (await loadPlan(auth.getUserId(), signal)) ?? "free",
|
|
92
|
+
}),
|
|
93
|
+
tools: [exportDataTool],
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Define conditional tools with the bound helper. The predicate is synchronous
|
|
98
|
+
and intentionally receives only `appContext`; handlers receive the same value
|
|
99
|
+
on `ctx.appContext`. Keep predicates pure: do not mutate `appContext` or depend
|
|
100
|
+
on tool evaluation order.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { defineAppTool } from "../context";
|
|
104
|
+
|
|
105
|
+
export default defineAppTool({
|
|
106
|
+
name: "export_data",
|
|
107
|
+
title: "Export data",
|
|
108
|
+
description: "Export the caller's data.",
|
|
109
|
+
enabled: ({ appContext }) => appContext.plan === "pro",
|
|
110
|
+
handler: async (_args, ctx) => {
|
|
111
|
+
const token = ctx.getToken();
|
|
112
|
+
if (!token) throw new Error("Authenticated caller required");
|
|
113
|
+
await exportDataForCaller(token, ctx.signal);
|
|
114
|
+
return { content: [{ type: "text", text: "Export ready." }] };
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The SDK runs `createAppContext` once per request: for `tools/list` when any tool
|
|
120
|
+
has `enabled`, and for a call to a conditional or app-context tool. Plain static
|
|
121
|
+
tool calls, discovery, and ping skip it. A throw from the factory or a predicate
|
|
122
|
+
fails the whole request with a redacted 500, including `tools/list`, so return a
|
|
123
|
+
least-privilege context for callers who lack access and throw only when the
|
|
124
|
+
lookup itself fails. Disabled tools are omitted from
|
|
125
|
+
`tools/list`; direct calls receive the standard unknown-tool error. `enabled`
|
|
126
|
+
gates discovery and dispatch, but the operation must still authorize current
|
|
127
|
+
permissions in its app service or database. Repeating the predicate against the
|
|
128
|
+
same `ctx.appContext` is not a fresh authorization check.
|
|
129
|
+
|
|
130
|
+
Modern responses advertise caching policy to clients: static catalogs are
|
|
131
|
+
`public` for 24 hours; catalogs containing any `enabled` predicate are `private`
|
|
132
|
+
for 12 hours. `private` means a client must not reuse the result across auth
|
|
133
|
+
contexts; it does not itself enforce endpoint access. The publish manifest
|
|
134
|
+
always contains the complete tool union and marks predicate-bearing entries as
|
|
135
|
+
`conditional`, so Lovable can review and display the full API surface.
|
|
136
|
+
|
|
56
137
|
`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.
|
|
57
138
|
|
|
58
139
|
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.
|
|
@@ -63,7 +144,7 @@ To serve the metadata from a different path — e.g. a workspace-private project
|
|
|
63
144
|
|
|
64
145
|
### `.lovable/mcp/manifest.json`
|
|
65
146
|
|
|
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
|
|
147
|
+
`.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 complete build-time tool catalog (`name`/`title`/`description`/`annotations`, JSON-Schema `inputSchema`/`outputSchema`, and `conditional: true` for tools with an `enabled` predicate). It's produced by loading the entry and reading the catalog off the `defineMcp` result, so it includes tools built programmatically (spreads, computed names, tools from npm) even when a live caller's `tools/list` narrows the catalog. **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.
|
|
67
148
|
|
|
68
149
|
Three caveats:
|
|
69
150
|
|
|
@@ -135,21 +216,29 @@ The SDK only uses discovery + JWKS. It never calls the authorization or token en
|
|
|
135
216
|
|
|
136
217
|
Tool handlers receive their input args plus a `ToolContext` as the second
|
|
137
218
|
argument. The SDK constructs one per request from the verified token and passes
|
|
138
|
-
it to the handler
|
|
139
|
-
|
|
219
|
+
it to the handler. `ToolContext` extends `AuthContext`, which is also what
|
|
220
|
+
`createAppContext` receives as `auth`; helpers that need the auth should take an
|
|
221
|
+
`AuthContext` parameter so both call sites can share them.
|
|
140
222
|
|
|
141
223
|
```ts
|
|
142
|
-
import type {
|
|
224
|
+
import type { AuthContext } from "@lovable.dev/mcp-js";
|
|
225
|
+
|
|
226
|
+
// Usable from tool handlers (`ctx`) and from `createAppContext` (`auth`).
|
|
227
|
+
function requireUserId(auth: AuthContext): string {
|
|
228
|
+
const userId = auth.getUserId(); // token `sub`, or undefined when unauthenticated
|
|
229
|
+
if (!userId) throw new Error("Authenticated caller required");
|
|
230
|
+
return userId;
|
|
231
|
+
}
|
|
143
232
|
|
|
144
233
|
handler: async ({ title }, ctx) => {
|
|
145
|
-
const userId = ctx
|
|
234
|
+
const userId = requireUserId(ctx);
|
|
146
235
|
// Apply app/business permissions here using ctx.getClaims() / ctx.getClientId().
|
|
147
236
|
// For Supabase RLS, pass ctx.getToken() through to Supabase.
|
|
148
237
|
return { content: [{ type: "text", text: `user=${userId}` }] };
|
|
149
238
|
};
|
|
150
239
|
```
|
|
151
240
|
|
|
152
|
-
| `
|
|
241
|
+
| `AuthContext` method (inherited by `ToolContext`) | Returns |
|
|
153
242
|
| --- | --- |
|
|
154
243
|
| `isAuthenticated()` | `true` when the call carries a verified auth context. |
|
|
155
244
|
| `getUserId()` | The token `sub` (subject) — the user id. |
|
|
@@ -257,11 +346,11 @@ setLogLevel("debug"); // "silent" | "error" | "warn" | "info" | "debug"
|
|
|
257
346
|
LOVABLE_MCP_LOG_LEVEL=debug
|
|
258
347
|
```
|
|
259
348
|
|
|
260
|
-
A `500` on an OAuth-protected route is always one of
|
|
349
|
+
A `500` on an OAuth-protected route is always one of three causes, logged at `error`: an `OAuthConfigurationError` from issuer-metadata discovery or a JWKS *fetch* failure (`oauth.discovery.config_error` / `oauth.jwks.fetch_failed` → `auth.config_error`), a throw from the app's own `createAppContext` or an `enabled` predicate (`mcp.app_context_error`, carrying that error's name and message), or a transport-level fault in the MCP handler (`mcp.transport_error`). Auth outcomes are logged at `info`: a granted request as `oauth.verify.ok`, a request with no parseable bearer as `auth.no_bearer_token`, and a rejected token as `auth.token_rejected` (a `jose` rejection such as an expired token carries the `jose` error name and code; the full reason appears at `debug` as `oauth.verify.rejected`) — so a request rejected for insufficient scope shows both `oauth.verify.ok` and the `auth.token_rejected` that follows it. A sustained flood of `auth.token_rejected` whose `code` is `ERR_JOSE_NOT_SUPPORTED` or `ERR_JOSE_ALG_NOT_ALLOWED` (rather than `ERR_JWT_EXPIRED`) points at server-side key or algorithm configuration, not client error.
|
|
261
350
|
|
|
262
351
|
## Usage metrics
|
|
263
352
|
|
|
264
|
-
Each MCP server records per-invocation telemetry — tool name, JSON-RPC method, outcome, latency, request and response sizes when available, and the verified token `sub` for authenticated calls — 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. The bearer token, full claims, tool arguments, and response payloads are never 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).
|
|
353
|
+
Each MCP server records per-invocation telemetry — tool name, JSON-RPC method, outcome, latency, request and response sizes when available, and the verified token `sub` for authenticated calls — 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`), `app_context_error` for a request failed by a throwing `createAppContext` or `enabled`, 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. The bearer token, full claims, tool arguments, and response payloads are never 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).
|
|
265
354
|
|
|
266
355
|
Configure it on the MCP definition:
|
|
267
356
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { j as McpProtocolEra } from "./types-BCKyT2wb.js";
|
|
2
2
|
//#region src/metrics/impl/base.d.ts
|
|
3
|
-
type InvocationOutcome = "ok" | "tool_error" | "handler_error" | "transport_error" | "auth_challenge" | "origin_rejected" | "auth_discovery_config_error" | "auth_discovery_unreachable" | "auth_jwks_config_error" | "auth_jwks_unreachable";
|
|
3
|
+
type InvocationOutcome = "ok" | "tool_error" | "handler_error" | "transport_error" | "app_context_error" | "auth_challenge" | "origin_rejected" | "auth_discovery_config_error" | "auth_discovery_unreachable" | "auth_jwks_config_error" | "auth_jwks_unreachable";
|
|
4
4
|
/** One recorded MCP call. Carries no arguments or response payloads — only the
|
|
5
5
|
* tool name, the JSON-RPC method, the result class, timing, and byte sizes.
|
|
6
6
|
* `errorText` is the sole exception: it is logged locally for debugging and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { j as McpProtocolEra } from "./types-BCKyT2wb.cjs";
|
|
2
2
|
//#region src/metrics/impl/base.d.ts
|
|
3
|
-
type InvocationOutcome = "ok" | "tool_error" | "handler_error" | "transport_error" | "auth_challenge" | "origin_rejected" | "auth_discovery_config_error" | "auth_discovery_unreachable" | "auth_jwks_config_error" | "auth_jwks_unreachable";
|
|
3
|
+
type InvocationOutcome = "ok" | "tool_error" | "handler_error" | "transport_error" | "app_context_error" | "auth_challenge" | "origin_rejected" | "auth_discovery_config_error" | "auth_discovery_unreachable" | "auth_jwks_config_error" | "auth_jwks_unreachable";
|
|
4
4
|
/** One recorded MCP call. Carries no arguments or response payloads — only the
|
|
5
5
|
* tool name, the JSON-RPC method, the result class, timing, and byte sizes.
|
|
6
6
|
* `errorText` is the sole exception: it is logged locally for debugging and
|
|
@@ -4,7 +4,7 @@ const projectRoot = process.argv[2] ?? process.cwd();
|
|
|
4
4
|
import("zod/v3").catch((err) => {
|
|
5
5
|
const message = err instanceof Error ? err.message : String(err);
|
|
6
6
|
throw new Error(`@lovable.dev/mcp-js requires zod ^3.25 or ^4.0 to extract MCP schemas. Verify the project's zod dependency and package resolution, then try again. Underlying error: ${message}`);
|
|
7
|
-
}).then(() => Promise.resolve().then(() => require("../io-
|
|
7
|
+
}).then(() => Promise.resolve().then(() => require("../io-DaqblVaG.cjs"))).then(({ runExtract }) => runExtract(projectRoot)).catch((err) => {
|
|
8
8
|
console.error(err instanceof Error ? err.message : String(err));
|
|
9
9
|
process.exit(1);
|
|
10
10
|
});
|
|
@@ -4,7 +4,7 @@ const projectRoot = process.argv[2] ?? process.cwd();
|
|
|
4
4
|
import("zod/v3").catch((err) => {
|
|
5
5
|
const message = err instanceof Error ? err.message : String(err);
|
|
6
6
|
throw new Error(`@lovable.dev/mcp-js requires zod ^3.25 or ^4.0 to extract MCP schemas. Verify the project's zod dependency and package resolution, then try again. Underlying error: ${message}`);
|
|
7
|
-
}).then(() => import("../io-
|
|
7
|
+
}).then(() => import("../io-DD8wjGjd.js")).then(({ runExtract }) => runExtract(projectRoot)).catch((err) => {
|
|
8
8
|
console.error(err instanceof Error ? err.message : String(err));
|
|
9
9
|
process.exit(1);
|
|
10
10
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
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-
|
|
2
|
-
import { t as version } from "./package-
|
|
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-CpLgYbCG.js";
|
|
2
|
+
import { t as version } from "./package-cPx-zYIb.js";
|
|
3
3
|
import "./metadata-path-CAkNcfyY.js";
|
|
4
4
|
import { createLocalJWKSet, decodeProtectedHeader, errors, jwtVerify } from "jose";
|
|
5
5
|
//#region src/core/http.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const require_logger = require("./logger-
|
|
2
|
-
const require_package = require("./package-
|
|
1
|
+
const require_logger = require("./logger-C-Hdu57K.cjs");
|
|
2
|
+
const require_package = require("./package-hWM0Ru0w.cjs");
|
|
3
3
|
require("./metadata-path-zd7NSNoa.cjs");
|
|
4
4
|
let jose = require("jose");
|
|
5
5
|
//#region src/core/http.ts
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
const require_logger = require("./logger-C-Hdu57K.cjs");
|
|
2
|
+
const require_schema = require("./schema-D3vClxb7.cjs");
|
|
3
|
+
//#region src/core/define.ts
|
|
4
|
+
function toolRequiresAppContext(tool) {
|
|
5
|
+
return "requiresAppContext" in tool && tool.requiresAppContext === true;
|
|
6
|
+
}
|
|
7
|
+
function assertUniqueNames(mcp) {
|
|
8
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9
|
+
for (const tool of mcp.tools) {
|
|
10
|
+
if (seen.has(tool.name)) throw new Error(`@lovable.dev/mcp-js: duplicate tool name "${tool.name}"`);
|
|
11
|
+
seen.add(tool.name);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function assertAppContextConfiguration(mcp) {
|
|
15
|
+
if (mcp.createAppContext && !mcp.auth) throw new Error(`@lovable.dev/mcp-js: createAppContext requires auth`);
|
|
16
|
+
if (!mcp.createAppContext && mcp.tools.some((tool) => tool.enabled || toolRequiresAppContext(tool))) throw new Error(`@lovable.dev/mcp-js: app-context or conditional tools require createAppContext`);
|
|
17
|
+
}
|
|
18
|
+
function assertNonEmptyString(label, value) {
|
|
19
|
+
if (value.trim() === "") throw new Error(`@lovable.dev/mcp-js: ${label} must not be empty`);
|
|
20
|
+
if (value !== value.trim()) throw new Error(`@lovable.dev/mcp-js: ${label} must not have leading or trailing whitespace`);
|
|
21
|
+
}
|
|
22
|
+
function assertHttpsUrlField(name, value) {
|
|
23
|
+
assertNonEmptyString(`auth.${name}`, value);
|
|
24
|
+
require_logger.parseSafeUrl(`@lovable.dev/mcp-js: auth.${name}`, value);
|
|
25
|
+
}
|
|
26
|
+
function assertScope(scope) {
|
|
27
|
+
if (scope.trim() === "" || /\s/.test(scope)) throw new Error(`@lovable.dev/mcp-js: OAuth scopes must be non-empty space-free tokens`);
|
|
28
|
+
}
|
|
29
|
+
function assertJwtAlgorithm(algorithm) {
|
|
30
|
+
if (algorithm.trim() === "" || /\s/.test(algorithm)) throw new Error(`@lovable.dev/mcp-js: auth.algorithms must contain non-empty space-free tokens`);
|
|
31
|
+
if (/^HS\d+$/i.test(algorithm) || algorithm.toLowerCase() === "none") throw new Error(`@lovable.dev/mcp-js: auth.algorithms cannot include "${algorithm}"; this verifier is JWKS-only`);
|
|
32
|
+
}
|
|
33
|
+
const MAX_CLOCK_TOLERANCE_SECONDS = 300;
|
|
34
|
+
function assertClockToleranceSeconds(value) {
|
|
35
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > MAX_CLOCK_TOLERANCE_SECONDS) throw new Error(`@lovable.dev/mcp-js: auth.clockToleranceSeconds must be a non-negative number of seconds no greater than ${MAX_CLOCK_TOLERANCE_SECONDS}`);
|
|
36
|
+
}
|
|
37
|
+
function assertOAuthConfig(auth) {
|
|
38
|
+
if (auth.type !== "oauth") {
|
|
39
|
+
const authType = String(auth.type);
|
|
40
|
+
throw new Error(`@lovable.dev/mcp-js: unsupported auth type "${authType}"`);
|
|
41
|
+
}
|
|
42
|
+
if (auth.issuer === void 0) throw new Error(`@lovable.dev/mcp-js: auth.issuer is required`);
|
|
43
|
+
assertHttpsUrlField("issuer", auth.issuer);
|
|
44
|
+
if (auth.jwksUri !== void 0) assertHttpsUrlField("jwksUri", auth.jwksUri);
|
|
45
|
+
if (auth.resource !== void 0) assertHttpsUrlField("resource", auth.resource);
|
|
46
|
+
if (auth.resourceName !== void 0) assertNonEmptyString("auth.resourceName", auth.resourceName);
|
|
47
|
+
if (auth.resourceDocumentation !== void 0) assertHttpsUrlField("resourceDocumentation", auth.resourceDocumentation);
|
|
48
|
+
if (auth.protectedResourceMetadataUrl !== void 0) assertHttpsUrlField("protectedResourceMetadataUrl", auth.protectedResourceMetadataUrl);
|
|
49
|
+
if (auth.acceptedAudiences !== void 0) {
|
|
50
|
+
if (!Array.isArray(auth.acceptedAudiences)) throw new Error(`@lovable.dev/mcp-js: auth.acceptedAudiences must be an array`);
|
|
51
|
+
if (auth.acceptedAudiences.length === 0) throw new Error(`@lovable.dev/mcp-js: auth.acceptedAudiences must not be empty`);
|
|
52
|
+
for (const audience of auth.acceptedAudiences) {
|
|
53
|
+
if (typeof audience !== "string") throw new Error(`@lovable.dev/mcp-js: auth.acceptedAudiences must contain strings`);
|
|
54
|
+
assertNonEmptyString("auth.acceptedAudiences", audience);
|
|
55
|
+
if (require_logger.trimTrailingSlash(audience) === "") throw new Error(`@lovable.dev/mcp-js: auth.acceptedAudiences entries must not be only slashes`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (auth.requireOAuthClientClaim !== void 0 && typeof auth.requireOAuthClientClaim !== "boolean") throw new Error(`@lovable.dev/mcp-js: auth.requireOAuthClientClaim must be a boolean`);
|
|
59
|
+
if (auth.acceptResourceClaim !== void 0 && typeof auth.acceptResourceClaim !== "boolean") throw new Error(`@lovable.dev/mcp-js: auth.acceptResourceClaim must be a boolean`);
|
|
60
|
+
if (auth.requiredScopes !== void 0) {
|
|
61
|
+
if (!Array.isArray(auth.requiredScopes)) throw new Error(`@lovable.dev/mcp-js: auth.requiredScopes must be an array`);
|
|
62
|
+
for (const scope of auth.requiredScopes) assertScope(scope);
|
|
63
|
+
}
|
|
64
|
+
if (auth.algorithms !== void 0) {
|
|
65
|
+
if (!Array.isArray(auth.algorithms)) throw new Error(`@lovable.dev/mcp-js: auth.algorithms must be an array`);
|
|
66
|
+
if (auth.algorithms.length === 0) throw new Error(`@lovable.dev/mcp-js: auth.algorithms must not be empty`);
|
|
67
|
+
for (const algorithm of auth.algorithms) assertJwtAlgorithm(algorithm);
|
|
68
|
+
}
|
|
69
|
+
if (auth.accessTokenTyp !== void 0) {
|
|
70
|
+
if (!Array.isArray(auth.accessTokenTyp)) throw new Error(`@lovable.dev/mcp-js: auth.accessTokenTyp must be an array`);
|
|
71
|
+
if (auth.accessTokenTyp.length === 0) throw new Error(`@lovable.dev/mcp-js: auth.accessTokenTyp must not be empty`);
|
|
72
|
+
for (const typ of auth.accessTokenTyp) {
|
|
73
|
+
if (typeof typ !== "string") throw new Error(`@lovable.dev/mcp-js: auth.accessTokenTyp must contain strings`);
|
|
74
|
+
assertNonEmptyString("auth.accessTokenTyp", typ);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (auth.clockToleranceSeconds !== void 0) assertClockToleranceSeconds(auth.clockToleranceSeconds);
|
|
78
|
+
if (auth.resource === void 0 && auth.acceptedAudiences === void 0) throw new Error(`@lovable.dev/mcp-js: auth.resource or auth.acceptedAudiences is required`);
|
|
79
|
+
}
|
|
80
|
+
function freezeAuth(auth) {
|
|
81
|
+
if (!auth) return;
|
|
82
|
+
if (auth.acceptedAudiences) Object.freeze(auth.acceptedAudiences);
|
|
83
|
+
if (auth.requiredScopes) Object.freeze(auth.requiredScopes);
|
|
84
|
+
if (auth.algorithms) Object.freeze(auth.algorithms);
|
|
85
|
+
if (auth.accessTokenTyp) Object.freeze(auth.accessTokenTyp);
|
|
86
|
+
Object.freeze(auth);
|
|
87
|
+
}
|
|
88
|
+
function assertAllowedOrigins(allowedOrigins) {
|
|
89
|
+
if (allowedOrigins === void 0 || allowedOrigins === "any") return;
|
|
90
|
+
if (!Array.isArray(allowedOrigins)) throw new Error(`@lovable.dev/mcp-js: allowedOrigins must be "any" or an array of exact origins`);
|
|
91
|
+
for (const origin of allowedOrigins) {
|
|
92
|
+
if (typeof origin !== "string") throw new Error(`@lovable.dev/mcp-js: allowedOrigins must contain strings`);
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = new URL(origin);
|
|
96
|
+
} catch {
|
|
97
|
+
throw new Error(`@lovable.dev/mcp-js: allowedOrigins must contain valid exact origins`);
|
|
98
|
+
}
|
|
99
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.origin !== origin) throw new Error(`@lovable.dev/mcp-js: allowedOrigins entry "${origin}" must be an exact http(s) origin without a path, query, fragment, or trailing slash`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function assertToolSchemas(mcp) {
|
|
103
|
+
for (const tool of mcp.tools) for (const [name, definition, strategy] of [[
|
|
104
|
+
"inputSchema",
|
|
105
|
+
tool.inputSchema,
|
|
106
|
+
"input"
|
|
107
|
+
], [
|
|
108
|
+
"outputSchema",
|
|
109
|
+
tool.outputSchema,
|
|
110
|
+
"output"
|
|
111
|
+
]]) {
|
|
112
|
+
if (!definition) continue;
|
|
113
|
+
try {
|
|
114
|
+
require_schema.jsonSchemaFromDefinition(definition, strategy);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
const detail = error instanceof Error ? `: ${error.message}` : "";
|
|
117
|
+
throw new Error(`@lovable.dev/mcp-js: tool "${tool.name}" ${name} cannot be represented as JSON Schema${detail}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const defineStaticTool = (def) => def;
|
|
122
|
+
const defineTool = Object.assign(defineStaticTool, { withAppContext: () => (def) => {
|
|
123
|
+
return {
|
|
124
|
+
...def,
|
|
125
|
+
requiresAppContext: true
|
|
126
|
+
};
|
|
127
|
+
} });
|
|
128
|
+
/**
|
|
129
|
+
* Declare the MCP server. `export default` the result from your MCP
|
|
130
|
+
* entrypoint (default `lib/mcp/index.ts`); the Vite plugin reads it to
|
|
131
|
+
* emit the framework-specific route(s) at build time.
|
|
132
|
+
*/
|
|
133
|
+
function defineMcp(def) {
|
|
134
|
+
assertNonEmptyString("name", def.name);
|
|
135
|
+
assertNonEmptyString("title", def.title);
|
|
136
|
+
assertNonEmptyString("version", def.version);
|
|
137
|
+
assertUniqueNames(def);
|
|
138
|
+
assertAppContextConfiguration(def);
|
|
139
|
+
if (def.auth) assertOAuthConfig(def.auth);
|
|
140
|
+
assertAllowedOrigins(def.allowedOrigins);
|
|
141
|
+
assertToolSchemas(def);
|
|
142
|
+
require_logger.resolveMetricsConfig(def.metrics);
|
|
143
|
+
freezeAuth(def.auth);
|
|
144
|
+
if (Array.isArray(def.allowedOrigins)) Object.freeze(def.allowedOrigins);
|
|
145
|
+
Object.freeze(def.tools);
|
|
146
|
+
for (const tool of def.tools) Object.freeze(tool);
|
|
147
|
+
return def;
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/auth/context.ts
|
|
151
|
+
const NEVER_ABORTED = new AbortController().signal;
|
|
152
|
+
/** Verified caller identity exposed to the app-context factory.
|
|
153
|
+
* `getToken()` is the sole bearer escape hatch. */
|
|
154
|
+
var AuthContext = class {
|
|
155
|
+
#auth;
|
|
156
|
+
constructor(auth) {
|
|
157
|
+
this.#auth = auth;
|
|
158
|
+
}
|
|
159
|
+
/** Whether the in-flight tool call carries a verified auth context. */
|
|
160
|
+
isAuthenticated() {
|
|
161
|
+
return this.#auth !== void 0;
|
|
162
|
+
}
|
|
163
|
+
/** The verified bearer token, or `undefined` when unauthenticated. Pass it to downstream APIs; never return or log it. */
|
|
164
|
+
getToken() {
|
|
165
|
+
return this.#auth?.bearer.token;
|
|
166
|
+
}
|
|
167
|
+
/** The verified user id (the token `sub`), or `undefined`. */
|
|
168
|
+
getUserId() {
|
|
169
|
+
return this.#auth?.principal.sub;
|
|
170
|
+
}
|
|
171
|
+
/** The verified user email, or `undefined` when absent. */
|
|
172
|
+
getUserEmail() {
|
|
173
|
+
return this.#auth?.principal.email;
|
|
174
|
+
}
|
|
175
|
+
/** The verified OAuth `client_id`, or `undefined`. */
|
|
176
|
+
getClientId() {
|
|
177
|
+
return this.#auth?.principal.clientId;
|
|
178
|
+
}
|
|
179
|
+
/** The verified OAuth scopes, or `undefined` when unauthenticated. */
|
|
180
|
+
getScopes() {
|
|
181
|
+
return this.#auth?.principal.scopes;
|
|
182
|
+
}
|
|
183
|
+
/** The verified token issuer, or `undefined`. */
|
|
184
|
+
getIssuer() {
|
|
185
|
+
return this.#auth?.principal.issuer;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The full verified JWT claims, or `undefined`. Use this for app/business
|
|
189
|
+
* authorization on issuer-specific claims that have no dedicated accessor.
|
|
190
|
+
*/
|
|
191
|
+
getClaims() {
|
|
192
|
+
return this.#auth?.principal.claims;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
/** The per-request context passed to every tool handler as its second argument. */
|
|
196
|
+
var ToolContext = class extends AuthContext {
|
|
197
|
+
#appContext;
|
|
198
|
+
#signal;
|
|
199
|
+
#client;
|
|
200
|
+
#sendProgress;
|
|
201
|
+
constructor(auth, request = {}, appContext) {
|
|
202
|
+
super(auth);
|
|
203
|
+
this.#appContext = appContext;
|
|
204
|
+
this.#signal = request.signal ?? NEVER_ABORTED;
|
|
205
|
+
this.#client = request.client ?? { protocol: "legacy" };
|
|
206
|
+
this.#sendProgress = request.sendProgress;
|
|
207
|
+
}
|
|
208
|
+
/** App-defined context created for this tool call. */
|
|
209
|
+
get appContext() {
|
|
210
|
+
return this.#appContext;
|
|
211
|
+
}
|
|
212
|
+
/** Aborts when the client cancels the request or disconnects. */
|
|
213
|
+
get signal() {
|
|
214
|
+
return this.#signal;
|
|
215
|
+
}
|
|
216
|
+
/** What the client declared about itself on this request. Unverified input. */
|
|
217
|
+
get client() {
|
|
218
|
+
return this.#client;
|
|
219
|
+
}
|
|
220
|
+
/** Send a progress notification for this call. A no-op when the client
|
|
221
|
+
* didn't request progress (no `progressToken` on the request). */
|
|
222
|
+
async progress(update) {
|
|
223
|
+
await this.#sendProgress?.(update);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
//#endregion
|
|
227
|
+
//#region src/core/errors.ts
|
|
228
|
+
const TOOL_ERROR_BRAND = Symbol.for("@lovable.dev/mcp-js/tool-error");
|
|
229
|
+
/** Error whose message is meant for the remote MCP caller; thrown from a handler
|
|
230
|
+
* it becomes an `isError` result carrying exactly `message`. */
|
|
231
|
+
var ToolError = class extends Error {
|
|
232
|
+
constructor(message, options) {
|
|
233
|
+
super(message, options);
|
|
234
|
+
this.name = "ToolError";
|
|
235
|
+
Object.defineProperty(this, TOOL_ERROR_BRAND, { value: true });
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
function isToolError(err) {
|
|
239
|
+
return err instanceof Error && Object.getOwnPropertyDescriptor(err, TOOL_ERROR_BRAND)?.value === true;
|
|
240
|
+
}
|
|
241
|
+
/** The caller-visible message of a thrown ToolError, or undefined for anything
|
|
242
|
+
* else — including when inspecting the value itself throws (fail closed). */
|
|
243
|
+
function resolveToolErrorMessage(err) {
|
|
244
|
+
try {
|
|
245
|
+
return isToolError(err) ? String(err.message) : void 0;
|
|
246
|
+
} catch {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const MAX_ERROR_SUMMARY = 500;
|
|
251
|
+
/** Bounded name+message line for local logs; never for wire responses. */
|
|
252
|
+
function errorSummary(err) {
|
|
253
|
+
try {
|
|
254
|
+
const text = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
255
|
+
return text.length > MAX_ERROR_SUMMARY ? `${text.slice(0, MAX_ERROR_SUMMARY)}…` : text;
|
|
256
|
+
} catch {
|
|
257
|
+
return "unprintable error";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
261
|
+
Object.defineProperty(exports, "AuthContext", {
|
|
262
|
+
enumerable: true,
|
|
263
|
+
get: function() {
|
|
264
|
+
return AuthContext;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
Object.defineProperty(exports, "ToolContext", {
|
|
268
|
+
enumerable: true,
|
|
269
|
+
get: function() {
|
|
270
|
+
return ToolContext;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
Object.defineProperty(exports, "ToolError", {
|
|
274
|
+
enumerable: true,
|
|
275
|
+
get: function() {
|
|
276
|
+
return ToolError;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
Object.defineProperty(exports, "defineMcp", {
|
|
280
|
+
enumerable: true,
|
|
281
|
+
get: function() {
|
|
282
|
+
return defineMcp;
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
Object.defineProperty(exports, "defineTool", {
|
|
286
|
+
enumerable: true,
|
|
287
|
+
get: function() {
|
|
288
|
+
return defineTool;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
Object.defineProperty(exports, "errorSummary", {
|
|
292
|
+
enumerable: true,
|
|
293
|
+
get: function() {
|
|
294
|
+
return errorSummary;
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
Object.defineProperty(exports, "resolveToolErrorMessage", {
|
|
298
|
+
enumerable: true,
|
|
299
|
+
get: function() {
|
|
300
|
+
return resolveToolErrorMessage;
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
Object.defineProperty(exports, "toolRequiresAppContext", {
|
|
304
|
+
enumerable: true,
|
|
305
|
+
get: function() {
|
|
306
|
+
return toolRequiresAppContext;
|
|
307
|
+
}
|
|
308
|
+
});
|