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

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,123 @@
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
+ * The protected resource identifier (RFC 8707 / RFC 9728) that access tokens
28
+ * are bound to. Published as `resource` in the protected resource metadata,
29
+ * added to `resources`, and used as the expected token audience.
30
+ */
31
+ resource: string;
32
+ }
33
+ /**
34
+ * Model Context Protocol authorization server.
35
+ *
36
+ * `mcp()` is the OAuth 2.1 / OIDC provider ({@link oauthProvider}) configured for
37
+ * MCP: it enables dynamic client registration, binds issued tokens to the MCP
38
+ * `resource`, and, as the resource server, serves the RFC 9728 protected resource
39
+ * metadata so MCP clients discover and use it through standard OAuth discovery.
40
+ * Because it is the OAuth provider, it cannot be combined with a separate
41
+ * {@link oauthProvider}.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import { betterAuth } from "better-auth";
46
+ * import { jwt } from "better-auth/plugins";
47
+ * import { mcp } from "@better-auth/mcp";
48
+ *
49
+ * export const auth = betterAuth({
50
+ * plugins: [
51
+ * jwt(),
52
+ * mcp({
53
+ * loginPage: "/login",
54
+ * consentPage: "/consent",
55
+ * resource: "https://api.example.com/mcp",
56
+ * }),
57
+ * ],
58
+ * });
59
+ * ```
60
+ */
61
+ declare const mcp: (options: McpOptions) => ReturnType<typeof oauthProvider>;
62
+ //#endregion
63
+ //#region src/require-mcp-auth.d.ts
64
+ interface RequireMcpAuthOptions {
65
+ /**
66
+ * The protected resource identifier the access token must be bound to.
67
+ * Defaults to the server's resolved base URL.
68
+ */
69
+ resource?: string;
70
+ /**
71
+ * Expected token issuer. Defaults to the server's resolved base URL. Override
72
+ * when the JWT plugin is configured with a custom `jwt.issuer`.
73
+ */
74
+ issuer?: string;
75
+ /**
76
+ * URL of the authorization server's JWKS. Defaults to `/jwks` under the
77
+ * server's resolved base URL.
78
+ */
79
+ jwksUrl?: string;
80
+ /**
81
+ * Space-delimited scopes to advertise in the `WWW-Authenticate` challenge
82
+ * (RFC 6750), hinting which scopes the client should request.
83
+ */
84
+ scope?: string;
85
+ /**
86
+ * Maps a non-URL `resource` (an RFC 8707 `urn:` identifier or a client id) to
87
+ * the URL of its protected resource metadata. Required when `resource` is not
88
+ * an origin-based URL, so the `WWW-Authenticate` challenge can point at it.
89
+ */
90
+ resourceMetadataMappings?: Record<string, string>;
91
+ /**
92
+ * DPoP proof validation settings. By default the replay store is backed by
93
+ * the auth instance's database adapter, so anti-replay holds across multiple
94
+ * server instances. Override `replayStore` only to point at a different store.
95
+ */
96
+ dpop?: {
97
+ proofMaxAgeSeconds?: number;
98
+ signingAlgorithms?: readonly string[];
99
+ replayStore?: DpopReplayStore;
100
+ };
101
+ }
102
+ /**
103
+ * Protects an MCP server route handler. Verifies the bearer access token
104
+ * against the authorization server's JWKS (checking signature, issuer,
105
+ * audience, and expiry) and forwards the verified JWT payload to the handler.
106
+ * Unauthenticated requests receive a JSON-RPC 401 with the RFC 9728
107
+ * `WWW-Authenticate` header so MCP clients can start the authorization flow.
108
+ *
109
+ * For a resource server that runs separately from the authorization server, or
110
+ * a server using a dynamic `baseURL`, use {@link mcpHandler} with explicit
111
+ * verification options instead.
112
+ *
113
+ * @external
114
+ */
115
+ declare const requireMcpAuth: <Auth extends {
116
+ options: BetterAuthOptions;
117
+ $context: Promise<{
118
+ baseURL: string;
119
+ internalAdapter: DpopReplayReservations;
120
+ }>;
121
+ }>(auth: Auth, handler: (req: Request, jwt: JWTPayload) => Awaitable<Response>, opts?: RequireMcpAuthOptions) => (req: Request) => Promise<Response>;
122
+ //#endregion
123
+ export { type McpOptions, mcp, mcpHandler, requireMcpAuth };