@better-auth/mcp 1.7.21 → 1.7.23

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/dist/index.d.mts CHANGED
@@ -1,9 +1,8 @@
1
1
  import { OAuthOptions, Scope, oauthProvider } from "@better-auth/oauth-provider";
2
2
  import { DpopReplayReservations, DpopReplayStore } from "better-auth/oauth2";
3
- import { Awaitable } from "@better-auth/core";
3
+ import { AuthContext, Awaitable } from "@better-auth/core";
4
4
  import { JWTPayload, JWTVerifyOptions } from "jose";
5
5
  import { BetterAuthOptions } from "better-auth/types";
6
- //#region src/handler.d.ts
7
6
  interface McpProtectedRequestHandlerOptions {
8
7
  /** Expected authorization-server issuer for the access token. */
9
8
  issuer: string;
@@ -37,9 +36,21 @@ interface McpProtectedRequestHandlerOptions {
37
36
  dpop?: {
38
37
  proofMaxAgeSeconds?: number;
39
38
  signingAlgorithms?: readonly string[];
39
+ /**
40
+ * Store that rejects a DPoP proof that was already used.
41
+ *
42
+ * Unlike `requireMcpAuth`, this handler has no auth instance to store
43
+ * proofs in, so it defaults to a process-local in-memory store. That is
44
+ * safe only for a single long-lived instance. For multiple instances or
45
+ * serverless, pass a shared store, such as
46
+ * `createDpopReplayStore((await auth.$context).internalAdapter)` from
47
+ * `better-auth/oauth2`. With secondary storage, that store also needs
48
+ * `verification.storeInDatabase: true`, or every DPoP request fails.
49
+ */
40
50
  replayStore?: DpopReplayStore;
41
51
  };
42
52
  }
53
+ type McpRequestHandler = (request: Request, accessTokenClaims: JWTPayload) => Awaitable<Response>;
43
54
  /**
44
55
  * A request middleware handler that verifies an MCP access token and responds
45
56
  * with an RFC 9728 `WWW-Authenticate` header for unauthenticated requests.
@@ -48,9 +59,7 @@ interface McpProtectedRequestHandlerOptions {
48
59
  *
49
60
  * @external
50
61
  */
51
- export declare const createMcpProtectedRequestHandler: (options: McpProtectedRequestHandlerOptions, handler: (request: Request, accessTokenClaims: JWTPayload) => Awaitable<Response>) => (request: Request) => Promise<Response>;
52
- //#endregion
53
- //#region src/plugin.d.ts
62
+ export declare const createMcpProtectedRequestHandler: (options: McpProtectedRequestHandlerOptions, handler: McpRequestHandler) => (request: Request) => Promise<Response>;
54
63
  /**
55
64
  * Options for the {@link mcp} plugin: the full OAuth provider configuration plus
56
65
  * the MCP resource identifier.
@@ -125,8 +134,6 @@ interface McpOptions extends OAuthOptions<Scope[]> {
125
134
  * ```
126
135
  */
127
136
  export declare const mcp: (options: McpOptions) => ReturnType<typeof oauthProvider>;
128
- //#endregion
129
- //#region src/require-mcp-auth.d.ts
130
137
  interface RequireMcpAuthOptions {
131
138
  /**
132
139
  * The protected resource identifier the access token must be bound to.
@@ -139,8 +146,9 @@ interface RequireMcpAuthOptions {
139
146
  */
140
147
  issuer?: string;
141
148
  /**
142
- * URL of the authorization server's JWKS. Defaults to `/jwks` under the
143
- * server's resolved base URL.
149
+ * URL of the authorization server's JWKS. When unset, the signing keys are
150
+ * read from the auth instance in-process, without an HTTP request, or
151
+ * fetched from the JWT plugin's `jwks.remoteUrl` when it sets one.
144
152
  */
145
153
  jwksUrl?: string;
146
154
  /**
@@ -189,10 +197,8 @@ interface RequireMcpAuthOptions {
189
197
  */
190
198
  export declare const requireMcpAuth: <Auth extends {
191
199
  options: BetterAuthOptions;
192
- $context: Promise<{
193
- baseURL: string;
200
+ $context: Promise<Pick<AuthContext, "baseURL" | "getPlugin"> & {
194
201
  internalAdapter: DpopReplayReservations;
195
202
  }>;
196
203
  }>(auth: Auth, handler: (request: Request, accessTokenClaims: JWTPayload) => Awaitable<Response>, opts?: RequireMcpAuthOptions) => (req: Request) => Promise<Response>;
197
- //#endregion
198
204
  export type { McpOptions, McpProtectedRequestHandlerOptions, RequireMcpAuthOptions };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { isLoopbackHost } from "@better-auth/core/utils/host";
2
2
  import { createResourceServerChallenge, getIssuer, metadataResponse, oauthProvider } from "@better-auth/oauth-provider";
3
3
  import { DPOP_SIGNING_ALGORITHMS, createDpopReplayStore, isInsufficientScopeError, requestToResourceInput, verifyAccessTokenRequest } from "better-auth/oauth2";
4
- //#region src/handler.ts
4
+ // src/handler.ts
5
5
  /**
6
6
  * Validates the canonical protected-resource identifier accepted by MCP.
7
7
  *
@@ -46,7 +46,14 @@ function toChallengeResponse(error, resource, opts) {
46
46
  *
47
47
  * @external
48
48
  */
49
- const createMcpProtectedRequestHandler = (options, handler) => {
49
+ const createMcpProtectedRequestHandler = (options, handler) => createProtectedRequestHandler(options, handler);
50
+ /**
51
+ * {@link createMcpProtectedRequestHandler} that can also read the
52
+ * authorization server's keys in-process.
53
+ *
54
+ * @internal
55
+ */
56
+ const createProtectedRequestHandler = (options, handler) => {
50
57
  const resource = validateMcpResource(options.audience);
51
58
  const resolvedChallengeOptions = {
52
59
  challengeScopes: options.challengeScopes ?? options.requiredScopes,
@@ -59,6 +66,8 @@ const createMcpProtectedRequestHandler = (options, handler) => {
59
66
  audience: options.audience
60
67
  },
61
68
  jwksUrl: options.jwksUrl,
69
+ jwksFetch: options.jwksFetch,
70
+ jwksCacheKey: options.jwksCacheKey,
62
71
  remoteVerify: options.remoteVerify,
63
72
  requiredScopes: options.requiredScopes,
64
73
  isScopeSatisfied: options.isScopeSatisfied,
@@ -69,18 +78,23 @@ const createMcpProtectedRequestHandler = (options, handler) => {
69
78
  try {
70
79
  accessTokenClaims = await verifyAccessTokenRequest(requestToResourceInput(request), accessTokenVerificationOptions);
71
80
  } catch (error) {
72
- return toChallengeResponse(error, resource, resolvedChallengeOptions);
81
+ return toChallengeResponse(error, resource, {
82
+ ...resolvedChallengeOptions,
83
+ request
84
+ });
73
85
  }
74
86
  try {
75
87
  return await handler(request, accessTokenClaims);
76
88
  } catch (error) {
77
- if (isInsufficientScopeError(error)) return toChallengeResponse(error, resource, resolvedChallengeOptions);
89
+ if (isInsufficientScopeError(error)) return toChallengeResponse(error, resource, {
90
+ ...resolvedChallengeOptions,
91
+ request
92
+ });
78
93
  throw error;
79
94
  }
80
95
  };
81
96
  };
82
- //#endregion
83
- //#region src/plugin.ts
97
+ // src/plugin.ts
84
98
  const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
85
99
  const AUTHORIZATION_SERVER_ONLY_SCOPES = /* @__PURE__ */ new Set([
86
100
  "openid",
@@ -199,8 +213,7 @@ const mcp = (options) => {
199
213
  }
200
214
  };
201
215
  };
202
- //#endregion
203
- //#region src/require-mcp-auth.ts
216
+ // src/require-mcp-auth.ts
204
217
  /**
205
218
  * Protects an MCP server route handler. Verifies the bearer access token
206
219
  * against the authorization server's JWKS (checking signature, issuer,
@@ -222,12 +235,19 @@ const mcp = (options) => {
222
235
  const requireMcpAuth = (auth, handler, opts) => {
223
236
  if (opts?.resource !== void 0) validateMcpResource(opts.resource);
224
237
  return async (req) => {
225
- const { baseURL, internalAdapter } = await auth.$context;
238
+ const authContext = await auth.$context;
239
+ const { baseURL, internalAdapter } = authContext;
226
240
  if (!baseURL) throw new Error("requireMcpAuth requires a resolvable base URL. For dynamic base URLs use `createMcpProtectedRequestHandler` with explicit verification options.");
227
241
  const issuer = opts?.issuer ?? baseURL;
228
242
  const resource = opts?.resource ?? baseURL;
229
- const jwksUrl = opts?.jwksUrl ?? `${baseURL}/jwks`;
230
- return createMcpProtectedRequestHandler({
243
+ const jwtPlugin = authContext.getPlugin("jwt");
244
+ const localJwks = !opts?.jwksUrl && jwtPlugin && !jwtPlugin.options?.jwks?.remoteUrl ? {
245
+ jwksFetch: async () => jwtPlugin.endpoints.getJwks({ context: authContext }),
246
+ jwksCacheKey: authContext
247
+ } : void 0;
248
+ const jwksUrl = opts?.jwksUrl ?? jwtPlugin?.options?.jwks?.remoteUrl ?? `${baseURL}/jwks`;
249
+ return createProtectedRequestHandler({
250
+ ...localJwks,
231
251
  issuer,
232
252
  audience: resource,
233
253
  jwtVerifyOptions: { requiredClaims: ["azp"] },
@@ -243,5 +263,4 @@ const requireMcpAuth = (auth, handler, opts) => {
243
263
  }, handler)(req);
244
264
  };
245
265
  };
246
- //#endregion
247
266
  export { createMcpProtectedRequestHandler, mcp, requireMcpAuth };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/mcp",
3
- "version": "1.7.21",
3
+ "version": "1.7.23",
4
4
  "description": "Model Context Protocol (MCP) plugin for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -46,21 +46,21 @@
46
46
  }
47
47
  },
48
48
  "dependencies": {
49
- "jose": "^6.1.3",
50
- "@better-auth/oauth-provider": "^1.7.21"
49
+ "@better-auth/oauth-provider": "^1.7.23",
50
+ "jose": "^6.1.3"
51
51
  },
52
52
  "devDependencies": {
53
+ "@better-auth/core": "1.7.23",
53
54
  "@modelcontextprotocol/client": "2.3.1",
54
55
  "@modelcontextprotocol/server": "2.3.1",
56
+ "better-auth": "1.7.23",
55
57
  "better-call": "1.4.5",
56
- "tsdown": "0.23.1",
57
- "@better-auth/core": "1.7.21",
58
- "better-auth": "1.7.21"
58
+ "tsdown": "0.23.1"
59
59
  },
60
60
  "peerDependencies": {
61
- "better-call": "1.4.5",
62
- "@better-auth/core": "^1.7.21",
63
- "better-auth": "^1.7.21"
61
+ "@better-auth/core": "^1.7.23",
62
+ "better-auth": "^1.7.23",
63
+ "better-call": "1.4.5"
64
64
  },
65
65
  "scripts": {
66
66
  "build": "tsdown",