Pular para o conteúdo principal
Ferramentas personalizadas estendem o Agent SDK permitindo que você defina suas próprias funções que Claude pode chamar durante uma conversa. Usando o servidor MCP em processo do SDK, você pode dar a Claude acesso a bancos de dados, APIs externas, lógica específica do domínio ou qualquer outra capacidade que sua aplicação necessite. Este guia cobre como definir ferramentas com esquemas de entrada e manipuladores, agrupá-las em um servidor MCP, passá-las para query e controlar quais ferramentas Claude pode acessar. Também cobre tratamento de erros, anotações de ferramentas e retorno de conteúdo não-texto como imagens.

Referência rápida

Se você quer…Faça isto
Definir uma ferramentaUse @tool (Python) ou tool() (TypeScript) com um nome, descrição, esquema e manipulador. Veja Criar uma ferramenta personalizada.
Registrar uma ferramenta com ClaudeEnvolva em create_sdk_mcp_server / createSdkMcpServer e passe para mcpServers em query(). Veja Chamar uma ferramenta personalizada.
Pré-aprovar uma ferramentaAdicione às suas ferramentas permitidas. Veja Configurar ferramentas permitidas.
Remover uma ferramenta integrada do contexto de ClaudePasse um array tools listando apenas os integrados que você quer. Veja Configurar ferramentas permitidas.
Deixar Claude chamar ferramentas em paraleloDefina readOnlyHint: true em ferramentas sem efeitos colaterais. Veja Adicionar anotações de ferramentas.
Tratar erros sem parar o loopRetorne isError: true em vez de lançar uma exceção. Veja Tratar erros.
Retornar imagens ou arquivosUse blocos image ou resource no array de conteúdo. Veja Retornar imagens e recursos.
Retornar um resultado JSON legível por máquinaDefina structuredContent no resultado. Veja Retornar dados estruturados.
Escalar para muitas ferramentasUse tool search para carregar ferramentas sob demanda.

Criar uma ferramenta personalizada

Uma ferramenta é definida por quatro partes, passadas como argumentos para o auxiliar tool() em TypeScript ou o decorador @tool em Python:
  • Nome: um identificador único que Claude usa para chamar a ferramenta.
  • Descrição: o que a ferramenta faz. Claude lê isto para decidir quando chamá-la.
  • Esquema de entrada: os argumentos que Claude deve fornecer. Em TypeScript isto é sempre um esquema Zod, e os args do manipulador são tipados automaticamente a partir dele. Em Python isto é um dict mapeando nomes para tipos, como {"latitude": float}, que o SDK converte para JSON Schema para você. O decorador Python também aceita um dict completo de JSON Schema diretamente quando você precisa de enums, intervalos, campos opcionais ou objetos aninhados.
  • Manipulador: a função assíncrona que executa quando Claude chama a ferramenta. Ela recebe os argumentos validados e deve retornar um objeto com:
    • content (obrigatório): um array de blocos de resultado, cada um com um type de "text", "image" ou "resource". Veja Retornar imagens e recursos para blocos não-texto.
    • structuredContent (opcional): um objeto JSON contendo o resultado como dados legíveis por máquina, retornado junto com content. Veja Retornar dados estruturados.
    • isError (opcional): defina como true para sinalizar uma falha de ferramenta para que Claude possa reagir a ela. Veja Tratar erros.
Depois de definir uma ferramenta, envolva-a em um servidor com createSdkMcpServer (TypeScript) ou create_sdk_mcp_server (Python). O servidor executa em processo dentro de sua aplicação, não como um processo separado.

Exemplo de ferramenta de clima

Este exemplo define uma ferramenta get_temperature e a envolve em um servidor MCP. Ele apenas configura a ferramenta; para passá-la para query e executá-la, veja Chamar uma ferramenta personalizada abaixo.
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],
)
Veja a referência TypeScript tool() ou a referência Python @tool para detalhes completos de parâmetros, incluindo formatos de entrada JSON Schema e estrutura de valor de retorno.
Para tornar um parâmetro opcional: em TypeScript, adicione .default() ao campo Zod. Em Python, o esquema dict trata cada chave como obrigatória, então deixe o parâmetro fora do esquema, mencione-o na string de descrição e leia-o com args.get() no manipulador. A ferramenta get_precipitation_chance abaixo mostra ambos os padrões.

Chamar uma ferramenta personalizada

Passe o servidor MCP que você criou para query via a opção mcpServers. A chave em mcpServers torna-se o segmento {server_name} no nome totalmente qualificado de cada ferramenta: mcp__{server_name}__{tool_name}. Liste esse nome em allowedTools para que a ferramenta execute sem um prompt de permissão. Estes trechos reutilizam o weatherServer do exemplo acima para perguntar a Claude qual é o clima em um local específico.
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())

Adicionar mais ferramentas

Um servidor contém quantas ferramentas você listar em seu array tools. Com mais de uma ferramenta em um servidor, você pode listar cada uma em allowedTools individualmente ou usar o curinga mcp__weather__* para cobrir cada ferramenta que o servidor expõe. O exemplo abaixo adiciona uma segunda ferramenta, get_precipitation_chance, ao weatherServer do exemplo de ferramenta de clima e o reconstrói com ambas as ferramentas no array.
# 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],
)
Cada ferramenta neste array consome espaço de janela de contexto a cada turno. Se você está definindo dezenas de ferramentas, veja tool search para carregá-las sob demanda em vez disso.

Adicionar anotações de ferramentas

Anotações de ferramentas são metadados opcionais descrevendo como uma ferramenta se comporta. Passe-as como o quinto argumento para o auxiliar tool() em TypeScript ou via o argumento de palavra-chave annotations para o decorador @tool em Python. Todos os campos de dica são Booleanos.
CampoPadrãoSignificado
readOnlyHintfalseA ferramenta não modifica seu ambiente. Controla se a ferramenta pode ser chamada em paralelo com outras ferramentas somente leitura.
destructiveHinttrueA ferramenta pode executar atualizações destrutivas. Apenas informativo.
idempotentHintfalseChamadas repetidas com os mesmos argumentos não têm efeito adicional. Apenas informativo.
openWorldHinttrueA ferramenta alcança sistemas fora de seu processo. Apenas informativo.
Anotações são metadados, não imposição. Uma ferramenta marcada com readOnlyHint: true ainda pode escrever em disco se é isso que o manipulador faz. Mantenha a anotação precisa em relação ao manipulador. Este exemplo adiciona readOnlyHint à ferramenta get_temperature do exemplo de ferramenta de clima.
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": "..."}]}
Veja ToolAnnotations na referência TypeScript ou Python.

Controlar acesso a ferramentas

O exemplo de ferramenta de clima registrou um servidor e listou ferramentas em allowedTools. Esta seção cobre como nomes de ferramentas são construídos e como escopar acesso quando você tem múltiplas ferramentas ou quer restringir integrados.

Formato de nome de ferramenta

Quando ferramentas MCP são expostas a Claude, seus nomes seguem um formato específico:
  • Padrão: mcp__{server_name}__{tool_name}
  • Exemplo: Uma ferramenta nomeada get_temperature no servidor weather torna-se mcp__weather__get_temperature

Configurar ferramentas permitidas

A opção tools e as listas de permitidas/não permitidas afetam duas camadas: disponibilidade, que controla se uma ferramenta aparece no contexto de Claude, e permissão, que controla se uma chamada é aprovada uma vez que Claude tenta. tools e entradas de disallowedTools com nome simples alteram a disponibilidade. allowedTools e regras de disallowedTools com escopo alteram apenas a permissão.
OpçãoCamadaEfeito
tools: ["Read", "Grep"]DisponibilidadeApenas os integrados listados estão no contexto de Claude. Integrados não listados são removidos. Ferramentas MCP não são afetadas.
tools: []DisponibilidadeTodos os integrados são removidos. Claude pode usar apenas suas ferramentas MCP.
ferramentas permitidasPermissãoFerramentas listadas executam sem um prompt de permissão. Ferramentas não listadas permanecem disponíveis; chamadas passam pelo fluxo de permissão.
ferramentas não permitidasAmbasUm nome de ferramenta simples como "Bash" remove a ferramenta do contexto de Claude, o mesmo que omiti-la de tools. Uma regra com escopo como "Bash(rm *)" deixa a ferramenta no contexto e nega apenas chamadas correspondentes.
Para remover um integrado completamente, omita-o de tools ou liste seu nome simples em disallowedTools (Python: disallowed_tools); ambos mantêm a ferramenta fora do contexto para que Claude nunca tente. Uma regra disallowedTools com escopo bloqueia chamadas correspondentes mas deixa a ferramenta visível, então Claude pode desperdiçar um turno tentando. Veja Configurar permissões para a ordem de avaliação completa.

Tratar erros

Como seu manipulador relata erros determina se o loop do agente continua ou para:
O que aconteceResultado
Manipulador lança uma exceção não capturadaLoop do agente para. Claude nunca vê o erro, e a chamada query falha.
Manipulador captura o erro e retorna isError: true (TS) / "is_error": True (Python)Loop do agente continua. Claude vê o erro como dados e pode tentar novamente, tentar uma ferramenta diferente ou explicar a falha.
O exemplo abaixo captura dois tipos de falhas dentro do manipulador em vez de deixá-las lançar. Um status HTTP não-200 é capturado da resposta e retornado como um resultado de erro. Um erro de rede ou JSON inválido é capturado pelo try/except (Python) ou try/catch (TypeScript) circundante e também retornado como um resultado de erro. Em ambos os casos o manipulador retorna normalmente e o loop do agente continua.
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,
        }

Retornar imagens e recursos

O array content em um resultado de ferramenta aceita blocos text, image e resource. Você pode misturá-los na mesma resposta.

Imagens

Um bloco de imagem carrega os bytes da imagem inline, codificados como base64. Não há campo de URL. Para retornar uma imagem que vive em uma URL, busque-a no manipulador, leia os bytes da resposta e codifique-os em base64 antes de retornar. O resultado é processado como entrada visual.
CampoTipoNotas
type"image"
datastringBytes codificados em base64. Apenas base64 bruto, sem prefixo data:image/...;base64,
mimeTypestringObrigatório. Por exemplo 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
            }
        ]
    }

Recursos

Um bloco de recurso incorpora um pedaço de conteúdo identificado por uma URI. A URI é um rótulo para Claude referenciar; o conteúdo real fica no campo text ou blob do bloco. Use isto quando sua ferramenta produz algo que faz sentido endereçar por nome depois, como um arquivo gerado ou um registro de um sistema externo.
CampoTipoNotas
type"resource"
resource.uristringIdentificador para o conteúdo. Qualquer esquema de URI
resource.textstringO conteúdo, se for texto. Forneça isto ou blob, não ambos
resource.blobstringO conteúdo codificado em base64, se for binário
resource.mimeTypestringOpcional
Este exemplo mostra um bloco de recurso retornado de dentro de um manipulador de ferramenta. A URI file:///tmp/report.md é um rótulo que Claude pode referenciar depois; o SDK não lê desse caminho.
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
      }
    }
  ]
};
Estas formas de bloco vêm do tipo MCP CallToolResult. Veja a especificação MCP para a definição completa.

Retornar dados estruturados

structuredContent é um objeto JSON opcional no resultado, separado do array content. Use-o para retornar valores brutos que Claude pode ler como campos exatos em vez de analisá-los de uma string de texto ou imagem. Quando structuredContent é definido, Claude recebe o JSON mais quaisquer blocos de imagem ou recurso de content. Blocos de texto em content não são encaminhados, já que são assumidos duplicar os dados estruturados. O exemplo abaixo renderiza um gráfico como um bloco de imagem e retorna os pontos de dados por trás dele em structuredContent do mesmo manipulador.
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]
  }
};
O decorador Python @tool encaminha apenas content e is_error do dict de retorno do manipulador. Para retornar structuredContent de Python, execute um servidor MCP autônomo em vez de um servidor SDK em processo.

Exemplo: conversor de unidades

Esta ferramenta converte valores entre unidades de comprimento, temperatura e peso. Um usuário pode perguntar “converter 100 quilômetros para milhas” ou “qual é 72°F em Celsius,” e Claude escolhe o tipo de unidade certo e unidades da solicitação. Demonstra dois padrões:
  • Esquemas de enum: unit_type é restrito a um conjunto fixo de valores. Em TypeScript, use z.enum(). Em Python, o esquema dict não suporta enums, então o dict completo de JSON Schema é necessário.
  • Tratamento de entrada não suportada: quando um par de conversão não é encontrado, o manipulador retorna isError: true para que Claude possa dizer ao usuário o que deu errado em vez de tratar uma falha como um resultado 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],
)
Uma vez que o servidor é definido, passe-o para query da mesma forma que o exemplo de clima. Este exemplo envia três prompts diferentes em um loop para mostrar a mesma ferramenta tratando diferentes tipos de unidades. Para cada resposta, ele inspeciona objetos AssistantMessage (que contêm as chamadas de ferramenta que Claude fez durante esse turno) e imprime cada ToolUseBlock antes de imprimir o texto final de ResultMessage. Isto permite que você veja quando Claude está usando a ferramenta versus respondendo de seu próprio conhecimento.
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())

Próximos passos

Ferramentas personalizadas envolvem funções assíncronas em uma interface padrão. Você pode misturar os padrões nesta página no mesmo servidor: um único servidor pode conter uma ferramenta de banco de dados, uma ferramenta de gateway de API e um renderizador de imagem lado a lado. A partir daqui:
  • Se seu servidor crescer para dezenas de ferramentas, veja tool search para adiar o carregamento delas até Claude precisar delas.
  • Para conectar a servidores MCP externos (sistema de arquivos, GitHub, Slack) em vez de construir os seus próprios, veja Conectar servidores MCP.
  • Para controlar quais ferramentas executam automaticamente versus exigindo aprovação, veja Configurar permissões.

Documentação relacionada