🎁 Perplexity PRO offert
Sécuriser votre Serveur MCP : Permissions, Validation et Protection
Votre serveur MCP expose maintenant plusieurs outils que les IA peuvent découvrir et utiliser. Génial ! Mais une question cruciale se pose : qui peut utiliser quoi ? Dans cet article, nous allons transformer votre serveur en une forteresse sécurisée, sans sacrifier sa simplicité d’utilisation. Parce qu’un serveur puissant doit aussi être un serveur protégé.
Introduction
En 15 ans de développement d’API, j’ai appris une règle d’or : la sécurité ne se rajoute pas après coup, elle se conçoit dès le départ. Un serveur MCP qui donne accès à vos fichiers, vos données, vos ressources sensibles nécessite plusieurs couches de protection. Mais rassurez-vous : sécuriser ne veut pas dire complexifier.
Aujourd’hui, nous allons implémenter quatre piliers de sécurité essentiels : la validation des entrées (pour éviter les données malveillantes), l’authentification (qui êtes-vous ?), l’autorisation (qu’avez-vous le droit de faire ?) et la limitation des ressources (pour éviter les abus). À la fin de cet article, votre serveur sera production-ready.
Les Quatre Piliers de la Sécurité MCP
Avant de coder, comprenons notre stratégie de défense en profondeur :
1. Validation des Entrées
Le principe : Ne jamais faire confiance aux données entrantes. Toujours valider, nettoyer, vérifier.
Pourquoi ? Un paramètre mal validé peut permettre un accès à des fichiers sensibles (../../etc/passwd), une injection de code, ou un crash du serveur.
2. Authentification
Le principe : Identifier qui utilise votre serveur. Chaque requête doit être associée à une identité vérifiée.
Pourquoi ? Sans authentification, n’importe qui peut utiliser vos outils. C’est comme laisser votre maison sans serrure.
3. Autorisation
Le principe : Vérifier les permissions. Même authentifié, tout le monde ne peut pas tout faire.
Pourquoi ? Votre stagiaire n’a pas besoin d’accéder aux fichiers RH. Les permissions granulaires protègent vos données sensibles.
4. Limitation des Ressources
Le principe : Imposer des quotas, des limites de taille, des timeouts.
Pourquoi ? Éviter qu’un utilisateur malveillant (ou une erreur) ne sature votre serveur avec 10 000 requêtes par seconde.
Validation Robuste des Entrées
Commençons par le plus important : valider toutes les entrées. Créez src/security/validator.ts :
// src/security/validator.ts
import path from 'path';
import { InputSchema } from '../mcp/protocol';
/**
* Erreur de validation
*/
export class ValidationError extends Error {
constructor(
message: string,
public field?: string,
public expected?: string
) {
super(message);
this.name = 'ValidationError';
}
}
/**
* Validateur de paramètres basé sur JSON Schema
*/
export class ParameterValidator {
/**
* Valider les paramètres selon un schéma
*/
static validate(params: any, schema: InputSchema): void {
// Vérifier que params est un objet
if (typeof params !== 'object' || params === null) {
throw new ValidationError('Les paramètres doivent être un objet');
}
// Vérifier les champs requis
for (const requiredField of schema.required) {
if (!(requiredField in params)) {
throw new ValidationError(
`Le champ '${requiredField}' est requis`,
requiredField
);
}
}
// Valider chaque propriété
for (const [fieldName, fieldValue] of Object.entries(params)) {
const fieldSchema = schema.properties[fieldName];
if (!fieldSchema) {
throw new ValidationError(
`Le champ '${fieldName}' n'est pas autorisé`,
fieldName
);
}
this.validateField(fieldName, fieldValue, fieldSchema);
}
}
/**
* Valider un champ spécifique
*/
private static validateField(
fieldName: string,
value: any,
schema: any
): void {
// Validation de type
const actualType = typeof value;
const expectedType = schema.type;
if (expectedType === 'string' && actualType !== 'string') {
throw new ValidationError(
`Le champ '${fieldName}' doit être une chaîne de caractères`,
fieldName,
expectedType
);
}
if (expectedType === 'number' && actualType !== 'number') {
throw new ValidationError(
`Le champ '${fieldName}' doit être un nombre`,
fieldName,
expectedType
);
}
if (expectedType === 'boolean' && actualType !== 'boolean') {
throw new ValidationError(
`Le champ '${fieldName}' doit être un booléen`,
fieldName,
expectedType
);
}
// Validation d'énumération
if (schema.enum && !schema.enum.includes(value)) {
throw new ValidationError(
`Le champ '${fieldName}' doit être l'une des valeurs : ${schema.enum.join(', ')}`,
fieldName
);
}
// Validation de longueur pour les strings
if (expectedType === 'string') {
if (schema.minLength && value.length < schema.minLength) {
throw new ValidationError(
`Le champ '${fieldName}' doit contenir au moins ${schema.minLength} caractères`,
fieldName
);
}
if (schema.maxLength && value.length > schema.maxLength) {
throw new ValidationError(
`Le champ '${fieldName}' ne peut pas dépasser ${schema.maxLength} caractères`,
fieldName
);
}
}
// Validation de plage pour les nombres
if (expectedType === 'number') {
if (schema.minimum !== undefined && value < schema.minimum) {
throw new ValidationError(
`Le champ '${fieldName}' doit être supérieur ou égal à ${schema.minimum}`,
fieldName
);
}
if (schema.maximum !== undefined && value > schema.maximum) {
throw new ValidationError(
`Le champ '${fieldName}' ne peut pas dépasser ${schema.maximum}`,
fieldName
);
}
}
// Validation de pattern pour les strings
if (expectedType === 'string' && schema.pattern) {
const regex = new RegExp(schema.pattern);
if (!regex.test(value)) {
throw new ValidationError(
`Le champ '${fieldName}' ne correspond pas au format attendu`,
fieldName
);
}
}
}
}
/**
* Validateur de chemins de fichiers
*/
export class PathValidator {
private allowedDirectories: string[];
private blockedPaths: string[];
constructor(allowedDirectories: string[], blockedPaths: string[] = []) {
// Résoudre tous les chemins en absolus
this.allowedDirectories = allowedDirectories.map(dir => path.resolve(dir));
this.blockedPaths = blockedPaths.map(p => path.resolve(p));
}
/**
* Valider qu'un chemin est sûr
*/
validatePath(filePath: string): string {
// Résoudre le chemin absolu
const absolutePath = path.resolve(filePath);
// Vérifier les path traversal (../)
if (absolutePath.includes('..')) {
throw new ValidationError(
'Les chemins avec ".." ne sont pas autorisés (path traversal)'
);
}
// Vérifier que le chemin est dans un répertoire autorisé
const isInAllowedDir = this.allowedDirectories.some(dir =>
absolutePath.startsWith(dir)
);
if (!isInAllowedDir) {
throw new ValidationError(
`Accès refusé : le chemin doit être dans l'un des répertoires autorisés`
);
}
// Vérifier que le chemin n'est pas bloqué
const isBlocked = this.blockedPaths.some(blocked =>
absolutePath.startsWith(blocked)
);
if (isBlocked) {
throw new ValidationError(
`Accès refusé : ce chemin est explicitement bloqué`
);
}
return absolutePath;
}
/**
* Ajouter un répertoire autorisé
*/
addAllowedDirectory(directory: string): void {
this.allowedDirectories.push(path.resolve(directory));
}
/**
* Bloquer un chemin spécifique
*/
blockPath(pathToBlock: string): void {
this.blockedPaths.push(path.resolve(pathToBlock));
}
}
/**
* Validateur de taille de fichier
*/
export class SizeValidator {
/**
* Valider qu'une taille est acceptable
*/
static validateSize(
size: number,
maxSize: number,
fieldName: string = 'fichier'
): void {
if (size > maxSize) {
throw new ValidationError(
`Le ${fieldName} est trop volumineux (max ${this.formatSize(maxSize)})`
);
}
}
/**
* Formater une taille en octets en format lisible
*/
static formatSize(bytes: number): string {
const units = ['octets', 'Ko', 'Mo', 'Go'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
}
Ce validateur complet vérifie :
Types de données : string, number, boolean
Valeurs requises : les champs obligatoires sont présents
Énumérations : les valeurs sont dans la liste autorisée
Longueurs : min/max pour les chaînes
Plages : min/max pour les nombres
Patterns : expressions régulières pour les formats
Chemins : protection contre path traversal et accès non autorisés
Tailles : limites de fichiers
Système d’Authentification JWT
Créons maintenant un système d’authentification basé sur JSON Web Tokens. Créez src/security/auth.ts :
// src/security/auth.ts
import crypto from 'crypto';
/**
* Interface utilisateur
*/
export interface User {
id: string;
username: string;
role: 'admin' | 'user' | 'readonly';
permissions: string[];
}
/**
* Token JWT simplifié (pour la démo - utilisez une vraie lib JWT en prod)
*/
interface Token {
userId: string;
username: string;
role: string;
permissions: string[];
expiresAt: number;
}
/**
* Gestionnaire d'authentification
*/
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 heures
constructor(secretKey: string) {
this.SECRET_KEY = secretKey;
// Créer quelques utilisateurs de test
this.createUser({
id: '1',
username: 'admin',
role: 'admin',
permissions: ['*'] // Toutes les permissions
});
this.createUser({
id: '2',
username: 'user',
role: 'user',
permissions: ['readFile', 'listFiles', 'searchFiles']
});
this.createUser({
id: '3',
username: 'readonly',
role: 'readonly',
permissions: ['readFile', 'listFiles']
});
}
/**
* Créer un utilisateur
*/
createUser(user: User): void {
this.users.set(user.username, user);
}
/**
* Authentifier un utilisateur et générer un token
*/
authenticate(username: string, password: string): string | null {
// En production, vérifier le mot de passe hashé !
// Ici c'est simplifié pour la démo
const user = this.users.get(username);
if (!user) {
return null;
}
// Générer un 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;
}
/**
* Valider un token
*/
validateToken(tokenId: string): Token | null {
const token = this.tokens.get(tokenId);
if (!token) {
return null;
}
// Vérifier l'expiration
if (Date.now() > token.expiresAt) {
this.tokens.delete(tokenId);
return null;
}
return token;
}
/**
* Révoquer un token
*/
revokeToken(tokenId: string): void {
this.tokens.delete(tokenId);
}
/**
* Obtenir un utilisateur
*/
getUser(username: string): User | undefined {
return this.users.get(username);
}
/**
* Nettoyer les tokens expirés
*/
cleanExpiredTokens(): void {
const now = Date.now();
for (const [tokenId, token] of this.tokens.entries()) {
if (now > token.expiresAt) {
this.tokens.delete(tokenId);
}
}
}
}
/**
* Middleware d'authentification pour Express
*/
export function authMiddleware(authManager: AuthManager) {
return (req: any, res: any, next: any) => {
// Récupérer le token de l'en-tête Authorization
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
error: 'Token d\'authentification manquant'
});
}
const tokenId = authHeader.substring(7); // Enlever "Bearer "
const token = authManager.validateToken(tokenId);
if (!token) {
return res.status(401).json({
success: false,
error: 'Token invalide ou expiré'
});
}
// Ajouter les infos utilisateur à la requête
req.user = token;
next();
};
}
Système de Permissions Granulaires
Maintenant, créons un système qui vérifie si un utilisateur peut exécuter un outil spécifique. Créez src/security/permissions.ts :
// src/security/permissions.ts
import { User } from './auth';
/**
* Erreur de permission
*/
export class PermissionError extends Error {
constructor(message: string) {
super(message);
this.name = 'PermissionError';
}
}
/**
* Gestionnaire de permissions
*/
export class PermissionManager {
/**
* Vérifier si un utilisateur a la permission d'utiliser un outil
*/
static hasPermission(
user: User,
toolName: string,
params?: any
): boolean {
// Les admins ont accès à tout
if (user.permissions.includes('*')) {
return true;
}
// Vérifier la permission spécifique
if (!user.permissions.includes(toolName)) {
return false;
}
// Permissions contextuelles supplémentaires
// Par exemple, vérifier les chemins autorisés pour readFile
if (toolName === 'readFile' && params?.chemin_du_fichier) {
return this.canAccessPath(user, params.chemin_du_fichier);
}
return true;
}
/**
* Vérifier l'accès à un chemin spécifique
*/
private static canAccessPath(user: User, filePath: string): boolean {
// En readonly, uniquement lecture dans certains dossiers
if (user.role === 'readonly') {
const allowedPaths = ['/public', '/docs'];
return allowedPaths.some(allowed =>
filePath.startsWith(allowed)
);
}
return true;
}
/**
* Obtenir les permissions d'un utilisateur
*/
static getPermissions(user: User): string[] {
return user.permissions;
}
/**
* Vérifier et lancer une erreur si pas de permission
*/
static requirePermission(
user: User,
toolName: string,
params?: any
): void {
if (!this.hasPermission(user, toolName, params)) {
throw new PermissionError(
`Permission refusée : vous n'avez pas accès à l'outil '${toolName}'`
);
}
}
}
/**
* Politique de permissions pour un outil
*/
export interface ToolPolicy {
allowedRoles: string[];
requiredPermissions: string[];
rateLimit?: {
maxRequests: number;
windowMs: number;
};
}
/**
* Gestionnaire de politiques d'outils
*/
export class PolicyManager {
private policies: Map<string, ToolPolicy> = new Map();
/**
* Définir une politique pour un outil
*/
setPolicy(toolName: string, policy: ToolPolicy): void {
this.policies.set(toolName, policy);
}
/**
* Obtenir la politique d'un outil
*/
getPolicy(toolName: string): ToolPolicy | undefined {
return this.policies.get(toolName);
}
/**
* Vérifier qu'un utilisateur respecte la politique
*/
checkPolicy(user: User, toolName: string): boolean {
const policy = this.policies.get(toolName);
if (!policy) {
return true; // Pas de politique = autorisé par défaut
}
// Vérifier le rôle
if (!policy.allowedRoles.includes(user.role) &&
!policy.allowedRoles.includes('*')) {
return false;
}
// Vérifier les permissions
const hasAllPermissions = policy.requiredPermissions.every(perm =>
user.permissions.includes(perm) || user.permissions.includes('*')
);
return hasAllPermissions;
}
}
Rate Limiting et Quotas
Protégeons notre serveur contre les abus avec un système de rate limiting. Créez src/security/rateLimit.ts :
// src/security/rateLimit.ts
/**
* Enregistrement d'utilisation
*/
interface UsageRecord {
count: number;
resetAt: number;
}
/**
* Gestionnaire de rate limiting
*/
export class RateLimiter {
private usage: Map<string, UsageRecord> = new Map();
constructor(
private maxRequests: number,
private windowMs: number
) {}
/**
* Vérifier et incrémenter le compteur pour un utilisateur
*/
checkLimit(userId: string): boolean {
const now = Date.now();
const record = this.usage.get(userId);
// Pas d'enregistrement ou fenêtre expirée
if (!record || now > record.resetAt) {
this.usage.set(userId, {
count: 1,
resetAt: now + this.windowMs
});
return true;
}
// Limite atteinte
if (record.count >= this.maxRequests) {
return false;
}
// Incrémenter le compteur
record.count++;
return true;
}
/**
* Obtenir les infos de limite pour un utilisateur
*/
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)
};
}
/**
* Réinitialiser le compteur pour un utilisateur
*/
reset(userId: string): void {
this.usage.delete(userId);
}
/**
* Nettoyer les enregistrements expirés
*/
cleanup(): void {
const now = Date.now();
for (const [userId, record] of this.usage.entries()) {
if (now > record.resetAt) {
this.usage.delete(userId);
}
}
}
}
/**
* Middleware de rate limiting pour 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: 'Limite de requêtes atteinte',
limit: {
max: info.max,
current: info.current,
resetsAt: info.resetsAt
}
});
}
next();
};
}
/**
* Gestionnaire de quotas par outil
*/
export class QuotaManager {
private quotas: Map<string, Map<string, number>> = new Map();
/**
* Définir un quota pour un utilisateur et un outil
*/
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);
}
/**
* Vérifier et décrémenter le quota
*/
checkQuota(userId: string, toolName: string): boolean {
const userQuotas = this.quotas.get(userId);
if (!userQuotas) {
return true; // Pas de quota = illimité
}
const remaining = userQuotas.get(toolName);
if (remaining === undefined) {
return true; // Pas de quota pour cet outil
}
if (remaining <= 0) {
return false; // Quota épuisé
}
userQuotas.set(toolName, remaining - 1);
return true;
}
/**
* Obtenir le quota restant
*/
getRemainingQuota(userId: string, toolName: string): number | null {
const userQuotas = this.quotas.get(userId);
if (!userQuotas) {
return null; // Illimité
}
return userQuotas.get(toolName) || null;
}
/**
* Réinitialiser le quota d'un utilisateur
*/
resetQuota(userId: string, toolName: string, maxUsage: number): void {
this.setQuota(userId, toolName, maxUsage);
}
}
Intégration dans le Serveur
Maintenant, intégrons toutes ces couches de sécurité dans notre serveur. Modifiez src/index.ts :
// src/index.ts
import express, { Request, Response, NextFunction } from 'express';
import { MCP_PROTOCOL_VERSION, SERVER_INFO, DiscoveryResponse } from './mcp/protocol';
import { toolRegistry } from './mcp/registry';
import { AuthManager, authMiddleware } from './security/auth';
import { PermissionManager, PermissionError } from './security/permissions';
import { RateLimiter, rateLimitMiddleware, QuotaManager } from './security/rateLimit';
import { ParameterValidator, ValidationError, PathValidator } from './security/validator';
const app = express();
const PORT = 3000;
// ============================================
// INITIALISATION SÉCURITÉ
// ============================================
const authManager = new AuthManager(process.env.SECRET_KEY || 'your-secret-key-change-me');
const rateLimiter = new RateLimiter(100, 60 * 1000); // 100 req/minute
const quotaManager = new QuotaManager();
const pathValidator = new PathValidator([
process.cwd(), // Répertoire courant
'/tmp', // Dossier temporaire
]);
// Quotas par défaut
quotaManager.setQuota('2', 'readFile', 1000); // user : 1000 lectures/jour
quotaManager.setQuota('3', 'readFile', 100); // readonly : 100 lectures/jour
// ============================================
// MIDDLEWARES
// ============================================
app.use(express.json());
// Logging
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
next();
});
// ============================================
// ROUTES PUBLIQUES (sans auth)
// ============================================
/**
* Page d'accueil
*/
app.get('/', (req, res) => {
res.json({
message: 'MCP File Server - Secured',
version: SERVER_INFO.version,
protocol_version: MCP_PROTOCOL_VERSION,
status: 'operational',
security: {
authentication: 'required',
rateLimit: '100 requests/minute',
endpoints: {
auth: '/auth/login',
discovery: '/mcp/tools',
execute: '/mcp/execute'
}
}
});
});
/**
* Endpoint d'authentification
*/
app.post('/auth/login', (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({
success: false,
error: 'Username et password requis'
});
}
const token = authManager.authenticate(username, password);
if (!token) {
return res.status(401).json({
success: false,
error: 'Identifiants invalides'
});
}
res.json({
success: true,
token: token,
message: 'Authentification réussie'
});
});
/**
* Health check (public)
*/
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
uptime: process.uptime(),
tools_count: toolRegistry.count(),
timestamp: new Date().toISOString()
});
});
// ============================================
// ROUTES PROTÉGÉES (avec auth + rate limit)
// ============================================
// Appliquer l'authentification à toutes les routes /mcp/*
app.use('/mcp', authMiddleware(authManager));
app.use('/mcp', rateLimitMiddleware(rateLimiter));
/**
* Endpoint de découverte (protégé)
*/
app.get('/mcp/tools', (req: any, res) => {
const user = req.user;
// Filtrer les outils selon les permissions
const allTools = toolRegistry.getAllDescriptions();
const allowedTools = allTools.filter(tool =>
PermissionManager.hasPermission(
{
id: user.userId,
username: user.username,
role: user.role,
permissions: user.permissions
},
tool.name
)
);
const response: DiscoveryResponse = {
protocol_version: MCP_PROTOCOL_VERSION,
server_info: SERVER_INFO,
tools: allowedTools
};
console.log(`📋 Découverte - ${allowedTools.length}/${allTools.length} outils visibles pour ${user.username}`);
res.json(response);
});
/**
* Endpoint d'exécution sécurisé
*/
app.post('/mcp/execute', async (req: any, res) => {
const { tool, params } = req.body;
const user = req.user;
try {
// Validation basique
if (!tool) {
return res.status(400).json({
success: false,
error: "Le paramètre 'tool' est requis"
});
}
// Vérifier les permissions
PermissionManager.requirePermission(
{
id: user.userId,
username: user.username,
role: user.role,
permissions: user.permissions
},
tool,
params
);
// Vérifier les quotas
if (!quotaManager.checkQuota(user.userId, tool)) {
return res.status(403).json({
success: false,
error: 'Quota épuisé pour cet outil',
remaining: quotaManager.getRemainingQuota(user.userId, tool)
});
}
// Obtenir la description de l'outil pour validation
const toolDescription = toolRegistry.getDescription(tool);
if (!toolDescription) {
return res.status(404).json({
success: false,
error: `Outil '${tool}' introuvable`
});
}
// Valider les paramètres selon le schéma
ParameterValidator.validate(params || {}, toolDescription.input_schema);
// Validation spécifique des chemins pour les outils de fichiers
if (tool === 'readFile' || tool === 'listFiles') {
const pathParam = params.chemin_du_fichier || params.chemin_du_dossier;
if (pathParam) {
params.validated_path = pathValidator.validatePath(pathParam);
}
}
console.log(`⚙️ Exécution sécurisée : ${tool} par ${user.username}`);
// Exécution via le registre
const result = await toolRegistry.execute(tool, params);
// Log du résultat
if (result.success) {
console.log(`✅ Succès : ${tool} par ${user.username}`);
} else {
console.log(`❌ Échec : ${tool} par ${user.username} - ${result.error}`);
}
res.json(result);
} catch (error: any) {
// Gestion des erreurs de sécurité
if (error instanceof ValidationError) {
return res.status(400).json({
success: false,
error: error.message,
field: error.field
});
}
if (error instanceof PermissionError) {
return res.status(403).json({
success: false,
error: error.message
});
}
// Erreur générique
console.error('Erreur serveur:', error);
res.status(500).json({
success: false,
error: 'Erreur interne du serveur'
});
}
});
/**
* Endpoint pour voir ses permissions
*/
app.get('/mcp/me', (req: any, res) => {
const user = req.user;
res.json({
userId: user.userId,
username: user.username,
role: user.role,
permissions: user.permissions,
rateLimit: rateLimiter.getLimitInfo(user.userId)
});
});
/**
* Endpoint pour voir ses quotas
*/
app.get('/mcp/quotas', (req: any, res) => {
const user = req.user;
const tools = toolRegistry.getAllDescriptions();
const quotas = tools.map(tool => ({
tool: tool.name,
remaining: quotaManager.getRemainingQuota(user.userId, tool.name)
}));
res.json({
userId: user.userId,
quotas: quotas
});
});
// ============================================
// GESTION DES ERREURS
// ============================================
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
console.error('Erreur non gérée:', err);
res.status(500).json({
success: false,
error: 'Erreur interne du serveur'
});
});
// ============================================
// TÂCHES DE MAINTENANCE
// ============================================
// Nettoyer les tokens expirés toutes les heures
setInterval(() => {
authManager.cleanExpiredTokens();
rateLimiter.cleanup();
console.log('🧹 Nettoyage des tokens et rate limits expirés');
}, 60 * 60 * 1000);
// ============================================
// DÉMARRAGE DU SERVEUR
// ============================================
app.listen(PORT, () => {
console.log('═══════════════════════════════════════');
console.log('🔒 MCP File Server - Secured Edition');
console.log('═══════════════════════════════════════');
console.log(`📍 URL: http://localhost:${PORT}`);
console.log(`🔐 Auth: POST http://localhost:${PORT}/auth/login`);
console.log(`📋 Découverte: GET http://localhost:${PORT}/mcp/tools`);
console.log(`⚙️ Exécution: POST http://localhost:${PORT}/mcp/execute`);
console.log(`🔧 Outils: ${toolRegistry.count()}`);
console.log('═══════════════════════════════════════');
console.log('👤 Utilisateurs de test:');
console.log(' - admin (toutes permissions)');
console.log(' - user (lecture + liste + recherche)');
console.log(' - readonly (lecture + liste seulement)');
console.log('═══════════════════════════════════════');
});
Tester le Système Sécurisé
Relançons notre serveur et testons toutes les couches de sécurité :
npm run dev
Test 1 : Authentification
D’abord, essayons d’accéder sans authentification :
curl http://localhost:3000/mcp/tools
Résultat :
{
"success": false,
"error": "Token d'authentification manquant"
}
Parfait ! Maintenant, authentifions-nous :
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "user", "password": "any"}'
Réponse :
{
"success": true,
"token": "a1b2c3d4e5f6...",
"message": "Authentification réussie"
}
Sauvegardez ce token dans une variable :
TOKEN="a1b2c3d4e5f6..."
Test 2 : Découverte avec Permissions
Maintenant, découvrons les outils avec notre token :
curl http://localhost:3000/mcp/tools \
-H "Authorization: Bearer $TOKEN"
L’utilisateur “user” ne verra que les outils auxquels il a accès (readFile, listFiles, searchFiles).
Comparons avec un utilisateur readonly :
# S'authentifier en readonly
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "readonly", "password": "any"}' \
| jq -r '.token'
# Il verra moins d'outils (seulement readFile et listFiles)
Test 3 : Validation des Paramètres
Testons avec des paramètres invalides :
curl -X POST http://localhost:3000/mcp/execute \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tool": "readFile",
"params": {
"encoding": "invalid_encoding"
}
}'
Réponse :
{
"success": false,
"error": "Le champ 'chemin_du_fichier' est requis",
"field": "chemin_du_fichier"
}
Testons avec une énumération invalide :
curl -X POST http://localhost:3000/mcp/execute \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tool": "readFile",
"params": {
"chemin_du_fichier": "test.txt",
"encoding": "invalid"
}
}'
Réponse :
{
"success": false,
"error": "Le champ 'encoding' doit être l'une des valeurs : utf-8, ascii, base64",
"field": "encoding"
}
Test 4 : Protection Path Traversal
Essayons d’accéder à un fichier sensible :
curl -X POST http://localhost:3000/mcp/execute \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tool": "readFile",
"params": {
"chemin_du_fichier": "../../../etc/passwd"
}
}'
Réponse :
{
"success": false,
"error": "Les chemins avec \"..\" ne sont pas autorisés (path traversal)"
}
Excellent ! Notre protection fonctionne.
Test 5 : Rate Limiting
Créons un script pour tester le rate limiting. Créez test-rate-limit.sh :
#!/bin/bash
TOKEN="votre-token-ici"
echo "Test de rate limiting - 100 requêtes rapides..."
for i in {1..105}; do
response=$(curl -s -w "\n%{http_code}" \
-X POST http://localhost:3000/mcp/execute \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"tool": "listFiles", "params": {"chemin_du_dossier": "."}}')
status_code=$(echo "$response" | tail -n1)
if [ "$status_code" = "429" ]; then
echo "Requête $i : Rate limit atteint ! ✓"
echo "$response" | head -n-1 | jq .
break
else
echo "Requête $i : OK"
fi
done
Après 100 requêtes, vous verrez :
{
"success": false,
"error": "Limite de requêtes atteinte",
"limit": {
"max": 100,
"current": 100,
"resetsAt": "2025-12-10T15:30:00.000Z"
}
}
Test 6 : Quotas
Testons les quotas. Créez un fichier de test puis exécutez plusieurs lectures :
# Voir ses quotas actuels
curl http://localhost:3000/mcp/quotas \
-H "Authorization: Bearer $TOKEN"
Réponse :
{
"userId": "2",
"quotas": [
{
"tool": "readFile",
"remaining": 1000
},
{
"tool": "listFiles",
"remaining": null
},
{
"tool": "searchFiles",
"remaining": null
}
]
}
Créer un Client Sécurisé
Pour faciliter les tests, créons un client qui gère automatiquement l’authentification. Créez src/secure-client.ts :
// src/secure-client.ts
const SERVER_URL = 'http://localhost:3000';
class SecureClient {
private token: string | null = null;
/**
* S'authentifier
*/
async login(username: string, password: string): Promise<boolean> {
const response = await fetch(`${SERVER_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
this.token = data.token;
console.log(`✅ Authentifié en tant que ${username}`);
return true;
}
console.log(`❌ Échec d'authentification`);
return false;
}
/**
* Découvrir les outils disponibles
*/
async discoverTools() {
if (!this.token) {
throw new Error('Non authentifié');
}
const response = await fetch(`${SERVER_URL}/mcp/tools`, {
headers: {
'Authorization': `Bearer ${this.token}`
}
});
return await response.json();
}
/**
* Exécuter un outil
*/
async executeTool(toolName: string, params: any) {
if (!this.token) {
throw new Error('Non authentifié');
}
const response = await fetch(`${SERVER_URL}/mcp/execute`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
tool: toolName,
params: params
})
});
return await response.json();
}
/**
* Voir ses informations
*/
async getMe() {
if (!this.token) {
throw new Error('Non authentifié');
}
const response = await fetch(`${SERVER_URL}/mcp/me`, {
headers: {
'Authorization': `Bearer ${this.token}`
}
});
return await response.json();
}
/**
* Voir ses quotas
*/
async getQuotas() {
if (!this.token) {
throw new Error('Non authentifié');
}
const response = await fetch(`${SERVER_URL}/mcp/quotas`, {
headers: {
'Authorization': `Bearer ${this.token}`
}
});
return await response.json();
}
}
/**
* Démonstration complète
*/
async function demo() {
console.log('═══════════════════════════════════════');
console.log('🔒 Démo Client Sécurisé MCP');
console.log('═══════════════════════════════════════\n');
const client = new SecureClient();
// Test 1 : Authentification
console.log('📝 Test 1 : Authentification\n');
await client.login('user', 'password');
// Test 2 : Voir ses infos
console.log('\n📝 Test 2 : Informations utilisateur\n');
const me = await client.getMe();
console.log(JSON.stringify(me, null, 2));
// Test 3 : Découverte des outils
console.log('\n📝 Test 3 : Découverte des outils\n');
const discovery = await client.discoverTools();
console.log(`Outils disponibles : ${discovery.tools.length}`);
discovery.tools.forEach((tool: any) => {
console.log(` - ${tool.name}: ${tool.description}`);
});
// Test 4 : Exécution d'un outil
console.log('\n📝 Test 4 : Exécution d\'un outil\n');
const result = await client.executeTool('listFiles', {
chemin_du_dossier: '.'
});
if (result.success) {
console.log('✅ Succès !');
} else {
console.log(`❌ Erreur : ${result.error}`);
}
// Test 5 : Vérifier les quotas
console.log('\n📝 Test 5 : Quotas restants\n');
const quotas = await client.getQuotas();
console.log(JSON.stringify(quotas, null, 2));
console.log('\n═══════════════════════════════════════');
}
demo().catch(console.error);
Ajoutez le script dans package.json :
"scripts": {
"dev": "ts-node src/index.ts",
"secure-client": "ts-node src/secure-client.ts"
}
Lancez-le :
npm run secure-client
Dashboard de Sécurité
Créons une interface web pour visualiser la sécurité. Créez src/public/security.html :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MCP Security Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: linear-gradient(135deg, #1e3a8a 0%, #ef4444 100%);
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
h1 {
color: #1e3a8a;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.subtitle {
color: #64748b;
margin-bottom: 30px;
}
.login-form {
background: #f8fafc;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.login-form input {
width: 200px;
padding: 8px 12px;
margin: 5px;
border: 1px solid #cbd5e1;
border-radius: 4px;
}
.login-form button {
padding: 8px 16px;
background: #1e3a8a;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
.login-form button:hover {
background: #1e40af;
}
.user-info {
background: #eff6ff;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.security-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.security-card {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 20px;
}
.security-card h3 {
color: #1e3a8a;
margin-bottom: 15px;
}
.permission-badge {
display: inline-block;
padding: 4px 8px;
background: #3b82f6;
color: white;
border-radius: 4px;
font-size: 0.85em;
margin: 2px;
}
.quota-bar {
height: 20px;
background: #e2e8f0;
border-radius: 4px;
overflow: hidden;
margin: 10px 0;
}
.quota-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
transition: width 0.3s;
}
.hidden { display: none; }
.error { color: #ef4444; padding: 10px; background: #fee2e2; border-radius: 4px; }
</style>
</head>
<body>
<div class="container">
<h1>🔒 MCP Security Dashboard</h1>
<p class="subtitle">Gestion de la sécurité et des permissions</p>
<!-- Formulaire de connexion -->
<div id="loginForm" class="login-form">
<h3>Connexion</h3>
<input type="text" id="username" placeholder="Username" value="user">
<input type="password" id="password" placeholder="Password" value="any">
<button onclick="login()">Se connecter</button>
<div id="loginError" class="error hidden"></div>
</div>
<!-- Informations utilisateur -->
<div id="userInfo" class="hidden user-info">
<h3>👤 Utilisateur connecté</h3>
<p><strong>Username:</strong> <span id="userUsername"></span></p>
<p><strong>Rôle:</strong> <span id="userRole"></span></p>
<p><strong>User ID:</strong> <span id="userId"></span></p>
<button onclick="logout()" style="margin-top: 10px;">Déconnexion</button>
</div>
<!-- Grille de sécurité -->
<div id="securityGrid" class="hidden security-grid">
<!-- Permissions -->
<div class="security-card">
<h3>🔑 Permissions</h3>
<div id="permissions"></div>
</div>
<!-- Rate Limit -->
<div class="security-card">
<h3>⏱️ Rate Limiting</h3>
<div id="rateLimit"></div>
</div>
<!-- Quotas -->
<div class="security-card">
<h3>📊 Quotas</h3>
<div id="quotas"></div>
</div>
</div>
</div>
<script>
let token = null;
async function login() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
try {
const response = await fetch('/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
token = data.token;
document.getElementById('loginForm').classList.add('hidden');
document.getElementById('userInfo').classList.remove('hidden');
document.getElementById('securityGrid').classList.remove('hidden');
await loadSecurityData();
} else {
showError('Identifiants invalides');
}
} catch (error) {
showError('Erreur de connexion');
}
}
function logout() {
token = null;
document.getElementById('loginForm').classList.remove('hidden');
document.getElementById('userInfo').classList.add('hidden');
document.getElementById('securityGrid').classList.remove('hidden');
}
function showError(message) {
const errorDiv = document.getElementById('loginError');
errorDiv.textContent = message;
errorDiv.classList.remove('hidden');
setTimeout(() => errorDiv.classList.add('hidden'), 3000);
}
async function loadSecurityData() {
// Charger les infos utilisateur
const meResponse = await fetch('/mcp/me', {
headers: { 'Authorization': `Bearer ${token}` }
});
const me = await meResponse.json();
document.getElementById('userUsername').textContent = me.username;
document.getElementById('userRole').textContent = me.role;
document.getElementById('userId').textContent = me.userId;
// Afficher les permissions
const permissionsDiv = document.getElementById('permissions');
permissionsDiv.innerHTML = me.permissions.map(p =>
`<span class="permission-badge">${p}</span>`
).join('');
// Afficher le rate limit
const rateLimitDiv = document.getElementById('rateLimit');
const rateLimit = me.rateLimit;
const percentage = (rateLimit.current / rateLimit.max) * 100;
rateLimitDiv.innerHTML = `
<p><strong>${rateLimit.current}</strong> / ${rateLimit.max} requêtes</p>
<div class="quota-bar">
<div class="quota-fill" style="width: ${percentage}%"></div>
</div>
<p style="font-size: 0.9em; color: #64748b;">
Reset: ${new Date(rateLimit.resetsAt).toLocaleTimeString()}
</p>
`;
// Charger les quotas
const quotasResponse = await fetch('/mcp/quotas', {
headers: { 'Authorization': `Bearer ${token}` }
});
const quotasData = await quotasResponse.json();
const quotasDiv = document.getElementById('quotas');
quotasDiv.innerHTML = quotasData.quotas.map(q => `
<p><strong>${q.tool}:</strong> ${q.remaining === null ? 'Illimité' : q.remaining}</p>
`).join('');
}
</script>
</body>
</html>
Ouvrez http://localhost:3000/security.html pour voir le dashboard en action !
Bonnes Pratiques de Sécurité
Maintenant que vous avez un système complet, voici les bonnes pratiques à suivre :
1. Gestion des Secrets
Ne jamais commit les secrets dans le code. Utilisez des variables d’environnement :
// Créez un fichier .env
SECRET_KEY=your-super-secret-key-change-me-in-production
JWT_EXPIRATION=86400
// Dans votre code
import dotenv from 'dotenv';
dotenv.config();
const SECRET_KEY = process.env.SECRET_KEY;
2. Hash des Mots de Passe
En production, utilisez bcrypt pour hasher les mots de passe :
npm install bcrypt @types/bcrypt
import bcrypt from 'bcrypt';
// Lors de la création d'un utilisateur
const hashedPassword = await bcrypt.hash(password, 10);
// Lors de l'authentification
const isValid = await bcrypt.compare(password, user.hashedPassword);
3. HTTPS Obligatoire
En production, forcez HTTPS :
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https' && process.env.NODE_ENV === 'production') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
4. Logging de Sécurité
Logguez tous les événements de sécurité :
function logSecurityEvent(event: string, details: any) {
// En production : envoyer vers un service d'audit
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
event: event,
...details
}));
}
// Utilisation
logSecurityEvent('AUTH_FAILED', { username, ip: req.ip });
logSecurityEvent('PERMISSION_DENIED', { user: req.user.username, tool });
5. Audits Réguliers
Créez un système d’audit :
class AuditLogger {
log(action: string, userId: string, details: any) {
// En production : envoyer vers un service d'audit
console.log({
timestamp: new Date().toISOString(),
action,
userId,
...details
});
}
}
Conclusion
Félicitations ! Vous avez maintenant un serveur MCP production-ready avec quatre couches de sécurité :
- ✅ Validation complète des entrées
- ✅ Authentification par tokens
- ✅ Autorisation granulaire
- ✅ Rate limiting et quotas
Votre serveur peut maintenant être exposé en production en toute confiance. Les IA peuvent l’utiliser de manière sécurisée, chaque utilisateur a ses permissions spécifiques, et les abus sont automatiquement bloqués.
Dans le prochain et dernier article de la série, nous allons connecter votre serveur sécurisé à Claude Desktop et tester l’intégration complète en conditions réelles. Vous verrez enfin tout le système fonctionner de bout en bout avec une véritable IA.
En attendant, testez votre système de sécurité ! Essayez de le contourner, testez les limites, vérifiez que tout est bien protégé. Un bon système de sécurité est un système qui a été attaqué et qui a résisté.
Article publié le 10 décembre 2025 par Nicolas Dabène - Expert PHP & PrestaShop avec 15+ ans d’expérience dans l’architecture logicielle et l’intégration d’IA
À lire aussi :
- Comprendre le Model Context Protocol (MCP) : Une Conversation Simple
- Créer son Premier Serveur MCP : Setup du Projet TypeScript
- Créer votre Premier Outil MCP : L’Outil readFile Expliqué
- Le Menu MCP : Comment l’IA Découvre et Utilise vos Outils
- Connecter votre Serveur MCP à Claude Desktop : L’Intégration Complète
Questions Fréquentes
Est-ce que ce système est production-ready ?
C’est une excellente base, mais pour la production, ajoutez : vrai JWT (avec jsonwebtoken), hash bcrypt, HTTPS obligatoire, logging vers un service externe, et tests de sécurité automatisés.
Comment gérer les permissions plus complexes ?
Implémentez un système RBAC (Role-Based Access Control) complet avec des rôles composables et des permissions hiérarchiques. Vous pouvez aussi utiliser CASL ou Casbin.
Que faire si un utilisateur abuse du système ?
Ajoutez un système de bannissement temporaire ou permanent, avec détection automatique des comportements suspects (trop d’erreurs, patterns anormaux).
Comment protéger contre les attaques DDoS ?
Utilisez un reverse proxy comme Nginx avec rate limiting, un WAF (Web Application Firewall), et des services comme Cloudflare en frontal.
Articles Liés
Gemini Canvas vs GPT-5 : qui crée la meilleure présentation ?
J'ai testé la nouvelle fonction Canvas de Google Gemini face à GPT-5 pour générer une présentation à partir de mon ar...
Et si l'IA rejetait ton code pour de mauvaises raisons ? Les biais cachés des outils de code review automatisés
Et si l'IA rejetait ton code non pas parce qu'il est mauvais, mais parce qu'elle *pense* qu'il l'est ? Cet article ex...
Créer votre Premier Outil MCP : L'Outil readFile Expliqué
Du setup à l'action ! Créez votre premier outil MCP fonctionnel qui permet à une IA de lire des fichiers. Code comple...
Évolution des Compétences des Développeurs : de l'Expertise Technique à l'Hybride Visionnaire
En quelques mois seulement, l'intelligence artificielle a redéfini le métier de développeur : moins de technique répé...
Automatiser vos Publications Facebook et Instagram avec n8n : Le Guide Salvateur
Si vous pensiez que l'intégration Meta serait un jeu d'enfant, ce guide détaillé va vous éviter des heures de frustra...
Vous laisseriez un Dev Junior coder sans supervision ? Alors pourquoi l'IA ?
84% des développeurs utilisent l'IA, mais 45% du code généré contient des vulnérabilités. Découvrez pourquoi l'IA néc...
Découvrez mes autres articles
Guides e-commerce, tutoriels PrestaShop et bonnes pratiques pour développeurs
Voir tous les articlesPlanification LinkedIn
Date de publication : 11 décembre 2025
Temps restant :