Skip to main content

Server Authentication

Add enterprise-grade OAuth 2.0/2.1 authentication to your MCP server with built-in support for popular identity providers. Secure your tools with bearer token authentication, implement role-based access control (RBAC), and access authenticated user information in your tool callbacks.

Quick Start

Basic OAuth Server

import { McpServer, auth0 } from 'mcp-use/server'

const server = new McpServer({
  name: 'my-secure-server',
  version: '1.0.0',
  oauth: auth0({
    domain: 'your-tenant.auth0.com',
    audience: 'https://your-api.example.com',
  })
})

// Tools now have access to authenticated user context
server.tool({
  name: 'get-user-profile',
  description: 'Get the authenticated user profile',
  cb: async (params, context) => {
    // Access authenticated user info
    const user = context.auth
    return {
      userId: user.userId,
      email: user.email,
      name: user.name,
      roles: user.roles
    }
  }
})

server.listen(3000)

OAuth Providers

mcp-use includes built-in support for major identity providers:

Auth0

Full OAuth 2.1 with PKCE, JWKS verification, and custom claims.
import { McpServer, auth0 } from 'mcp-use/server'

const server = new McpServer({
  name: 'my-server',
  version: '1.0.0',
  oauth: auth0({
    domain: 'your-tenant.auth0.com',
    audience: 'https://your-api.example.com',
    // Optional: Skip JWT verification for development
    verifyJwt: process.env.NODE_ENV === 'production'
  })
})
See the Auth0 OAuth Example for complete setup instructions.

WorkOS

Direct mode OAuth where clients authenticate directly with WorkOS (recommended for enterprise SSO).
import { McpServer, workos } from 'mcp-use/server'

const server = new McpServer({
  name: 'my-server',
  version: '1.0.0',
  oauth: workos({
    apiKey: process.env.WORKOS_API_KEY!,
    clientId: process.env.WORKOS_CLIENT_ID!,
    // Direct mode: clients handle OAuth flow
    mode: 'direct'
  })
})
See the WorkOS OAuth Example for complete setup instructions.

Supabase

Authentication for Supabase projects with support for both HS256 and ES256 tokens.
import { McpServer, supabase } from 'mcp-use/server'

const server = new McpServer({
  name: 'my-server',
  version: '1.0.0',
  oauth: supabase({
    projectId: process.env.SUPABASE_PROJECT_ID!,
    // Required for HS256 tokens (legacy)
    jwtSecret: process.env.SUPABASE_JWT_SECRET,
    // ES256 tokens (new) use JWKS automatically
  })
})
See the Supabase OAuth Example for complete setup instructions.

Keycloak

Enterprise SSO with realm roles and client-specific permissions.
import { McpServer, keycloak } from 'mcp-use/server'

const server = new McpServer({
  name: 'my-server',
  version: '1.0.0',
  oauth: keycloak({
    serverUrl: 'https://keycloak.example.com',
    realm: 'my-realm',
    clientId: 'my-client-id',
    verifyJwt: true
  })
})

Custom Provider

Use any OAuth provider with custom JWT verification logic.
import { McpServer, custom } from 'mcp-use/server'
import { jwtVerify, createRemoteJWKSet } from 'jose'

const server = new McpServer({
  name: 'my-server',
  version: '1.0.0',
  oauth: custom({
    issuer: 'https://auth.example.com',
    authEndpoint: 'https://auth.example.com/oauth/authorize',
    tokenEndpoint: 'https://auth.example.com/oauth/token',
    
    // Custom verification logic
    async verifyToken(token: string) {
      const JWKS = createRemoteJWKSet(
        new URL('https://auth.example.com/.well-known/jwks.json')
      )
      const result = await jwtVerify(token, JWKS, {
        issuer: 'https://auth.example.com',
        audience: 'your-audience'
      })
      return result.payload
    },
    
    // Optional: Custom user info extraction
    getUserInfo(payload: any) {
      return {
        userId: payload.sub,
        email: payload.email,
        name: payload.name,
        roles: payload.roles || [],
        permissions: payload.permissions || []
      }
    }
  })
})

OAuth Modes

mcp-use supports two OAuth modes:

Proxy Mode (Default)

The MCP server proxies OAuth requests. Clients authenticate through your server’s endpoints. When to use:
  • Simple setup and deployment
  • Full control over OAuth flow
  • Single authentication endpoint
Flow:
  1. Client requests authorization → http://your-server.com/authorize
  2. User authenticates with provider
  3. Server exchanges code for token → http://your-server.com/token
  4. Client uses bearer token for MCP requests
// Proxy mode (default)
const server = new McpServer({
  oauth: auth0({
    domain: 'your-tenant.auth0.com',
    mode: 'proxy' // or omit (default)
  })
})

Direct Mode

Clients communicate directly with the auth provider. Your server only verifies bearer tokens. When to use:
  • Enterprise SSO requirements
  • Reduce server load
  • Provider requires direct authentication (e.g., WorkOS)
Flow:
  1. Client requests authorization → https://auth-provider.com/authorize
  2. User authenticates with provider
  3. Client exchanges code for token → https://auth-provider.com/token
  4. Client uses bearer token for MCP requests → Your server verifies
// Direct mode
const server = new McpServer({
  oauth: workos({
    apiKey: process.env.WORKOS_API_KEY!,
    clientId: process.env.WORKOS_CLIENT_ID!,
    mode: 'direct'
  })
})

Accessing User Context

Once OAuth is configured, all tool callbacks receive authenticated user information via the context parameter:
server.tool({
  name: 'create-document',
  schema: z.object({
    title: z.string(),
    content: z.string()
  }),
  cb: async ({ title, content }, context) => {
    // Access authenticated user
    const user = context.auth
    
    // User info available:
    console.log('User ID:', user.userId)      // Unique user identifier
    console.log('Email:', user.email)          // User email
    console.log('Name:', user.name)            // Display name
    console.log('Roles:', user.roles)          // User roles
    console.log('Permissions:', user.permissions) // Permissions
    
    // Create document with user context
    const doc = await db.documents.create({
      title,
      content,
      createdBy: user.userId,
      createdByName: user.name
    })
    
    return text(`Document created by ${user.name}`)
  }
})

User Info Type

interface UserInfo {
  userId: string              // Unique user identifier (from 'sub' claim)
  email?: string              // User email
  name?: string               // Full name
  username?: string           // Username
  nickname?: string           // Nickname or display name
  picture?: string            // Profile picture URL
  roles?: string[]            // User roles
  permissions?: string[]      // User permissions
  scopes?: string[]           // OAuth scopes granted
  
  // Provider-specific fields
  [key: string]: any          // Additional claims from provider
}

Role-Based Access Control (RBAC)

Implement role-based access control by checking user roles in your tools:
// Simple role check
server.tool({
  name: 'admin-action',
  description: 'Perform admin action',
  cb: async (params, context) => {
    const user = context.auth
    
    if (!user.roles?.includes('admin')) {
      return error('Forbidden: Admin role required')
    }
    
    // Perform admin action
    return text('Admin action completed')
  }
})

// Permission-based check
server.tool({
  name: 'delete-user',
  description: 'Delete a user account',
  schema: z.object({ userId: z.string() }),
  cb: async ({ userId }, context) => {
    const user = context.auth
    
    // Check for specific permission
    if (!user.permissions?.includes('users:delete')) {
      return error('Forbidden: Missing users:delete permission')
    }
    
    await deleteUser(userId)
    return text(`User ${userId} deleted`)
  }
})

// Scope-based check
server.tool({
  name: 'read-emails',
  description: 'Read user emails',
  cb: async (params, context) => {
    const user = context.auth
    
    if (!user.scopes?.includes('email:read')) {
      return error('Forbidden: email:read scope required')
    }
    
    const emails = await fetchEmails(user.userId)
    return json({ emails })
  }
})

Create a Reusable Authorization Middleware

// middleware/auth.ts
import type { McpContext } from 'mcp-use/server'

export function requireRole(role: string) {
  return (context: McpContext) => {
    if (!context.auth.roles?.includes(role)) {
      throw new Error(`Forbidden: ${role} role required`)
    }
  }
}

export function requirePermission(permission: string) {
  return (context: McpContext) => {
    if (!context.auth.permissions?.includes(permission)) {
      throw new Error(`Forbidden: ${permission} permission required`)
    }
  }
}

// Use in tools
server.tool({
  name: 'admin-tool',
  cb: async (params, context) => {
    requireRole('admin')(context)
    // Tool logic...
  }
})

OAuth Endpoints

When OAuth is configured, your server automatically exposes these endpoints:

Authorization Endpoint

GET /authorize
Initiates the OAuth authorization flow. Query Parameters:
  • response_type=code - Response type
  • client_id - OAuth client ID
  • redirect_uri - Callback URL
  • scope - Requested scopes
  • state - CSRF protection token
  • code_challenge - PKCE challenge
  • code_challenge_method=S256 - PKCE method

Token Endpoint

POST /token
Exchanges authorization code for access token. Body Parameters:
  • grant_type=authorization_code - Grant type
  • code - Authorization code
  • redirect_uri - Callback URL
  • client_id - OAuth client ID
  • code_verifier - PKCE verifier
Response:
{
  "access_token": "eyJhbGci...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid profile email"
}

Discovery Endpoints

GET /.well-known/oauth-authorization-server
GET /.well-known/openid-configuration
Returns OAuth/OIDC discovery metadata for automatic client configuration.

Bearer Token Authentication

All /mcp/* endpoints require a valid bearer token when OAuth is configured:
Authorization: Bearer eyJhbGci...
Example Request:
curl http://localhost:3000/mcp/messages \
  -H "Authorization: Bearer eyJhbGci..." \
  -H "Content-Type: application/json" \
  -d '{"method":"tools/list"}'
Unauthorized Response:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="http://localhost:3000", error="invalid_token"

{
  "error": "invalid_token",
  "error_description": "The access token is invalid or expired"
}

Security Best Practices

JWT Verification

Always enable JWT verification in production:
const server = new McpServer({
  oauth: auth0({
    domain: 'your-tenant.auth0.com',
    // IMPORTANT: Always verify in production
    verifyJwt: process.env.NODE_ENV === 'production'
  })
})

Environment Variables

Never hardcode credentials. Use environment variables:
// ✅ Good
const server = new McpServer({
  oauth: auth0({
    domain: process.env.AUTH0_DOMAIN!,
    audience: process.env.AUTH0_AUDIENCE!
  })
})

// ❌ Bad
const server = new McpServer({
  oauth: auth0({
    domain: 'my-tenant.auth0.com',  // Don't hardcode!
    audience: 'https://api.example.com'
  })
})

CORS Configuration

Configure CORS appropriately for your OAuth clients:
const server = new McpServer({
  name: 'my-server',
  oauth: auth0({ /* ... */ })
})

// Configure CORS
server.use('*', cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
  credentials: true,
  allowMethods: ['GET', 'POST', 'OPTIONS']
}))

Token Expiration

Implement token refresh logic in your clients to handle expired tokens gracefully.

Rate Limiting

Consider adding rate limiting to prevent abuse:
import { rateLimiter } from 'hono-rate-limiter'

server.use('/mcp/*', rateLimiter({
  windowMs: 15 * 60 * 1000, // 15 minutes
  limit: 100, // Limit each user to 100 requests per window
  keyGenerator: (c) => c.get('auth')?.userId || c.req.header('x-forwarded-for') || 'anonymous'
}))

Testing OAuth

Development Mode

Skip JWT verification during development:
const server = new McpServer({
  oauth: auth0({
    domain: 'your-tenant.auth0.com',
    audience: 'https://your-api.example.com',
    verifyJwt: process.env.NODE_ENV === 'production'
  })
})
⚠️ Warning: Never deploy with verifyJwt: false to production!

MCP Inspector

Test your OAuth server using the built-in MCP Inspector:
const server = new McpServer({
  name: 'my-server',
  oauth: auth0({ /* ... */ })
})

server.listen(3000)
// Open http://localhost:3000/inspector
The Inspector includes full OAuth flow support with automatic token management.

Manual Testing

Generate a test token from your provider’s dashboard and use it directly:
# Get a token from your OAuth provider
TOKEN="eyJhbGci..."

# Test MCP endpoint
curl http://localhost:3000/mcp/messages \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list"
  }'

Examples

Complete Auth0 Server

import { McpServer, auth0, text, error } from 'mcp-use/server'
import { z } from 'zod'

const server = new McpServer({
  name: 'secure-docs-api',
  version: '1.0.0',
  oauth: auth0({
    domain: process.env.AUTH0_DOMAIN!,
    audience: process.env.AUTH0_AUDIENCE!,
    verifyJwt: true
  })
})

// Public tool (no auth check)
server.tool({
  name: 'list-public-docs',
  description: 'List public documents',
  cb: async () => {
    const docs = await db.documents.findMany({ public: true })
    return json({ documents: docs })
  }
})

// Protected tool with role check
server.tool({
  name: 'create-document',
  description: 'Create a new document',
  schema: z.object({
    title: z.string(),
    content: z.string()
  }),
  cb: async ({ title, content }, context) => {
    const user = context.auth
    
    // Verify user has write permission
    if (!user.roles?.includes('editor') && !user.roles?.includes('admin')) {
      return error('Forbidden: editor or admin role required')
    }
    
    const doc = await db.documents.create({
      title,
      content,
      authorId: user.userId,
      authorName: user.name
    })
    
    return text(`Document "${title}" created successfully`)
  }
})

// Admin-only tool
server.tool({
  name: 'delete-document',
  description: 'Delete a document (admin only)',
  schema: z.object({
    documentId: z.string()
  }),
  cb: async ({ documentId }, context) => {
    if (!context.auth.roles?.includes('admin')) {
      return error('Forbidden: admin role required')
    }
    
    await db.documents.delete({ id: documentId })
    return text('Document deleted')
  }
})

server.listen(3000)
console.log('🔒 Secure server running on http://localhost:3000')
console.log('🔍 Inspector: http://localhost:3000/inspector')

Troubleshooting

”Invalid token” errors

Symptoms: 401 Unauthorized responses with invalid_token error Solutions:
  1. Verify token is correctly formatted in Authorization header
  2. Check token hasn’t expired
  3. Ensure OAuth provider domain/issuer is correct
  4. Verify audience matches between client and server
  5. Check JWT signature verification is working

”Forbidden” errors despite valid token

Symptoms: Token verifies but role/permission checks fail Solutions:
  1. Verify roles/permissions are included in JWT claims
  2. Check custom claim configuration in OAuth provider
  3. Ensure role claim path matches your extraction logic
  4. Use Inspector to view decoded token contents

OAuth discovery fails

Symptoms: Clients can’t discover OAuth endpoints Solutions:
  1. Verify server is accessible at the configured URL
  2. Check CORS configuration allows OPTIONS requests
  3. Ensure /.well-known/* endpoints are accessible
  4. Try accessing discovery endpoint directly in browser

PKCE errors

Symptoms: “Invalid code verifier” or PKCE-related errors Solutions:
  1. Ensure client supports PKCE
  2. Verify code_challenge and code_verifier match
  3. Check code hasn’t been used already
  4. Verify code hasn’t expired (typically 10 minutes)

Next Steps