Introduction
Quand on expose une application à un agent IA via le Model Context Protocol (MCP), on a tendance à réutiliser les mécanismes de sécurité déjà en place : middlewares, scopes globaux, contexte de session. Le problème, c'est que ces mécanismes ont été pensés pour des requêtes web classiques, authentifiées par session — pas pour des appels machine-to-machine authentifiés par token. Un développeur de la communauté dev.to, dans son journal de bord technique, met le doigt sur une faille silencieuse mais critique : sous authentification par token, le contexte « tenant courant » n'existe tout simplement pas. Et un scope global qui s'appuie dessus ne filtre rien du tout.
Cet article reprend ce constat et l'explique en détail, avec une application concrète en PHP/Symfony et Laravel.
Le piège du scope global en contexte MCP
Dans une application multi-tenant classique, le pattern habituel consiste à définir un scope global sur les modèles Eloquent (Laravel) ou un filtre Doctrine (Symfony) qui restreint automatiquement chaque requête à l'organisation de l'utilisateur connecté. Ce scope lit généralement le tenant courant depuis :
- la session HTTP,
- un middleware qui hydrate un service
CurrentTenant, - ou un contexte applicatif initialisé au démarrage de la requête web.
Le souci, c'est que les outils MCP s'authentifient par token, pas par session. Il n'y a donc aucun moment dans le cycle de vie de la requête où ce fameux "current tenant" est peuplé. Résultat : le scope global s'exécute, ne trouve aucun contexte, et... ne filtre rien. La requête censée être "fencée" (cloisonnée) retourne silencieusement les lignes de tous les tenants.
C'est le pire type de bug de sécurité : aucune erreur, aucun crash, juste une fuite de données entre organisations qui passe inaperçue tant que personne ne va vérifier manuellement les résultats.
La solution : un filtrage explicite, dans un seul endroit
La règle adoptée dans le journal de bord est simple et directe : sous authentification par token, on filtre explicitement par organisation, à chaque fois, dans un seul trait partagé — jamais via un scope global dont on espère qu'il est actif.
En pratique, cela donne en Laravel quelque chose comme :
trait ScopedToOrganization
{
public function scopeForCurrentOrganization(Builder $query): Builder
{
$organizationId = request()->user()?->currentAccessToken()?->organization_id;
if (! $organizationId) {
throw new UnauthorizedHttpException('Missing organization context');
}
return $query->where('organization_id', $organizationId);
}
}
Chaque requête MCP appelle explicitement Model::forCurrentOrganization()->get(). Pas de magie implicite, pas de scope qu'on espère branché : si le contexte organisation est absent, l'exception part immédiatement plutôt que de laisser passer une requête non filtrée.
En Symfony, le même principe se traduit par un filtre Doctrine activé explicitement plutôt que par défaut, ou mieux, par un repository custom qui exige toujours un paramètre d'organisation en entrée — sans valeur par défaut nullable qui inviterait à l'oubli :
final class EventRepository extends ServiceEntityRepository
{
public function findAllForOrganization(string $organizationId): array
{
return $this->createQueryBuilder('e')
->andWhere('e.organization = :orgId')
->setParameter('orgId', $organizationId)
->getQuery()
->getResult();
}
}
L'idée centrale : le cloisonnement doit être un acte explicite et testable, pas une dépendance implicite à un état global. C'est plus verbeux, mais c'est précisément ce qu'on veut quand la couche d'authentification change de nature (session vs token) sans que le reste du code n'en ait conscience.
Pourquoi cette distinction compte particulièrement avec les agents IA
Ce qui rend le sujet d'autant plus important aujourd'hui, c'est la multiplication des outils MCP qui donnent à des agents IA la capacité d'agir directement sur une application métier : lister des événements, vérifier leur état, les publier, déclencher un cycle de vie complet. Un agent IA qui interroge votre API via MCP n'a pas de "session navigateur" : il porte un token, et c'est tout. Toute hypothèse implicite sur le contexte de la requête — tenant courant, utilisateur courant via session, rôle déduit d'un cookie — devient une faille potentielle dès lors qu'un outil MCP est branché sur le même code applicatif que le front web.
La bonne pratique qui se dégage est donc de traiter chaque point d'entrée par token comme un contexte d'exécution à part entière, avec ses propres garde-fous explicites, plutôt que de supposer que la sécurité "hérite" automatiquement du reste de l'application.
Conclusion
Le constat de ce journal de bord est limpide : exposer une application à un agent IA révèle souvent des hypothèses implicites qui tenaient depuis des années simplement parce que personne n'avait testé un chemin d'authentification différent. La leçon à retenir pour toute équipe PHP/Symfony ou Laravel qui prépare l'intégration d'outils MCP : centraliser le filtrage par organisation dans un trait ou un service unique, le rendre obligatoire (avec exception si le contexte manque), et ne jamais s'appuyer sur un scope global dont l'activation dépend implicitement du mode d'authentification.
Cet article s'appuie sur le journal de bord original « Dev Log: 2026-06-24 — agent guardrails and runtime LDAP config » publié par Nasrul Hazim Bin Mohamad sur dev.to.