@better-auth/mcp 1.5.0-beta.9 → 1.7.0-beta.7

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 ADDED
@@ -0,0 +1,21 @@
1
+ # @better-auth/mcp
2
+
3
+ Model Context Protocol (MCP) plugin for [Better Auth](https://www.better-auth.com).
4
+
5
+ `mcp()` turns your Better Auth app into an OAuth 2.1 authorization server for MCP
6
+ clients, built on [`@better-auth/oauth-provider`](https://www.better-auth.com/docs/plugins/oauth-provider).
7
+ It serves the RFC 9728 protected resource metadata so MCP clients can discover it.
8
+ To protect an MCP route, wrap its handler with `requireMcpAuth` (or `mcpHandler`),
9
+ which verifies bearer tokens against the published JWKS.
10
+
11
+ ```ts
12
+ import { betterAuth } from "better-auth";
13
+ import { jwt } from "better-auth/plugins";
14
+ import { mcp } from "@better-auth/mcp";
15
+
16
+ export const auth = betterAuth({
17
+ plugins: [jwt(), mcp({ loginPage: "/login", consentPage: "/consent" })],
18
+ });
19
+ ```
20
+
21
+ See the [MCP plugin documentation](https://www.better-auth.com/docs/plugins/mcp).
@@ -0,0 +1,56 @@
1
+ import { McpResourceClient, McpResourceClientOptions, McpSession } from "./index.mjs";
2
+
3
+ //#region src/client/adapters.d.ts
4
+ interface HonoContext {
5
+ req: {
6
+ header: (name: string) => string | undefined;
7
+ raw: Request;
8
+ };
9
+ set: (key: string, value: unknown) => void;
10
+ json: (data: unknown, status?: number, headers?: Record<string, string>) => Response;
11
+ header: (name: string, value: string) => void;
12
+ }
13
+ type HonoNext = () => Promise<void>;
14
+ type HonoMiddleware = (c: HonoContext, next: HonoNext) => Promise<Response | void>;
15
+ interface HonoApp {
16
+ get: (path: string, handler: (c: HonoContext) => Promise<Response>) => void;
17
+ }
18
+ declare function mcpAuthHono(options: McpResourceClientOptions): {
19
+ client: McpResourceClient;
20
+ middleware: HonoMiddleware;
21
+ discoveryRoutes: (app: HonoApp, serverURL: string) => void;
22
+ };
23
+ declare function mcpAuthOfficial(options: McpResourceClientOptions): {
24
+ client: McpResourceClient;
25
+ handler: McpResourceClient["handler"];
26
+ verifyToken: McpResourceClient["verifyToken"];
27
+ };
28
+ type OAuthMode = "direct" | "proxy";
29
+ interface McpUseUserInfo {
30
+ userId: string;
31
+ roles?: string[];
32
+ permissions?: string[];
33
+ scopes?: string;
34
+ clientId?: string;
35
+ [key: string]: unknown;
36
+ }
37
+ interface OAuthProvider {
38
+ verifyToken(token: string): Promise<{
39
+ payload: Record<string, unknown>;
40
+ }>;
41
+ getUserInfo(payload: Record<string, unknown>): McpUseUserInfo;
42
+ getIssuer(): string;
43
+ getAuthEndpoint(): string;
44
+ getTokenEndpoint(): string;
45
+ getScopesSupported(): string[];
46
+ getGrantTypesSupported(): string[];
47
+ getMode(): OAuthMode;
48
+ getRegistrationEndpoint?(): string;
49
+ }
50
+ interface McpUseBetterAuthConfig {
51
+ authURL: string;
52
+ getUserInfo?: (payload: Record<string, unknown>) => McpUseUserInfo;
53
+ }
54
+ declare function mcpAuthMcpUse(config: McpUseBetterAuthConfig): OAuthProvider;
55
+ //#endregion
56
+ export { type McpResourceClient, type McpResourceClientOptions, type McpSession, McpUseBetterAuthConfig, mcpAuthHono, mcpAuthMcpUse, mcpAuthOfficial };
@@ -0,0 +1,126 @@
1
+ import { createMcpResourceClient, makeDpopWWWAuthenticate } from "./index.mjs";
2
+ import { DPOP_SIGNING_ALGORITHMS, isDpopBindingError, parseAccessTokenAuthorization } from "better-auth/oauth2";
3
+ //#region src/client/adapters.ts
4
+ const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
5
+ function getProtectedResourceMetadataPath(resource) {
6
+ const resourceUrl = new URL(resource);
7
+ if (resourceUrl.origin === "null") return PROTECTED_RESOURCE_METADATA_PATH;
8
+ return `${PROTECTED_RESOURCE_METADATA_PATH}${resourceUrl.pathname === "/" ? "" : resourceUrl.pathname.replace(/\/$/, "")}`;
9
+ }
10
+ function getProtectedResourceMetadataURL(resource) {
11
+ const resourceUrl = new URL(resource);
12
+ if (resourceUrl.origin === "null") throw new Error("MCP resource_metadata requires an origin-based resource URL");
13
+ return `${resourceUrl.origin}${getProtectedResourceMetadataPath(resource)}${resourceUrl.search}`;
14
+ }
15
+ function mcpAuthHono(options) {
16
+ const client = createMcpResourceClient(options);
17
+ const resourceMetadata = getProtectedResourceMetadataURL(options.resource ?? client.authURL);
18
+ const dpopChallenge = makeDpopWWWAuthenticate(options.dpop?.signingAlgorithms ?? DPOP_SIGNING_ALGORITHMS);
19
+ const unauthorized = (c, challenge, message) => {
20
+ c.header("WWW-Authenticate", challenge);
21
+ return c.json({
22
+ jsonrpc: "2.0",
23
+ error: {
24
+ code: -32e3,
25
+ message
26
+ },
27
+ id: null
28
+ }, 401);
29
+ };
30
+ const middleware = async (c, next) => {
31
+ const authHeader = c.req.header("Authorization");
32
+ let session;
33
+ try {
34
+ session = await client.verifyRequest(c.req.raw);
35
+ } catch (error) {
36
+ if (isDpopBindingError(error)) return unauthorized(c, dpopChallenge, "Invalid or expired token");
37
+ throw error;
38
+ }
39
+ if (!session) return unauthorized(c, parseAccessTokenAuthorization(authHeader)?.scheme === "DPoP" || !!c.req.header("DPoP") ? dpopChallenge : `Bearer resource_metadata="${resourceMetadata}"`, authHeader ? "Invalid or expired token" : "Unauthorized: Authentication required");
40
+ c.set("mcpSession", session);
41
+ await next();
42
+ };
43
+ const discoveryRoutes = (app, serverURL) => {
44
+ const discoveryFn = client.discoveryHandler();
45
+ const protectedResourceFn = client.protectedResourceHandler(serverURL);
46
+ const protectedResourcePaths = new Set([PROTECTED_RESOURCE_METADATA_PATH, getProtectedResourceMetadataPath(options.resource ?? client.authURL)]);
47
+ app.get("/.well-known/oauth-authorization-server", async (c) => {
48
+ const response = await discoveryFn(c.req.raw);
49
+ const data = await response.json().catch(() => ({ error: "Invalid response from auth server" }));
50
+ return c.json(data, response.status);
51
+ });
52
+ for (const path of protectedResourcePaths) app.get(path, async (c) => {
53
+ const response = await protectedResourceFn(c.req.raw);
54
+ const data = await response.json().catch(() => ({ error: "Invalid response from auth server" }));
55
+ return c.json(data, response.status);
56
+ });
57
+ };
58
+ return {
59
+ client,
60
+ middleware,
61
+ discoveryRoutes
62
+ };
63
+ }
64
+ function mcpAuthOfficial(options) {
65
+ const client = createMcpResourceClient(options);
66
+ return {
67
+ client,
68
+ handler: client.handler,
69
+ verifyToken: client.verifyToken
70
+ };
71
+ }
72
+ function mcpAuthMcpUse(config) {
73
+ const authURL = normalizeURL(config.authURL);
74
+ if (!authURL) throw new Error("Better Auth authURL is required. Pass authURL in config, e.g.: mcpAuthMcpUse({ authURL: 'http://localhost:3000/api/auth' })");
75
+ const client = createMcpResourceClient({ authURL });
76
+ return {
77
+ async verifyToken(token) {
78
+ const session = await client.verifyToken(token);
79
+ if (!session) throw new Error("Invalid or expired token");
80
+ return { payload: session };
81
+ },
82
+ getUserInfo(payload) {
83
+ if (config.getUserInfo) return config.getUserInfo(payload);
84
+ const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : [];
85
+ return {
86
+ userId: payload.sub,
87
+ roles: [],
88
+ permissions: scopes,
89
+ scopes: payload.scope,
90
+ clientId: payload.azp ?? payload.client_id
91
+ };
92
+ },
93
+ getIssuer() {
94
+ return authURL;
95
+ },
96
+ getAuthEndpoint() {
97
+ return `${authURL}/oauth2/authorize`;
98
+ },
99
+ getTokenEndpoint() {
100
+ return `${authURL}/oauth2/token`;
101
+ },
102
+ getScopesSupported() {
103
+ return [
104
+ "openid",
105
+ "profile",
106
+ "email",
107
+ "offline_access"
108
+ ];
109
+ },
110
+ getGrantTypesSupported() {
111
+ return ["authorization_code", "refresh_token"];
112
+ },
113
+ getMode() {
114
+ return "direct";
115
+ },
116
+ getRegistrationEndpoint() {
117
+ return `${authURL}/oauth2/register`;
118
+ }
119
+ };
120
+ }
121
+ function normalizeURL(url) {
122
+ if (!url || url.trim() === "") return void 0;
123
+ return url.endsWith("/") ? url.slice(0, -1) : url;
124
+ }
125
+ //#endregion
126
+ export { mcpAuthHono, mcpAuthMcpUse, mcpAuthOfficial };
@@ -0,0 +1,52 @@
1
+ import { VerifyAccessTokenRequestOptions } from "better-auth/oauth2";
2
+ import { JWTPayload } from "jose";
3
+
4
+ //#region src/client/index.d.ts
5
+ interface McpResourceClientOptions {
6
+ authURL: string;
7
+ resource?: string;
8
+ allowedOrigin?: string;
9
+ fetch?: typeof globalThis.fetch;
10
+ dpop?: VerifyAccessTokenRequestOptions["dpop"];
11
+ }
12
+ interface McpSession extends JWTPayload {
13
+ sub?: string;
14
+ scope?: string;
15
+ client_id?: string;
16
+ }
17
+ interface NodeLikeRequest {
18
+ headers: Record<string, string | string[] | undefined> & {
19
+ get?: (name: string) => string | undefined;
20
+ authorization?: string;
21
+ host?: string;
22
+ "x-forwarded-proto"?: string;
23
+ };
24
+ get?: (name: string) => string | undefined;
25
+ method?: string;
26
+ originalUrl?: string;
27
+ protocol?: string;
28
+ url?: string;
29
+ mcpSession?: McpSession;
30
+ }
31
+ interface NodeLikeResponse {
32
+ set?: (name: string, value: string) => void;
33
+ setHeader?: (name: string, value: string) => void;
34
+ status?: (code: number) => {
35
+ json: (body: unknown) => void;
36
+ };
37
+ writeHead?: (code: number, headers: Record<string, string>) => void;
38
+ end?: (body: string) => void;
39
+ }
40
+ interface McpResourceClient {
41
+ verifyToken: (token: string) => Promise<McpSession | null>;
42
+ verifyRequest: (req: Request) => Promise<McpSession | null>;
43
+ handler: (fn: (req: Request, session: McpSession) => Response | Promise<Response>) => (req: Request) => Promise<Response>;
44
+ discoveryHandler: () => (req: Request) => Promise<Response>;
45
+ protectedResourceHandler: (serverURL: string) => (req: Request) => Promise<Response>;
46
+ middleware: () => (req: NodeLikeRequest, res: NodeLikeResponse, next: () => void) => Promise<void>;
47
+ authURL: string;
48
+ }
49
+ declare function makeDpopWWWAuthenticate(algorithms: readonly string[]): string;
50
+ declare function createMcpResourceClient(options: McpResourceClientOptions): McpResourceClient;
51
+ //#endregion
52
+ export { McpResourceClient, McpResourceClientOptions, McpSession, createMcpResourceClient, makeDpopWWWAuthenticate };
@@ -0,0 +1,244 @@
1
+ import { DPOP_SIGNING_ALGORITHMS, createInMemoryDpopReplayStore, enforceDpopBinding, getDpopJktFromPayload, isDpopBindingError, parseAccessTokenAuthorization } from "better-auth/oauth2";
2
+ import { createRemoteJWKSet, jwtVerify } from "jose";
3
+ //#region src/client/index.ts
4
+ const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
5
+ function buildCorsHeaders(authURL, allowedOrigin) {
6
+ let origin;
7
+ if (allowedOrigin) origin = allowedOrigin;
8
+ else try {
9
+ origin = new URL(authURL).origin;
10
+ } catch {
11
+ origin = authURL;
12
+ }
13
+ return {
14
+ "Access-Control-Allow-Origin": origin,
15
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
16
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, DPoP",
17
+ "Access-Control-Expose-Headers": "WWW-Authenticate",
18
+ "Access-Control-Max-Age": "86400"
19
+ };
20
+ }
21
+ function getProtectedResourceMetadataURL(resource) {
22
+ const resourceUrl = new URL(resource);
23
+ if (resourceUrl.origin === "null") throw new Error("MCP resource_metadata requires an origin-based resource URL");
24
+ const resourcePath = resourceUrl.pathname === "/" ? "" : resourceUrl.pathname.replace(/\/$/, "");
25
+ return `${resourceUrl.origin}${PROTECTED_RESOURCE_METADATA_PATH}${resourcePath}${resourceUrl.search}`;
26
+ }
27
+ function makeWWWAuthenticate(authURL, resource) {
28
+ return `Bearer resource_metadata="${getProtectedResourceMetadataURL(resource ?? authURL)}"`;
29
+ }
30
+ function makeDpopWWWAuthenticate(algorithms) {
31
+ return `DPoP algs="${algorithms.map((alg) => alg.replace(/[\r\n"\\]/g, "")).join(" ")}"`;
32
+ }
33
+ function make401Response(wwwAuth) {
34
+ return Response.json({
35
+ jsonrpc: "2.0",
36
+ error: {
37
+ code: -32e3,
38
+ message: "Unauthorized: Authentication required",
39
+ "www-authenticate": wwwAuth
40
+ },
41
+ id: null
42
+ }, {
43
+ status: 401,
44
+ headers: { "WWW-Authenticate": wwwAuth }
45
+ });
46
+ }
47
+ function send401Node(res, wwwAuth, message) {
48
+ const body = JSON.stringify({
49
+ jsonrpc: "2.0",
50
+ error: {
51
+ code: -32e3,
52
+ message
53
+ },
54
+ id: null
55
+ });
56
+ if (typeof res.set === "function") {
57
+ res.set("WWW-Authenticate", wwwAuth);
58
+ res.status?.(401).json(JSON.parse(body));
59
+ } else if (typeof res.writeHead === "function") {
60
+ res.writeHead(401, {
61
+ "Content-Type": "application/json",
62
+ "WWW-Authenticate": wwwAuth
63
+ });
64
+ res.end?.(body);
65
+ }
66
+ }
67
+ function createMcpResourceClient(options) {
68
+ const authURL = options.authURL.endsWith("/") ? options.authURL.slice(0, -1) : options.authURL;
69
+ const fetchFn = options.fetch ?? globalThis.fetch;
70
+ const corsHeaders = buildCorsHeaders(authURL, options.allowedOrigin);
71
+ const expectedAudience = options.resource ?? authURL;
72
+ const dpopReplayStore = options.dpop?.replayStore ?? createInMemoryDpopReplayStore();
73
+ const dpopSigningAlgorithms = options.dpop?.signingAlgorithms ?? DPOP_SIGNING_ALGORITHMS;
74
+ const selectChallenge = (authHeader, dpopHeaderPresent) => {
75
+ return parseAccessTokenAuthorization(authHeader)?.scheme === "DPoP" || dpopHeaderPresent ? makeDpopWWWAuthenticate(dpopSigningAlgorithms) : makeWWWAuthenticate(authURL, options.resource);
76
+ };
77
+ let discovery = null;
78
+ let jwks = null;
79
+ const loadVerifier = async () => {
80
+ if (discovery && jwks) return {
81
+ discovery,
82
+ jwks
83
+ };
84
+ const response = await fetchFn(`${authURL}/.well-known/oauth-authorization-server`);
85
+ if (!response.ok) throw new Error("Failed to fetch discovery metadata");
86
+ const metadata = await response.json();
87
+ if (!metadata.jwks_uri || !metadata.issuer) throw new Error("Discovery metadata missing jwks_uri or issuer");
88
+ discovery = {
89
+ issuer: metadata.issuer,
90
+ jwks_uri: metadata.jwks_uri
91
+ };
92
+ jwks = createRemoteJWKSet(new URL(metadata.jwks_uri));
93
+ return {
94
+ discovery,
95
+ jwks
96
+ };
97
+ };
98
+ const verifyJwtToken = async (token) => {
99
+ try {
100
+ const { discovery: meta, jwks: keySet } = await loadVerifier();
101
+ const { payload } = await jwtVerify(token, keySet, {
102
+ issuer: meta.issuer,
103
+ audience: expectedAudience
104
+ });
105
+ return payload;
106
+ } catch {
107
+ return null;
108
+ }
109
+ };
110
+ const verifyToken = async (token) => {
111
+ const session = await verifyJwtToken(token);
112
+ if (!session || getDpopJktFromPayload(session)) return null;
113
+ return session;
114
+ };
115
+ /**
116
+ * Verifies a request's access token and, when the token is DPoP-bound, its
117
+ * RFC 9449 sender-constraint. Returns `null` when there is no usable token
118
+ * or the JWT itself is invalid. Throws a `DpopBindingError` when a
119
+ * DPoP-bound token fails the binding check, so the caller can answer with a
120
+ * `WWW-Authenticate: DPoP` challenge rather than a bearer one.
121
+ */
122
+ const verifyRequest = async (req) => {
123
+ const authorization = parseAccessTokenAuthorization(req.headers.get("Authorization"));
124
+ if (!authorization?.token || authorization.scheme === "Unknown") return null;
125
+ const session = await verifyJwtToken(authorization.token);
126
+ if (!session) return null;
127
+ await enforceDpopBinding({
128
+ payload: session,
129
+ authorization,
130
+ proofJwt: req.headers.get("DPoP"),
131
+ method: req.method,
132
+ url: req.url,
133
+ proofMaxAgeSeconds: options.dpop?.proofMaxAgeSeconds,
134
+ signingAlgorithms: dpopSigningAlgorithms,
135
+ replayStore: dpopReplayStore
136
+ });
137
+ return session;
138
+ };
139
+ const getHeader = (req, name) => {
140
+ const lower = name.toLowerCase();
141
+ const value = req.headers?.[lower] ?? req.headers?.[name] ?? req.headers?.get?.(name) ?? req.get?.(name);
142
+ if (Array.isArray(value)) return value[0];
143
+ return value;
144
+ };
145
+ const getNodeRequestUrl = (req) => {
146
+ const rawUrl = req.originalUrl ?? req.url ?? "/";
147
+ if (URL.canParse(rawUrl)) return rawUrl;
148
+ const fallbackUrl = new URL(authURL);
149
+ const host = getHeader(req, "host") ?? fallbackUrl.host;
150
+ return `${getHeader(req, "x-forwarded-proto")?.split(",")[0]?.trim() ?? req.protocol ?? fallbackUrl.protocol.replace(":", "")}://${host}${rawUrl.startsWith("/") ? rawUrl : `/${rawUrl}`}`;
151
+ };
152
+ const handler = (fn) => {
153
+ return async (req) => {
154
+ if (req.method === "OPTIONS") return new Response(null, {
155
+ status: 204,
156
+ headers: corsHeaders
157
+ });
158
+ let session;
159
+ try {
160
+ session = await verifyRequest(req);
161
+ } catch (error) {
162
+ if (isDpopBindingError(error)) return make401Response(makeDpopWWWAuthenticate(dpopSigningAlgorithms));
163
+ throw error;
164
+ }
165
+ if (!session) return make401Response(selectChallenge(req.headers.get("Authorization"), req.headers.has("DPoP")));
166
+ return fn(req, session);
167
+ };
168
+ };
169
+ const discoveryHandler = () => {
170
+ let cachedMetadata = null;
171
+ let cacheTime = 0;
172
+ const CACHE_TTL = 6e4;
173
+ return async (_req) => {
174
+ const now = Date.now();
175
+ if (cachedMetadata && now - cacheTime < CACHE_TTL) return Response.json(cachedMetadata, { headers: corsHeaders });
176
+ try {
177
+ const response = await fetchFn(`${authURL}/.well-known/oauth-authorization-server`);
178
+ if (!response.ok) return Response.json({ error: "Failed to fetch discovery metadata" }, {
179
+ status: 502,
180
+ headers: corsHeaders
181
+ });
182
+ cachedMetadata = await response.json();
183
+ cacheTime = now;
184
+ return Response.json(cachedMetadata, { headers: corsHeaders });
185
+ } catch {
186
+ return Response.json({ error: "Better Auth server unreachable" }, {
187
+ status: 502,
188
+ headers: corsHeaders
189
+ });
190
+ }
191
+ };
192
+ };
193
+ const protectedResourceHandler = (serverURL) => {
194
+ const metadata = {
195
+ resource: options.resource ?? new URL(serverURL).origin,
196
+ authorization_servers: [authURL],
197
+ bearer_methods_supported: ["header"],
198
+ dpop_signing_alg_values_supported: [...dpopSigningAlgorithms]
199
+ };
200
+ return async (_req) => {
201
+ return Response.json(metadata, { headers: corsHeaders });
202
+ };
203
+ };
204
+ const middleware = () => {
205
+ return async (req, res, next) => {
206
+ const authHeader = getHeader(req, "authorization");
207
+ const requestHeaders = new Headers();
208
+ if (authHeader) requestHeaders.set("Authorization", authHeader);
209
+ const dpop = getHeader(req, "dpop");
210
+ if (dpop) requestHeaders.set("DPoP", dpop);
211
+ const request = new Request(getNodeRequestUrl(req), {
212
+ method: req.method ?? "GET",
213
+ headers: requestHeaders
214
+ });
215
+ let session;
216
+ try {
217
+ session = await verifyRequest(request);
218
+ } catch (error) {
219
+ if (isDpopBindingError(error)) {
220
+ send401Node(res, makeDpopWWWAuthenticate(dpopSigningAlgorithms), "Invalid or expired token");
221
+ return;
222
+ }
223
+ throw error;
224
+ }
225
+ if (!session) {
226
+ send401Node(res, selectChallenge(authHeader, !!getHeader(req, "dpop")), authHeader ? "Invalid or expired token" : "Unauthorized: Authentication required");
227
+ return;
228
+ }
229
+ req.mcpSession = session;
230
+ next();
231
+ };
232
+ };
233
+ return {
234
+ verifyToken,
235
+ verifyRequest,
236
+ handler,
237
+ discoveryHandler,
238
+ protectedResourceHandler,
239
+ middleware,
240
+ authURL
241
+ };
242
+ }
243
+ //#endregion
244
+ export { createMcpResourceClient, makeDpopWWWAuthenticate };
package/dist/index.d.mts CHANGED
@@ -1 +1,138 @@
1
- export { };
1
+ import { OAuthOptions, Scope, oauthProvider } from "@better-auth/oauth-provider";
2
+ import { DpopReplayReservations, DpopReplayStore, verifyAccessTokenRequest } from "better-auth/oauth2";
3
+ import { JWTPayload } from "jose";
4
+ import { Awaitable } from "@better-auth/core";
5
+ import { BetterAuthOptions } from "better-auth/types";
6
+
7
+ //#region src/handler.d.ts
8
+ /**
9
+ * A request middleware handler that verifies an MCP access token and responds
10
+ * with an RFC 9728 `WWW-Authenticate` header for unauthenticated requests.
11
+ *
12
+ * @external
13
+ */
14
+ declare const mcpHandler: (/** Verifier options. `audience` must match the protected resource identifier. */
15
+
16
+ verifyOptions: Parameters<typeof verifyAccessTokenRequest>[1], handler: (req: Request, jwt: JWTPayload) => Awaitable<Response>, opts?: {
17
+ /** Maps non-url (ie urn, client) resources to resource_metadata */resourceMetadataMappings: Record<string, string>;
18
+ }) => (req: Request) => Promise<Response>;
19
+ //#endregion
20
+ //#region src/plugin.d.ts
21
+ /**
22
+ * Options for the {@link mcp} plugin: the full OAuth provider configuration plus
23
+ * the MCP resource identifier.
24
+ */
25
+ interface McpOptions extends OAuthOptions<Scope[]> {
26
+ /**
27
+ * Seconds that a rotated refresh token can be reused to receive the same
28
+ * token response for the same effective scopes, requested resources, and
29
+ * sender constraint.
30
+ *
31
+ * MCP overrides the OAuth Provider default because native/public MCP clients
32
+ * can have multiple local sessions racing the same refresh token. Set to `0`
33
+ * to keep strict replay handling.
34
+ *
35
+ * @default 30
36
+ */
37
+ refreshTokenReuseInterval?: OAuthOptions<Scope[]>["refreshTokenReuseInterval"];
38
+ /**
39
+ * The protected resource identifier (RFC 8707 / RFC 9728) that access tokens
40
+ * are bound to. Published as `resource` in the protected resource metadata,
41
+ * added to `resources`, and used as the expected token audience.
42
+ */
43
+ resource: string;
44
+ }
45
+ /**
46
+ * Model Context Protocol authorization server.
47
+ *
48
+ * `mcp()` is the OAuth 2.1 / OIDC provider ({@link oauthProvider}) configured for
49
+ * MCP: it enables dynamic client registration, binds issued tokens to the MCP
50
+ * `resource`, and, as the resource server, serves the RFC 9728 protected resource
51
+ * metadata so MCP clients discover and use it through standard OAuth discovery.
52
+ * It also defaults `refreshTokenReuseInterval` to 30 seconds for native/public
53
+ * MCP clients that may retry a refresh after a local process loses the rotated
54
+ * response.
55
+ * Because it is the OAuth provider, it cannot be combined with a separate
56
+ * {@link oauthProvider}.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import { betterAuth } from "better-auth";
61
+ * import { jwt } from "better-auth/plugins";
62
+ * import { mcp } from "@better-auth/mcp";
63
+ *
64
+ * export const auth = betterAuth({
65
+ * plugins: [
66
+ * jwt(),
67
+ * mcp({
68
+ * loginPage: "/login",
69
+ * consentPage: "/consent",
70
+ * resource: "https://api.example.com/mcp",
71
+ * }),
72
+ * ],
73
+ * });
74
+ * ```
75
+ */
76
+ declare const mcp: (options: McpOptions) => ReturnType<typeof oauthProvider>;
77
+ //#endregion
78
+ //#region src/require-mcp-auth.d.ts
79
+ interface RequireMcpAuthOptions {
80
+ /**
81
+ * The protected resource identifier the access token must be bound to.
82
+ * Defaults to the server's resolved base URL.
83
+ */
84
+ resource?: string;
85
+ /**
86
+ * Expected token issuer. Defaults to the server's resolved base URL. Override
87
+ * when the JWT plugin is configured with a custom `jwt.issuer`.
88
+ */
89
+ issuer?: string;
90
+ /**
91
+ * URL of the authorization server's JWKS. Defaults to `/jwks` under the
92
+ * server's resolved base URL.
93
+ */
94
+ jwksUrl?: string;
95
+ /**
96
+ * Space-delimited scopes to advertise in the `WWW-Authenticate` challenge
97
+ * (RFC 6750), hinting which scopes the client should request.
98
+ */
99
+ scope?: string;
100
+ /**
101
+ * Maps a non-URL `resource` (an RFC 8707 `urn:` identifier or a client id) to
102
+ * the URL of its protected resource metadata. Required when `resource` is not
103
+ * an origin-based URL, so the `WWW-Authenticate` challenge can point at it.
104
+ */
105
+ resourceMetadataMappings?: Record<string, string>;
106
+ /**
107
+ * DPoP proof validation settings. By default the replay store is backed by
108
+ * the auth instance's database adapter, so anti-replay holds across multiple
109
+ * server instances. Override `replayStore` only to point at a different store.
110
+ */
111
+ dpop?: {
112
+ proofMaxAgeSeconds?: number;
113
+ signingAlgorithms?: readonly string[];
114
+ replayStore?: DpopReplayStore;
115
+ };
116
+ }
117
+ /**
118
+ * Protects an MCP server route handler. Verifies the bearer access token
119
+ * against the authorization server's JWKS (checking signature, issuer,
120
+ * audience, and expiry) and forwards the verified JWT payload to the handler.
121
+ * Unauthenticated requests receive a JSON-RPC 401 with the RFC 9728
122
+ * `WWW-Authenticate` header so MCP clients can start the authorization flow.
123
+ *
124
+ * For a resource server that runs separately from the authorization server, or
125
+ * a server using a dynamic `baseURL`, use {@link mcpHandler} with explicit
126
+ * verification options instead.
127
+ *
128
+ * @external
129
+ */
130
+ declare const requireMcpAuth: <Auth extends {
131
+ options: BetterAuthOptions;
132
+ $context: Promise<{
133
+ baseURL: string;
134
+ internalAdapter: DpopReplayReservations;
135
+ }>;
136
+ }>(auth: Auth, handler: (req: Request, jwt: JWTPayload) => Awaitable<Response>, opts?: RequireMcpAuthOptions) => (req: Request) => Promise<Response>;
137
+ //#endregion
138
+ export { type McpOptions, mcp, mcpHandler, requireMcpAuth };