The authHandler pattern transforms any MCP server into an enterprise-ready service. Test both public and authenticated endpoints below, then explore the complete implementation.
This demo implements the exact authHandler pattern described above. Test both endpoints to see how authentication works in practice—one is public, one requires WorkOS login.
Works for everyone • No login required
This calls the MCP ping
tool, which demonstrates a public endpoint in your MCP server. Notice it works whether you're signed in or not—ideal for health checks and monitoring.
Click "Test Now" to see the response
Enterprise security • WorkOS verification required
This calls the MCP getUserProfile
tool, demonstrating the authHandler pattern. It validates your JWT, fetches your profile from WorkOS, and returns authenticated user data.
Sign in first to test this endpoint
Official testing tool • Test connectivity
Use the official MCP Inspector to list available tools and verify your server is responding. This tests basic connectivity without requiring authentication.
npx @modelcontextprotocol/inspector --cli http://localhost:3000/mcp --method tools/list --transport http
Test with authentication tokens
Test authenticated MCP tools using the Inspector with your auth token.Sign in first to get a valid token.
npx @modelcontextprotocol/inspector --cli http://localhost:3000/mcp --method tools/call --tool-name getUserProfile --transport http --header "Authorization=Bearer YOUR_TOKEN"
This demonstrates the core value proposition: add authentication to any MCP server with minimal code changes using the authHandler pattern.
Transform any MCP server into an enterprise-ready, authenticated service
Create your normal MCP server with tools using `createMcpHandler()` - mix public and authenticated tools
import { createMcpHandler } from '@vercel/mcp-adapter';
// Create your regular MCP handler with tools
const handler = createMcpHandler((server) => {
// Public tool - works without authentication
server.tool(
'ping',
'Ping the MCP server to check if it is alive.',
{},
async () => {
return {
content: [{ type: 'text', text: JSON.stringify({ result: 'pong' }, null, 2) }],
};
},
);
// Authenticated tool - requires user context
server.tool(
'getUserProfile',
"Returns the authenticated user's profile information.",
{},
async (args, { authInfo }) => {
// authInfo.extra contains user data from auth wrapper
const user = getUserFromAuthInfo(authInfo);
return {
content: [{ type: 'text', text: JSON.stringify(user, null, 2) }],
};
},
);
});
Transform your handler into an `authHandler` that optionally validates JWTs—public tools work without auth, secured tools get user data
// 🔐 THE AUTHHANDLER PATTERN 🔐
// Wrap your MCP handler with optional enterprise authentication
const authHandler = experimental_withMcpAuth(
handler, // Your regular MCP handler from step 1
async (request, token) => {
// No token? Return undefined (allows public tools like ping)
if (!token) {
return undefined;
}
// Verify the JWT using WorkOS JWKS
const { payload } = await jwtVerify(token, JWKS);
if (!payload.sub) {
throw new Error('Invalid token: missing sub claim');
}
// Fetch user profile from WorkOS
const userProfile = await workos.userManagement.getUser(payload.sub);
const user = {
id: userProfile.id,
email: userProfile.email,
firstName: userProfile.firstName,
lastName: userProfile.lastName,
profilePictureUrl: userProfile.profilePictureUrl,
};
// Return AuthInfo with user data in extra field
return {
token,
clientId: clientId!,
scopes: [],
extra: { user, claims: payload }, // Available in authInfo.extra
};
},
{ required: false } // Optional auth: tools decide if they need it
);
Export the `authHandler` as HTTP endpoints to expose your authenticated MCP server
// Expose the authenticated MCP server as HTTP endpoints
export { authHandler as GET, authHandler as POST };
// How it works:
// • ping tool → works for everyone (no auth required)
// • getUserProfile tool → requires authentication
// • authHandler validates JWT → calls handler with user in authInfo.extra