Guide · IA & agents

Outil local pour un agent Claude

Déclarez un outil Python dans le Claude Agent SDK, retirez les outils intégrés, puis vérifiez que Claude appelle votre fonction.

Un outil personnalisé n’est pas une consigne dans le prompt. Dans le Claude Agent SDK, c’est une fonction de votre processus, exposée à Claude par un serveur MCP interne. Claude choisit de l’appeler. Votre code exécute l’appel et renvoie le résultat. Tant que vous n’avez pas vu le nom de l’outil dans le flux de messages, vous ne savez pas si la réponse vient du fichier ou d’une invention.

Ce brouillon suit la page Give Claude custom tools et le quickstart Python. Il ne reprend pas leur exemple météo. Les commandes n’ont pas été rejouées le 2026-10-18.

Prérequis

  • Python 3.10 ou plus récent, comme l’indique le quickstart.
  • Un compte Anthropic et une clé API. Le SDK lit ANTHROPIC_API_KEY dans l’environnement du processus. Il ne charge pas un fichier .env tout seul.
  • Un répertoire de travail vide, hors d’un dépôt qui contient des secrets.

Le quickstart installe le paquet ainsi, après un environnement virtuel :

python3 -m venv .venv
source .venv/bin/activate
pip install claude-agent-sdk

Sous Windows, le même quickstart active l’environnement avec .venv\Scripts\Activate.ps1. Si pip installe une distribution source sans binaire, ce qui arrive par exemple sur Windows ARM64, le SDK ne démarre pas tant que Claude Code n’est pas installé et visible dans PATH. Ce cas est décrit dans le quickstart, pas rejoué ici.

N’utilisez pas une session claude.ai à la place de la clé API. Le quickstart dit qu’Anthropic n’autorise pas, sans accord préalable, les produits tiers à offrir le login claude.ai.

Étape 1 : un catalogue que Claude ne peut pas deviner

Créez catalog.json avec une référence que vous choisissez. Le libellé doit être assez particulier pour qu’une réponse inventée se voie.

{
  "NORD-17": {
    "libelle": "joint-atelier-bleu",
    "statut": "reserve-local"
  }
}

Remplacez le code et le libellé par les vôtres. Le fichier reste sur la machine. L’outil ne doit pas appeler un service externe.

Vérification : catalog.json est dans le répertoire du script, et le libellé n’est pas un mot que le modèle pourrait sortir sans lire le fichier.

Étape 2 : déclarer l’outil

Un outil a quatre parties : un nom, une description, un schéma d’entrée, un handler asynchrone. Le handler reçoit les arguments déjà validés et doit renvoyer un objet avec content. En Python, une erreur métier se signale avec is_error à vrai, pas avec une exception laissée remonter si vous voulez contrôler le texte que Claude lit.

La page custom tools dit aussi que le décorateur Python ne transmet que content et is_error depuis le dict renvoyé. structuredContent ne revient pas à Claude avec un serveur MCP interne. Pour ce premier outil, un bloc texte suffit.

import json
from pathlib import Path
from typing import Any

from claude_agent_sdk import tool

CATALOG = Path("catalog.json")


@tool(
    "chercher_reference",
    "Cherche un code SKU dans catalog.json, à côté du script. "
    "À utiliser seulement si l’utilisateur donne un code. "
    "Si le code est absent, le dire. Ne pas inventer de libellé.",
    {"sku": str},
)
async def chercher_reference(args: dict[str, Any]) -> dict[str, Any]:
    sku = str(args.get("sku", "")).strip()
    try:
        data = json.loads(CATALOG.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        return {
            "content": [{"type": "text", "text": f"Catalogue illisible : {exc}"}],
            "is_error": True,
        }

    item = data.get(sku)
    if not isinstance(item, dict):
        return {
            "content": [{"type": "text", "text": f"SKU absent du catalogue : {sku}"}],
            "is_error": True,
        }

    libelle = item.get("libelle", "")
    statut = item.get("statut", "")
    return {
        "content": [
            {
                "type": "text",
                "text": f"SKU {sku} : libellé {libelle}, statut {statut}.",
            }
        ]
    }

Un schéma dict Python rend chaque clé obligatoire. Pour un paramètre optionnel, la page indique d’utiliser la forme JSON Schema et de laisser la clé hors de required, puis de la lire avec args.get. Ce n’est pas le cas ici.

Les annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) se passent par l’argument annotations du décorateur. Ce sont des indications, pas des verrous : un outil marqué lecture seule peut quand même écrire si le handler le fait. Ce script n’écrit pas. Si le décorateur de votre version refuse annotations, ouvrez la référence Python liée depuis la page custom tools avant de changer l’appel. Cet argument n’a pas été rejoué le 2026-10-18.

Vérification : le handler renvoie toujours content. Une absence de SKU passe par is_error, sinon Claude peut prendre le message d’échec pour une fiche produit.

Étape 3 : enregistrer le serveur et couper les outils intégrés

create_sdk_mcp_server enveloppe l’outil dans un serveur qui tourne dans le processus, pas dans un autre programme. La clé de mcp_servers devient le segment du nom complet : mcp__catalog__chercher_reference. Si la clé contient un point ou une espace, la page dit de le remplacer par un soulignement dans le nom autorisé.

allowed_tools autorise l’appel sans invite de permission. tools=[] retire les outils intégrés du contexte. Sans cela, Claude peut encore utiliser Read et ouvrir catalog.json lui-même. Vous croiriez alors que l’outil a servi.

import asyncio

from claude_agent_sdk import (
    AssistantMessage,
    ClaudeAgentOptions,
    ResultMessage,
    ToolUseBlock,
    create_sdk_mcp_server,
    query,
)

catalog_server = create_sdk_mcp_server(
    name="catalog",
    version="1.0.0",
    tools=[chercher_reference],
)


async def main() -> None:
    options = ClaudeAgentOptions(
        mcp_servers={"catalog": catalog_server},
        allowed_tools=["mcp__catalog__chercher_reference"],
        tools=[],
    )
    prompt = (
        "Quel est le libellé du SKU NORD-17 ? "
        "Utilise l’outil de catalogue. N’invente pas si l’outil échoue."
    )
    try:
        async for message in query(prompt=prompt, options=options):
            if isinstance(message, AssistantMessage):
                for block in message.content:
                    if isinstance(block, ToolUseBlock):
                        print(f"appel {block.name} {block.input}")
            elif isinstance(message, ResultMessage) and message.subtype == "success":
                print(message.result)
    except Exception as exc:
        print(f"echec {exc}")


asyncio.run(main())

Adaptez le SKU du prompt à celui de votre fichier. La recherche d’outils est activée par défaut : le flux peut contenir un appel ToolSearch avant le vôtre. Ce n’est pas un second catalogue.

Un query en un tour lève une exception après un résultat d’erreur. Le try évite de prendre ce silence pour une réponse vide. Le quickstart rappelle aussi qu’une erreur Not logged in ou Invalid API key vient souvent d’une clé absente du shell qui lance le script.

Vérification : la ligne appel contient mcp__catalog__chercher_reference et le SKU demandé. Le texte final contient le libellé de votre fichier, pas un autre. S’il n’y a aucun ToolUseBlock, l’outil n’a pas été appelé.

Ce qui peut casser

L’outil absent de allowed_tools reste visible, mais l’appel passe par le flux de permission. Dans un script sans terminal interactif, cela bloque ou refuse. Un nom mal orthographié a le même effet : la clé du serveur et le nom de l’outil doivent correspondre, avec le préfixe mcp__.

Si tools n’est pas une liste vide, Read peut rester disponible. Claude lit alors le JSON et répond sans votre fonction. Le libellé peut être juste, et le test ne prouve plus que l’outil a tourné.

Une exception non rattrapée ne coupe pas la boucle. Le serveur MCP la transforme en message d’erreur brut. Claude le voit et peut réessayer. Rattrapez l’erreur quand ce texte brut ne dit pas quoi faire, et renvoyez is_error.

Si le catalogue est illisible, si le SKU est absent, ou si le JSON n’est pas un objet, le handler doit le dire. Sinon le modèle peut remplir le trou avec un libellé qui n’est pas dans le fichier.

Comment vérifier

  1. Lancez le script avec le SKU présent. Vous devez voir l’appel, puis le libellé exact du fichier.
  2. Relancez avec un SKU absent du fichier. Vous devez voir is_error dans le comportement de l’outil, et une réponse qui dit que le code est absent. Pas un libellé inventé.
  3. Remettez temporairement Read dans tools, relancez, et regardez si l’appel mcp__catalog__chercher_reference disparaît. Remettez tools=[] ensuite. Cette comparaison n’a pas été faite le 2026-10-18 ; c’est le contrôle à faire chez vous.

Le modèle ne répond pas deux fois de la même façon. Le critère reste l’appel visible et le libellé recopié du fichier.

Sources

Sources recontrôlées le 19 octobre 2026. Les extraits lus le 2026-10-18 décrivent le décorateur @tool, create_sdk_mcp_server, le nom mcp__serveur__outil, allowed_tools, tools=[], et is_error. Les commandes de ce brouillon n’ont pas été rejouées ce jour-là. Si un import ou un argument de décorateur diffère, la page liée prime sur ce texte.