Skip to main content
Glama
dyeoman2

Clerk MCP Server Template

by dyeoman2
auth.ts7.03 kB
import { type OAuthHelpers } from '@cloudflare/workers-oauth-provider' import { Hono } from 'hono' import { verifyClerkToken, getClerkUser } from './clerk' import { type Env } from './types' import { log, encodeAuthState, decodeAuthState, cleanupExpiredRequests, type AuthSession, } from './utils' // Create Hono app with appropriate bindings const app = new Hono<{ Bindings: Env & { OAUTH_PROVIDER: OAuthHelpers } }>() /** * GET /authorize - Entry point for OAuth authorization flow */ app.get('/authorize', async (c) => { log.info('Starting /authorize request') try { // Clean up expired requests periodically await cleanupExpiredRequests(c.env.OAUTH_KV) const oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw) log.info('Parsed OAuth request', { clientId: oauthReqInfo.clientId }) const { clientId } = oauthReqInfo if (!clientId) { log.error('Missing client ID in request') return c.json( { error: 'invalid_request', error_description: 'Missing client_id' }, 400, ) } // Look up client to verify it exists const clientInfo = await c.env.OAUTH_PROVIDER.lookupClient(clientId) if (!clientInfo) { log.error('Client not found', { clientId }) return c.json( { error: 'invalid_client', error_description: 'Client not found' }, 400, ) } log.info('Retrieved client info', { clientName: clientInfo.clientName }) // Create minimal auth session in KV const sessionId = crypto.randomUUID() const authSession: AuthSession = { sessionId, clientId, timestamp: Date.now(), expiresAt: Date.now() + 600000, // 10 minutes } await c.env.OAUTH_KV.put( `auth_session:${sessionId}`, JSON.stringify(authSession), { expirationTtl: 600 }, ) // Encode the full auth request and session ID in state const state = await encodeAuthState( { sessionId, authRequest: oauthReqInfo, }, c.env.CLERK_SECRET_KEY, ) log.info('Redirecting to app for authentication', { sessionId, clientId: oauthReqInfo.clientId, }) // Build app auth URL const url = new URL(c.req.url) const baseUrl = `${url.protocol}//${url.host}` const appUrl = c.env.APP_URL const authUrl = new URL('/auth/mcp', appUrl) authUrl.searchParams.set('state', state) authUrl.searchParams.set('callback_url', `${baseUrl}/callback`) authUrl.searchParams.set( 'client_name', clientInfo.clientName || 'Unknown Client', ) return Response.redirect(authUrl.toString(), 302) } catch (error) { log.error('Error in /authorize', { error: error instanceof Error ? error.message : String(error), }) return c.json( { error: 'server_error', error_description: 'Authorization request failed', }, 500, ) } }) /** * GET /callback - Handle callback from app with clerk_token */ app.get('/callback', async (c) => { try { const url = new URL(c.req.url) const clerkToken = url.searchParams.get('clerk_token') const state = url.searchParams.get('state') const error = url.searchParams.get('error') log.info('Received callback from app', { hasToken: !!clerkToken, hasState: !!state, error, }) if (error) { log.info('User denied authorization in app', { error }) return c.html(` <!DOCTYPE html> <html> <head> <title>Authorization Denied</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 2rem; max-width: 600px; margin: 0 auto; text-align: center; } .error { color: #dc2626; margin: 2rem 0; } </style> </head> <body> <h1>Authorization Denied</h1> <div class="error">You have denied access to the MCP client.</div> <p>You can close this window and try again if needed.</p> </body> </html> `) } if (!clerkToken || !state) { log.error('Missing required parameters in callback', { hasToken: !!clerkToken, hasState: !!state, }) return c.json( { error: 'invalid_request', error_description: 'Missing required parameters', }, 400, ) } // Decode and verify the state const stateData = await decodeAuthState(state, c.env.CLERK_SECRET_KEY) if (!stateData) { log.error('Invalid or tampered state parameter') return c.json( { error: 'invalid_request', error_description: 'Invalid state parameter', }, 400, ) } const { sessionId, authRequest: mcpAuthRequest } = stateData // Verify session still exists and is valid const sessionData = await c.env.OAUTH_KV.get(`auth_session:${sessionId}`) if (!sessionData) { log.error('Auth session not found or expired', { sessionId }) return c.json( { error: 'invalid_request', error_description: 'Auth session expired', }, 400, ) } const authSession: AuthSession = JSON.parse(sessionData) // Verify session hasn't expired if (Date.now() > authSession.expiresAt) { log.error('Auth session expired', { sessionId }) await c.env.OAUTH_KV.delete(`auth_session:${sessionId}`) return c.json( { error: 'invalid_request', error_description: 'Auth session expired', }, 400, ) } log.info('Retrieved and validated auth session', { clientId: mcpAuthRequest.clientId, }) // Verify the clerk_token with Clerk API to get user info let clerkSession try { clerkSession = await verifyClerkToken(clerkToken, c.env.CLERK_SECRET_KEY) } catch (error) { log.error('Invalid Clerk token', { sessionId, error: error instanceof Error ? error.message : String(error), }) return c.json( { error: 'invalid_grant', error_description: 'Invalid authentication token', }, 401, ) } // Get user information from Clerk const user = await getClerkUser(clerkSession.userId, c.env.CLERK_SECRET_KEY) const userProps = { user: { id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, imageUrl: user.imageUrl, }, sessionToken: clerkToken, sessionId: clerkSession.id, expiresAt: new Date(clerkSession.expireAt).toISOString(), appUrl: c.env.APP_URL, } // Complete the OAuth authorization const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({ request: mcpAuthRequest, userId: userProps.user.id, metadata: { label: `${userProps.user.firstName} ${userProps.user.lastName}`, clerkToken, }, scope: mcpAuthRequest.scope, props: userProps, }) log.info('OAuth authorization completed, redirecting', { userId: userProps.user.id, clientId: mcpAuthRequest.clientId, }) // Clean up the auth session await c.env.OAUTH_KV.delete(`auth_session:${sessionId}`) return Response.redirect(redirectTo) } catch (error) { log.error('Error in callback handler', { error: error instanceof Error ? error.message : String(error), }) return c.json( { error: 'server_error', error_description: 'Callback processing failed', }, 500, ) } }) export { app as AuthHandler }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dyeoman2/clerk-mcp-template'

If you have feedback or need assistance with the MCP directory API, please join our Discord server