Imaginez un retour passé pendant la nuit dans la colonne Declined de votre tableau. L’historique dit que c’est le projet qui l’a fait.
Personne dans l’équipe ne se souvient de l’avoir fait. C’est l’assistant de quelqu’un, qui tournait avec la clé MCP que tout le monde a copiée depuis la même page de paramètres, et il faut faire le tour de l’équipe pour savoir lequel.
C’est ainsi que fonctionnent la plupart des serveurs MCP aujourd’hui, parce qu’ils reprennent le modèle de la clé API. Je viens de le remplacer dans Kookee par des jetons personnels. Voici pourquoi la clé partagée ne tient pas, ce que j’ai livré à la place, et les détails qui se sont révélés importants.
Une clé partagée, c’est une habitude héritée des clés API
Une clé API identifie une application. Une application, une clé, et la clé ne peut faire que ce dont l’application a besoin — pour l’API publique de Kookee, lire le contenu publié. Pour des applications, ce modèle convient.
Un serveur MCP connecte autre chose : l’assistant d’une personne, qui agit sur ses instructions et modifie des choses. Donnez-lui une clé de projet, et trois choses cassent d’un coup.
Personne ne l’a fait. Chaque modification est attribuée au projet. Quand un retour change de catégorie ou de responsable, ou reçoit une réponse, impossible de savoir quel coéquipier a fait agir son assistant, ni même si c’était une personne.
Tout le monde peut tout faire. Une clé, c’est un seul niveau d’accès. Le coéquipier qui voulait seulement que son assistant résume les retours détient une clé qui peut publier.
Impossible de la retirer à une seule personne. Quand quelqu’un quitte l’équipe, le seul moyen de couper son assistant est de renouveler la clé, ce qui casse la configuration de tous les autres. En pratique, personne ne la renouvelle donc.
Et une clé que tout le monde copie finit partout : dans des dotfiles, dans un .mcp.json commité, dans une capture d’écran au milieu d’un fil de support.
Ce que j’ai livré : un jeton par personne et par projet
Chaque membre crée désormais son propre jeton sur la page Serveur MCP du projet. Le jeton agit en tant que ce membre — son rôle, ses permissions, vérifiés à chaque requête —, et chaque modification qu’il fait est enregistrée à son nom.

La lecture seule est le choix par défaut. L’accès en écriture demande un second clic, volontaire.
Six détails qui décident si ça marche
1. Lecture seule veut dire que les outils d’écriture n’existent pas
L’implémentation évidente refuse une écriture au moment de l’appel. Mais elle l’affiche quand même : le modèle voit update_feedback dans sa liste d’outils, planifie tout un tri autour, et échoue à la dernière étape avec une erreur qu’il essaie ensuite de contourner.
Une session en lecture seule n’enregistre donc jamais les outils d’écriture, et ses instructions serveur omettent tous les scénarios d’écriture pour expliquer à la place comment obtenir l’accès en écriture. Dans les grandes lignes :
registerReadTools(server, auth);
if (auth.token.canWrite) {
registerWriteTools(server, auth);
}
L’assistant peut alors dire clairement à l’utilisateur que son jeton est en lecture seule, au lieu d’improviser.
2. Vérifier la personne derrière le jeton à chaque requête
Un jeton survit à la raison pour laquelle il a été émis. Le valider ne s’arrête donc pas à « le hash correspond » : on charge le membre derrière le jeton et on vérifie que son compte est actif, que le projet n’est pas banni et qu’il en est toujours membre.
if (!plain.startsWith('kkm_')) return fail('not_mcp_token');
const token = await findTokenByHash(sha256(plain));
if (!token) return fail('not_found');
if (!token.user.isActive) return fail('inactive');
if (token.project.bannedAt) return fail('banned');
if (!(await isMember(token.projectId, token.userId))) return fail('not_member');
Retirer ou bannir un membre supprime aussi ses jetons, dans la même transaction que son appartenance au projet. La vérification à chaque requête le bloque déjà ; supprimer les lignes garde la liste des jetons fidèle à la réalité.
3. Stocker un hash, l’afficher une fois, lui donner un préfixe
Seul un hash SHA-256 du jeton est stocké. Le jeton en clair s’affiche une seule fois, dans la fenêtre qui le crée, et la liste n’en montre ensuite que les quatre derniers caractères.
Le préfixe sert à deux choses. kkm_ se distingue du kk_ d’une clé API : un scanner de secrets, ou une personne, sait quel type de secret a fuité. Et le serveur peut renvoyer une erreur utile : une clé API envoyée à l’endpoint MCP reçoit MCP_PERSONAL_TOKEN_REQUIRED, qui dit exactement quoi faire. Tous les autres échecs reçoivent le même MCP_TOKEN_INVALID générique, pour qu’on ne puisse pas sonder un jeton et savoir pourquoi il a été refusé ; la raison précise ne va que dans le journal du serveur.
4. Mettre l’attribution là où les gens regardent
Un journal d’audit, c’est l’endroit où la traçabilité va se faire oublier. J’y écris une entrée pour chaque modification MCP, mais ce qui compte est visible : chaque écriture stocke l’id du jeton avec l’entrée d’activité ou le commentaire qu’elle crée, et le tableau de bord les marque via MCP.

L’outil d’historique que lit l’assistant renvoie le même indicateur, si bien que la question « qui a déplacé ce retour dans Planned, et était-ce un assistant ? » a une réponse des deux côtés. L’API publique des commentaires le retire : les visiteurs voient une réponse de l’équipe, jamais un id de jeton.
5. Limiter les écritures par personne et par outil
Un agent emballé qui relance une modification en échec peut faire des centaines d’appels par minute. Une limite d’écriture par projet bloquerait les coéquipiers qui n’ont rien fait de mal, et une limite par jeton se contourne en créant un second jeton.
Les outils d’écriture sont donc limités par membre et par outil : 60 appels par minute vers update_feedback, et 60 autres, comptés à part, vers create_comment. La fenêtre MCP à l’échelle du projet est aussi séparée de celle de l’API publique : un agent qui multiplie les appels ne peut pas épuiser le quota de votre site public.
6. Garder la configuration partagée sans secrets
Une équipe veut un seul .mcp.json commité, pour que tout le monde ait la même configuration. Avec des jetons personnels, ce fichier ne doit pas en contenir. Il lit à la place le jeton dans l’environnement de chacun :
{
"mcpServers": {
"kookee": {
"type": "http",
"url": "https://api.kookee.dev/api/mcp",
"headers": { "Authorization": "Bearer ${KOOKEE_MCP_TOKEN}" }
}
}
}
Chaque coéquipier définit KOOKEE_MCP_TOKEN avec son propre jeton. La commande que le tableau de bord affiche pour Claude Code fait de même avec $KOOKEE_MCP_TOKEN, sauf juste après la création d’un jeton : elle y insère alors ce jeton pour vous.
Si vous faites tourner un serveur MCP
Émettez les identifiants par personne et par espace de travail. Un client MCP est l’agent d’une personne, pas une application.
Faites de la lecture seule le choix par défaut, et n’enregistrez pas les outils qu’un jeton ne peut pas utiliser.
Revérifiez la personne derrière le jeton à chaque requête : compte, appartenance, suspension.
Stockez des hashs, n’affichez les jetons qu’une fois, et donnez-leur un préfixe qui leur est propre.
Distinguez « mauvais type d’identifiant » d’« invalide », et n’en dites pas plus.
Enregistrez le jeton avec chaque modification, et montrez-le là où les gens lisent l’historique.
Limitez les écritures par personne et par outil, séparément de votre API publique.
Gardez la configuration client partagée sans secrets, grâce aux variables d’environnement.
Le serveur MCP de Kookee utilise désormais des jetons personnels, et les assistants peuvent lire et trier votre tableau de retours. Consultez Gérer votre contenu depuis un assistant IA (MCP) pour la configuration, ou Trier les retours depuis un assistant IA pour les nouveaux outils.