Fortifying Your MCP Server: A Comprehensive Guide to Permissions, Validation, and Abuse Prevention
As your Model Context Protocol (MCP) server unveils its capabilities for AI interaction, a fundamental security challenge emerges: controlling access and usage. This guide will walk you through transforming your server into an impregnable digital stronghold, ensuring robust protection without compromising its utility. A truly effective server is always a well-guarded one.
Introduction: Security as a Core Principle
Drawing from over a decade and a half in API development, I've internalized a critical principle: security must be foundational, not an afterthought. For an MCP server managing your files, data, and critical assets, multi-layered defense is indispensable. Yet, bolstering security doesn't equate to increased complexity. This article outlines the implementation of four vital security safeguards: rigorous input validation to thwart malicious data, robust authentication to verify user identities, precise authorization to define access privileges, and intelligent resource limiting to prevent misuse. Upon completion, your server will be primed for a production environment.
Core Principles of MCP Server Security
Before diving into code, let's establish our defense-in-depth strategy:
1. Input Validation
Principle: Treat all incoming data with suspicion. Rigorously validate, sanitize, and verify every piece of information.
Rationale: Inadequate parameter validation can open doors to critical vulnerabilities, such as directory traversal attacks (e.g., accessing sensitive files like ../../etc/passwd), code injection exploits, or even server instability.
2. Authentication
Principle: Confirm the identity of every entity interacting with your server. Every incoming request needs to be linked to a confirmed and verified identity.
Rationale: Lacking authentication leaves your tools completely exposed to unauthorized access, much like an unlocked front door invites uninvited guests.
3. Authorization
Principle: Validate specific access rights. Even after successful authentication, users should only be permitted to perform actions aligned with their roles.
Rationale: An intern, for instance, has no business accessing human resources records. Implementing fine-grained permissions is key to safeguarding sensitive data.
4. Resource Limiting
Principle: Establish clear quotas, set size caps, and implement connection timeouts.
Rationale: These measures are crucial to prevent a single malicious actor or an unforeseen error from overwhelming your server with an exorbitant number of requests, such as 10,000 per second.
Robust Input Validation
We begin with perhaps the most critical defense: validating all incoming data. Implement this by creating src/security/validator.ts as follows:
// src/security/validator.ts
import path from 'path';
import { InputSchema } from '../mcp/protocol';
/**
* Validation error
*/
export class ValidationError extends Error {
constructor(
message: string,
public field?: string,
public expected?: string
) {
super(message);
this.name = 'ValidationError';
}
}
/**
* Parameter validator based on JSON Schema
*/
export class ParameterValidator {
/**
* Validate parameters according to schema
*/
static validate(params: any, schema: InputSchema): void {
// Check that params is an object
if (typeof params !== 'object' || params === null) {
throw new ValidationError('Parameters must be an object');
}
// Check required fields
for (const requiredField of schema.required) {
if (!(requiredField in params)) {
throw new ValidationError(
`Field '${requiredField}' is required`,
requiredField
);
}
}
// Validate each property
for (const [fieldName, fieldValue] of Object.entries(params)) {
const fieldSchema = schema.properties[fieldName];
if (!fieldSchema) {
throw new ValidationError(
`Field '${fieldName}' is not allowed`,
fieldName
);
}
this.validateField(fieldName, fieldValue, fieldSchema);
}
}
/**
* Validate a specific field
*/
private static validateField(
fieldName: string,
value: any,
schema: any
): void {
// Type validation
const actualType = typeof value;
const expectedType = schema.type;
if (expectedType === 'string' && actualType !== 'string') {
throw new ValidationError(
`Field '${fieldName}' must be a string`,
fieldName,
expectedType
);
}
if (expectedType === 'number' && actualType !== 'number') {
throw new ValidationError(
`Field '${fieldName}' must be a number`,
fieldName,
expectedType
);
}
if (expectedType === 'boolean' && actualType !== 'boolean') {
throw new ValidationError(
`Field '${fieldName}' must be a boolean`,
fieldName,
expectedType
);
}
// Enumeration validation
if (schema.enum && !schema.enum.includes(value)) {
throw new ValidationError(
`Field '${fieldName}' must be one of: ${schema.enum.join(', ')}`,
fieldName
);
}
// Length validation for strings
if (expectedType === 'string') {
if (schema.minLength && value.length < schema.minLength) {
throw new ValidationError(
`Field '${fieldName}' must contain at least ${schema.minLength} characters`,
fieldName
);
}
if (schema.maxLength && value.length > schema.maxLength) {
throw new ValidationError(
`Field '${fieldName}' cannot exceed ${schema.maxLength} characters`,
fieldName
);
}
}
// Range validation for numbers
if (expectedType === 'number') {
if (schema.minimum !== undefined && value < schema.minimum) {
throw new ValidationError(
`Field '${fieldName}' must be greater than or equal to ${schema.minimum}`,
fieldName
);
}
if (schema.maximum !== undefined && value > schema.maximum) {
throw new ValidationError(
`Field '${fieldName}' cannot exceed ${schema.maximum}`,
fieldName
);
}
}
// Pattern validation for strings
if (expectedType === 'string' && schema.pattern) {
const regex = new RegExp(schema.pattern);
if (!regex.test(value)) {
throw new ValidationError(
`Field '${fieldName}' doesn't match expected format`,
fieldName
);
}
}
}
}
/**
* File path validator
*/
export class PathValidator {
private allowedDirectories: string[];
private blockedPaths: string[];
constructor(allowedDirectories: string[], blockedPaths: string[] = []) {
// Resolve all paths to absolute
this.allowedDirectories = allowedDirectories.map(dir => path.resolve(dir));
this.blockedPaths = blockedPaths.map(p => path.resolve(p));
}
/**
* Validate that a path is safe
*/
validatePath(filePath: string): string {
// Resolve absolute path
const absolutePath = path.resolve(filePath);
// Check path traversal (../)
if (absolutePath.includes('..')) {
throw new ValidationError(
'Paths with ".." are not allowed (path traversal)'
);
}
// Check that path is in an allowed directory
const isInAllowedDir = this.allowedDirectories.some(dir =>
absolutePath.startsWith(dir)
);
if (!isInAllowedDir) {
throw new ValidationError(
`Access denied: path must be in one of the allowed directories`
);
}
// Check that path is not blocked
const isBlocked = this.blockedPaths.some(blocked =>
absolutePath.startsWith(blocked)
);
if (isBlocked) {
throw new ValidationError(
`Access denied: this path is explicitly blocked`
);
}
return absolutePath;
}
/**
* Add an allowed directory
*/
addAllowedDirectory(directory: string): void {
this.allowedDirectories.push(path.resolve(directory));
}
/**
* Block a specific path
*/
blockPath(pathToBlock: string): void {
this.blockedPaths.push(path.resolve(pathToBlock));
}
}
/**
* File size validator
*/
export class SizeValidator {
/**
* Validate that a size is acceptable
*/
static validateSize(
size: number,
maxSize: number,
fieldName: string = 'file'
): void {
if (size > maxSize) {
throw new ValidationError(
`The ${fieldName} is too large (max ${this.formatSize(maxSize)})`
);
}
}
/**
* Format size in bytes to readable format
*/
static formatSize(bytes: number): string {
const units = ['bytes', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
}
This comprehensive validation module meticulously verifies:
- Data Types: Ensures inputs conform to expected types (e.g., string, number, boolean).
- Mandatory Fields: Confirms the presence of all required parameters.
- Enumerated Values: Checks if values are drawn from a predefined, permissible list.
- String Constraints: Enforces minimum and maximum length restrictions for text fields.
- Numeric Bounds: Applies lower and upper limits for numerical inputs.
- Format Patterns: Validates string formats using regular expressions.
- Path Security: Guards against directory traversal exploits and restricts access to approved directories.
- Size Restrictions: Imposes limits on file or data sizes.
JWT Authentication System
Next, we'll develop an authentication mechanism leveraging JSON Web Tokens (JWT). Set up src/security/auth.ts with the following content:
// src/security/auth.ts
import crypto from 'crypto';
/**
* User interface
*/
export interface User {
id: string;
username: string;
role: 'admin' | 'user' | 'readonly';
permissions: string[];
}
/**
* Simplified JWT token (for demo - use a real JWT lib in prod)
*/
interface Token {
userId: string;
username: string;
role: string;
permissions: string[];
expiresAt: number;
}
/**
* Authentication manager
*/
export class AuthManager {
private users: Map<string, User> = new Map();
private tokens: Map<string, Token> = new Map();
private readonly SECRET_KEY: string;
private readonly TOKEN_DURATION = 24 * 60 * 60 * 1000; // 24 hours
constructor(secretKey: string) {
this.SECRET_KEY = secretKey;
// Create some test users
this.createUser({
id: '1',
username: 'admin',
role: 'admin',
permissions: ['*'] // All permissions
});
this.createUser({
id: '2',
username: 'user',
role: 'user',
permissions: ['readFile', 'listFiles', 'searchFiles']
});
this.createUser({
id: '3',
username: 'readonly',
role: 'readonly',
permissions: ['readFile', 'listFiles']
});
}
/**
* Create a user
*/
createUser(user: User): void {
this.users.set(user.username, user);
}
/**
* Authenticate a user and generate a token
*/
authenticate(username: string, password: string): string | null {
// In production, verify hashed password!
// This is simplified for demo
const user = this.users.get(username);
if (!user) {
return null;
}
// Generate a token
const tokenId = crypto.randomBytes(32).toString('hex');
const token: Token = {
userId: user.id,
username: user.username,
role: user.role,
permissions: user.permissions,
expiresAt: Date.now() + this.TOKEN_DURATION
};
this.tokens.set(tokenId, token);
return tokenId;
}
/**
* Validate a token
*/
validateToken(tokenId: string): Token | null {
const token = this.tokens.get(tokenId);
if (!token) {
return null;
}
// Check expiration
if (Date.now() > token.expiresAt) {
this.tokens.delete(tokenId);
return null;
}
return token;
}
/**
* Revoke a token
*/
revokeToken(tokenId: string): void {
this.tokens.delete(tokenId);
}
/**
* Get a user
*/
getUser(username: string): User | undefined {
return this.users.get(username);
}
/**
* Clean expired tokens
*/
cleanExpiredTokens(): void {
const now = Date.now();
for (const [tokenId, token] of this.tokens.entries()) {
if (now > token.expiresAt) {
this.tokens.delete(tokenId);
}
}
}
}
/**
* Authentication middleware for Express
*/
export function authMiddleware(authManager: AuthManager) {
return (req: any, res: any, next: any) => {
// Get token from Authorization header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
error: 'Missing authentication token'
});
}
const tokenId = authHeader.substring(7); // Remove "Bearer "
const token = authManager.validateToken(tokenId);
if (!token) {
return res.status(401).json({
success: false,
error: 'Invalid or expired token'
});
}
// Add user info to request
req.user = token;
next();
};
}
Granular Permission System
Moving forward, we'll build a permission system designed to ascertain whether a specific user is authorized to invoke a particular tool. Construct src/security/permissions.ts with the code below:
// src/security/permissions.ts
import { User } from './auth';
/**
* Permission error
*/
export class PermissionError extends Error {
constructor(message: string) {
super(message);
this.name = 'PermissionError';
}
}
/**
* Permission manager
*/
export class PermissionManager {
/**
* Check if a user has permission to use a tool
*/
static hasPermission(
user: User,
toolName: string,
params?: any
): boolean {
// Admins have access to everything
if (user.permissions.includes('*')) {
return true;
}
// Check specific permission
if (!user.permissions.includes(toolName)) {
return false;
}
// Additional contextual permissions
// For example, check allowed paths for readFile
if (toolName === 'readFile' && params?.file_path) {
return this.canAccessPath(user, params.file_path);
}
return true;
}
/**
* Check access to a specific path
*/
private static canAccessPath(user: User, filePath: string): boolean {
// In readonly, only read in certain folders
if (user.role === 'readonly') {
const allowedPaths = ['/public', '/docs'];
return allowedPaths.some(allowed =>
filePath.startsWith(allowed)
);
}
return true;
}
/**
* Get user permissions
*/
static getPermissions(user: User): string[] {
return user.permissions;
}
/**
* Check and throw error if no permission
*/
static requirePermission(
user: User,
toolName: string,
params?: any
): void {
if (!this.hasPermission(user, toolName, params)) {
throw new PermissionError(
`Permission denied: you don't have access to tool '${toolName}'`
);
}
}
}
/**
* Permission policy for a tool
*/
export interface ToolPolicy {
allowedRoles: string[];
requiredPermissions: string[];
rateLimit?: {
maxRequests: number;
windowMs: number;
};
}
/**
* Tool policy manager
*/
export class PolicyManager {
private policies: Map<string, ToolPolicy> = new Map();
/**
* Set a policy for a tool
*/
setPolicy(toolName: string, policy: ToolPolicy): void {
this.policies.set(toolName, policy);
}
/**
* Get a tool's policy
*/
getPolicy(toolName: string): ToolPolicy | undefined {
return this.policies.get(toolName);
}
/**
* Check that a user respects the policy
*/
checkPolicy(user: User, toolName: string): boolean {
const policy = this.policies.get(toolName);
if (!policy) {
return true; // No policy = allowed by default
}
// Check role
if (!policy.allowedRoles.includes(user.role) &&
!policy.allowedRoles.includes('*')) {
return false;
}
// Check permissions
const hasAllPermissions = policy.requiredPermissions.every(perm =>
user.permissions.includes(perm) || user.permissions.includes('*')
);
return hasAllPermissions;
}
}
Rate Limiting and Quotas
To shield our server from potential abuse, let's implement a robust rate limiting framework. Create src/security/rateLimit.ts containing:
// src/security/rateLimit.ts
/**
* Usage record
*/
interface UsageRecord {
count: number;
resetAt: number;
}
/**
* Rate limiting manager
*/
export class RateLimiter {
private usage: Map<string, UsageRecord> = new Map();
constructor(
private maxRequests: number,
private windowMs: number
) {}
/**
* Check and increment counter for a user
*/
checkLimit(userId: string): boolean {
const now = Date.now();
const record = this.usage.get(userId);
// No record or expired window
if (!record || now > record.resetAt) {
this.usage.set(userId, {
count: 1,
resetAt: now + this.windowMs
});
return true;
}
// Limit reached
if (record.count >= this.maxRequests) {
return false;
}
// Increment counter
record.count++;
return true;
}
/**
* Get limit info for a user
*/
getLimitInfo(userId: string): {
current: number;
max: number;
resetsAt: Date;
} {
const record = this.usage.get(userId);
if (!record) {
return {
current: 0,
max: this.maxRequests,
resetsAt: new Date(Date.now() + this.windowMs)
};
}
return {
current: record.count,
max: this.maxRequests,
resetsAt: new Date(record.resetAt)
};
}
/**
* Reset counter for a user
*/
reset(userId: string): void {
this.usage.delete(userId);
}
/**
* Clean expired records
*/
cleanup(): void {
const now = Date.now();
for (const [userId, record] of this.usage.entries()) {
if (now > record.resetAt) {
this.usage.delete(userId);
}
}
}
}
/**
* Rate limiting middleware for Express
*/
export function rateLimitMiddleware(rateLimiter: RateLimiter) {
return (req: any, res: any, next: any) => {
const userId = req.user?.userId || req.ip;
if (!rateLimiter.checkLimit(userId)) {
const info = rateLimiter.getLimitInfo(userId);
return res.status(429).json({
success: false,
error: 'Request limit reached',
limit: {
max: info.max,
current: info.current,
resetsAt: info.resetsAt
}
});
}
next();
};
}
/**
* Quota manager per tool
*/
export class QuotaManager {
private quotas: Map<string, Map<string, number>> = new Map();
/**
* Set a quota for a user and tool
*/
setQuota(userId: string, toolName: string, maxUsage: number): void {
if (!this.quotas.has(userId)) {
this.quotas.set(userId, new Map());
}
this.quotas.get(userId)!.set(toolName, maxUsage);
}
/**
* Check and decrement quota
*/
checkQuota(userId: string, toolName: string): boolean {
const userQuotas = this.quotas.get(userId);
if (!userQuotas) {
return true; // No quota = unlimited
}
const remaining = userQuotas.get(toolName);
if (remaining === undefined) {
return true; // No quota for this tool
}
if (remaining <= 0) {
return false; // Quota exhausted
}
userQuotas.set(toolName, remaining - 1);
return true;
}
/**
* Get remaining quota
*/
getRemainingQuota(userId: string, toolName: string): number | null {
const userQuotas = this.quotas.get(userId);
if (!userQuotas) {
return null; // Unlimited
}
return userQuotas.get(toolName) || null;
}
/**
* Reset a user's quota
*/
resetQuota(userId: string, toolName: string, maxUsage: number): void {
this.setQuota(userId, toolName, maxUsage);
}
}
Conclusion: A Production-Ready, Secure MCP Server
You've done it! Your MCP server is now equipped with four critical security layers, making it ready for production deployment:
- ✅ Thorough input validation
- ✅ Secure token-based authentication
- ✅ Precise, granular authorization
- ✅ Effective rate limiting and usage quotas
With these safeguards in place, your server can confidently operate in a live environment. AIs can interact with it securely, users are assigned specific, appropriate permissions, and potential misuse is automatically curtailed. In the forthcoming, concluding installment of this series, we will integrate your fortified server with Claude Desktop, demonstrating the complete ecosystem functioning seamlessly with a real AI. Until then, I encourage you to challenge your newly built security mechanisms! Attempt to circumvent them, push their boundaries, and confirm every aspect is meticulously protected. A truly robust security system is one that has endured attempts at breach and emerged resilient.
Article published on December 10, 2025 by Nicolas Dabène - PHP & PrestaShop Expert with 15+ years of experience in software architecture and AI integration
Also read:
- Understanding the Model Context Protocol (MCP): A Simple Conversation
- Create Your First MCP Server: TypeScript Project Setup
- Create Your First MCP Tool: The readFile Tool Explained
- The MCP Menu: How AI Discovers and Uses Your Tools
- Connect Your MCP Server to Claude Desktop: The Complete Integration
If you found this guide helpful and want to dive deeper into software architecture, AI integration, and more development insights, be sure to connect with me!
- Subscribe to my YouTube channel: youtube.com/@ndabene06 for regular tutorials and deep dives.
- Follow me on LinkedIn: Nicolas Dabène to stay updated with my latest articles and thoughts.
Top comments (0)