Clerk MCP Server
Clerk MCP Server
Section titled “Clerk MCP Server”The Clerk MCP server provides comprehensive user authentication and organization management capabilities, enabling you to manage users, organizations, and access control directly through Claude.
Overview
Section titled “Overview”This server enables complete authentication management including:
- User creation, updates, and profile management
- Organization creation and membership management
- Invitation system for users and organizations
- Role-based access control
- Metadata management (public, private, unsafe)
- Session and authentication state management
Authentication Setup
Section titled “Authentication Setup”# Add to your environment fileCLERK_SECRET_KEY=sk_test_your_secret_key_hereCLERK_PUBLISHABLE_KEY=pk_test_your_publishable_key_hereGet your keys from the Clerk Dashboard:
- Select your application
- Go to Developers > API Keys
- Copy the Secret Key and Publishable Key
Available Functions
Section titled “Available Functions”User Management
Section titled “User Management”// Get current authenticated user IDconst userId = await getUserId()
// Get detailed user informationconst user = await getUser({ userId: "user_123" })
// Get total user countconst count = await getUserCount()
// Update user profileawait updateUser({userId: "user_123",firstName: "John",lastName: "Doe",username: "johndoe",profileImageUrl: "https://example.com/avatar.jpg"})// Update public metadata (visible to frontend)await updateUserPublicMetadata({userId: "user_123",metadata: { preferences: { theme: "dark", language: "en", notifications: true }, subscription: { plan: "pro", expires: "2024-12-31" }}})
// Update unsafe metadata (accessible from frontend & backend)await updateUserUnsafeMetadata({userId: "user_123",metadata: { onboardingCompleted: true, lastLoginIP: "192.168.1.1", featureFlags: { betaFeatures: true }}})
// Remove metadata keys by setting to nullawait updateUserPublicMetadata({userId: "user_123",metadata: { oldProperty: null // This removes the property}})Organization Management
Section titled “Organization Management”// Get organization by ID or slugconst org = await getOrganization({organizationId: "org_123",includeMembersCount: true})
// Or get by slugconst orgBySlug = await getOrganization({slug: "my-company"})
// Create new organizationconst newOrg = await createOrganization({name: "Acme Corporation",slug: "acme-corp",createdBy: "user_123",maxAllowedMemberships: 50,publicMetadata: { industry: "Technology", website: "https://acme.com"}})
// Update organizationawait updateOrganization({organizationId: "org_123",name: "Acme Corp (Updated)",maxAllowedMemberships: 100})
// Delete organization (permanent)await deleteOrganization({organizationId: "org_123"})// Add user to organizationawait createOrganizationMembership({organizationId: "org_123",userId: "user_456",role: "admin"})
// Update user's role in organizationawait updateOrganizationMembership({organizationId: "org_123",userId: "user_456",role: "member"})
// Update membership metadataawait updateOrganizationMembershipMetadata({organizationId: "org_123",userId: "user_456",publicMetadata: { department: "Engineering", startDate: "2024-01-15", permissions: ["read", "write"]}})
// Remove user from organizationawait deleteOrganizationMembership({organizationId: "org_123",userId: "user_456"})Invitation System
Section titled “Invitation System”// Create organization invitationawait createOrganizationInvitation({organizationId: "org_123",role: "member",inviterUserId: "user_123",redirectUrl: "https://yourapp.com/accept-invitation",publicMetadata: { department: "Marketing", welcomeMessage: "Welcome to the team!"}})
// Revoke organization invitationawait revokeOrganizationInvitation({organizationId: "org_123",invitationId: "inv_123"})
// Create general application invitationawait createInvitation({redirectUrl: "https://yourapp.com/signup",publicMetadata: { source: "referral", referredBy: "user_123"},notify: true,ignoreExisting: false})
// Revoke application invitationawait revokeInvitation({ invitationId: "inv_456" })Usage Examples
Section titled “Usage Examples”Complete user onboarding workflow:
// 1. Get current userconst currentUserId = await getUserId()
if (!currentUserId) {console.log("User not authenticated")return}
// 2. Get user details to check onboarding statusconst user = await getUser({ userId: currentUserId })
// 3. Check if onboarding is completeconst onboardingComplete = user.unsafeMetadata?.onboardingCompleted
if (!onboardingComplete) {// 4. Update user profile with onboarding dataawait updateUser({ userId: currentUserId, firstName: "John", lastName: "Doe"})
// 5. Set user preferencesawait updateUserPublicMetadata({ userId: currentUserId, metadata: { preferences: { theme: "dark", emailNotifications: true, weeklyDigest: true }, onboardingStep: "profile-complete" }})
// 6. Mark onboarding as completeawait updateUserUnsafeMetadata({ userId: currentUserId, metadata: { onboardingCompleted: true, onboardingCompletedAt: new Date().toISOString() }})
console.log("User onboarding completed successfully")}Create and configure a new organization:
// 1. Create organizationconst organization = await createOrganization({name: "Development Team",slug: "dev-team",maxAllowedMemberships: 20,publicMetadata: { department: "Engineering", budget: "50000", projectCount: 0},privateMetadata: { internalNotes: "New development team for Q2 projects", budgetCode: "ENG-2024-Q2"}})
// 2. Add initial team membersconst teamMembers = []
for (const member of teamMembers) {// Create invitation for each memberawait createOrganizationInvitation({ organizationId: organization.id, emailAddress: member.email, role: member.role, redirectUrl: "https://yourapp.com/join-organization", publicMetadata: { welcomeMessage: "Welcome to the Development Team!", department: "Engineering" }})}
// 3. Set up organization metadataawait updateOrganizationMetadata({organizationId: organization.id,publicMetadata: { setupComplete: true, setupCompletedAt: new Date().toISOString(), initialMemberCount: teamMembers.length}})
console.log(`Organization "${organization.name}" created with ${teamMembers.length} invitations sent`)Implement role-based access control:
// Define role hierarchy and permissionsconst rolePermissions = {admin: [ 'user.create', 'user.read', 'user.update', 'user.delete', 'org.create', 'org.read', 'org.update', 'org.delete', 'invite.create', 'invite.revoke'],manager: [ 'user.read', 'user.update', 'org.read', 'org.update', 'invite.create'],member: [ 'user.read', 'org.read']}
// Function to check user permissionsasync function hasPermission(userId, organizationId, permission) {// Get user's organization membershipconst org = await getOrganization({ organizationId, includeMembersCount: true})
// In a real implementation, you'd get the membership details// This is a simplified exampleconst userRole = 'member' // This would come from membership data
return rolePermissions[userRole]?.includes(permission) || false}
// Update user role with proper permissionsasync function updateUserRole(organizationId, userId, newRole) {// Verify current user has permission to change rolesconst currentUserId = await getUserId()const canManageRoles = await hasPermission( currentUserId, organizationId, 'user.update')
if (!canManageRoles) { throw new Error('Insufficient permissions to update user roles')}
// Update the user's roleawait updateOrganizationMembership({ organizationId, userId, role: newRole})
// Log the change in metadataawait updateOrganizationMembershipMetadata({ organizationId, userId, privateMetadata: { roleHistory: { changedAt: new Date().toISOString(), changedBy: currentUserId, previousRole: 'member', // In real implementation, get from current data newRole: newRole } }})}Integration with astro-basics
Section titled “Integration with astro-basics”// Common patterns for astro-basics project
// 1. User profile management for dashboardasync function updateUserProfile(profileData) {const userId = await getUserId()
if (!userId) { throw new Error('User not authenticated')}
// Update basic profileawait updateUser({ userId, firstName: profileData.firstName, lastName: profileData.lastName, username: profileData.username})
// Update preferences in public metadataawait updateUserPublicMetadata({ userId, metadata: { preferences: { theme: profileData.theme || 'light', emailNotifications: profileData.emailNotifications || false, language: profileData.language || 'en' }, profile: { bio: profileData.bio, website: profileData.website, location: profileData.location } }})}
// 2. Comment system integrationasync function getUserForComment() {const userId = await getUserId()
if (!userId) { return null}
const user = await getUser({ userId })
return { id: user.id, name: `${user.firstName} ${user.lastName}`.trim(), username: user.username, avatar: user.profileImageUrl, role: user.publicMetadata?.role || 'user'}}
// 3. Organization-based content accessasync function checkContentAccess(organizationId, contentType) {const userId = await getUserId()
if (!userId) { return false}
try { const org = await getOrganization({ organizationId })
// Check if user is member of organization // In real implementation, you'd check membership return true // Simplified for example} catch (error) { return false}}Security Best Practices
Section titled “Security Best Practices”Metadata Security Guidelines
Section titled “Metadata Security Guidelines”// ✅ Good: Safe public metadataawait updateUserPublicMetadata({userId,metadata: { preferences: { theme: "dark" }, displayName: "John D.", publicProfile: { bio: "Developer" }}})
// ❌ Bad: Sensitive data in public metadataawait updateUserPublicMetadata({userId,metadata: { creditCard: "4111-1111-1111-1111", // Never do this! socialSecurity: "123-45-6789", // Never do this! password: "secret123" // Never do this!}})
// ✅ Good: Internal data in unsafe/private metadataawait updateUserUnsafeMetadata({userId,metadata: { lastLoginIP: "192.168.1.1", loginCount: 42, internalNotes: "VIP customer"}})Limitations
Section titled “Limitations”- Rate Limits: API calls are subject to Clerk’s rate limiting
- Metadata Size: Limited metadata storage per user/organization
- Role Flexibility: Built-in roles, custom RBAC requires additional logic
- Bulk Operations: No native bulk user operations
Error Handling
Section titled “Error Handling”// Handle common Clerk errorstry {await updateUser({ userId: "invalid-user-id", firstName: "John"})} catch (error) {if (error.message.includes('User not found')) { console.log('User does not exist')} else if (error.message.includes('Invalid user ID')) { console.log('Malformed user ID provided')} else if (error.message.includes('Rate limit exceeded')) { console.log('Too many requests, please retry later')} else { console.log('Unexpected error:', error.message)}}Related Resources
Section titled “Related Resources”- Supabase Server - For user data storage integration
- Netlify Server - For deployment with authentication
- MCP Examples - See complete authentication workflows
Troubleshooting
Section titled “Troubleshooting”Authentication failures:
- Verify your secret key is correct and not expired
- Check if the user ID exists in your Clerk instance
- Ensure proper environment variable configuration
Permission errors:
- Verify the API key has necessary permissions
- Check organization membership and roles
- Ensure proper role hierarchy implementation
Metadata updates not working:
- Check for metadata size limits (typically 10KB per user)
- Verify JSON structure is valid
- Use null values to remove metadata keys