import OAuthProvider from '@cloudflare/workers-oauth-provider'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { DurableMCP } from 'workers-mcp'
import { z } from 'zod'
import { AuthHandler } from './auth'
import { getToken } from './clerk'
import { type Env, type UserProps } from './types'
export class ClerkAppMCP extends DurableMCP<UserProps, Env> {
server = new McpServer({
name: 'MCP Server with Clerk Auth', // TODO: Replace with your own name
version: '1.0.0', // TODO: Replace with your own version
})
private async buildAuthHeaders(): Promise<HeadersInit> {
const token = await getToken(
{
sessionToken: this.props.sessionToken,
sessionId: this.props.sessionId,
updateToken: (newToken: string) => (this.props.sessionToken = newToken),
},
(this as any).env.CLERK_SECRET_KEY,
// Specify your JWT template name here (create one in Clerk Dashboard)
'default',
)
return {
Authorization: `Bearer ${token}`,
Cookie: `__session=${token}`,
Accept: 'application/json',
}
}
private requireAuth<T extends any[], R>(
handler: (...args: T) => Promise<R>,
): (...args: T) => Promise<R> {
return async (...args: T) => {
if (!this.props.user) {
return {
content: [{ type: 'text', text: 'Error: Not authenticated' }],
} as R
}
return handler(...args)
}
}
private async makeApiRequest<T = any>(
path: string,
options: {
method?: string
queryParams?: Record<string, string | number | boolean>
body?: any
} = {},
): Promise<T> {
const { method = 'GET', queryParams, body } = options
const baseUrl = this.props.appUrl
const endpoint = `${baseUrl}/${path.startsWith('/') ? path.slice(1) : path}`
// Build query string
const params = new URLSearchParams()
if (queryParams) {
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined && value !== null) {
params.set(key, String(value))
}
}
}
const url = params.toString()
? `${endpoint}?${params.toString()}`
: endpoint
const headers = await this.buildAuthHeaders()
const response = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
})
if (!response.ok) {
const errorText = await response.text().catch(() => '')
throw new Error(
`API request failed: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ''}`,
)
}
return response.json()
}
async init() {
// Example tool 1: Get current user information
this.server.tool(
'getUserInfo',
'Get current authenticated user information from Clerk',
{},
this.requireAuth(async () => {
return {
content: [
{
type: 'text',
text: JSON.stringify(this.props.user, null, 2),
},
],
}
}),
)
// Example tool 2: Echo/ping test
this.server.tool(
'ping',
'Simple ping test that returns the current timestamp',
{
message: z
.string()
.optional()
.describe('Optional message to include in response'),
},
this.requireAuth(async (args) => {
const response = {
timestamp: new Date().toISOString(),
message: args?.message || 'pong',
user: this.props.user.firstName,
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2),
},
],
}
}),
)
// Example tool 3: Make authenticated API call to your service
this.server.tool(
'getUsers',
'Fetch the current users from your application',
{},
this.requireAuth(async () => {
const users = await this.makeApiRequest('api/users') // TODO: Replace with your own endpoint
return {
content: [
{
type: 'text',
text: JSON.stringify(users, null, 2),
},
],
}
}),
)
}
}
// OAuth provider configuration
export default new OAuthProvider({
apiRoute: '/sse',
apiHandler: ClerkAppMCP.mount('/sse') as any,
defaultHandler: AuthHandler as any,
authorizeEndpoint: '/authorize',
tokenEndpoint: '/token',
clientRegistrationEndpoint: '/register',
})