Passer au contenu principal
Les outils personnalisés étendent le SDK Agent en vous permettant de définir vos propres fonctions que Claude peut appeler lors d’une conversation. En utilisant le serveur MCP en processus du SDK, vous pouvez donner à Claude accès aux bases de données, aux API externes, à la logique spécifique au domaine ou à toute autre capacité dont votre application a besoin. Ce guide couvre comment définir des outils avec des schémas d’entrée et des gestionnaires, les regrouper dans un serveur MCP, les transmettre à query, et contrôler les outils auxquels Claude peut accéder. Il couvre également la gestion des erreurs, les annotations d’outils et le retour de contenu non textuel comme les images.

Référence rapide

Si vous voulez…Faites ceci
Définir un outilUtilisez @tool (Python) ou tool() (TypeScript) avec un nom, une description, un schéma et un gestionnaire. Voir Créer un outil personnalisé.
Enregistrer un outil auprès de ClaudeEnveloppez dans create_sdk_mcp_server / createSdkMcpServer et transmettez à mcpServers dans query(). Voir Appeler un outil personnalisé.
Pré-approuver un outilAjoutez à vos outils autorisés. Voir Configurer les outils autorisés.
Supprimer un outil intégré du contexte de ClaudeTransmettez un tableau tools listant uniquement les outils intégrés que vous souhaitez. Voir Configurer les outils autorisés.
Laisser Claude appeler les outils en parallèleDéfinissez readOnlyHint: true sur les outils sans effets secondaires. Voir Ajouter des annotations d’outils.
Gérer les erreurs sans arrêter la boucleRetournez isError: true au lieu de lever une exception. Voir Gérer les erreurs.
Retourner des images ou des fichiersUtilisez des blocs image ou resource dans le tableau de contenu. Voir Retourner des images et des ressources.
Retourner un résultat JSON lisible par machineDéfinissez structuredContent sur le résultat. Voir Retourner des données structurées.
Adapter à de nombreux outilsUtilisez la recherche d’outils pour charger les outils à la demande.

Créer un outil personnalisé

Un outil est défini par quatre parties, transmises comme arguments à l’assistant tool() en TypeScript ou au décorateur @tool en Python :
  • Nom : un identifiant unique que Claude utilise pour appeler l’outil.
  • Description : ce que fait l’outil. Claude lit ceci pour décider quand l’appeler.
  • Schéma d’entrée : les arguments que Claude doit fournir. En TypeScript, c’est toujours un schéma Zod, et les args du gestionnaire sont typés automatiquement à partir de celui-ci. En Python, c’est un dictionnaire mappant les noms aux types, comme {"latitude": float}, que le SDK convertit en JSON Schema pour vous. Le décorateur Python accepte également un dictionnaire JSON Schema complet directement quand vous avez besoin d’énumérations, de plages, de champs optionnels ou d’objets imbriqués.
  • Gestionnaire : la fonction asynchrone qui s’exécute quand Claude appelle l’outil. Elle reçoit les arguments validés et doit retourner un objet avec :
    • content (obligatoire) : un tableau de blocs de résultats, chacun avec un type de "text", "image" ou "resource". Voir Retourner des images et des ressources pour les blocs non textuels.
    • structuredContent (optionnel) : un objet JSON contenant le résultat sous forme de données lisibles par machine, retourné aux côtés de content. Voir Retourner des données structurées.
    • isError (optionnel) : définissez sur true pour signaler un échec d’outil afin que Claude puisse y réagir. Voir Gérer les erreurs.
Après avoir défini un outil, enveloppez-le dans un serveur avec createSdkMcpServer (TypeScript) ou create_sdk_mcp_server (Python). Le serveur s’exécute en processus à l’intérieur de votre application, pas comme un processus séparé.

Exemple d’outil météo

Cet exemple définit un outil get_temperature et l’enveloppe dans un serveur MCP. Il configure uniquement l’outil ; pour le transmettre à query et l’exécuter, voir Appeler un outil personnalisé ci-dessous.
from typing import Any
import httpx
from claude_agent_sdk import tool, create_sdk_mcp_server


# Define a tool: name, description, input schema, handler
@tool(
    "get_temperature",
    "Get the current temperature at a location",
    {"latitude": float, "longitude": float},
)
async def get_temperature(args: dict[str, Any]) -> dict[str, Any]:
    async with httpx.AsyncClient() as client:
        response = await client.get(
            "https://api.open-meteo.com/v1/forecast",
            params={
                "latitude": args["latitude"],
                "longitude": args["longitude"],
                "current": "temperature_2m",
                "temperature_unit": "fahrenheit",
            },
        )
        data = response.json()

    # Return a content array - Claude sees this as the tool result
    return {
        "content": [
            {
                "type": "text",
                "text": f"Temperature: {data['current']['temperature_2m']}°F",
            }
        ]
    }


# Wrap the tool in an in-process MCP server
weather_server = create_sdk_mcp_server(
    name="weather",
    version="1.0.0",
    tools=[get_temperature],
)
Voir la référence TypeScript tool() ou la référence Python @tool pour les détails complets des paramètres, y compris les formats de schéma JSON et la structure des valeurs de retour.
Pour rendre un paramètre optionnel : en TypeScript, ajoutez .default() au champ Zod. En Python, le schéma dict traite chaque clé comme obligatoire, donc omettez le paramètre du schéma, mentionnez-le dans la chaîne de description, et lisez-le avec args.get() dans le gestionnaire. L’outil get_precipitation_chance ci-dessous montre les deux modèles.

Appeler un outil personnalisé

Transmettez le serveur MCP que vous avez créé à query via l’option mcpServers. La clé dans mcpServers devient le segment {server_name} dans le nom complètement qualifié de chaque outil : mcp__{server_name}__{tool_name}. Listez ce nom dans allowedTools afin que l’outil s’exécute sans invite de permission. Ces extraits réutilisent le weatherServer de l’exemple ci-dessus pour demander à Claude quel est le temps dans un endroit spécifique.
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage


async def main():
    options = ClaudeAgentOptions(
        mcp_servers={"weather": weather_server},
        allowed_tools=["mcp__weather__get_temperature"],
    )

    async for message in query(
        prompt="What's the temperature in San Francisco?",
        options=options,
    ):
        # ResultMessage is the final message after all tool calls complete
        if isinstance(message, ResultMessage) and message.subtype == "success":
            print(message.result)


asyncio.run(main())

Ajouter plus d’outils

Un serveur contient autant d’outils que vous en listez dans son tableau tools. Avec plus d’un outil sur un serveur, vous pouvez lister chacun dans allowedTools individuellement ou utiliser le caractère générique mcp__weather__* pour couvrir tous les outils que le serveur expose. L’exemple ci-dessous ajoute un deuxième outil, get_precipitation_chance, au weatherServer de l’exemple d’outil météo et le reconstruit avec les deux outils dans le tableau.
# Define a second tool for the same server
@tool(
    "get_precipitation_chance",
    "Get the hourly precipitation probability for a location. "
    "Optionally pass 'hours' (1-24) to control how many hours to return.",
    {"latitude": float, "longitude": float},
)
async def get_precipitation_chance(args: dict[str, Any]) -> dict[str, Any]:
    # 'hours' isn't in the schema - read it with .get() to make it optional
    hours = args.get("hours", 12)
    async with httpx.AsyncClient() as client:
        response = await client.get(
            "https://api.open-meteo.com/v1/forecast",
            params={
                "latitude": args["latitude"],
                "longitude": args["longitude"],
                "hourly": "precipitation_probability",
                "forecast_days": 1,
            },
        )
        data = response.json()
    chances = data["hourly"]["precipitation_probability"][:hours]

    return {
        "content": [
            {
                "type": "text",
                "text": f"Next {hours} hours: {'%, '.join(map(str, chances))}%",
            }
        ]
    }


# Rebuild the server with both tools in the array
weather_server = create_sdk_mcp_server(
    name="weather",
    version="1.0.0",
    tools=[get_temperature, get_precipitation_chance],
)
Chaque outil dans ce tableau consomme de l’espace de fenêtre de contexte à chaque tour. Si vous définissez des dizaines d’outils, voir la recherche d’outils pour les charger à la demande à la place.

Ajouter des annotations d’outils

Les annotations d’outils sont des métadonnées optionnelles décrivant le comportement d’un outil. Transmettez-les comme cinquième argument à l’assistant tool() en TypeScript ou via l’argument de mot-clé annotations pour le décorateur @tool en Python. Tous les champs d’indice sont des booléens.
ChampPar défautSignification
readOnlyHintfalseL’outil ne modifie pas son environnement. Contrôle si l’outil peut être appelé en parallèle avec d’autres outils en lecture seule.
destructiveHinttrueL’outil peut effectuer des mises à jour destructrices. Informatif uniquement.
idempotentHintfalseLes appels répétés avec les mêmes arguments n’ont aucun effet supplémentaire. Informatif uniquement.
openWorldHinttrueL’outil atteint les systèmes en dehors de votre processus. Informatif uniquement.
Les annotations sont des métadonnées, pas une application. Un outil marqué readOnlyHint: true peut toujours écrire sur le disque si c’est ce que fait le gestionnaire. Gardez l’annotation exacte par rapport au gestionnaire. Cet exemple ajoute readOnlyHint à l’outil get_temperature de l’exemple d’outil météo.
from claude_agent_sdk import tool, ToolAnnotations


@tool(
    "get_temperature",
    "Get the current temperature at a location",
    {"latitude": float, "longitude": float},
    annotations=ToolAnnotations(
        readOnlyHint=True
    ),  # Lets Claude batch this with other read-only calls
)
async def get_temperature(args):
    return {"content": [{"type": "text", "text": "..."}]}
Voir ToolAnnotations dans la référence TypeScript ou Python.

Contrôler l’accès aux outils

L’exemple d’outil météo a enregistré un serveur et listé les outils dans allowedTools. Cette section couvre comment les noms d’outils sont construits et comment limiter l’accès quand vous avez plusieurs outils ou que vous souhaitez restreindre les outils intégrés.

Format du nom d’outil

Quand les outils MCP sont exposés à Claude, leurs noms suivent un format spécifique :
  • Modèle : mcp__{server_name}__{tool_name}
  • Exemple : Un outil nommé get_temperature dans le serveur weather devient mcp__weather__get_temperature

Configurer les outils autorisés

L’option tools et les listes autorisées/interdites affectent deux couches : la disponibilité, qui contrôle si un outil apparaît dans le contexte de Claude, et la permission, qui contrôle si un appel est approuvé une fois que Claude tente de l’utiliser. tools et les entrées disallowedTools sans portée changent la disponibilité. allowedTools et les règles disallowedTools avec portée changent la permission uniquement.
OptionCoucheEffet
tools: ["Read", "Grep"]DisponibilitéSeuls les outils intégrés listés sont dans le contexte de Claude. Les outils intégrés non listés sont supprimés. Les outils MCP ne sont pas affectés.
tools: []DisponibilitéTous les outils intégrés sont supprimés. Claude ne peut utiliser que vos outils MCP.
outils autorisésPermissionLes outils listés s’exécutent sans invite de permission. Les outils non listés restent disponibles ; les appels passent par le flux de permission.
outils interditsLes deuxUn nom d’outil simple tel que "Bash" supprime l’outil du contexte de Claude, comme l’omission de tools. Une règle avec portée telle que "Bash(rm *)" laisse l’outil dans le contexte et refuse uniquement les appels correspondants.
Pour supprimer complètement un outil intégrés, omettez-le de tools ou listez son nom simple dans disallowedTools (Python : disallowed_tools) ; les deux gardent l’outil hors du contexte afin que Claude ne le tente jamais. Une règle disallowedTools avec portée bloque les appels correspondants mais laisse l’outil visible, donc Claude peut gaspiller un tour en le tentant. Voir Configurer les permissions pour l’ordre d’évaluation complet.

Gérer les erreurs

La façon dont votre gestionnaire signale les erreurs détermine si la boucle d’agent continue ou s’arrête :
Ce qui se passeRésultat
Le gestionnaire lève une exception non capturéeLa boucle d’agent s’arrête. Claude ne voit jamais l’erreur, et l’appel query échoue.
Le gestionnaire capture l’erreur et retourne isError: true (TS) / "is_error": True (Python)La boucle d’agent continue. Claude voit l’erreur comme des données et peut réessayer, essayer un outil différent ou expliquer l’échec.
L’exemple ci-dessous capture deux types d’échecs à l’intérieur du gestionnaire au lieu de les laisser lever. Un statut HTTP non-200 est capturé de la réponse et retourné comme un résultat d’erreur. Une erreur réseau ou un JSON invalide est capturé par le try/except (Python) ou try/catch (TypeScript) environnant et est également retourné comme un résultat d’erreur. Dans les deux cas, le gestionnaire retourne normalement et la boucle d’agent continue.
import json
import httpx
from typing import Any


@tool(
    "fetch_data",
    "Fetch data from an API",
    {"endpoint": str},  # Simple schema
)
async def fetch_data(args: dict[str, Any]) -> dict[str, Any]:
    try:
        async with httpx.AsyncClient() as client:
            response = await client.get(args["endpoint"])
            if response.status_code != 200:
                # Return the failure as a tool result so Claude can react to it.
                # is_error marks this as a failed call rather than odd-looking data.
                return {
                    "content": [
                        {
                            "type": "text",
                            "text": f"API error: {response.status_code} {response.reason_phrase}",
                        }
                    ],
                    "is_error": True,
                }

            data = response.json()
            return {"content": [{"type": "text", "text": json.dumps(data, indent=2)}]}
    except Exception as e:
        # Catching here keeps the agent loop alive. An uncaught exception
        # would end the whole query() call.
        return {
            "content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
            "is_error": True,
        }

Retourner des images et des ressources

Le tableau content dans un résultat d’outil accepte les blocs text, image et resource. Vous pouvez les mélanger dans la même réponse.

Images

Un bloc image porte les octets d’image en ligne, codés en base64. Il n’y a pas de champ URL. Pour retourner une image qui se trouve à une URL, récupérez-la dans le gestionnaire, lisez les octets de réponse et encodez-les en base64 avant de les retourner. Le résultat est traité comme une entrée visuelle.
ChampTypeNotes
type"image"
datastringOctets codés en base64. Base64 brut uniquement, pas de préfixe data:image/...;base64,
mimeTypestringObligatoire. Par exemple image/png, image/jpeg, image/webp, image/gif
import base64
import httpx


# Define a tool that fetches an image from a URL and returns it to Claude
@tool("fetch_image", "Fetch an image from a URL and return it to Claude", {"url": str})
async def fetch_image(args):
    async with httpx.AsyncClient() as client:  # Fetch the image bytes
        response = await client.get(args["url"])

    return {
        "content": [
            {
                "type": "image",
                "data": base64.b64encode(response.content).decode(
                    "ascii"
                ),  # Base64-encode the raw bytes
                "mimeType": response.headers.get(
                    "content-type", "image/png"
                ),  # Read MIME type from the response
            }
        ]
    }

Ressources

Un bloc ressource intègre un morceau de contenu identifié par un URI. L’URI est un libellé pour que Claude le référence ; le contenu réel se trouve dans le champ text ou blob du bloc. Utilisez ceci quand votre outil produit quelque chose qui a du sens à adresser par nom plus tard, comme un fichier généré ou un enregistrement d’un système externe.
ChampTypeNotes
type"resource"
resource.uristringIdentifiant du contenu. N’importe quel schéma URI
resource.textstringLe contenu, s’il est textuel. Fournissez ceci ou blob, pas les deux
resource.blobstringLe contenu codé en base64, s’il est binaire
resource.mimeTypestringOptionnel
Cet exemple montre un bloc ressource retourné de l’intérieur d’un gestionnaire d’outil. L’URI file:///tmp/report.md est un libellé que Claude peut référencer plus tard ; le SDK ne lit pas à partir de ce chemin.
return {
  content: [
    {
      type: "resource",
      resource: {
        uri: "file:///tmp/report.md", // Label for Claude to reference, not a path the SDK reads
        mimeType: "text/markdown",
        text: "# Report\n..." // The actual content, inline
      }
    }
  ]
};
Ces formes de bloc proviennent du type MCP CallToolResult. Voir la spécification MCP pour la définition complète.

Retourner des données structurées

structuredContent est un objet JSON optionnel sur le résultat, séparé du tableau content. Utilisez-le pour retourner des valeurs brutes que Claude peut lire comme des champs exacts au lieu de les analyser à partir d’une chaîne de texte ou d’une image. Quand structuredContent est défini, Claude reçoit le JSON plus tous les blocs image ou ressource de content. Les blocs de texte dans content ne sont pas transmis, car on suppose qu’ils dupliquent les données structurées. L’exemple ci-dessous rend un graphique sous forme de bloc image et retourne les points de données derrière lui dans structuredContent du même gestionnaire.
TypeScript
return {
  content: [
    {
      type: "image",
      data: chartPngBuffer.toString("base64"),
      mimeType: "image/png"
    }
  ],
  structuredContent: {
    series: "temperature_2m",
    unit: "fahrenheit",
    points: [62.1, 63.4, 65.0, 64.2]
  }
};
Le décorateur Python @tool transmet uniquement content et is_error du dictionnaire de retour du gestionnaire. Pour retourner structuredContent de Python, exécutez un serveur MCP autonome à la place d’un serveur SDK en processus.

Exemple : convertisseur d’unités

Cet outil convertit les valeurs entre les unités de longueur, température et poids. Un utilisateur peut demander « convertir 100 kilomètres en miles » ou « qu’est-ce que 72°F en Celsius », et Claude choisit le bon type d’unité et les unités de la demande. Il démontre deux modèles :
  • Schémas d’énumération : unit_type est limité à un ensemble fixe de valeurs. En TypeScript, utilisez z.enum(). En Python, le schéma dict ne supporte pas les énumérations, donc le dictionnaire JSON Schema complet est requis.
  • Gestion des entrées non supportées : quand une paire de conversion n’est pas trouvée, le gestionnaire retourne isError: true afin que Claude puisse dire à l’utilisateur ce qui s’est mal passé au lieu de traiter un échec comme un résultat normal.
from typing import Any
from claude_agent_sdk import tool, create_sdk_mcp_server


# z.enum() in TypeScript becomes an "enum" constraint in JSON Schema.
# The dict schema has no equivalent, so full JSON Schema is required.
@tool(
    "convert_units",
    "Convert a value from one unit to another",
    {
        "type": "object",
        "properties": {
            "unit_type": {
                "type": "string",
                "enum": ["length", "temperature", "weight"],
                "description": "Category of unit",
            },
            "from_unit": {
                "type": "string",
                "description": "Unit to convert from, e.g. kilometers, fahrenheit, pounds",
            },
            "to_unit": {"type": "string", "description": "Unit to convert to"},
            "value": {"type": "number", "description": "Value to convert"},
        },
        "required": ["unit_type", "from_unit", "to_unit", "value"],
    },
)
async def convert_units(args: dict[str, Any]) -> dict[str, Any]:
    conversions = {
        "length": {
            "kilometers_to_miles": lambda v: v * 0.621371,
            "miles_to_kilometers": lambda v: v * 1.60934,
            "meters_to_feet": lambda v: v * 3.28084,
            "feet_to_meters": lambda v: v * 0.3048,
        },
        "temperature": {
            "celsius_to_fahrenheit": lambda v: (v * 9) / 5 + 32,
            "fahrenheit_to_celsius": lambda v: (v - 32) * 5 / 9,
            "celsius_to_kelvin": lambda v: v + 273.15,
            "kelvin_to_celsius": lambda v: v - 273.15,
        },
        "weight": {
            "kilograms_to_pounds": lambda v: v * 2.20462,
            "pounds_to_kilograms": lambda v: v * 0.453592,
            "grams_to_ounces": lambda v: v * 0.035274,
            "ounces_to_grams": lambda v: v * 28.3495,
        },
    }

    key = f"{args['from_unit']}_to_{args['to_unit']}"
    fn = conversions.get(args["unit_type"], {}).get(key)

    if not fn:
        return {
            "content": [
                {
                    "type": "text",
                    "text": f"Unsupported conversion: {args['from_unit']} to {args['to_unit']}",
                }
            ],
            "is_error": True,
        }

    result = fn(args["value"])
    return {
        "content": [
            {
                "type": "text",
                "text": f"{args['value']} {args['from_unit']} = {result:.4f} {args['to_unit']}",
            }
        ]
    }


converter_server = create_sdk_mcp_server(
    name="converter",
    version="1.0.0",
    tools=[convert_units],
)
Une fois le serveur défini, transmettez-le à query de la même manière que l’exemple météo. Cet exemple envoie trois invites différentes dans une boucle pour montrer le même outil gérant différents types d’unités. Pour chaque réponse, il inspecte les objets AssistantMessage (qui contiennent les appels d’outils que Claude a effectués pendant ce tour) et imprime chaque ToolUseBlock avant d’imprimer le texte final de ResultMessage. Cela vous permet de voir quand Claude utilise l’outil par rapport à répondre à partir de ses propres connaissances.
import asyncio
from claude_agent_sdk import (
    query,
    ClaudeAgentOptions,
    ResultMessage,
    AssistantMessage,
    ToolUseBlock,
)


async def main():
    options = ClaudeAgentOptions(
        mcp_servers={"converter": converter_server},
        allowed_tools=["mcp__converter__convert_units"],
    )

    prompts = [
        "Convert 100 kilometers to miles.",
        "What is 72°F in Celsius?",
        "How many pounds is 5 kilograms?",
    ]

    for prompt in prompts:
        async for message in query(prompt=prompt, options=options):
            if isinstance(message, AssistantMessage):
                for block in message.content:
                    if isinstance(block, ToolUseBlock):
                        print(f"[tool call] {block.name}({block.input})")
            elif isinstance(message, ResultMessage) and message.subtype == "success":
                print(f"Q: {prompt}\nA: {message.result}\n")


asyncio.run(main())

Étapes suivantes

Les outils personnalisés enveloppent les fonctions asynchrones dans une interface standard. Vous pouvez mélanger les modèles de cette page dans le même serveur : un seul serveur peut contenir un outil de base de données, un outil de passerelle API et un moteur de rendu d’image côte à côte. À partir d’ici :
  • Si votre serveur grandit à des dizaines d’outils, voir la recherche d’outils pour différer le chargement jusqu’à ce que Claude en ait besoin.
  • Pour vous connecter à des serveurs MCP externes (système de fichiers, GitHub, Slack) au lieu de construire les vôtres, voir Connecter les serveurs MCP.
  • Pour contrôler quels outils s’exécutent automatiquement par rapport à ceux nécessitant une approbation, voir Configurer les permissions.

Documentation connexe