@prmichaelsen/mcp-auth
v7.5.1
Published
Authentication and multi-tenancy framework for MCP servers
Maintainers
Readme
@prmichaelsen/mcp-auth
Authentication and multi-tenancy framework for MCP (Model Context Protocol) servers.
Overview
@prmichaelsen/mcp-auth provides a pluggable authentication system for MCP servers, enabling:
- Zero modification: Wrap existing MCP servers without code changes
- Multi-tenancy: Multiple users with separate resource tokens
- Auth-agnostic: Support for JWT, environment variables, or custom auth schemes
- Transport-agnostic: Works with stdio, HTTP, and SSE transports
- Type-safe: Full TypeScript support
- Composable: Middleware for rate limiting, logging, timeouts, retries
Two Patterns
Pattern 1: Server Wrapping (Recommended for Production)
Wrap existing MCP servers without modification:
import { wrapServer, JWTAuthProvider, APITokenResolver } from '@prmichaelsen/mcp-auth';
import { createServer } from '@myorg/my-mcp-server';
const wrapped = wrapServer({
serverFactory: createServer,
// Validates JWT from tenant manager
authProvider: new JWTAuthProvider({
jwtSecret: process.env.JWT_SECRET
}),
// Resolves tokens via tenant manager API (recommended)
tokenResolver: new APITokenResolver({
tenantManagerUrl: process.env.TENANT_MANAGER_URL,
serviceToken: process.env.SERVICE_TOKEN
}),
resourceType: 'myapi',
transport: { type: 'sse', port: 3000 }
});
await wrapped.start();Pattern 2: Tool-Level Auth
Build new servers with integrated authentication:
import { AuthenticatedMCPServer, withAuth, EnvAuthProvider, SimpleTokenResolver } from '@prmichaelsen/mcp-auth';
const server = new AuthenticatedMCPServer({
name: 'my-server',
authProvider: new EnvAuthProvider(),
tokenResolver: new SimpleTokenResolver({ tokenEnvVar: 'API_TOKEN' }),
resourceType: 'myapi',
transport: { type: 'stdio' }
});
server.registerTool('get_data', withAuth(async (args, accessToken, userId) => {
const client = new MyAPIClient(accessToken);
return client.getData(args);
}));
await server.start();Installation
# Core package
npm install @prmichaelsen/mcp-auth @modelcontextprotocol/sdk
# For JWT support
npm install jsonwebtoken
# For SSE/HTTP transports
npm install express corsAuthentication Providers
EnvAuthProvider (Single-User)
For local development and single-user scenarios:
import { EnvAuthProvider, SimpleTokenResolver } from '@prmichaelsen/mcp-auth';
const authProvider = new EnvAuthProvider({
userIdEnvVar: 'MCP_USER_ID',
defaultUserId: 'local-user'
});
const tokenResolver = new SimpleTokenResolver({
tokenEnvVar: 'API_TOKEN'
});JWTAuthProvider + APITokenResolver (Multi-Tenant Production) ⭐ RECOMMENDED
For production multi-tenant deployments:
import { JWTAuthProvider, APITokenResolver } from '@prmichaelsen/mcp-auth';
// Validates JWT tokens from tenant manager
const authProvider = new JWTAuthProvider({
jwtSecret: process.env.JWT_SECRET,
userIdClaim: 'sub'
});
// Resolves tokens via tenant manager API
const tokenResolver = new APITokenResolver({
tenantManagerUrl: 'https://tenant-manager.example.com',
serviceToken: process.env.SERVICE_TOKEN,
cacheTokens: true, // Cache for performance
cacheTtl: 300000 // 5 minutes
});Why API-Based is Better:
- ✅ Automatic token refresh (no new JWT needed)
- ✅ Immediate token revocation
- ✅ Tokens not exposed in JWT
- ✅ Small JWT size (~200 bytes)
- ✅ Centralized token management
JWTAuthProvider + JWTTokenResolver (MVP/Prototyping)
For quick setup with JWT-embedded tokens:
import { JWTAuthProvider, JWTTokenResolver } from '@prmichaelsen/mcp-auth';
const authProvider = new JWTAuthProvider({
jwtSecret: process.env.JWT_SECRET,
extractTokens: true // Extract tokens from JWT
});
const tokenResolver = new JWTTokenResolver({ authProvider });Trade-offs:
- ✅ Zero API calls (faster)
- ✅ Simpler to implement
- ❌ Token rotation requires new JWT
- ❌ Larger JWT size
- ❌ Tokens exposed in JWT payload
JWTAuthProvider (Static Servers) ⭐ NEW
For servers that manage their own data and only need user identification:
import { wrapServer, JWTAuthProvider } from '@prmichaelsen/mcp-auth';
const wrapped = wrapServer({
serverFactory: (accessToken, userId) => {
// accessToken will be empty string - use userId only
return createMyStaticServer(userId);
},
authProvider: new JWTAuthProvider({
jwtSecret: process.env.JWT_SECRET
}),
// No tokenResolver needed! ✨
resourceType: 'my-service',
transport: {
type: 'sse',
port: 3000,
cors: true,
corsOrigin: process.env.CORS_ORIGIN
}
});Perfect for:
- ✅ Multi-tenant SaaS with own database
- ✅ User-scoped services
- ✅ Internal tools without external APIs
- ✅ Static data management servers
Benefits:
- ✅ Simplest configuration
- ✅ No external credential management
- ✅ JWT validation only (userId extraction)
- ✅ Complete user isolation via ephemeral instances
APITokenResolver (API-Based)
For resolving tokens via tenant manager API:
import { JWTAuthProvider, APITokenResolver } from '@prmichaelsen/mcp-auth';
const authProvider = new JWTAuthProvider({
jwtSecret: process.env.JWT_SECRET
});
const tokenResolver = new APITokenResolver({
tenantManagerUrl: 'https://tenant-manager.example.com',
serviceToken: process.env.SERVICE_TOKEN,
endpointPath: '/api/credentials/:userId/:resourceType'
});Custom Provider
Implement your own authentication logic:
import { AuthProvider, AuthResult, RequestContext } from '@prmichaelsen/mcp-auth';
class CustomAuthProvider implements AuthProvider {
async authenticate(context: RequestContext): Promise<AuthResult> {
const apiKey = context.headers?.['x-api-key'];
if (!apiKey) {
return { authenticated: false, error: 'No API key' };
}
// Your validation logic
return {
authenticated: true,
userId: 'user-123'
};
}
}Middleware Composition
import { compose, withAuth, withRateLimit, withLogging, withTimeout } from '@prmichaelsen/mcp-auth';
const getTool = compose(
withLogging({ logArgs: true }),
withRateLimit({ maxRequests: 100, windowMs: 60000 }),
withTimeout(5000),
withAuth(),
async (args, accessToken, userId) => {
// Your tool logic
}
);
server.registerTool('get_data', getTool);Transports
Stdio (Local)
transport: { type: 'stdio' }SSE (Remote Multi-Tenant)
transport: {
type: 'sse',
port: 3000,
host: '0.0.0.0',
basePath: '/mcp',
cors: true,
corsOrigin: 'https://your-app.example.com' // REQUIRED when cors: true
}Endpoints created:
GET /mcp- Server info and available endpointsPOST /mcp/message- MCP protocol messages (requires JWT)GET /mcp/health- Health check endpoint
⚠️ CORS Security
When enabling CORS, you MUST specify corsOrigin with explicit origins:
// ✅ SECURE - Single origin
transport: {
type: 'sse',
cors: true,
corsOrigin: 'https://app.example.com'
}
// ✅ SECURE - Multiple origins
transport: {
type: 'sse',
cors: true,
corsOrigin: ['https://app1.example.com', 'https://app2.example.com']
}
// ❌ INSECURE - Wildcard blocked in production
transport: {
type: 'sse',
cors: true,
corsOrigin: '*' // ConfigurationError in production!
}
// ❌ INSECURE - Missing corsOrigin
transport: {
type: 'sse',
cors: true // ConfigurationError: corsOrigin required!
}Security Requirements:
corsOriginis REQUIRED whencors: true- Wildcard (
*) is ONLY allowed in development (NODE_ENV !== 'production') - In production, wildcard throws
ConfigurationErrorto prevent CSRF attacks - Use specific origins to ensure only your applications can access the MCP server
HTTP (Remote)
transport: {
type: 'http',
port: 3000,
host: '0.0.0.0'
}Progress Streaming
mcp-auth supports MCP progress notifications, allowing wrapped servers to stream progress updates to clients during long-running operations.
How It Works
When a client provides a progressToken in the request, mcp-auth automatically:
- Extracts the progress token from the request
- Passes it through to the wrapped MCP server
- The wrapped server can send progress notifications back to the client
- Progress is isolated per user (multi-tenant safe)
Client-Side Usage
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
const client = new Client({
name: 'my-client',
version: '1.0.0'
});
// Call tool with progress support
const result = await client.request({
method: 'tools/call',
params: {
name: 'long_running_operation',
arguments: { /* ... */ }
}
}, {
progressToken: 'operation-123',
onprogress: (progress) => {
console.log(`Progress: ${progress.progress}/${progress.total}`);
console.log(`Message: ${progress.message}`);
}
});Server-Side Implementation
Wrapped MCP servers can send progress notifications:
// In your MCP server tool handler
export async function handleLongOperation(
args: any,
extra?: { progressToken?: string | number }
): Promise<any> {
const progressToken = extra?.progressToken;
if (progressToken) {
// Send progress notifications
server.notification({
method: 'notifications/progress',
params: {
progressToken,
progress: 50,
total: 100,
message: 'Processing...'
}
});
}
// ... perform operation
}Features
- ✅ Automatic Pass-Through: Progress tokens automatically forwarded to wrapped servers
- ✅ Multi-Tenant Safe: Progress notifications isolated per user
- ✅ Backward Compatible: Works with or without progress token (graceful degradation)
- ✅ Zero Configuration: No additional setup required
Monitoring
Monitor active progress streams via authenticated endpoints:
User Stats (shows your own progress streams):
GET /mcp/progress/stats
Authorization: Bearer <jwt>Response:
{
"user": {
"userId": "user123",
"activeStreams": 2,
"totalMessages": 1543,
"totalBytes": 45231,
"oldestStreamAge": 120000
},
"global": {
"activeStreams": 15,
"totalMessages": 8234,
"userCount": 8
},
"timestamp": "2026-02-23T21:00:00.000Z"
}All Metrics (detailed stream information):
GET /mcp/progress/metrics
Authorization: Bearer <jwt>Response:
{
"metrics": [
{
"userId": "user123",
"progressToken": "op-456",
"startTime": 1708725600000,
"lastUpdate": 1708725650000,
"duration": 50000,
"messageCount": 125,
"bytesTransferred": 45231,
"averageMessageSize": 361.8,
"messagesPerSecond": 2.5
}
],
"health": {
"healthy": true,
"issues": [],
"warnings": []
},
"timestamp": "2026-02-23T21:00:00.000Z"
}Performance
- Memory: ~1KB per active stream
- Network: ~100-500 bytes per progress notification
- CPU: <1% overhead for typical workloads
- Cleanup: Stale streams (>5 minutes idle) automatically removed every minute
Notes
- Progress streaming requires SSE or HTTP transport (not available with stdio)
- Progress tokens are automatically cleaned up after request completion
- Wrapped servers must implement progress notification support to send updates
- Health checks detect stale streams and high message rates
- Monitoring endpoints require authentication
MCP Server Contract
To make your MCP server compatible with wrapServer(), export a factory function:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
export function createServer(accessToken: string, userId?: string): Server {
const server = new Server({ name: 'my-server', version: '1.0.0' });
const client = new MyAPIClient(accessToken);
// Register your tool handlers...
return server;
}That's it! No mcp-auth imports needed in your server.
Architecture
Three-Tier Deployment
- Chat Platform - Sends MCP requests with JWT
- Tenant Manager - Issues JWTs, manages user credentials
- MCP Server Instance - Uses mcp-auth to wrap MCP servers
Token Resolution Approaches
Approach 1: JWT with Embedded Tokens (Recommended)
- Tenant manager includes resource tokens in JWT
- Zero external calls
- Fastest performance
Approach 2: API-Based Resolution
- Tenant manager provides API for token lookup
- Better separation of concerns
- Easier token rotation
See agent/token-resolution-approaches.md for details.
Documentation
Comprehensive architecture documentation in agent/:
server-wrapping-pattern.md- Server wrapping architectureserver-contract.md- MCP server compatibility guidedual-pattern-architecture.md- Both patterns explainedtoken-resolution-approaches.md- Token resolution strategieswhy-two-steps.md- AuthProvider vs TokenResolverINTEGRATION.md- Integration guide for MCP server authors
Examples
Working examples coming soon in examples/:
simple-stdio/- Single-user stdio serverwrapped-server/- Server wrapping with JWTtool-level-auth/- Tool-level authenticationjwt-multi-tenant/- Multi-tenant JWT deployment
API Reference
Core Functions
wrapServer(config)- Wrap MCP server with authenticationwithAuth(handler)- Add auth to function-based toolscompose(...middlewares)- Compose middleware functions
Classes
AuthenticatedMCPServer- MCP server with integrated authAuthenticatedTool- Wrapper for class-based toolsBaseAuthProvider- Base class for auth providers
Providers
EnvAuthProvider- Environment variable authenticationSimpleTokenResolver- Environment variable token resolutionJWTAuthProvider- JWT validation with token extractionJWTTokenResolver- JWT-embedded token resolutionAPITokenResolver- API-based token resolution
Middleware
withAuth()- AuthenticationwithLogging()- Request/response loggingwithRateLimit()- Rate limiting per userwithTimeout()- Request timeoutwithRetry()- Automatic retry on failure
Example Projects
Real-world projects using @prmichaelsen/mcp-auth:
@prmichaelsen/agentbase-mcp-server
Multi-tenant Instagram MCP server with JWT authentication
- Pattern: Server Wrapping (Dynamic Mode)
- Auth: JWT + API-based token resolution
- Use Case: External API credentials (Instagram)
- Transport: SSE over HTTP
@prmichaelsen/remember-mcp-server
Multi-tenant MCP server for remember-mcp with Platform JWT authentication
- Pattern: Server Wrapping (Static Mode)
- Auth: JWT only (no tokenResolver)
- Use Case: User-scoped data in own database
- Transport: SSE over HTTP
License
MIT
Contributing
Contributions welcome! Please open an issue or PR on GitHub.
