Aller au contenu principal

Tutoriel : les racines en pratique

Ce tutoriel montre comment implémenter les roots dans une application de conversion vidéo. Avant d’entrer dans les étapes, rappelez-vous le point établi dans la leçon théorique, car il commande tout le code qui suit : le SDK ne fait rien appliquer. Déclarer des roots ne protège aucun fichier — c’est un canal d’information entre le client et le serveur, et la vérification d’accès reste entièrement à votre charge, à l’étape 6. Un serveur qui oublie cette vérification fonctionne parfaitement… en accédant à tout.

Étape 1 : Définir les roots

Idéalement, c’est l’utilisateur qui dicte quels fichiers et dossiers le serveur MCP peut atteindre — et ce choix de conception n’est pas anodin. Des chemins codés en dur dans le serveur obligeraient chaque utilisateur à adapter le code à sa machine ; des chemins passés au lancement font de la permission un acte explicite, visible dans la ligne de commande. C’est le même modèle mental que le montage de volumes : on donne accès à ce qu’on nomme, rien d’autre.

Dans cet exemple, le programme accepte des arguments CLI qui sont interprétés comme les chemins auxquels l’utilisateur veut accorder l’accès :

# L'utilisateur spécifie les dossiers accessibles au démarrage
uv run main.py /Users/alice/Videos /Users/alice/Desktop

Ces chemins sont passés au MCPClient lors de l’initialisation.

Étape 2 : Créer des objets Root

Convertissez les chemins en objets Root du SDK MCP :

from mcp.types import Root
from pathlib import Path

def create_roots(paths: list[str]) -> list[Root]:
    roots = []
    for path in paths:
        abs_path = Path(path).resolve()
        roots.append(Root(
            uri=f"file://{abs_path}",
            name=abs_path.name
        ))
    return roots

Étape 3 : Callback de roots

Le serveur peut demander la liste des roots via un callback. Définissez ce callback côté client :

async def roots_callback() -> list[Root]:
    # Retourne la liste des roots que l'utilisateur a approuvées
    return client_roots  # défini au démarrage de l'application

Passez-le lors de la création de la session :

async with ClientSession(
    read,
    write,
    roots_callback=roots_callback  # ← ici
) as session:
    await session.initialize()

Étape 4 : Utiliser les roots dans les outils

Côté serveur, vos outils peuvent récupérer les roots et les utiliser :

@mcp.tool()
async def list_accessible_files(
    extension: str = Field(description="Extension de fichier (ex: mp4)"),
    ctx: Context
) -> list[str]:
    # Récupérer les roots disponibles
    roots_result = await ctx.session.list_roots()
    
    files = []
    for root in roots_result.roots:
        root_path = Path(root.uri.replace("file://", ""))
        # Trouver tous les fichiers avec l'extension demandée
        for f in root_path.rglob(f"*.{extension}"):
            files.append(str(f))
    
    return files

Étape 5 : Accéder aux roots

Pour accéder aux roots dans un outil, utilisez ctx.session.list_roots() :

@mcp.tool()
async def convert_video(
    filename: str = Field(description="Nom du fichier vidéo à convertir"),
    ctx: Context
) -> str:
    roots_result = await ctx.session.list_roots()
    
    # Chercher le fichier dans les roots
    video_path = find_file_in_roots(filename, roots_result.roots)
    if not video_path:
        return f"Fichier '{filename}' introuvable dans les dossiers accessibles"
    
    # Convertir...
    return await do_convert(video_path)

Étape 6 : Autoriser l’accès

Voici l’étape qui fait la différence entre une application sûre et une application qui semble l’être. La vérification doit travailler sur des chemins résolus — c’est ce que fait le code ci-dessous — car un chemin relatif contenant .. peut désigner n’importe quel point du disque tout en paraissant anodin. Comparer des chaînes de caractères ne suffit pas ; on compare des chemins canoniques.

def find_file_in_roots(filename: str, roots) -> Path | None:
    for root in roots:
        root_path = Path(root.uri.replace("file://", ""))
        # Chercher récursivement
        matches = list(root_path.rglob(filename))
        if matches:
            return matches[0]
    return None

def is_path_in_roots(path: Path, roots) -> bool:
    for root in roots:
        root_path = Path(root.uri.replace("file://", "")).resolve()
        if path.resolve().is_relative_to(root_path):
            return True
    return False

Flux complet

Utilisateur: "Convertis biking.mp4 en MOV"
    ↓
Claude appelle list_accessible_files(extension="mp4")
    ↓
Serveur → list_roots() → roots_callback → ["/Users/alice/Videos", ...]
    ↓
Serveur parcourt les roots → trouve "/Users/alice/Videos/biking.mp4"
    ↓
Claude appelle convert_video(filename="biking.mp4")
    ↓
Serveur trouve le fichier dans les roots → convertit
    ↓
"biking.mov créé dans /Users/alice/Videos/"

Points clés

  • Les roots sont définies par l’utilisateur lors du démarrage de l’application
  • Le SDK ne les applique pas automatiquement — vous devez vérifier vous-même
  • Toujours vérifier is_path_in_roots() avant tout accès au système de fichiers
  • Les roots rendent l’expérience fluide : les utilisateurs n’ont pas à taper des chemins complets