diff --git a/docs/content/docs/plugins/mcp.mdx b/docs/content/docs/plugins/mcp.mdx index d6c027a8..9e4058cc 100644 --- a/docs/content/docs/plugins/mcp.mdx +++ b/docs/content/docs/plugins/mcp.mdx @@ -66,6 +66,17 @@ import { auth } from "../../../lib/auth"; export const GET = oAuthDiscoveryMetadata(auth); ``` +### OAuth Protected Resource Metadata + +Add a route to expose protected resource metadata for MCP clients: + +```ts title=".well-known/oauth-authorization-server/route.ts" +import { oAuthProtectedResourceMetadata } from "better-auth/plugins"; +import { auth } from "@/lib/auth"; + +export const GET = oAuthProtectedResourceMetadata(auth); +``` + ### MCP Session Handling You can use the helper function `withMcpAuth` to get the session and handle unauthenticated calls automatically. diff --git a/packages/better-auth/src/plugins/mcp/index.ts b/packages/better-auth/src/plugins/mcp/index.ts index 39d0476c..16bfa444 100644 --- a/packages/better-auth/src/plugins/mcp/index.ts +++ b/packages/better-auth/src/plugins/mcp/index.ts @@ -83,6 +83,27 @@ export const getMCPProviderMetadata = ( }; }; +export const getMCPProtectedResourceMetadata = ( + ctx: GenericEndpointContext, + options?: OIDCOptions, +) => { + const baseURL = ctx.context.baseURL; + + return { + resource: baseURL, + authorization_servers: [baseURL], + jwks_uri: options?.metadata?.jwks_uri ?? `${baseURL}/mcp/jwks`, + scopes_supported: options?.metadata?.scopes_supported ?? [ + "openid", + "profile", + "email", + "offline_access", + ], + bearer_methods_supported: ["header"], + resource_signing_alg_values_supported: ["RS256", "none"], + }; +}; + export const mcp = (options: MCPOptions) => { const opts = { codeExpiresIn: 600, @@ -168,6 +189,19 @@ export const mcp = (options: MCPOptions) => { } }, ), + getMCPProtectedResource: createAuthEndpoint( + "/.well-known/oauth-protected-resource", + { + method: "GET", + metadata: { + client: false, + }, + }, + async (c) => { + const metadata = getMCPProtectedResourceMetadata(c, options); + return c.json(metadata); + }, + ), mcpOAuthAuthroize: createAuthEndpoint( "/mcp/authorize", { @@ -912,7 +946,7 @@ export const withMcpAuth = < const session = await auth.api.getMcpSession({ headers: req.headers, }); - const wwwAuthenticateValue = `Bearer resource_metadata=${baseURL}/api/auth/.well-known/oauth-authorization-server`; + const wwwAuthenticateValue = `Bearer resource_metadata=${baseURL}/api/auth/.well-known/oauth-protected-resource`; if (!session) { return Response.json( { @@ -959,3 +993,27 @@ export const oAuthDiscoveryMetadata = < }); }; }; + +export const oAuthProtectedResourceMetadata = < + Auth extends { + api: { + getMCPProtectedResource: (...args: any) => any; + }; + }, +>( + auth: Auth, +) => { + return async (request: Request) => { + const res = await auth.api.getMCPProtectedResource(); + return new Response(JSON.stringify(res), { + status: 200, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + "Access-Control-Max-Age": "86400", + }, + }); + }; +};