AI

MCP Roots déprécié : migrer le périmètre fichiers sans élargir les accès

Un runbook de production pour remplacer MCP Roots par des paramètres d'outil, URI de ressources ou une configuration serveur, avec contrôle des chemins, canari, preuves et rollback.

10 sept. 2026 aiagentopsmcprootsfilesystemsecuritydeveloper-toolsautomationobservabilityguardrailsmigrationrunbookrollbackproduction

Un assistant de développement utilise un serveur MCP pour rechercher des fichiers, lire des manifests et préparer un patch dans le dépôt ouvert par l’utilisateur. Après une mise à jour du SDK, les journaux signalent que MCP Roots est déprécié. L’équipe veut migrer rapidement, mais remplacer la racine du workspace par un paramètre path libre transformerait une évolution de protocole en extension d’accès au système de fichiers.

Roots permettait au client d’indiquer au serveur les URI de fichiers pertinentes pour une session. Dans les versions récentes du protocole, cette fonctionnalité est dépréciée au profit de paramètres d’outil, d’URI de ressources ou d’une configuration serveur. Le cas d’usage ici est un outil interne capable de lire et de proposer des changements, jamais de parcourir arbitrairement la machine. Le runbook doit décider entre migration, maintien temporaire de compatibilité ou rollback de la montée de version.

Figer le contrat de fichiers avant de modifier le protocole

Commencez par une session saine et une session candidate. Conservez la version de protocole négociée, les capacités client et serveur, la réponse roots/list si elle existe encore, les outils qui la consomment et les fichiers réellement touchés.

yaml mcp-roots-migration-scope.yml
migration_id: mcp-filescope-20260910
client: developer-assistant
server: repository-tools
current_protocol: <current-negotiated-version>
candidate_protocol: <candidate-negotiated-version>

current_scope:
roots:
  - file:///workspace/orders-api
allowed_operations: [search, read, propose_patch]
forbidden_operations: [read_home, read_credentials, write_directly]

evidence:
- initialize_and_capabilities
- roots_list_response
- tool_calls_using_roots
- canonical_paths_accessed
- authorization_decisions
- patch_diff

temporary_controls:
direct_write: disabled
candidate_mode: read_only
fallback_server_release: repository-tools-41

Le nom convivial d’une racine n’est pas un identifiant d’autorisation. L’URI observée, le chemin canonique obtenu et l’identité qui lit le fichier sont les éléments à comparer.

Ne pas confondre contexte et frontière de sécurité

Roots aidait le serveur à comprendre le workspace pertinent. Cette indication ne remplace pas les permissions du processus, l’isolation du conteneur ou une validation de chemin côté serveur. Une migration sûre commence donc par deux frontières indépendantes :

  • la sélection fonctionnelle indique sur quel projet l’outil doit travailler ;
  • l’autorisation technique empêche le processus de sortir du périmètre permis.

Un serveur exécuté avec accès à tout le volume ne devient pas sûr parce que le client lui fournit une seule racine. À l’inverse, un paramètre d’outil explicite reste dangereux si ../, un lien symbolique ou une différence de casse permet de sortir du répertoire autorisé.

Choisir le remplacement selon le cas d’usage

La migration ne consiste pas à recopier l’URI Root dans tous les appels. Utilisez le mécanisme le plus étroit :

text mcp-file-scope-replacement.txt
Parametre d'outil
Pour une cible necessaire a une operation precise
Preferer project_id et relative_path a un chemin absolu libre
Le serveur mappe project_id vers une racine approuvee

URI de ressource ou template de ressource
Pour publier un corpus de lecture decouvrable
L'URI reste opaque pour le modele et resolue par le serveur
La lecture conserve autorisation, taille limite et type de contenu

Configuration serveur
Pour un workspace fixe, monte et possede par le service
La racine vient du deploiement, pas du prompt
Chaque environnement garde sa propre allowlist

Refuser
Un path absolu choisi par le modele
Une racine deduite du repertoire courant
Une variable partagee entre plusieurs tenants
Un fallback vers le repertoire parent

Pour un assistant multi-repositories, un identifiant logique est généralement plus exploitable qu’un chemin fourni par le modèle. Le serveur contrôle le mapping, la durée de validité et l’identité autorisée à utiliser chaque projet.

Construire un résolveur de chemin fermé

Centralisez la résolution avant toute lecture. Le même contrôle doit être utilisé par search_files, read_file et propose_patch, sinon un outil secondaire peut contourner le périmètre du premier.

typescript resolve-project-path.ts
import path from "node:path";
import fs from "node:fs/promises";

export async function resolveProjectPath(
approvedRoot: string,
relativePath: string
) {
if (path.isAbsolute(relativePath)) throw new Error("absolute_path_denied");

const root = await fs.realpath(approvedRoot);
const candidate = await fs.realpath(path.resolve(root, relativePath));
const prefix = root.endsWith(path.sep) ? root : root + path.sep;

if (candidate !== root && !candidate.startsWith(prefix)) {
  throw new Error("path_outside_project");
}

return candidate;
}

Cette base doit être complétée selon la plateforme : traitement de la casse, liens symboliques créés après validation, montages, fichiers spéciaux, taille maximale et extensions autorisées. Pour une écriture, ouvrez la cible avec des primitives qui réduisent les courses entre contrôle et usage, puis validez encore le parent canonique.

Rendre le périmètre visible dans le schéma d’outil

Le contrat d’entrée doit exprimer le projet et un chemin relatif. Il ne doit pas laisser le modèle inventer une racine.

json read-file-input-schema.json
{
"type": "object",
"additionalProperties": false,
"required": ["project_id", "relative_path"],
"properties": {
  "project_id": {
    "type": "string",
    "enum": ["orders-api-canary"]
  },
  "relative_path": {
    "type": "string",
    "minLength": 1,
    "pattern": "^(?!/)(?!.*(?:^|/)\.\.(?:/|$)).+$"
  }
}
}

Le pattern améliore le retour d’erreur, mais le résolveur canonique reste la décision de sécurité. Une chaîne peut respecter le schéma et atteindre un lien symbolique qui sort du projet.

Comparer ancien et nouveau chemin en mode miroir

Pendant la transition, exécutez le calcul de périmètre Roots et le nouveau mapping sur les mêmes appels de lecture. Ne retournez qu’un résultat à l’agent ; journalisez les différences de sélection de fichiers.

yaml mcp-filescope-canary.yml
cases:
- id: read_known_manifest
  project_id: orders-api-canary
  relative_path: package.json
  expect: allowed_and_same_file

- id: block_parent_traversal
  relative_path: ../platform/secrets.env
  expect: denied_before_open

- id: block_encoded_traversal
  relative_path: docs/%2e%2e/%2e%2e/secret
  expect: denied_after_decode_once

- id: block_symlink_escape
  relative_path: tmp/external-link/token
  expect: denied_after_realpath

- id: reject_unknown_project
  project_id: finance-prod
  expect: denied_without_scope_discovery

- id: preserve_patch_boundary
  relative_path: src/config.ts
  expect: patch_contains_only_approved_project

Ajoutez un changement de workspace en cours de session. L’ancien client pouvait annoncer une modification de Roots ; le nouveau contrat doit invalider l’ancien project_id ou créer une nouvelle session. Un agent ne doit pas conserver le périmètre du dépôt précédent.

Tracer la décision, pas le contenu sensible

Journalisez l’identité, la session, la version de protocole, l’outil, project_id, le digest de racine approuvée, le chemin relatif, le résultat de canonicalisation et la décision. Évitez d’envoyer le contenu des fichiers ou les secrets rencontrés dans la télémétrie.

kusto 01-mcp-file-scope-decisions.kql
let Window = 24h;
McpFileScopeEvents
| where TimeGenerated > ago(Window)
| project TimeGenerated,
        AgentName,
        SessionId,
        ProtocolVersion,
        ToolName,
        ProjectId,
        ApprovedRootDigest,
        RelativePath,
        CanonicalizationResult,
        Decision,
        Reason,
        CorrelationId
| order by TimeGenerated desc

Alertez sur les traversées refusées, les projets inconnus, les écarts entre ancien et nouveau périmètre et tout appel d’écriture sans décision d’autorisation corrélée.

Décider migration, maintien temporaire ou rollback

Migrez lorsque chaque usage de Roots a un remplacement explicite, que les chemins canoniques restent identiques sur le canari, que les cas négatifs sont refusés et que l’identité d’exécution ne peut pas lire hors périmètre.

Maintenez temporairement l’ancienne version négociée si un client dépend encore de Roots, avec une date de retrait, une liste de consommateurs et les écritures désactivées pour les clients non migrés. La compatibilité ne doit pas devenir un fallback silencieux.

Rollbackez le client ou le serveur candidat si le nouveau mapping sélectionne davantage de fichiers, si le changement de workspace conserve une ancienne autorisation, si une sortie de lien symbolique passe ou si les décisions ne sont pas traçables. Le bon rollback restaure la version connue tout en conservant l’isolation du processus ; il n’ouvre jamais un répertoire parent pour rétablir le service.

Conclusion

La dépréciation de MCP Roots oblige à rendre explicite un contrat qui était souvent implicite : quel projet, quel chemin et quelle identité peuvent participer à une opération d’agent.

Remplacez Roots par un identifiant de projet et un chemin relatif, une URI de ressource ou une configuration serveur selon le besoin. Validez ensuite le chemin canonique, l’isolation réelle, les refus et le changement de workspace. La migration est terminée lorsque l’agent conserve son utilité avec un périmètre plus prouvable, pas lorsqu’un avertissement de SDK a disparu.