@better-auth/mcp 1.7.22 → 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,6 +1,6 @@
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
6
  interface McpProtectedRequestHandlerOptions {
@@ -36,9 +36,21 @@ interface McpProtectedRequestHandlerOptions {
36
36
  dpop?: {
37
37
  proofMaxAgeSeconds?: number;
38
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
+ */
39
50
  replayStore?: DpopReplayStore;
40
51
  };
41
52
  }
53
+ type McpRequestHandler = (request: Request, accessTokenClaims: JWTPayload) => Awaitable<Response>;
42
54
  /**
43
55
  * A request middleware handler that verifies an MCP access token and responds
44
56
  * with an RFC 9728 `WWW-Authenticate` header for unauthenticated requests.
@@ -47,7 +59,7 @@ interface McpProtectedRequestHandlerOptions {
47
59
  *
48
60
  * @external
49
61
  */
50
- export declare const createMcpProtectedRequestHandler: (options: McpProtectedRequestHandlerOptions, handler: (request: Request, accessTokenClaims: JWTPayload) => Awaitable<Response>) => (request: Request) => Promise<Response>;
62
+ export declare const createMcpProtectedRequestHandler: (options: McpProtectedRequestHandlerOptions, handler: McpRequestHandler) => (request: Request) => Promise<Response>;
51
63
  /**
52
64
  * Options for the {@link mcp} plugin: the full OAuth provider configuration plus
53
65
  * the MCP resource identifier.
@@ -134,8 +146,9 @@ interface RequireMcpAuthOptions {
134
146
  */
135
147
  issuer?: string;
136
148
  /**
137
- * URL of the authorization server's JWKS. Defaults to `/jwks` under the
138
- * 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.
139
152
  */
140
153
  jwksUrl?: string;
141
154
  /**
@@ -184,8 +197,7 @@ interface RequireMcpAuthOptions {
184
197
  */
185
198
  export declare const requireMcpAuth: <Auth extends {
186
199
  options: BetterAuthOptions;
187
- $context: Promise<{
188
- baseURL: string;
200
+ $context: Promise<Pick<AuthContext, "baseURL" | "getPlugin"> & {
189
201
  internalAdapter: DpopReplayReservations;
190
202
  }>;
191
203
  }>(auth: Auth, handler: (request: Request, accessTokenClaims: JWTPayload) => Awaitable<Response>, opts?: RequireMcpAuthOptions) => (req: Request) => Promise<Response>;
package/dist/index.mjs CHANGED
@@ -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,
@@ -77,7 +86,10 @@ const createMcpProtectedRequestHandler = (options, handler) => {
77
86
  try {
78
87
  return await handler(request, accessTokenClaims);
79
88
  } catch (error) {
80
- if (isInsufficientScopeError(error)) return toChallengeResponse(error, resource, resolvedChallengeOptions);
89
+ if (isInsufficientScopeError(error)) return toChallengeResponse(error, resource, {
90
+ ...resolvedChallengeOptions,
91
+ request
92
+ });
81
93
  throw error;
82
94
  }
83
95
  };
@@ -223,12 +235,19 @@ const mcp = (options) => {
223
235
  const requireMcpAuth = (auth, handler, opts) => {
224
236
  if (opts?.resource !== void 0) validateMcpResource(opts.resource);
225
237
  return async (req) => {
226
- const { baseURL, internalAdapter } = await auth.$context;
238
+ const authContext = await auth.$context;
239
+ const { baseURL, internalAdapter } = authContext;
227
240
  if (!baseURL) throw new Error("requireMcpAuth requires a resolvable base URL. For dynamic base URLs use `createMcpProtectedRequestHandler` with explicit verification options.");
228
241
  const issuer = opts?.issuer ?? baseURL;
229
242
  const resource = opts?.resource ?? baseURL;
230
- const jwksUrl = opts?.jwksUrl ?? `${baseURL}/jwks`;
231
- 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,
232
251
  issuer,
233
252
  audience: resource,
234
253
  jwtVerifyOptions: { requiredClaims: ["azp"] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/mcp",
3
- "version": "1.7.22",
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,20 +46,20 @@
46
46
  }
47
47
  },
48
48
  "dependencies": {
49
- "@better-auth/oauth-provider": "^1.7.22",
49
+ "@better-auth/oauth-provider": "^1.7.23",
50
50
  "jose": "^6.1.3"
51
51
  },
52
52
  "devDependencies": {
53
- "@better-auth/core": "1.7.22",
53
+ "@better-auth/core": "1.7.23",
54
54
  "@modelcontextprotocol/client": "2.3.1",
55
55
  "@modelcontextprotocol/server": "2.3.1",
56
- "better-auth": "1.7.22",
56
+ "better-auth": "1.7.23",
57
57
  "better-call": "1.4.5",
58
58
  "tsdown": "0.23.1"
59
59
  },
60
60
  "peerDependencies": {
61
- "@better-auth/core": "^1.7.22",
62
- "better-auth": "^1.7.22",
61
+ "@better-auth/core": "^1.7.23",
62
+ "better-auth": "^1.7.23",
63
63
  "better-call": "1.4.5"
64
64
  },
65
65
  "scripts": {