Un outil MCP peut exposer une action à un assistant. Le protocole décrit comment présenter et appeler cet outil. Il ne décide pas à ta place des données que le serveur a le droit de lire. Pour commencer, une action limitée à des informations publiques est plus facile à contrôler.

Décrire une seule action

Imaginons un outil qui retourne le nom d’un projet public à partir de son slug. Pas de requête SQL libre, pas de chemin de fichier et pas d’URL fournie par l’assistant. Le contrat reste petit.

get_public_project.json
{
  "name": "get_public_project",
  "description": "Lire le nom d’un projet public à partir de son slug.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "slug": {
        "type": "string",
        "pattern": "^[a-z0-9-]{1,80}$"
      }
    },
    "required": [
      "slug"
    ],
    "additionalProperties": false
  },
  "annotations": {
    "readOnlyHint": true,
    "destructiveHint": false,
    "openWorldHint": false
  }
}
Définition d’un outil, à intégrer à un serveur MCP

Le schéma indique les arguments attendus. Le gestionnaire doit quand même valider ce qu’il reçoit. Une description précise aide l’assistant à choisir l’outil, mais elle ne fait pas office de contrôle d’accès.

Faire respecter la limite dans le code

Voici la fonction que le gestionnaire peut appeler. Le catalogue vient du serveur. Un slug inconnu ou un projet privé ne retourne rien. Même en devinant son identifiant, l’appelant ne récupère pas un nom confidentiel.

public-project-access.ts
type Project = { name: string; visibility: "public" | "private" };

// Exemple de contrôle dans le gestionnaire serveur, avant toute lecture.
// Le catalogue est fourni par le serveur, jamais par les arguments de l’outil.
export function readPublicProject(
  catalog: Readonly<Record<string, Project>>,
  input: unknown,
): { name: string } | null {
  if (!input || typeof input !== "object" || !("slug" in input)) return null;
  const slug = input.slug;
  if (typeof slug !== "string" || !/^[a-z0-9-]{1,80}$/.test(slug)) return null;
  if (!Object.hasOwn(catalog, slug)) return null;
  const project = catalog[slug];
  if (project.visibility !== "public") return null;
  return { name: project.name };
}
Extrait de contrôle serveur, sans transport MCP ni base de données

Ce code est volontairement limité à la règle de lecture. Pour un vrai serveur, il faut aussi gérer le transport, la validation du schéma complet, les erreurs du protocole et les limites d’usage. Un outil qui expose des données privées doit vérifier l’identité et les droits à chaque appel.

readOnlyHint reste une indication

L’annotation annonce une intention de lecture seule. Elle n’empêche pas un serveur malveillant d’écrire ou de supprimer des données. La spécification demande de traiter ces annotations comme des indications et de ne pas faire confiance à celles d’un serveur non fiable.

La protection concrète vient des permissions du compte utilisé, des opérations exposées et des contrôles du serveur. Si la source est une base de données, un compte limité à la lecture réduit ce que le code peut faire en cas d’erreur. Pour une action destructive, prévois une confirmation distincte et une vérification des droits au moment de l’exécution.

Tester aussi ce qui doit être refusé

Dans l’exemple, les tests couvrent un projet public, un projet privé, un slug inconnu et des arguments malformés. Le bon résultat n’est pas seulement « l’assistant a trouvé le projet ». C’est aussi « il ne peut pas lire celui qui ne le concerne pas ».