MCP Servers - Additional Reading
Explore advanced topics for MCP servers, including OAuth 2.1 flows, scope design, dynamic client registration, and security best practices.
MCP Clients that want to get authorized to access your MCP Server need to follow either of the below OAuth 2.1 Flows Supported by Scalekit.
OAuth 2.1 Flows Supported
Section titled “OAuth 2.1 Flows Supported”Authorization Code Flow
Section titled “Authorization Code Flow”Ideal when an AI agent or MCP Client acts on behalf of a human user:
// Step 1: Redirect user to authorization serverconst authURL = new URL('https://your-org.scalekit.com/oauth/authorize');authURL.searchParams.set('response_type', 'code');authURL.searchParams.set('client_id', 'your-client-id');authURL.searchParams.set('redirect_uri', 'https://your-app.com/callback');authURL.searchParams.set('scope', 'mcp:tools:calendar:read mcp:tools:email:send');authURL.searchParams.set('state', generateSecureRandomString());authURL.searchParams.set('code_challenge', generatePKCEChallenge());authURL.searchParams.set('code_challenge_method', 'S256');
// Step 2: Handle callback and exchange code for tokenapp.get('/callback', async (req, res) => { const { code, state } = req.query;
// Verify state parameter to prevent CSRF if (!isValidState(state)) { return res.status(400).json({ error: 'Invalid state parameter' }); }
const tokenResponse = await fetch('https://your-org.scalekit.com/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code, client_id: 'your-client-id', redirect_uri: 'https://your-app.com/callback', code_verifier: getPKCEVerifier() // From PKCE challenge generation }) });
const tokens = await tokenResponse.json(); // Store tokens securely and proceed with MCP calls});Client Credentials Flow
Section titled “Client Credentials Flow”Perfect for automated agents that don’t represent a specific user but want to access your MCP Server on their own behalf. This is typically used for Machine-to-Machine (M2M) authentication.
const getMachineToken = async () => { const response = await fetch('https://your-org.scalekit.com/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: 'your-service-client-id', client_secret: 'your-service-client-secret', scope: 'mcp:tools:inventory:check mcp:resources:store-data', audience: 'https://your-mcp-server.com', }) });
return await response.json();};Scope Design Best Practices
Section titled “Scope Design Best Practices”Design OAuth scopes that reflect your MCP server’s actual capabilities and security requirements:
Hierarchical Scopes
Section titled “Hierarchical Scopes”// Resource-based scopes'mcp:resources:customer-data:read' // Read customer data'mcp:resources:customer-data:write' // Modify customer data'mcp:resources:*' // All resources (admin-level)
// Tool-based scopes'mcp:tools:weather' // Weather API access'mcp:tools:calendar:read' // Read calendar events'mcp:tools:calendar:write' // Create/modify calendar events'mcp:tools:email:send' // Send emails'mcp:tools:*' // All tools access
// Action-based scopes'mcp:exec:workflows:risk-assessment' // Execute risk assessment workflow'mcp:exec:functions:data-analysis' // Run data analysis functionsScope Validation Helpers
Section titled “Scope Validation Helpers”const ScopeValidator = { hasScope: (userScopes, requiredScope) => { return userScopes.includes(requiredScope) || userScopes.includes(requiredScope.split(':').slice(0, -1).join(':') + ':*'); },
hasAnyScope: (userScopes, allowedScopes) => { return allowedScopes.some(scope => ScopeValidator.hasScope(userScopes, scope)); },
validateToolAccess: (userScopes, toolName) => { const toolScope = `mcp:tools:${toolName}`; const wildcardScope = 'mcp:tools:*'; return userScopes.includes(toolScope) || userScopes.includes(wildcardScope); }};
// Usage in MCP tool handlersapp.post('/mcp/tools/:toolName', (req, res) => { const { toolName } = req.params; const userScopes = req.auth.scopes;
if (!ScopeValidator.validateToolAccess(userScopes, toolName)) { return res.status(403).json({ error: 'insufficient_scope', error_description: `Access to tool '${toolName}' requires appropriate scope` }); }
// Process tool request});Dynamic Client Registration
Section titled “Dynamic Client Registration”Scalekit supports Dynamic Client Registration (DCR) to enable seamless integration for new MCP clients that want to connect to your MCP Server. MCP clients can auto-register using DCR:
// MCP clients can auto-register using DCRconst registerClient = async (clientMetadata) => { const response = await fetch('https://your-org.scalekit.com/resource-server/oauth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_name: 'AI Sales Assistant', client_uri: 'https://sales-ai.company.com', redirect_uris: ['https://sales-ai.company.com/oauth/callback'], grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], scope: 'mcp:tools:crm:read mcp:tools:email:send', audience: 'https://your-mcp-server.com', token_endpoint_auth_method: 'client_secret_basic', ...clientMetadata }) });
return await response.json(); // Returns: { client_id, client_secret, client_id_issued_at, ... }};Security Implementation
Section titled “Security Implementation”Rate Limiting by Client
Section titled “Rate Limiting by Client”Implement client-specific rate limits:
import rateLimit from 'express-rate-limit';
const createClientRateLimit = () => { return rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes limit: (req) => { // Different limits based on client type or scopes const scopes = req.auth?.scopes || []; if (scopes.includes('mcp:tools:*')) return 1000; // Premium client if (scopes.includes('mcp:tools:basic')) return 100; // Basic client return 50; // Default limit }, keyGenerator: (req) => req.auth?.clientId || req.ip, message: { error: 'rate_limit_exceeded', error_description: 'Too many requests from this client' } });};
app.use('/mcp', createClientRateLimit());Comprehensive Logging
Section titled “Comprehensive Logging”Track all OAuth and MCP interactions:
const auditLogger = { logTokenRequest: (clientId, grantType, scopes, success) => { console.log(JSON.stringify({ event: 'oauth_token_request', timestamp: new Date().toISOString(), client_id: clientId, grant_type: grantType, requested_scopes: scopes, success })); },
logMCPAccess: (req, toolName, success, error = null) => { console.log(JSON.stringify({ event: 'mcp_tool_access', timestamp: new Date().toISOString(), user_id: req.auth?.userId, client_id: req.auth?.clientId, tool_name: toolName, scopes: req.auth?.scopes, success, error: error?.message, ip_address: req.ip, user_agent: req.get('User-Agent') })); }};
// Use in your MCP handlersapp.post('/mcp/tools/:toolName', async (req, res) => { const { toolName } = req.params;
try { // Process tool request const result = await processToolRequest(toolName, req.body);
auditLogger.logMCPAccess(req, toolName, true); res.json(result); } catch (error) { auditLogger.logMCPAccess(req, toolName, false, error); res.status(500).json({ error: 'Tool execution failed' }); }});Health Check Endpoints
Section titled “Health Check Endpoints”Monitor your MCP server and authorization integration:
app.get('/health', async (req, res) => { const health = { status: 'healthy', timestamp: new Date().toISOString(), services: { mcp_server: 'healthy', oauth_server: 'unknown' } };
try { // Test OAuth server connectivity const oauthTest = await fetch('https://your-org.scalekit.com/.well-known/oauth-authorization-server'); health.services.oauth_server = oauthTest.ok ? 'healthy' : 'degraded'; } catch (error) { health.services.oauth_server = 'unhealthy'; health.status = 'degraded'; }
const statusCode = health.status === 'healthy' ? 200 : 503; res.status(statusCode).json(health);});Troubleshooting
Section titled “Troubleshooting”Common Issues and Solutions
Section titled “Common Issues and Solutions”Token Validation Failures
// Debug token validation issuesconst debugTokenValidation = async (token) => { try { // Check token structure const [header, payload, signature] = token.split('.'); console.log('Token Header:', JSON.parse(atob(header))); console.log('Token Payload:', JSON.parse(atob(payload)));
// Validate with detailed error info await jwtVerify(token, JWKS, { issuer: 'https://your-org.scalekit.com', audience: 'https://your-mcp-server.com' }); } catch (error) { console.error('Token validation error:', { name: error.name, message: error.message, code: error.code }); }};CORS Issues with Authorization Server
// Configure CORS for OAuth endpointsapp.use('/oauth', cors({ origin: 'https://your-org.scalekit.com', credentials: true, methods: ['GET', 'POST', 'OPTIONS'], allowedHeaders: ['Authorization', 'Content-Type', 'MCP-Protocol-Version']}));Scope Permission Debugging
const debugScopes = (req, res, next) => { console.log('Request Scopes:', { user_scopes: req.auth?.scopes, required_scope: req.requiredScope, has_permission: req.auth?.scopes?.includes(req.requiredScope) }); next();};Error Response Standards
Section titled “Error Response Standards”Follow OAuth 2.1 and MCP error response formats:
const sendOAuthError = (res, error, description, statusCode = 400) => { res.status(statusCode).json({ error, error_description: description, error_uri: 'https://your-mcp-server.com/docs/errors' });};
// Usage examplesapp.use((error, req, res, next) => { if (error.name === 'TokenExpiredError') { return sendOAuthError(res, 'invalid_token', 'Access token has expired', 401); }
if (error.name === 'InsufficientScopeError') { return sendOAuthError(res, 'insufficient_scope', `Required scope: ${error.requiredScope}`, 403); }
// Default error sendOAuthError(res, 'server_error', 'An unexpected error occurred', 500);});Advanced Configuration
Section titled “Advanced Configuration”Custom Scope Mapping
Section titled “Custom Scope Mapping”Map OAuth scopes to internal permissions:
const scopePermissionMap = { 'mcp:tools:weather': ['weather:read'], 'mcp:tools:calendar:read': ['calendar:events:read'], 'mcp:tools:calendar:write': ['calendar:events:read', 'calendar:events:write'], 'mcp:tools:email:send': ['email:send', 'contacts:read'], 'mcp:resources:customer-data': ['customers:read', 'customers:write']};
const getPermissionsFromScopes = (scopes) => { const permissions = new Set(); scopes.forEach(scope => { const scopePermissions = scopePermissionMap[scope] || []; scopePermissions.forEach(permission => permissions.add(permission)); }); return Array.from(permissions);};Refresh Token Management
Section titled “Refresh Token Management”Handle token refresh for long-running agents:
const TokenManager = { async refreshToken(refreshToken) { const response = await fetch('https://your-org.scalekit.com/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: 'your-client-id', client_secret: 'your-client-secret' }) });
return await response.json(); },
async autoRefreshWrapper(tokenStore, makeRequest) { try { return await makeRequest(tokenStore.accessToken); } catch (error) { if (error.status === 401) { // Token expired, try refresh const newTokens = await this.refreshToken(tokenStore.refreshToken); tokenStore.accessToken = newTokens.access_token; tokenStore.refreshToken = newTokens.refresh_token;
// Retry original request return await makeRequest(tokenStore.accessToken); } throw error; } }};