Implémenter des Tools : JSON Schema et Handlers
L’anatomie d’un tool MCP
Un tool MCP est plus qu’une simple fonction Python. C’est un contrat entre votre serveur et le modèle de langage : un nom, une description, un schéma d’entrée JSON, et un handler qui exécute la logique. La qualité de ce contrat détermine l’efficacité du modèle à utiliser vos outils.
Le décorateur @app.tool()
FastMCP transforme automatiquement une fonction Python en tool MCP grâce au décorateur @app.tool(). Il génère le schéma JSON des paramètres à partir des annotations de type Python :
from mcp.server.fastmcp import FastMCP
app = FastMCP("Mon Serveur")
@app.tool()
def search_documents(
query: str,
max_results: int = 10,
language: str = "fr",
include_archived: bool = False,
) -> str:
"""Recherche des documents dans la base de connaissances.
Args:
query: Termes de recherche (2-500 caractères)
max_results: Nombre maximum de résultats (1-100, défaut: 10)
language: Code langue ISO 639-1 (défaut: fr)
include_archived: Inclure les documents archivés (défaut: false)
Returns:
JSON avec la liste des documents trouvés et le nombre total
"""
# Logique de recherche
...
Ce décorateur génère automatiquement le schéma JSON suivant pour le modèle :
{
"name": "search_documents",
"description": "Recherche des documents dans la base de connaissances.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Termes de recherche (2-500 caractères)"
},
"max_results": {
"type": "integer",
"description": "Nombre maximum de résultats (1-100, défaut: 10)",
"default": 10
},
"language": {
"type": "string",
"description": "Code langue ISO 639-1 (défaut: fr)",
"default": "fr"
},
"include_archived": {
"type": "boolean",
"description": "Inclure les documents archivés (défaut: false)",
"default": false
}
},
"required": ["query"]
}
}
Règles de nommage
Le nom du tool est le nom de la fonction Python. Choisissez-le avec soin car le modèle le voit :
# Bon : verbe d'action + objet clair
@app.tool()
def create_issue(title: str, body: str) -> str: ...
@app.tool()
def search_repositories(query: str) -> str: ...
@app.tool()
def get_user_profile(username: str) -> str: ...
# Mauvais : vague, ambigu, technique
@app.tool()
def process(data: str) -> str: ... # Trop vague
@app.tool()
def handle_request(req: str) -> str: ... # Incompréhensible pour le LLM
@app.tool()
def db_query(sql: str) -> str: ... # Trop technique
Descriptions : le guide du modèle
La docstring est la pièce la plus critique. C’est la seule information que le modèle a pour décider quand et comment appeler votre tool.
Structure recommandée
@app.tool()
def book_restaurant(
restaurant_name: str,
date: str,
party_size: int,
special_requests: str = "",
) -> str:
"""Réserve une table dans un restaurant.
Effectue une réservation complète incluant la confirmation
par email. Utilisez ce tool quand l'utilisateur veut
réserver un restaurant spécifique.
Args:
restaurant_name: Nom exact du restaurant
date: Date et heure au format ISO 8601 (ex: 2026-04-15T19:30:00)
party_size: Nombre de convives (1-20)
special_requests: Demandes spéciales (allergies, occasion, etc.)
Returns:
JSON avec le numéro de réservation, la confirmation
et les détails du restaurant
"""
...
Ce que le modèle a besoin de savoir
- Quand utiliser ce tool (contexte d’utilisation)
- Quels paramètres fournir (format, contraintes, exemples)
- Quoi attendre en retour (format de la réponse)
Types Python supportés
FastMCP traduit les annotations Python en types JSON Schema :
| Python | JSON Schema | Exemple |
|---|---|---|
str | string | "texte" |
int | integer | 42 |
float | number | 3.14 |
bool | boolean | true |
list[str] | array of string | ["a", "b"] |
dict | object | {"key": "val"} |
Optional[str] | string (nullable) | "texte" ou null |
Handlers asynchrones
Pour les tools qui font des appels réseau ou base de données, utilisez async :
import httpx
@app.tool()
async def fetch_api_data(endpoint: str) -> str:
"""Récupère des données depuis une API externe.
Args:
endpoint: Le chemin de l'endpoint API (ex: /users/123)
Returns:
Les données JSON de l'API
"""
async with httpx.AsyncClient() as client:
response = await client.get(f"https://api.example.com{endpoint}")
response.raise_for_status()
return response.text
Validation des entrées
Ne faites jamais confiance aux paramètres reçus. Validez systématiquement :
import json
@app.tool()
def create_user(name: str, email: str, role: str = "viewer") -> str:
"""Crée un nouvel utilisateur.
Args:
name: Nom complet (2-100 caractères)
email: Adresse email valide
role: Rôle : viewer, editor, admin (défaut: viewer)
Returns:
JSON avec les détails de l'utilisateur créé
"""
# Validation
if len(name) < 2 or len(name) > 100:
return json.dumps({"error": "Le nom doit faire entre 2 et 100 caractères"})
if "@" not in email or "." not in email:
return json.dumps({"error": "Adresse email invalide"})
valid_roles = ("viewer", "editor", "admin")
if role not in valid_roles:
return json.dumps({"error": f"Rôle invalide. Choix : {', '.join(valid_roles)}"})
# Création...
return json.dumps({"success": True, "user": {"name": name, "email": email, "role": role}})
Format de retour
Les tools MCP retournent toujours du texte. Mais ce texte peut être du JSON structuré, que le modèle sait interpréter :
# Bon : JSON structuré et lisible
return json.dumps({
"success": True,
"user": {
"name": "Alice Dupont",
"email": "[email protected]",
"role": "editor",
},
"message": "Utilisateur créé avec succès"
}, ensure_ascii=False)
# Mauvais : données brutes difficilement exploitables
return "OK:user_id=abc123:status=created"
Préférez des retours lisibles par un humain : le modèle comprend mieux "Alice Dupont" qu’un UUID.
Points clés à retenir
- Le décorateur
@app.tool()génère automatiquement le JSON Schema des paramètres - Le nom de la fonction = nom du tool (verbe + objet)
- La docstring est cruciale : elle guide le modèle sur quand et comment appeler le tool
- Utilisez des annotations de type Python pour le schéma (str, int, float, bool, list, dict)
- Validez toujours les entrées et retournez des erreurs exploitables
- Retournez du JSON structuré avec des valeurs lisibles par un humain