Skip to content
Talk to an Engineer Dashboard

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.

Ideal when an AI agent or MCP Client acts on behalf of a human user:

// Step 1: Redirect user to authorization server
const 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 token
app.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
});

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();
};

Design OAuth scopes that reflect your MCP server’s actual capabilities and security requirements:

// 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 functions
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 handlers
app.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
});

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 DCR
const 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, ... }
};

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());

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 handlers
app.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' });
}
});

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);
});

Token Validation Failures

// Debug token validation issues
const 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 endpoints
app.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();
};

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 examples
app.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);
});

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);
};

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;
}
}
};