Aller au contenu principal

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

  1. Quand utiliser ce tool (contexte d’utilisation)
  2. Quels paramètres fournir (format, contraintes, exemples)
  3. Quoi attendre en retour (format de la réponse)

Types Python supportés

FastMCP traduit les annotations Python en types JSON Schema :

PythonJSON SchemaExemple
strstring"texte"
intinteger42
floatnumber3.14
boolbooleantrue
list[str]array of string["a", "b"]
dictobject{"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