Concevoir des tools MCP pour un agent, pas un wrapper d'API
Quand on crée son premier serveur MCP, le réflexe naturel est d'exposer son API existante telle quelle : un endpoint REST, un tool. Le résultat compile, répond correctement à chaque appel... et fonctionne mal. L'agent doit faire les jointures lui-même, et paie en contexte chaque résultat intermédiaire. Un tool MCP n'est pas une API pour un développeur : c'est une interface pour un lecteur qui a une mémoire de travail petite et chère. Voici les règles de conception que j'applique désormais. Pour la mise en place technique d'un serveur, voir Créer votre server MCP dans Spring Boot ; cet article porte sur ce que le serveur expose.
Règle 1 : des tools orientés tâche, pas orientés table
Réserver une réunion avec list_users, list_events et create_event coûte trois allers-retours et deux jeux de résultats que l'agent doit garder en contexte. Un seul tool schedule_event fait la jointure côté serveur pour le prix d'un appel.
La question à se poser pour chaque tool : que ferait une personne avec les mêmes accès ? Le tool doit être une étape de ce travail, pas une ligne de votre schéma.
Le même principe remplace l'énumération par la recherche dès qu'une collection peut dépasser la taille du contexte :
search_logsqui renvoie les lignes pertinentes, plutôt queread_logsqui renvoie le fichier ;search_projects(query)plutôt quelist_projects().
La consolidation réduit aussi le risque que l'agent choisisse le mauvais tool, un mode d'échec bien réel au-delà d'une douzaine de tools.
Règle 2 : renvoyer des noms, pas des identifiants
uuid, mime_type et 256px_image_url sont pensés pour un client de base de données. Un agent n'a pas de table de correspondance : il devine, ou dépense un appel pour résoudre l'identifiant.
// À éviter
{"owner": "7f3a9c2e-...", "mime_type": "application/vnd.google-apps.document"}
// À préférer
{"owner": "Marie Dupont", "owner_id": "7f3a9c2e-...", "file_type": "Google Doc"}Quand un identifiant doit vraiment circuler (parce qu'une écriture ultérieure en a besoin), renvoyez-le à côté du nom, jamais seul.
Règle 3 : donner un réglage de verbosité
Un paramètre response_format avec les valeurs concise et detailed déplace la décision de taille vers l'appelant, qui sait s'il explore ou s'il s'apprête à agir. Anthropic a mesuré la même réponse Slack à 206 tokens en mode détaillé et 72 tokens en mode concis, les deux suffisant à enchaîner l'appel suivant (Writing effective tools for AI agents).
@mcp.tool()
def search_tickets(query: str, response_format: Literal["concise", "detailed"] = "concise"):
...Quelques lignes de code serveur, et l'une des réductions de contexte les moins chères qui existent.
Règle 4 : des troncatures et des erreurs qui enseignent
Une réponse coupée à une limite doit dire quoi faire autrement. 47 résultats supplémentaires, affinez avec theme: apprend à l'agent son prochain appel ; une coupure sèche l'invite à relancer la même requête trop large.
Même principe pour les erreurs : nommez ce qui ne va pas et montrez une valeur bien formée.
# Opaque
Error 400: invalid parameter
# Instructif
Invalid date "next monday". Expected ISO 8601, e.g. "2026-10-12".
Un code d'erreur opaque envoie l'agent tester des variantes qui coûtent plus cher que l'échec initial. Et fixez une limite de taille explicite à vos réponses (Claude Code plafonne la sortie d'un tool autour de 25 000 tokens) plutôt que de laisser un gros résultat décider à votre place.
Règle 5 : écrire les descriptions pour un nouveau collègue
Les descriptions de tools sont du prompt, et elles sont lues à froid. Une bonne description :
- définit les termes propres à votre domaine ;
- explique comment les ressources sont liées entre elles ;
- documente la syntaxe de requête attendue ;
- nomme les paramètres sans ambiguïté (
user_idplutôt queuser, qui laisse le type ouvert).
De petits changements de formulation modifient fortement le comportement : Anthropic attribue une partie du score de Claude Sonnet 3.5 sur SWE-bench Verified au seul affinage des descriptions de tools.
Sur le namespacing, mon expérience nuance les recommandations publiées : les hôtes préfixent déjà chaque tool par le nom du serveur (mcp__mon-serveur__search), donc les collisions entre serveurs sont rares. Le préfixe redevient utile quand un même serveur expose plusieurs familles de ressources : projects_search et users_search évitent la confusion.
Règle 6 : annoter chaque tool
La spécification MCP prévoit des annotations : readOnlyHint, destructiveHint, idempotentHint, openWorldHint. Sans elles, un agent (ou l'hôte qui le pilote) ne peut pas juger si un appel est sûr à faire de façon autonome. C'est aussi ce qui permet à un client de demander confirmation avant un delete_* et de laisser passer un search_*.
@mcp.tool(annotations={"readOnlyHint": True, "openWorldHint": False})
def search_tickets(query: str): ...Règle 7 : mesurer la surface avec un agent
Une surface de tools ne révèle sa forme que lorsqu'un agent l'utilise sur des tâches réalistes. La méthode :
- écrire des tâches qui demandent plusieurs appels et portent du vrai contexte ;
- les faire tourner dans une boucle simple appel modèle / appel tool ;
- enregistrer par tâche : succès ou échec, nombre d'appels, tokens consommés, erreurs rencontrées ;
- lire les transcripts.
La forme de l'échec nomme le correctif. Des appels presque identiques répétés pointent vers la pagination. Des paramètres erronés pointent vers les descriptions. Une piste abandonnée pointe vers un tool qui n'existe pas. Donner ces transcripts à un agent de code produit des propositions sur toute la surface d'un coup, plutôt qu'un tool à la fois.
Mon propre contre-exemple
Par honnêteté : le dernier serveur que j'ai construit expose seize tools, dont cinq simples list_* qui enveloppent chacun une collection, et aucune campagne d'évaluation n'a validé cette forme. Il fonctionne. Mais "fonctionne" n'est pas "bien conçu", et c'est précisément le piège : un wrapper 1:1 passe tous les tests fonctionnels. Seule une mesure avec un agent le révèle.
Conclusion
Concevoir un tool MCP, c'est concevoir une interface pour un utilisateur qui lit tout, ne retient rien d'un appel à l'autre, et paie chaque token. Moins de tools, mieux nommés, qui font les jointures côté serveur, renvoient des noms lisibles et des erreurs qui expliquent quoi faire : c'est ce qui sépare un serveur qui répond d'un serveur qu'un agent sait utiliser.

