@lovable.dev/mcp-js 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.internal.md +1 -1
  2. package/README.md +3 -3
  3. package/dist/{chunk-LAFJDJRY.js → chunk-G4XA7IJM.js} +17 -1
  4. package/dist/{chunk-X7V2CL7C.js → chunk-QDKOF4UF.js} +1 -1
  5. package/dist/{chunk-D6KB4Q7R.js → chunk-S3YHTZKJ.js} +1 -1
  6. package/dist/{chunk-PJXKJNSZ.js → chunk-VNRQPA4K.js} +1 -1
  7. package/dist/{chunk-DF3DD3KD.js → chunk-WLNT2FX7.js} +1 -1
  8. package/dist/{chunk-SZLSAGDF.js → chunk-YWLMMDET.js} +1 -1
  9. package/dist/cli/extract-manifest.cjs +1 -1
  10. package/dist/cli/extract-manifest.js +3 -3
  11. package/dist/index.cjs +18 -1
  12. package/dist/index.d.cts +3 -2
  13. package/dist/index.d.ts +3 -2
  14. package/dist/index.js +18 -1
  15. package/dist/protocols/mcp/index.cjs +17 -1
  16. package/dist/protocols/mcp/index.d.cts +1 -1
  17. package/dist/protocols/mcp/index.d.ts +1 -1
  18. package/dist/protocols/mcp/index.js +2 -2
  19. package/dist/protocols/oauth-metadata.cjs +17 -1
  20. package/dist/protocols/oauth-metadata.d.cts +1 -1
  21. package/dist/protocols/oauth-metadata.d.ts +1 -1
  22. package/dist/protocols/oauth-metadata.js +2 -2
  23. package/dist/protocols/rest/index.cjs +17 -1
  24. package/dist/protocols/rest/index.d.cts +1 -1
  25. package/dist/protocols/rest/index.d.ts +1 -1
  26. package/dist/protocols/rest/index.js +3 -3
  27. package/dist/stacks/supabase/index.cjs +17 -1
  28. package/dist/stacks/supabase/index.d.cts +1 -1
  29. package/dist/stacks/supabase/index.d.ts +1 -1
  30. package/dist/stacks/supabase/index.js +5 -5
  31. package/dist/stacks/supabase/vite.cjs +80 -49
  32. package/dist/stacks/supabase/vite.d.cts +7 -4
  33. package/dist/stacks/supabase/vite.d.ts +7 -4
  34. package/dist/stacks/supabase/vite.js +80 -49
  35. package/dist/stacks/tanstack/index.cjs +17 -1
  36. package/dist/stacks/tanstack/index.d.cts +1 -1
  37. package/dist/stacks/tanstack/index.d.ts +1 -1
  38. package/dist/stacks/tanstack/index.js +5 -5
  39. package/dist/{types-C5SSORxe.d.ts → types-BanzncFh.d.ts} +8 -0
  40. package/package.json +2 -1
@@ -145,7 +145,7 @@ src/
145
145
  index.ts # barrel
146
146
  supabase/
147
147
  handler.ts # createSupabaseHandler (URL-suffix dispatcher to protocols/*)
148
- emit.ts # syncSupabaseFunction (emits supabase/functions/<name>/{index.ts,deno.json})
148
+ emit.ts # syncSupabaseFunction (esbuild-bundles supabase/functions/<name>/index.ts)
149
149
  vite.ts # mcpPlugin (Vite-lifecycle trigger; exposes McpPluginApi with urlPath=/functions/v1/<functionName>)
150
150
  index.ts # barrel
151
151
  tests/
package/README.md CHANGED
@@ -217,7 +217,7 @@ A `500` on an OAuth-protected route is always one of two causes, logged at `erro
217
217
 
218
218
  ## Supabase Edge Functions
219
219
 
220
- Ship the same `defineMcp` server as a single Supabase Edge Function. Authoring is identical to the TanStack flow; only the build-time step differs — a different Vite plugin emits a single Deno function entry (plus a `deno.json` import map) instead of a tree of file-router routes.
220
+ Ship the same `defineMcp` server as a single Supabase Edge Function. Authoring is identical to the TanStack flow; only the build-time step differs — a different Vite plugin bundles your MCP entry and its local imports into one self-contained Deno function (with esbuild) instead of emitting a tree of file-router routes. Third-party dependencies are left as `npm:` specifiers that Deno resolves at runtime, so no import map / `deno.json` is needed.
221
221
 
222
222
  ```ts
223
223
  // src/lib/mcp/index.ts — same as the TanStack flow
@@ -249,7 +249,7 @@ import { mcpPlugin } from "@lovable.dev/mcp-js/stacks/supabase/vite";
249
249
  export default defineConfig({ plugins: [mcpPlugin()] });
250
250
  ```
251
251
 
252
- The plugin emits `supabase/functions/mcp/index.ts` and `supabase/functions/mcp/deno.json` on `configResolved`, re-emits on `buildStart`, and watches `src/lib/mcp/index.ts` for changes during `vite dev`. The emitted function calls `Deno.serve(createSupabaseHandler(mcp, { functionName: "mcp" }))`. The handler is one Web-Standard `Request → Response` that dispatches by URL suffix to the same protocol handlers the TanStack stack uses:
252
+ The plugin bundles `src/lib/mcp/index.ts` (and the tool files it imports) into `supabase/functions/mcp/index.ts` on `configResolved`, re-emits on `buildStart`, and watches the bundled source files for changes during `vite dev`. The emitted function calls `Deno.serve(createSupabaseHandler(mcp, { functionName: "mcp" }))`. The handler is one Web-Standard `Request → Response` that dispatches by URL suffix to the same protocol handlers the TanStack stack uses:
253
253
 
254
254
  | URL suffix | Target |
255
255
  | --- | --- |
@@ -260,7 +260,7 @@ The plugin emits `supabase/functions/mcp/index.ts` and `supabase/functions/mcp/d
260
260
 
261
261
  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.
262
262
 
263
- Ownership: every emitted file carries a `// AUTO-GENERATED by @lovable.dev/mcp-js` banner. The plugin rewrites banner-tagged files in place and cleans up orphans; banner-less files (yours) are never touched. Delete the banner line to take ownership of a file. Deleting the `defineMcp` entry and re-running removes the SDK-owned function files.
263
+ Ownership: the emitted `index.ts` carries a `// AUTO-GENERATED by @lovable.dev/mcp-js` banner. The plugin rewrites the banner-tagged file in place and cleans up orphans (including a `deno.json` left by a pre-bundling SDK version); banner-less files (yours) are never touched. Delete the banner line to take ownership. Deleting the `defineMcp` entry and re-running removes the SDK-owned function file.
264
264
 
265
265
  ## Subpath exports
266
266
 
@@ -190,6 +190,7 @@ function stringClaim(claims, name) {
190
190
  // src/auth/verifier.ts
191
191
  var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
192
192
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
193
+ var DEFAULT_ACCESS_TOKEN_TYPS = ["at+jwt", "JWT"];
193
194
  var JWKS_FETCH_TIMEOUT_MS = 5e3;
194
195
  var OAuthTokenError = class extends Error {
195
196
  constructor(status, oauthError, message) {
@@ -222,10 +223,23 @@ async function fetchVerificationKeySet(jwksUri) {
222
223
  throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
223
224
  }
224
225
  }
226
+ function assertAccessTokenTyp(token, allowed) {
227
+ let header;
228
+ try {
229
+ header = decodeProtectedHeader(token);
230
+ } catch (err) {
231
+ log.debug("oauth.verify.bad_header", { ...describeError(err), outcome: "401 invalid_token" });
232
+ throw new OAuthTokenError(401, "invalid_token", "Malformed JWT header");
233
+ }
234
+ const typ = header.typ;
235
+ if (typeof typ !== "string" || !allowed.includes(typ)) {
236
+ log.debug("oauth.verify.bad_typ", { jwtTyp: typ, allowed, outcome: "401 invalid_token" });
237
+ throw new OAuthTokenError(401, "invalid_token", "Access token typ header is not accepted");
238
+ }
239
+ }
225
240
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
226
241
  try {
227
242
  const { payload } = await jwtVerify(token, keySet, {
228
- typ: "at+jwt",
229
243
  // `issuer` is trimmed of any trailing slash; accept both forms so a token whose
230
244
  // `iss` carries the slash the AS publishes still verifies.
231
245
  issuer: [issuer, `${issuer}/`],
@@ -274,6 +288,7 @@ function buildMcpAuthContext(args) {
274
288
  };
275
289
  }
276
290
  function createOAuthTokenVerifier(auth, discovery) {
291
+ const allowedTyps = auth.accessTokenTyp ?? DEFAULT_ACCESS_TOKEN_TYPS;
277
292
  return async (token, request, options) => {
278
293
  const resource = resolveProtectedResource(auth, request, options);
279
294
  const issuer = await discovery.resolveIssuer();
@@ -282,6 +297,7 @@ function createOAuthTokenVerifier(auth, discovery) {
282
297
  const jwksUri = await discovery.resolveJwksUri();
283
298
  log.debug("oauth.jwks.fetch", { jwksUri });
284
299
  const keySet = await fetchVerificationKeySet(jwksUri);
300
+ assertAccessTokenTyp(token, allowedTyps);
285
301
  const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
286
302
  assertNonEmptySubject(claims);
287
303
  const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
@@ -7,7 +7,7 @@ import {
7
7
  oauthConfigurationErrorResponse,
8
8
  resolveProtectedResource,
9
9
  withCors
10
- } from "./chunk-LAFJDJRY.js";
10
+ } from "./chunk-G4XA7IJM.js";
11
11
  import {
12
12
  describeError,
13
13
  log
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "0.10.1";
2
+ var version = "0.12.0";
3
3
 
4
4
  export {
5
5
  version
@@ -5,7 +5,7 @@ import {
5
5
  headResponse,
6
6
  methodNotAllowed,
7
7
  withCors
8
- } from "./chunk-LAFJDJRY.js";
8
+ } from "./chunk-G4XA7IJM.js";
9
9
 
10
10
  // src/protocols/rest/list-tools.ts
11
11
  import { objectFromShape } from "@modelcontextprotocol/sdk/server/zod-compat.js";
@@ -5,7 +5,7 @@ import {
5
5
  corsPreflightResponse,
6
6
  createRequestAuthorizer,
7
7
  withCors
8
- } from "./chunk-LAFJDJRY.js";
8
+ } from "./chunk-G4XA7IJM.js";
9
9
  import {
10
10
  describeError,
11
11
  log
@@ -8,7 +8,7 @@ import {
8
8
  createRequestAuthorizer,
9
9
  methodNotAllowed,
10
10
  withCors
11
- } from "./chunk-LAFJDJRY.js";
11
+ } from "./chunk-G4XA7IJM.js";
12
12
 
13
13
  // src/protocols/rest/invoke-tool.ts
14
14
  import { getParseErrorMessage, objectFromShape, safeParseAsync } from "@modelcontextprotocol/sdk/server/zod-compat.js";
@@ -13,7 +13,7 @@ function isFileMissing(err) {
13
13
  }
14
14
 
15
15
  // package.json
16
- var version = "0.10.1";
16
+ var version = "0.12.0";
17
17
 
18
18
  // src/protocols/rest/list-tools.ts
19
19
  var import_zod_compat = require("@modelcontextprotocol/sdk/server/zod-compat.js");
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  buildMcpListing
4
- } from "../chunk-PJXKJNSZ.js";
5
- import "../chunk-LAFJDJRY.js";
4
+ } from "../chunk-VNRQPA4K.js";
5
+ import "../chunk-G4XA7IJM.js";
6
6
  import "../chunk-QC3DXQTH.js";
7
7
  import "../chunk-6DXGZZA4.js";
8
8
  import {
9
9
  version
10
- } from "../chunk-D6KB4Q7R.js";
10
+ } from "../chunk-S3YHTZKJ.js";
11
11
  import {
12
12
  isFileMissing
13
13
  } from "../chunk-Y3ZFPEQH.js";
package/dist/index.cjs CHANGED
@@ -159,6 +159,20 @@ function assertOAuthConfig(auth2) {
159
159
  for (const algorithm of auth2.algorithms)
160
160
  assertJwtAlgorithm(algorithm);
161
161
  }
162
+ if (auth2.accessTokenTyp !== void 0) {
163
+ if (!Array.isArray(auth2.accessTokenTyp)) {
164
+ throw new Error(`@lovable.dev/mcp-js: auth.accessTokenTyp must be an array`);
165
+ }
166
+ if (auth2.accessTokenTyp.length === 0) {
167
+ throw new Error(`@lovable.dev/mcp-js: auth.accessTokenTyp must not be empty`);
168
+ }
169
+ for (const typ of auth2.accessTokenTyp) {
170
+ if (typeof typ !== "string") {
171
+ throw new Error(`@lovable.dev/mcp-js: auth.accessTokenTyp must contain strings`);
172
+ }
173
+ assertNonEmptyString("auth.accessTokenTyp", typ);
174
+ }
175
+ }
162
176
  if (auth2.clockToleranceSeconds !== void 0)
163
177
  assertClockToleranceSeconds(auth2.clockToleranceSeconds);
164
178
  if (auth2.resource === void 0 && auth2.acceptedAudiences === void 0) {
@@ -174,6 +188,8 @@ function freezeAuth(auth2) {
174
188
  Object.freeze(auth2.requiredScopes);
175
189
  if (auth2.algorithms)
176
190
  Object.freeze(auth2.algorithms);
191
+ if (auth2.accessTokenTyp)
192
+ Object.freeze(auth2.accessTokenTyp);
177
193
  Object.freeze(auth2);
178
194
  }
179
195
  function defineTool(def) {
@@ -214,7 +230,8 @@ function issuer(options) {
214
230
  ...options,
215
231
  acceptedAudiences: frozenAudiences(options.acceptedAudiences),
216
232
  requiredScopes: frozenArray(options.requiredScopes),
217
- algorithms: frozenArray(options.algorithms)
233
+ algorithms: frozenArray(options.algorithms),
234
+ accessTokenTyp: frozenArray(options.accessTokenTyp)
218
235
  })
219
236
  );
220
237
  }
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, j as ToolDefinition, M as McpAuthConfig } from './types-C5SSORxe.js';
2
- export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink, f as ResourceLinkIcon, T as TextContent, g as ToolAnnotations, h as ToolContent, i as ToolContext, k as ToolHandlerResult, l as ZodSchema } from './types-C5SSORxe.js';
1
+ import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, j as ToolDefinition, M as McpAuthConfig } from './types-BanzncFh.js';
2
+ export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink, f as ResourceLinkIcon, T as TextContent, g as ToolAnnotations, h as ToolContent, i as ToolContext, k as ToolHandlerResult, l as ZodSchema } from './types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
5
5
  /**
@@ -29,6 +29,7 @@ interface IssuerOAuthOptionsBase {
29
29
  readonly jwksUri?: string;
30
30
  readonly algorithms?: readonly string[];
31
31
  readonly clockToleranceSeconds?: number;
32
+ readonly accessTokenTyp?: readonly string[];
32
33
  }
33
34
  type IssuerOAuthOptions = IssuerOAuthOptionsBase & ({
34
35
  readonly resource: string;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, j as ToolDefinition, M as McpAuthConfig } from './types-C5SSORxe.js';
2
- export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink, f as ResourceLinkIcon, T as TextContent, g as ToolAnnotations, h as ToolContent, i as ToolContext, k as ToolHandlerResult, l as ZodSchema } from './types-C5SSORxe.js';
1
+ import { e as McpDefinitionInput, d as McpDefinition, Z as ZodRawShape, j as ToolDefinition, M as McpAuthConfig } from './types-BanzncFh.js';
2
+ export { A as AudioContent, C as ContentAnnotations, a as ContentBlock, E as EmbeddedBlobResource, b as EmbeddedResource, c as EmbeddedTextResource, I as ImageContent, J as JwtClaims, R as ResourceLink, f as ResourceLinkIcon, T as TextContent, g as ToolAnnotations, h as ToolContent, i as ToolContext, k as ToolHandlerResult, l as ZodSchema } from './types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
5
5
  /**
@@ -29,6 +29,7 @@ interface IssuerOAuthOptionsBase {
29
29
  readonly jwksUri?: string;
30
30
  readonly algorithms?: readonly string[];
31
31
  readonly clockToleranceSeconds?: number;
32
+ readonly accessTokenTyp?: readonly string[];
32
33
  }
33
34
  type IssuerOAuthOptions = IssuerOAuthOptionsBase & ({
34
35
  readonly resource: string;
package/dist/index.js CHANGED
@@ -104,6 +104,20 @@ function assertOAuthConfig(auth2) {
104
104
  for (const algorithm of auth2.algorithms)
105
105
  assertJwtAlgorithm(algorithm);
106
106
  }
107
+ if (auth2.accessTokenTyp !== void 0) {
108
+ if (!Array.isArray(auth2.accessTokenTyp)) {
109
+ throw new Error(`@lovable.dev/mcp-js: auth.accessTokenTyp must be an array`);
110
+ }
111
+ if (auth2.accessTokenTyp.length === 0) {
112
+ throw new Error(`@lovable.dev/mcp-js: auth.accessTokenTyp must not be empty`);
113
+ }
114
+ for (const typ of auth2.accessTokenTyp) {
115
+ if (typeof typ !== "string") {
116
+ throw new Error(`@lovable.dev/mcp-js: auth.accessTokenTyp must contain strings`);
117
+ }
118
+ assertNonEmptyString("auth.accessTokenTyp", typ);
119
+ }
120
+ }
107
121
  if (auth2.clockToleranceSeconds !== void 0)
108
122
  assertClockToleranceSeconds(auth2.clockToleranceSeconds);
109
123
  if (auth2.resource === void 0 && auth2.acceptedAudiences === void 0) {
@@ -119,6 +133,8 @@ function freezeAuth(auth2) {
119
133
  Object.freeze(auth2.requiredScopes);
120
134
  if (auth2.algorithms)
121
135
  Object.freeze(auth2.algorithms);
136
+ if (auth2.accessTokenTyp)
137
+ Object.freeze(auth2.accessTokenTyp);
122
138
  Object.freeze(auth2);
123
139
  }
124
140
  function defineTool(def) {
@@ -159,7 +175,8 @@ function issuer(options) {
159
175
  ...options,
160
176
  acceptedAudiences: frozenAudiences(options.acceptedAudiences),
161
177
  requiredScopes: frozenArray(options.requiredScopes),
162
- algorithms: frozenArray(options.algorithms)
178
+ algorithms: frozenArray(options.algorithms),
179
+ accessTokenTyp: frozenArray(options.accessTokenTyp)
163
180
  })
164
181
  );
165
182
  }
@@ -281,6 +281,7 @@ function stringClaim(claims, name) {
281
281
  // src/auth/verifier.ts
282
282
  var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
283
283
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
284
+ var DEFAULT_ACCESS_TOKEN_TYPS = ["at+jwt", "JWT"];
284
285
  var JWKS_FETCH_TIMEOUT_MS = 5e3;
285
286
  var OAuthTokenError = class extends Error {
286
287
  constructor(status, oauthError, message) {
@@ -313,10 +314,23 @@ async function fetchVerificationKeySet(jwksUri) {
313
314
  throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
314
315
  }
315
316
  }
317
+ function assertAccessTokenTyp(token, allowed) {
318
+ let header;
319
+ try {
320
+ header = (0, import_jose.decodeProtectedHeader)(token);
321
+ } catch (err) {
322
+ log.debug("oauth.verify.bad_header", { ...describeError(err), outcome: "401 invalid_token" });
323
+ throw new OAuthTokenError(401, "invalid_token", "Malformed JWT header");
324
+ }
325
+ const typ = header.typ;
326
+ if (typeof typ !== "string" || !allowed.includes(typ)) {
327
+ log.debug("oauth.verify.bad_typ", { jwtTyp: typ, allowed, outcome: "401 invalid_token" });
328
+ throw new OAuthTokenError(401, "invalid_token", "Access token typ header is not accepted");
329
+ }
330
+ }
316
331
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
317
332
  try {
318
333
  const { payload } = await (0, import_jose.jwtVerify)(token, keySet, {
319
- typ: "at+jwt",
320
334
  // `issuer` is trimmed of any trailing slash; accept both forms so a token whose
321
335
  // `iss` carries the slash the AS publishes still verifies.
322
336
  issuer: [issuer, `${issuer}/`],
@@ -365,6 +379,7 @@ function buildMcpAuthContext(args) {
365
379
  };
366
380
  }
367
381
  function createOAuthTokenVerifier(auth, discovery) {
382
+ const allowedTyps = auth.accessTokenTyp ?? DEFAULT_ACCESS_TOKEN_TYPS;
368
383
  return async (token, request, options) => {
369
384
  const resource = resolveProtectedResource(auth, request, options);
370
385
  const issuer = await discovery.resolveIssuer();
@@ -373,6 +388,7 @@ function createOAuthTokenVerifier(auth, discovery) {
373
388
  const jwksUri = await discovery.resolveJwksUri();
374
389
  log.debug("oauth.jwks.fetch", { jwksUri });
375
390
  const keySet = await fetchVerificationKeySet(jwksUri);
391
+ assertAccessTokenTyp(token, allowedTyps);
376
392
  const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
377
393
  assertNonEmptySubject(claims);
378
394
  const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
@@ -1,5 +1,5 @@
1
1
  import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
2
- import { d as McpDefinition } from '../../types-C5SSORxe.js';
2
+ import { d as McpDefinition } from '../../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
5
5
  type McpProtocolHandler = (request: Request) => Promise<Response>;
@@ -1,5 +1,5 @@
1
1
  import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
2
- import { d as McpDefinition } from '../../types-C5SSORxe.js';
2
+ import { d as McpDefinition } from '../../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
5
5
  type McpProtocolHandler = (request: Request) => Promise<Response>;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createMcpProtocolHandler
3
- } from "../../chunk-DF3DD3KD.js";
3
+ } from "../../chunk-WLNT2FX7.js";
4
4
  import "../../chunk-MA5H6PSF.js";
5
- import "../../chunk-LAFJDJRY.js";
5
+ import "../../chunk-G4XA7IJM.js";
6
6
  import "../../chunk-QC3DXQTH.js";
7
7
  import "../../chunk-6DXGZZA4.js";
8
8
  export {
@@ -283,6 +283,7 @@ function stringClaim(claims, name) {
283
283
  // src/auth/verifier.ts
284
284
  var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
285
285
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
286
+ var DEFAULT_ACCESS_TOKEN_TYPS = ["at+jwt", "JWT"];
286
287
  var JWKS_FETCH_TIMEOUT_MS = 5e3;
287
288
  var OAuthTokenError = class extends Error {
288
289
  constructor(status, oauthError, message) {
@@ -315,10 +316,23 @@ async function fetchVerificationKeySet(jwksUri) {
315
316
  throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
316
317
  }
317
318
  }
319
+ function assertAccessTokenTyp(token, allowed) {
320
+ let header;
321
+ try {
322
+ header = (0, import_jose.decodeProtectedHeader)(token);
323
+ } catch (err) {
324
+ log.debug("oauth.verify.bad_header", { ...describeError(err), outcome: "401 invalid_token" });
325
+ throw new OAuthTokenError(401, "invalid_token", "Malformed JWT header");
326
+ }
327
+ const typ = header.typ;
328
+ if (typeof typ !== "string" || !allowed.includes(typ)) {
329
+ log.debug("oauth.verify.bad_typ", { jwtTyp: typ, allowed, outcome: "401 invalid_token" });
330
+ throw new OAuthTokenError(401, "invalid_token", "Access token typ header is not accepted");
331
+ }
332
+ }
318
333
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
319
334
  try {
320
335
  const { payload } = await (0, import_jose.jwtVerify)(token, keySet, {
321
- typ: "at+jwt",
322
336
  // `issuer` is trimmed of any trailing slash; accept both forms so a token whose
323
337
  // `iss` carries the slash the AS publishes still verifies.
324
338
  issuer: [issuer, `${issuer}/`],
@@ -367,6 +381,7 @@ function buildMcpAuthContext(args) {
367
381
  };
368
382
  }
369
383
  function createOAuthTokenVerifier(auth, discovery) {
384
+ const allowedTyps = auth.accessTokenTyp ?? DEFAULT_ACCESS_TOKEN_TYPS;
370
385
  return async (token, request, options) => {
371
386
  const resource = resolveProtectedResource(auth, request, options);
372
387
  const issuer = await discovery.resolveIssuer();
@@ -375,6 +390,7 @@ function createOAuthTokenVerifier(auth, discovery) {
375
390
  const jwksUri = await discovery.resolveJwksUri();
376
391
  log.debug("oauth.jwks.fetch", { jwksUri });
377
392
  const keySet = await fetchVerificationKeySet(jwksUri);
393
+ assertAccessTokenTyp(token, allowedTyps);
378
394
  const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
379
395
  assertNonEmptySubject(claims);
380
396
  const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
@@ -1,5 +1,5 @@
1
1
  import { M as McpRuntimeOptions } from '../authorize-HTd0GKmB.js';
2
- import { d as McpDefinition } from '../types-C5SSORxe.js';
2
+ import { d as McpDefinition } from '../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
5
5
  type OAuthProtectedResourceMetadataHandler = (request: Request) => Promise<Response>;
@@ -1,5 +1,5 @@
1
1
  import { M as McpRuntimeOptions } from '../authorize-HTd0GKmB.js';
2
- import { d as McpDefinition } from '../types-C5SSORxe.js';
2
+ import { d as McpDefinition } from '../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
5
5
  type OAuthProtectedResourceMetadataHandler = (request: Request) => Promise<Response>;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createOAuthProtectedResourceMetadataHandler
3
- } from "../chunk-X7V2CL7C.js";
4
- import "../chunk-LAFJDJRY.js";
3
+ } from "../chunk-QDKOF4UF.js";
4
+ import "../chunk-G4XA7IJM.js";
5
5
  import "../chunk-QC3DXQTH.js";
6
6
  import "../chunk-6DXGZZA4.js";
7
7
  export {
@@ -291,6 +291,7 @@ function stringClaim(claims, name) {
291
291
  // src/auth/verifier.ts
292
292
  var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
293
293
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
294
+ var DEFAULT_ACCESS_TOKEN_TYPS = ["at+jwt", "JWT"];
294
295
  var JWKS_FETCH_TIMEOUT_MS = 5e3;
295
296
  var OAuthTokenError = class extends Error {
296
297
  constructor(status, oauthError, message) {
@@ -323,10 +324,23 @@ async function fetchVerificationKeySet(jwksUri) {
323
324
  throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
324
325
  }
325
326
  }
327
+ function assertAccessTokenTyp(token, allowed) {
328
+ let header;
329
+ try {
330
+ header = (0, import_jose.decodeProtectedHeader)(token);
331
+ } catch (err) {
332
+ log.debug("oauth.verify.bad_header", { ...describeError(err), outcome: "401 invalid_token" });
333
+ throw new OAuthTokenError(401, "invalid_token", "Malformed JWT header");
334
+ }
335
+ const typ = header.typ;
336
+ if (typeof typ !== "string" || !allowed.includes(typ)) {
337
+ log.debug("oauth.verify.bad_typ", { jwtTyp: typ, allowed, outcome: "401 invalid_token" });
338
+ throw new OAuthTokenError(401, "invalid_token", "Access token typ header is not accepted");
339
+ }
340
+ }
326
341
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
327
342
  try {
328
343
  const { payload } = await (0, import_jose.jwtVerify)(token, keySet, {
329
- typ: "at+jwt",
330
344
  // `issuer` is trimmed of any trailing slash; accept both forms so a token whose
331
345
  // `iss` carries the slash the AS publishes still verifies.
332
346
  issuer: [issuer, `${issuer}/`],
@@ -375,6 +389,7 @@ function buildMcpAuthContext(args) {
375
389
  };
376
390
  }
377
391
  function createOAuthTokenVerifier(auth, discovery) {
392
+ const allowedTyps = auth.accessTokenTyp ?? DEFAULT_ACCESS_TOKEN_TYPS;
378
393
  return async (token, request, options) => {
379
394
  const resource = resolveProtectedResource(auth, request, options);
380
395
  const issuer = await discovery.resolveIssuer();
@@ -383,6 +398,7 @@ function createOAuthTokenVerifier(auth, discovery) {
383
398
  const jwksUri = await discovery.resolveJwksUri();
384
399
  log.debug("oauth.jwks.fetch", { jwksUri });
385
400
  const keySet = await fetchVerificationKeySet(jwksUri);
401
+ assertAccessTokenTyp(token, allowedTyps);
386
402
  const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
387
403
  assertNonEmptySubject(claims);
388
404
  const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
@@ -1,5 +1,5 @@
1
1
  import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
2
- import { d as McpDefinition } from '../../types-C5SSORxe.js';
2
+ import { d as McpDefinition } from '../../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
5
5
  type RestListToolsHandler = (request: Request) => Promise<Response>;
@@ -1,5 +1,5 @@
1
1
  import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
2
- import { d as McpDefinition } from '../../types-C5SSORxe.js';
2
+ import { d as McpDefinition } from '../../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
5
5
  type RestListToolsHandler = (request: Request) => Promise<Response>;
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createInvokeToolHandler
3
- } from "../../chunk-SZLSAGDF.js";
3
+ } from "../../chunk-YWLMMDET.js";
4
4
  import {
5
5
  createListToolsHandler
6
- } from "../../chunk-PJXKJNSZ.js";
6
+ } from "../../chunk-VNRQPA4K.js";
7
7
  import "../../chunk-MA5H6PSF.js";
8
- import "../../chunk-LAFJDJRY.js";
8
+ import "../../chunk-G4XA7IJM.js";
9
9
  import "../../chunk-QC3DXQTH.js";
10
10
  import "../../chunk-6DXGZZA4.js";
11
11
  export {
@@ -290,6 +290,7 @@ function stringClaim(claims, name) {
290
290
  // src/auth/verifier.ts
291
291
  var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
292
292
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
293
+ var DEFAULT_ACCESS_TOKEN_TYPS = ["at+jwt", "JWT"];
293
294
  var JWKS_FETCH_TIMEOUT_MS = 5e3;
294
295
  var OAuthTokenError = class extends Error {
295
296
  constructor(status, oauthError, message) {
@@ -322,10 +323,23 @@ async function fetchVerificationKeySet(jwksUri) {
322
323
  throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
323
324
  }
324
325
  }
326
+ function assertAccessTokenTyp(token, allowed) {
327
+ let header;
328
+ try {
329
+ header = (0, import_jose.decodeProtectedHeader)(token);
330
+ } catch (err) {
331
+ log.debug("oauth.verify.bad_header", { ...describeError(err), outcome: "401 invalid_token" });
332
+ throw new OAuthTokenError(401, "invalid_token", "Malformed JWT header");
333
+ }
334
+ const typ = header.typ;
335
+ if (typeof typ !== "string" || !allowed.includes(typ)) {
336
+ log.debug("oauth.verify.bad_typ", { jwtTyp: typ, allowed, outcome: "401 invalid_token" });
337
+ throw new OAuthTokenError(401, "invalid_token", "Access token typ header is not accepted");
338
+ }
339
+ }
325
340
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
326
341
  try {
327
342
  const { payload } = await (0, import_jose.jwtVerify)(token, keySet, {
328
- typ: "at+jwt",
329
343
  // `issuer` is trimmed of any trailing slash; accept both forms so a token whose
330
344
  // `iss` carries the slash the AS publishes still verifies.
331
345
  issuer: [issuer, `${issuer}/`],
@@ -374,6 +388,7 @@ function buildMcpAuthContext(args) {
374
388
  };
375
389
  }
376
390
  function createOAuthTokenVerifier(auth, discovery) {
391
+ const allowedTyps = auth.accessTokenTyp ?? DEFAULT_ACCESS_TOKEN_TYPS;
377
392
  return async (token, request, options) => {
378
393
  const resource = resolveProtectedResource(auth, request, options);
379
394
  const issuer = await discovery.resolveIssuer();
@@ -382,6 +397,7 @@ function createOAuthTokenVerifier(auth, discovery) {
382
397
  const jwksUri = await discovery.resolveJwksUri();
383
398
  log.debug("oauth.jwks.fetch", { jwksUri });
384
399
  const keySet = await fetchVerificationKeySet(jwksUri);
400
+ assertAccessTokenTyp(token, allowedTyps);
385
401
  const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
386
402
  assertNonEmptySubject(claims);
387
403
  const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
@@ -1,4 +1,4 @@
1
- import { d as McpDefinition } from '../../types-C5SSORxe.js';
1
+ import { d as McpDefinition } from '../../types-BanzncFh.js';
2
2
  import 'zod';
3
3
 
4
4
  type SupabaseHandler = (request: Request) => Promise<Response>;
@@ -1,4 +1,4 @@
1
- import { d as McpDefinition } from '../../types-C5SSORxe.js';
1
+ import { d as McpDefinition } from '../../types-BanzncFh.js';
2
2
  import 'zod';
3
3
 
4
4
  type SupabaseHandler = (request: Request) => Promise<Response>;
@@ -1,19 +1,19 @@
1
1
  import {
2
2
  createMcpProtocolHandler
3
- } from "../../chunk-DF3DD3KD.js";
3
+ } from "../../chunk-WLNT2FX7.js";
4
4
  import {
5
5
  createOAuthProtectedResourceMetadataHandler
6
- } from "../../chunk-X7V2CL7C.js";
6
+ } from "../../chunk-QDKOF4UF.js";
7
7
  import {
8
8
  createInvokeToolHandler
9
- } from "../../chunk-SZLSAGDF.js";
9
+ } from "../../chunk-YWLMMDET.js";
10
10
  import {
11
11
  createListToolsHandler
12
- } from "../../chunk-PJXKJNSZ.js";
12
+ } from "../../chunk-VNRQPA4K.js";
13
13
  import "../../chunk-MA5H6PSF.js";
14
14
  import {
15
15
  assertResourcePathShape
16
- } from "../../chunk-LAFJDJRY.js";
16
+ } from "../../chunk-G4XA7IJM.js";
17
17
  import {
18
18
  trimTrailingSlash
19
19
  } from "../../chunk-QC3DXQTH.js";
@@ -27,11 +27,12 @@ module.exports = __toCommonJS(vite_exports);
27
27
  var import_node_path2 = require("path");
28
28
 
29
29
  // src/stacks/supabase/emit.ts
30
+ var import_esbuild = require("esbuild");
30
31
  var import_node_fs = require("fs");
31
32
  var import_node_path = require("path");
32
33
 
33
34
  // package.json
34
- var version = "0.10.1";
35
+ var version = "0.12.0";
35
36
 
36
37
  // src/core/fs-errors.ts
37
38
  function isFileMissing(err) {
@@ -54,10 +55,6 @@ var DEFAULT_FUNCTIONS_DIR = "supabase/functions";
54
55
  var DEFAULT_FUNCTION_NAME = "mcp";
55
56
  var GENERATED_BANNER = "// AUTO-GENERATED by @lovable.dev/mcp-js \u2014 do not edit. Regenerated by the Vite plugin.\n// To take ownership, delete this banner line; the plugin then leaves the file alone.";
56
57
  var GENERATED_JSON_MARKER_KEY = "_lovableMcpGenerated";
57
- var GENERATED_JSON_MARKER_VALUE = "AUTO-GENERATED by @lovable.dev/mcp-js \u2014 do not edit. Regenerated by the Vite plugin. To take ownership, remove this key; the plugin then leaves the file alone.";
58
- function normalizePath(p) {
59
- return p.split(import_node_path.sep).join("/");
60
- }
61
58
  function fileExists(path) {
62
59
  try {
63
60
  (0, import_node_fs.lstatSync)(path);
@@ -100,32 +97,63 @@ function writeIfChanged(file, content, ownershipCheck) {
100
97
  (0, import_node_fs.writeFileSync)(file, content, "utf8");
101
98
  return true;
102
99
  }
103
- function relativeTsSpecifier(fromFile, toFile) {
104
- const rel = normalizePath((0, import_node_path.relative)((0, import_node_path.dirname)(fromFile), toFile));
105
- return rel.startsWith(".") ? rel : `./${rel}`;
100
+ function readProjectDependencyVersions(projectRoot) {
101
+ try {
102
+ const pkg = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.join)(projectRoot, "package.json"), "utf8"));
103
+ return { ...pkg.peerDependencies, ...pkg.devDependencies, ...pkg.dependencies };
104
+ } catch {
105
+ return {};
106
+ }
106
107
  }
107
- function buildFunctionEntry(mcpEntryAbs, functionEntryAbs, functionName) {
108
- const entryImport = relativeTsSpecifier(functionEntryAbs, mcpEntryAbs);
109
- return `${GENERATED_BANNER}
110
- // supabase function: ${functionName}
111
- // emitted to: supabase/functions/${functionName}/index.ts
112
-
113
- import { createSupabaseHandler } from "npm:@lovable.dev/mcp-js@${version}/stacks/supabase";
114
-
115
- import mcp from "${entryImport}";
116
-
117
- Deno.serve(createSupabaseHandler(mcp, { functionName: ${JSON.stringify(functionName)} }));
118
- `;
108
+ function npmSpecifierFor(bare, versions) {
109
+ const pkg = bare.startsWith("@") ? bare.split("/").slice(0, 2).join("/") : bare.split("/")[0];
110
+ const subpath = bare.slice(pkg.length);
111
+ if (pkg === "@lovable.dev/mcp-js")
112
+ return `npm:${pkg}@${version}${subpath}`;
113
+ const range = versions[pkg];
114
+ const version2 = range && /^[\dv^~>=]|^latest$/.test(range) ? range : void 0;
115
+ return version2 ? `npm:${pkg}@${version2}${subpath}` : `npm:${pkg}${subpath}`;
119
116
  }
120
- function buildDenoJson() {
121
- const config = {
122
- [GENERATED_JSON_MARKER_KEY]: GENERATED_JSON_MARKER_VALUE,
123
- imports: {
124
- "@lovable.dev/mcp-js": `npm:@lovable.dev/mcp-js@${version}`
117
+ function externalizeBareAsNpm(versions) {
118
+ return {
119
+ name: "lovable-mcp-externalize-bare-as-npm",
120
+ setup(pluginBuild) {
121
+ pluginBuild.onResolve({ filter: /.*/ }, (args) => {
122
+ const p = args.path;
123
+ if (p.startsWith(".") || p.startsWith("/"))
124
+ return null;
125
+ if (/^(npm|node|jsr|https?|data):/.test(p))
126
+ return { path: p, external: true };
127
+ return { path: npmSpecifierFor(p, versions), external: true };
128
+ });
125
129
  }
126
130
  };
127
- return `${JSON.stringify(config, null, 2)}
131
+ }
132
+ async function bundleFunctionEntry(projectRoot, mcpEntryAbs, functionName) {
133
+ const versions = readProjectDependencyVersions(projectRoot);
134
+ const wrapper = `import mcp from ${JSON.stringify(mcpEntryAbs)};
135
+ import { createSupabaseHandler } from "@lovable.dev/mcp-js/stacks/supabase";
136
+ Deno.serve(createSupabaseHandler(mcp, { functionName: ${JSON.stringify(functionName)} }));
128
137
  `;
138
+ const result = await (0, import_esbuild.build)({
139
+ stdin: { contents: wrapper, resolveDir: projectRoot, sourcefile: "lovable-mcp-supabase-entry.ts", loader: "ts" },
140
+ bundle: true,
141
+ write: false,
142
+ metafile: true,
143
+ format: "esm",
144
+ platform: "neutral",
145
+ target: "esnext",
146
+ absWorkingDir: projectRoot,
147
+ logLevel: "silent",
148
+ plugins: [externalizeBareAsNpm(versions)]
149
+ });
150
+ const out = result.outputFiles[0]?.text ?? "";
151
+ const inputs = Object.keys(result.metafile?.inputs ?? {}).filter((p) => !p.startsWith("<") && !p.startsWith("npm:")).map((p) => (0, import_node_path.resolve)(projectRoot, p));
152
+ const code = `${GENERATED_BANNER}
153
+ // supabase function: ${functionName}
154
+ // Bundled from ${(0, import_node_path.relative)(projectRoot, mcpEntryAbs).split(import_node_path.sep).join("/")} by @lovable.dev/mcp-js.
155
+ ` + out;
156
+ return { code, inputs };
129
157
  }
130
158
  function resolvePaths(options) {
131
159
  const projectRoot = (0, import_node_path.resolve)(options.projectRoot);
@@ -142,31 +170,25 @@ function resolvePaths(options) {
142
170
  return {
143
171
  functionDir,
144
172
  functionEntry: (0, import_node_path.join)(functionDir, "index.ts"),
145
- denoJson: (0, import_node_path.join)(functionDir, "deno.json"),
146
173
  mcpEntryAbs: (0, import_node_path.resolve)(projectRoot, mcpEntry)
147
174
  };
148
175
  }
149
- function syncSupabaseFunction(options) {
176
+ async function syncSupabaseFunction(options) {
177
+ const projectRoot = (0, import_node_path.resolve)(options.projectRoot);
150
178
  const paths = resolvePaths(options);
151
179
  const functionName = options.functionName ?? DEFAULT_FUNCTION_NAME;
152
180
  const written = [];
153
181
  const removed = [];
154
182
  if (!fileExists(paths.mcpEntryAbs)) {
155
183
  cleanupOrphans(paths.functionDir, /* @__PURE__ */ new Set(), removed);
156
- return { written, removed };
184
+ return { written, removed, inputs: [] };
157
185
  }
158
- if (writeIfChanged(
159
- paths.functionEntry,
160
- buildFunctionEntry(paths.mcpEntryAbs, paths.functionEntry, functionName),
161
- hasGeneratedBanner
162
- )) {
186
+ const { code, inputs } = await bundleFunctionEntry(projectRoot, paths.mcpEntryAbs, functionName);
187
+ if (writeIfChanged(paths.functionEntry, code, hasGeneratedBanner)) {
163
188
  written.push(paths.functionEntry);
164
189
  }
165
- if (writeIfChanged(paths.denoJson, buildDenoJson(), hasGeneratedJsonMarker)) {
166
- written.push(paths.denoJson);
167
- }
168
- cleanupOrphans(paths.functionDir, /* @__PURE__ */ new Set([paths.functionEntry, paths.denoJson]), removed);
169
- return { written, removed };
190
+ cleanupOrphans(paths.functionDir, /* @__PURE__ */ new Set([paths.functionEntry]), removed);
191
+ return { written, removed, inputs };
170
192
  }
171
193
  function cleanupOrphans(functionDir, expected, removed) {
172
194
  let entries;
@@ -202,7 +224,7 @@ function cleanupOrphans(functionDir, expected, removed) {
202
224
  }
203
225
 
204
226
  // src/stacks/supabase/vite.ts
205
- function normalizePath2(p) {
227
+ function normalizePath(p) {
206
228
  return p.split(import_node_path2.sep).join("/");
207
229
  }
208
230
  function mcpPlugin(options = {}) {
@@ -212,13 +234,15 @@ function mcpPlugin(options = {}) {
212
234
  const urlPath = `${FUNCTIONS_MOUNT_PREFIX}${functionName}`;
213
235
  let projectRoot = process.cwd();
214
236
  let mcpEntryAbs = (0, import_node_path2.resolve)(projectRoot, mcpEntryOption);
215
- const regenerate = () => {
216
- syncSupabaseFunction({
237
+ let watchedInputs = /* @__PURE__ */ new Set();
238
+ const regenerate = async () => {
239
+ const { inputs } = await syncSupabaseFunction({
217
240
  projectRoot,
218
241
  mcpEntry: mcpEntryOption,
219
242
  functionsDir: functionsDirOption,
220
243
  functionName
221
244
  });
245
+ watchedInputs = new Set(inputs.map(normalizePath));
222
246
  };
223
247
  return {
224
248
  name: "@lovable.dev/mcp-js/supabase",
@@ -231,16 +255,23 @@ function mcpPlugin(options = {}) {
231
255
  },
232
256
  urlPath
233
257
  },
234
- configResolved(config) {
258
+ async configResolved(config) {
235
259
  projectRoot = config.root;
236
260
  mcpEntryAbs = (0, import_node_path2.resolve)(projectRoot, mcpEntryOption);
237
- regenerate();
261
+ await regenerate();
238
262
  },
239
263
  configureServer(server) {
240
264
  const onChange = (file) => {
241
- if (normalizePath2(file) === normalizePath2(mcpEntryAbs)) {
242
- regenerate();
243
- }
265
+ const norm = normalizePath(file);
266
+ if (norm !== normalizePath(mcpEntryAbs) && !watchedInputs.has(norm))
267
+ return;
268
+ regenerate().catch((err) => {
269
+ const message = `[@lovable.dev/mcp-js] failed to regenerate the Supabase function: ${err instanceof Error ? err.message : String(err)}`;
270
+ if (server.config?.logger?.error)
271
+ server.config.logger.error(message);
272
+ else
273
+ console.error(message);
274
+ });
244
275
  };
245
276
  server.watcher.on("add", onChange);
246
277
  server.watcher.on("change", onChange);
@@ -251,8 +282,8 @@ function mcpPlugin(options = {}) {
251
282
  server.watcher.off("unlink", onChange);
252
283
  });
253
284
  },
254
- buildStart() {
255
- regenerate();
285
+ async buildStart() {
286
+ await regenerate();
256
287
  }
257
288
  };
258
289
  }
@@ -34,11 +34,14 @@ interface McpPluginOptions {
34
34
  * Lifecycle, mirroring `stacks/tanstack/vite`:
35
35
  * - `configResolved` finalizes the project root and runs the first emit.
36
36
  * - `buildStart` re-runs the emit so a `vite build` always picks up the
37
- * latest entry.
38
- * - `configureServer` re-emits when the entry file appears, changes, or
39
- * is removed during `vite dev`, so a developer editing tools sees the
40
- * function regenerate without restarting the server.
37
+ * latest source.
38
+ * - `configureServer` re-emits when the entry or any bundled source file
39
+ * (tools, local helpers) changes during `vite dev`, so editing a tool
40
+ * regenerates the function without restarting the server.
41
41
  *
42
+ * The entry and its local import graph are bundled by esbuild into one
43
+ * self-contained Deno module (deps left as `npm:` specifiers), so the deployed
44
+ * function carries no cross-project imports the Supabase deploy can't upload.
42
45
  * Idempotent: `syncSupabaseFunction` writes only on byte-level change and
43
46
  * cleans up SDK-owned files when the entry is deleted.
44
47
  */
@@ -34,11 +34,14 @@ interface McpPluginOptions {
34
34
  * Lifecycle, mirroring `stacks/tanstack/vite`:
35
35
  * - `configResolved` finalizes the project root and runs the first emit.
36
36
  * - `buildStart` re-runs the emit so a `vite build` always picks up the
37
- * latest entry.
38
- * - `configureServer` re-emits when the entry file appears, changes, or
39
- * is removed during `vite dev`, so a developer editing tools sees the
40
- * function regenerate without restarting the server.
37
+ * latest source.
38
+ * - `configureServer` re-emits when the entry or any bundled source file
39
+ * (tools, local helpers) changes during `vite dev`, so editing a tool
40
+ * regenerates the function without restarting the server.
41
41
  *
42
+ * The entry and its local import graph are bundled by esbuild into one
43
+ * self-contained Deno module (deps left as `npm:` specifiers), so the deployed
44
+ * function carries no cross-project imports the Supabase deploy can't upload.
42
45
  * Idempotent: `syncSupabaseFunction` writes only on byte-level change and
43
46
  * cleans up SDK-owned files when the entry is deleted.
44
47
  */
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  version
3
- } from "../../chunk-D6KB4Q7R.js";
3
+ } from "../../chunk-S3YHTZKJ.js";
4
4
  import {
5
5
  isFileMissing
6
6
  } from "../../chunk-Y3ZFPEQH.js";
@@ -13,6 +13,7 @@ import {
13
13
  import { resolve as resolve2, sep as sep2 } from "path";
14
14
 
15
15
  // src/stacks/supabase/emit.ts
16
+ import { build } from "esbuild";
16
17
  import { lstatSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
17
18
  import { dirname, join, relative, resolve, sep } from "path";
18
19
  var DEFAULT_MCP_ENTRY = "src/lib/mcp/index.ts";
@@ -20,10 +21,6 @@ var DEFAULT_FUNCTIONS_DIR = "supabase/functions";
20
21
  var DEFAULT_FUNCTION_NAME = "mcp";
21
22
  var GENERATED_BANNER = "// AUTO-GENERATED by @lovable.dev/mcp-js \u2014 do not edit. Regenerated by the Vite plugin.\n// To take ownership, delete this banner line; the plugin then leaves the file alone.";
22
23
  var GENERATED_JSON_MARKER_KEY = "_lovableMcpGenerated";
23
- var GENERATED_JSON_MARKER_VALUE = "AUTO-GENERATED by @lovable.dev/mcp-js \u2014 do not edit. Regenerated by the Vite plugin. To take ownership, remove this key; the plugin then leaves the file alone.";
24
- function normalizePath(p) {
25
- return p.split(sep).join("/");
26
- }
27
24
  function fileExists(path) {
28
25
  try {
29
26
  lstatSync(path);
@@ -66,32 +63,63 @@ function writeIfChanged(file, content, ownershipCheck) {
66
63
  writeFileSync(file, content, "utf8");
67
64
  return true;
68
65
  }
69
- function relativeTsSpecifier(fromFile, toFile) {
70
- const rel = normalizePath(relative(dirname(fromFile), toFile));
71
- return rel.startsWith(".") ? rel : `./${rel}`;
66
+ function readProjectDependencyVersions(projectRoot) {
67
+ try {
68
+ const pkg = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf8"));
69
+ return { ...pkg.peerDependencies, ...pkg.devDependencies, ...pkg.dependencies };
70
+ } catch {
71
+ return {};
72
+ }
72
73
  }
73
- function buildFunctionEntry(mcpEntryAbs, functionEntryAbs, functionName) {
74
- const entryImport = relativeTsSpecifier(functionEntryAbs, mcpEntryAbs);
75
- return `${GENERATED_BANNER}
76
- // supabase function: ${functionName}
77
- // emitted to: supabase/functions/${functionName}/index.ts
78
-
79
- import { createSupabaseHandler } from "npm:@lovable.dev/mcp-js@${version}/stacks/supabase";
80
-
81
- import mcp from "${entryImport}";
82
-
83
- Deno.serve(createSupabaseHandler(mcp, { functionName: ${JSON.stringify(functionName)} }));
84
- `;
74
+ function npmSpecifierFor(bare, versions) {
75
+ const pkg = bare.startsWith("@") ? bare.split("/").slice(0, 2).join("/") : bare.split("/")[0];
76
+ const subpath = bare.slice(pkg.length);
77
+ if (pkg === "@lovable.dev/mcp-js")
78
+ return `npm:${pkg}@${version}${subpath}`;
79
+ const range = versions[pkg];
80
+ const version2 = range && /^[\dv^~>=]|^latest$/.test(range) ? range : void 0;
81
+ return version2 ? `npm:${pkg}@${version2}${subpath}` : `npm:${pkg}${subpath}`;
85
82
  }
86
- function buildDenoJson() {
87
- const config = {
88
- [GENERATED_JSON_MARKER_KEY]: GENERATED_JSON_MARKER_VALUE,
89
- imports: {
90
- "@lovable.dev/mcp-js": `npm:@lovable.dev/mcp-js@${version}`
83
+ function externalizeBareAsNpm(versions) {
84
+ return {
85
+ name: "lovable-mcp-externalize-bare-as-npm",
86
+ setup(pluginBuild) {
87
+ pluginBuild.onResolve({ filter: /.*/ }, (args) => {
88
+ const p = args.path;
89
+ if (p.startsWith(".") || p.startsWith("/"))
90
+ return null;
91
+ if (/^(npm|node|jsr|https?|data):/.test(p))
92
+ return { path: p, external: true };
93
+ return { path: npmSpecifierFor(p, versions), external: true };
94
+ });
91
95
  }
92
96
  };
93
- return `${JSON.stringify(config, null, 2)}
97
+ }
98
+ async function bundleFunctionEntry(projectRoot, mcpEntryAbs, functionName) {
99
+ const versions = readProjectDependencyVersions(projectRoot);
100
+ const wrapper = `import mcp from ${JSON.stringify(mcpEntryAbs)};
101
+ import { createSupabaseHandler } from "@lovable.dev/mcp-js/stacks/supabase";
102
+ Deno.serve(createSupabaseHandler(mcp, { functionName: ${JSON.stringify(functionName)} }));
94
103
  `;
104
+ const result = await build({
105
+ stdin: { contents: wrapper, resolveDir: projectRoot, sourcefile: "lovable-mcp-supabase-entry.ts", loader: "ts" },
106
+ bundle: true,
107
+ write: false,
108
+ metafile: true,
109
+ format: "esm",
110
+ platform: "neutral",
111
+ target: "esnext",
112
+ absWorkingDir: projectRoot,
113
+ logLevel: "silent",
114
+ plugins: [externalizeBareAsNpm(versions)]
115
+ });
116
+ const out = result.outputFiles[0]?.text ?? "";
117
+ const inputs = Object.keys(result.metafile?.inputs ?? {}).filter((p) => !p.startsWith("<") && !p.startsWith("npm:")).map((p) => resolve(projectRoot, p));
118
+ const code = `${GENERATED_BANNER}
119
+ // supabase function: ${functionName}
120
+ // Bundled from ${relative(projectRoot, mcpEntryAbs).split(sep).join("/")} by @lovable.dev/mcp-js.
121
+ ` + out;
122
+ return { code, inputs };
95
123
  }
96
124
  function resolvePaths(options) {
97
125
  const projectRoot = resolve(options.projectRoot);
@@ -108,31 +136,25 @@ function resolvePaths(options) {
108
136
  return {
109
137
  functionDir,
110
138
  functionEntry: join(functionDir, "index.ts"),
111
- denoJson: join(functionDir, "deno.json"),
112
139
  mcpEntryAbs: resolve(projectRoot, mcpEntry)
113
140
  };
114
141
  }
115
- function syncSupabaseFunction(options) {
142
+ async function syncSupabaseFunction(options) {
143
+ const projectRoot = resolve(options.projectRoot);
116
144
  const paths = resolvePaths(options);
117
145
  const functionName = options.functionName ?? DEFAULT_FUNCTION_NAME;
118
146
  const written = [];
119
147
  const removed = [];
120
148
  if (!fileExists(paths.mcpEntryAbs)) {
121
149
  cleanupOrphans(paths.functionDir, /* @__PURE__ */ new Set(), removed);
122
- return { written, removed };
150
+ return { written, removed, inputs: [] };
123
151
  }
124
- if (writeIfChanged(
125
- paths.functionEntry,
126
- buildFunctionEntry(paths.mcpEntryAbs, paths.functionEntry, functionName),
127
- hasGeneratedBanner
128
- )) {
152
+ const { code, inputs } = await bundleFunctionEntry(projectRoot, paths.mcpEntryAbs, functionName);
153
+ if (writeIfChanged(paths.functionEntry, code, hasGeneratedBanner)) {
129
154
  written.push(paths.functionEntry);
130
155
  }
131
- if (writeIfChanged(paths.denoJson, buildDenoJson(), hasGeneratedJsonMarker)) {
132
- written.push(paths.denoJson);
133
- }
134
- cleanupOrphans(paths.functionDir, /* @__PURE__ */ new Set([paths.functionEntry, paths.denoJson]), removed);
135
- return { written, removed };
156
+ cleanupOrphans(paths.functionDir, /* @__PURE__ */ new Set([paths.functionEntry]), removed);
157
+ return { written, removed, inputs };
136
158
  }
137
159
  function cleanupOrphans(functionDir, expected, removed) {
138
160
  let entries;
@@ -168,7 +190,7 @@ function cleanupOrphans(functionDir, expected, removed) {
168
190
  }
169
191
 
170
192
  // src/stacks/supabase/vite.ts
171
- function normalizePath2(p) {
193
+ function normalizePath(p) {
172
194
  return p.split(sep2).join("/");
173
195
  }
174
196
  function mcpPlugin(options = {}) {
@@ -178,13 +200,15 @@ function mcpPlugin(options = {}) {
178
200
  const urlPath = `${FUNCTIONS_MOUNT_PREFIX}${functionName}`;
179
201
  let projectRoot = process.cwd();
180
202
  let mcpEntryAbs = resolve2(projectRoot, mcpEntryOption);
181
- const regenerate = () => {
182
- syncSupabaseFunction({
203
+ let watchedInputs = /* @__PURE__ */ new Set();
204
+ const regenerate = async () => {
205
+ const { inputs } = await syncSupabaseFunction({
183
206
  projectRoot,
184
207
  mcpEntry: mcpEntryOption,
185
208
  functionsDir: functionsDirOption,
186
209
  functionName
187
210
  });
211
+ watchedInputs = new Set(inputs.map(normalizePath));
188
212
  };
189
213
  return {
190
214
  name: "@lovable.dev/mcp-js/supabase",
@@ -197,16 +221,23 @@ function mcpPlugin(options = {}) {
197
221
  },
198
222
  urlPath
199
223
  },
200
- configResolved(config) {
224
+ async configResolved(config) {
201
225
  projectRoot = config.root;
202
226
  mcpEntryAbs = resolve2(projectRoot, mcpEntryOption);
203
- regenerate();
227
+ await regenerate();
204
228
  },
205
229
  configureServer(server) {
206
230
  const onChange = (file) => {
207
- if (normalizePath2(file) === normalizePath2(mcpEntryAbs)) {
208
- regenerate();
209
- }
231
+ const norm = normalizePath(file);
232
+ if (norm !== normalizePath(mcpEntryAbs) && !watchedInputs.has(norm))
233
+ return;
234
+ regenerate().catch((err) => {
235
+ const message = `[@lovable.dev/mcp-js] failed to regenerate the Supabase function: ${err instanceof Error ? err.message : String(err)}`;
236
+ if (server.config?.logger?.error)
237
+ server.config.logger.error(message);
238
+ else
239
+ console.error(message);
240
+ });
210
241
  };
211
242
  server.watcher.on("add", onChange);
212
243
  server.watcher.on("change", onChange);
@@ -217,8 +248,8 @@ function mcpPlugin(options = {}) {
217
248
  server.watcher.off("unlink", onChange);
218
249
  });
219
250
  },
220
- buildStart() {
221
- regenerate();
251
+ async buildStart() {
252
+ await regenerate();
222
253
  }
223
254
  };
224
255
  }
@@ -293,6 +293,7 @@ function stringClaim(claims, name) {
293
293
  // src/auth/verifier.ts
294
294
  var DEFAULT_JWT_ALGORITHMS = ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"];
295
295
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
296
+ var DEFAULT_ACCESS_TOKEN_TYPS = ["at+jwt", "JWT"];
296
297
  var JWKS_FETCH_TIMEOUT_MS = 5e3;
297
298
  var OAuthTokenError = class extends Error {
298
299
  constructor(status, oauthError, message) {
@@ -325,10 +326,23 @@ async function fetchVerificationKeySet(jwksUri) {
325
326
  throw new OAuthConfigurationError(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
326
327
  }
327
328
  }
329
+ function assertAccessTokenTyp(token, allowed) {
330
+ let header;
331
+ try {
332
+ header = (0, import_jose.decodeProtectedHeader)(token);
333
+ } catch (err) {
334
+ log.debug("oauth.verify.bad_header", { ...describeError(err), outcome: "401 invalid_token" });
335
+ throw new OAuthTokenError(401, "invalid_token", "Malformed JWT header");
336
+ }
337
+ const typ = header.typ;
338
+ if (typeof typ !== "string" || !allowed.includes(typ)) {
339
+ log.debug("oauth.verify.bad_typ", { jwtTyp: typ, allowed, outcome: "401 invalid_token" });
340
+ throw new OAuthTokenError(401, "invalid_token", "Access token typ header is not accepted");
341
+ }
342
+ }
328
343
  async function verifyJwtClaims(token, keySet, issuer, audience, auth) {
329
344
  try {
330
345
  const { payload } = await (0, import_jose.jwtVerify)(token, keySet, {
331
- typ: "at+jwt",
332
346
  // `issuer` is trimmed of any trailing slash; accept both forms so a token whose
333
347
  // `iss` carries the slash the AS publishes still verifies.
334
348
  issuer: [issuer, `${issuer}/`],
@@ -377,6 +391,7 @@ function buildMcpAuthContext(args) {
377
391
  };
378
392
  }
379
393
  function createOAuthTokenVerifier(auth, discovery) {
394
+ const allowedTyps = auth.accessTokenTyp ?? DEFAULT_ACCESS_TOKEN_TYPS;
380
395
  return async (token, request, options) => {
381
396
  const resource = resolveProtectedResource(auth, request, options);
382
397
  const issuer = await discovery.resolveIssuer();
@@ -385,6 +400,7 @@ function createOAuthTokenVerifier(auth, discovery) {
385
400
  const jwksUri = await discovery.resolveJwksUri();
386
401
  log.debug("oauth.jwks.fetch", { jwksUri });
387
402
  const keySet = await fetchVerificationKeySet(jwksUri);
403
+ assertAccessTokenTyp(token, allowedTyps);
388
404
  const claims = await verifyJwtClaims(token, keySet, issuer, acceptedAudiences, auth);
389
405
  assertNonEmptySubject(claims);
390
406
  const context = buildMcpAuthContext({ token, claims, issuer, resource, acceptedAudiences });
@@ -1,5 +1,5 @@
1
1
  import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
2
- import { d as McpDefinition } from '../../types-C5SSORxe.js';
2
+ import { d as McpDefinition } from '../../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
5
5
  interface TanStackRouteCtx {
@@ -1,5 +1,5 @@
1
1
  import { M as McpRuntimeOptions } from '../../authorize-HTd0GKmB.js';
2
- import { d as McpDefinition } from '../../types-C5SSORxe.js';
2
+ import { d as McpDefinition } from '../../types-BanzncFh.js';
3
3
  import 'zod';
4
4
 
5
5
  interface TanStackRouteCtx {
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  createMcpProtocolHandler
3
- } from "../../chunk-DF3DD3KD.js";
3
+ } from "../../chunk-WLNT2FX7.js";
4
4
  import {
5
5
  createOAuthProtectedResourceMetadataHandler
6
- } from "../../chunk-X7V2CL7C.js";
6
+ } from "../../chunk-QDKOF4UF.js";
7
7
  import {
8
8
  createInvokeToolHandler
9
- } from "../../chunk-SZLSAGDF.js";
9
+ } from "../../chunk-YWLMMDET.js";
10
10
  import {
11
11
  createListToolsHandler
12
- } from "../../chunk-PJXKJNSZ.js";
12
+ } from "../../chunk-VNRQPA4K.js";
13
13
  import "../../chunk-MA5H6PSF.js";
14
- import "../../chunk-LAFJDJRY.js";
14
+ import "../../chunk-G4XA7IJM.js";
15
15
  import "../../chunk-QC3DXQTH.js";
16
16
  import "../../chunk-6DXGZZA4.js";
17
17
 
@@ -64,6 +64,14 @@ interface McpAuthConfig {
64
64
  readonly jwksUri?: string;
65
65
  readonly algorithms?: readonly string[];
66
66
  readonly clockToleranceSeconds?: number;
67
+ /**
68
+ * Allowed values for the access token's JOSE `typ` header. Defaults to
69
+ * `["at+jwt", "JWT"]` so both the RFC 9068 access-token shape and the plain
70
+ * user-JWT shape (notably Supabase GoTrue's OAuth tokens) are accepted out of
71
+ * the box. Pin to `["at+jwt"]` to enforce strict RFC 9068. The header `typ`
72
+ * must be present and string-typed; this option only controls which values pass.
73
+ */
74
+ readonly accessTokenTyp?: readonly string[];
67
75
  }
68
76
 
69
77
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovable.dev/mcp-js",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "Author MCP servers for Lovable apps. Declare tools with defineTool, register them in defineMcp, and a framework adapter (TanStack or Supabase Edge Functions) emits the route(s) at build time.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -76,6 +76,7 @@
76
76
  "license": "MIT",
77
77
  "dependencies": {
78
78
  "@modelcontextprotocol/sdk": "1.28.0",
79
+ "esbuild": "^0.27.0",
79
80
  "jose": "^6.2.2"
80
81
  },
81
82
  "peerDependencies": {