99 * See: https://www.better-auth.com/docs/plugins/mcp
1010 */
1111
12+ import type { OAuthTokenVerifier } from '@modelcontextprotocol/express' ;
13+ import type { AuthInfo } from '@modelcontextprotocol/server' ;
14+ import { OAuthError , OAuthErrorCode } from '@modelcontextprotocol/server' ;
1215import { toNodeHandler } from 'better-auth/node' ;
1316import { oAuthDiscoveryMetadata , oAuthProtectedResourceMetadata } from 'better-auth/plugins' ;
1417import cors from 'cors' ;
@@ -21,7 +24,6 @@ import { createDemoAuth, DEMO_USER_CREDENTIALS } from './auth.js';
2124export interface SetupAuthServerOptions {
2225 authServerUrl : URL ;
2326 mcpServerUrl : URL ;
24- strictResource ?: boolean ;
2527 /**
2628 * Examples should be used for **demo** only and not for production purposes, however this mode disables some logging and other features.
2729 */
@@ -284,60 +286,29 @@ export function createProtectedResourceMetadataRouter(resourcePath = '/mcp'): Ro
284286}
285287
286288/**
287- * Verifies an access token using better-auth's getMcpSession.
288- * This can be used by MCP servers to validate tokens.
289+ * Demo {@link OAuthTokenVerifier} backed by better-auth's `getMcpSession`.
290+ * Pass this to `requireBearerAuth({ verifier: demoTokenVerifier, ... })` from
291+ * `@modelcontextprotocol/express` to validate Bearer tokens against the demo
292+ * Authorization Server started by `setupAuthServer`.
289293 */
290- export async function verifyAccessToken (
291- token : string ,
292- options ?: { strictResource ?: boolean ; expectedResource ?: URL }
293- ) : Promise < {
294- token : string ;
295- clientId : string ;
296- scopes : string [ ] ;
297- expiresAt : number ;
298- } > {
299- const auth = getAuth ( ) ;
294+ export const demoTokenVerifier : OAuthTokenVerifier = {
295+ async verifyAccessToken ( token : string ) : Promise < AuthInfo > {
296+ const auth = getAuth ( ) ;
300297
301- try {
302- // Create a mock request with the Authorization header
303298 const headers = new Headers ( ) ;
304299 headers . set ( 'Authorization' , `Bearer ${ token } ` ) ;
305300
306- // Use better-auth's getMcpSession API
307301 // eslint-disable-next-line @typescript-eslint/no-explicit-any
308- const session = await ( auth . api as any ) . getMcpSession ( {
309- headers
310- } ) ;
311-
302+ const session = await ( auth . api as any ) . getMcpSession ( { headers } ) ;
312303 if ( ! session ) {
313- throw new Error ( 'Invalid token' ) ;
304+ throw new OAuthError ( OAuthErrorCode . InvalidToken , 'Invalid token' ) ;
314305 }
315306
316- // OAuthAccessToken has:
317- // - accessToken, refreshToken: string
318- // - accessTokenExpiresAt, refreshTokenExpiresAt: Date
319- // - clientId, userId: string
320- // - scopes: string (space-separated)
321307 const scopes = typeof session . scopes === 'string' ? session . scopes . split ( ' ' ) : [ 'openid' ] ;
322308 const expiresAt = session . accessTokenExpiresAt
323309 ? Math . floor ( new Date ( session . accessTokenExpiresAt ) . getTime ( ) / 1000 )
324310 : Math . floor ( Date . now ( ) / 1000 ) + 3600 ;
325311
326- // Note: better-auth's OAuthAccessToken doesn't have a resource field
327- // Resource validation would need to be done at a different layer
328- if ( options ?. strictResource && options . expectedResource ) {
329- // For now, we skip resource validation as it's not in the session
330- // In production, you'd store and validate this separately
331- console . warn ( '[Auth] Resource validation requested but not available in better-auth session' ) ;
332- }
333-
334- return {
335- token,
336- clientId : session . clientId ,
337- scopes,
338- expiresAt
339- } ;
340- } catch ( error ) {
341- throw new Error ( `Token verification failed: ${ error instanceof Error ? error . message : 'Unknown error' } ` ) ;
312+ return { token, clientId : session . clientId , scopes, expiresAt } ;
342313 }
343- }
314+ } ;
0 commit comments