Ferramentas
Tradução automática
Esta página foi traduzida automaticamente a partir da documentação em inglês, e a página em inglês é a versão de referência. Se algo parecer errado, Traduções explica como avisar.
Uma ferramenta (tool) é uma função que o modelo pode chamar.
Você declara uma colocando @mcp.tool() em uma função Python comum. A API inteira é essa.
Sua primeira ferramenta
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.tool()
def search_books(query: str, limit: int) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r} (showing up to {limit})."
Veja o que você escreveu. Não há schemas, nem JSON, nem protocolo, só uma função. O SDK lê três coisas dela:
- O nome da ferramenta é o nome da função:
search_books. - A descrição que o modelo vê é a docstring:
Search the catalog by title or author. - Os argumentos que o modelo pode passar vêm das anotações de tipo:
query: strelimit: int.
O schema de entrada
A partir dessas anotações de tipo, o SDK gera um JSON Schema e o envia ao cliente durante tools/list:
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"title": "Limit", "type": "integer"}
},
"required": ["query", "limit"],
"title": "search_booksArguments"
}
Os dois argumentos estão em required porque nenhum deles tem valor padrão. Você vai resolver isso daqui a pouco. (As chaves title são artefatos do Pydantic; as propriedades, seus tipos e required são o contrato.)
Tip
Aqui, as anotações de tipo não são documentação. Elas são o contrato. Se um cliente enviar "limit": "ten",
o SDK rejeita isso antes mesmo de a sua função executar.
O que o modelo recebe de volta
Chame a ferramenta com {"query": "dune", "limit": 5} e o resultado tem duas partes:
result.content # [TextContent(text="Found 3 books matching 'dune' (showing up to 5).")]
result.structured_content # {'result': "Found 3 books matching 'dune' (showing up to 5)."}
content é o texto que o modelo lê. structured_content são dados tipados para a aplicação cliente. Ele está ali porque você declarou o tipo de retorno como -> str.
Não se preocupe com structured_content por enquanto. Retorne objetos Python de verdade das suas ferramentas e a coisa certa acontece; a página Saída estruturada trata exatamente disso.
Experimente
Execute o servidor com o MCP Inspector:
uv run mcp dev server.py
Abra a URL que ele imprime, vá até a aba Tools e chame search_books.
O Inspector renderiza um formulário com um campo de texto obrigatório query e um campo numérico obrigatório limit. Ele montou esse formulário a partir das suas anotações de tipo. Todos os outros clientes MCP vão fazer o mesmo.
Argumentos opcionais
Dê um valor padrão a um parâmetro e ele deixa de ser obrigatório. É só isso. É apenas Python.
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.tool()
def search_books(query: str, limit: int = 10) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r} (showing up to {limit})."
O schema acompanha:
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"}
},
"required": ["query"],
"title": "search_booksArguments"
}
limit saiu de required e ganhou "default": 10. Um cliente que o omite recebe 10, exatamente como aconteceria em Python.
Schemas mais ricos com Field
As anotações de tipo levam você longe, mas às vezes você quer descrever um argumento, ou restringi-lo.
Envolva o tipo em Annotated e adicione um Field do Pydantic:
from typing import Annotated, Literal
from pydantic import Field
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.tool()
def search_books(
query: Annotated[str, Field(description="Title or author to search for.")],
limit: Annotated[int, Field(ge=1, le=50, description="Maximum number of results.")] = 10,
genre: Literal["fiction", "non-fiction", "poetry"] | None = None,
) -> str:
"""Search the catalog by title or author."""
where = f" in {genre}" if genre else ""
return f"Found 3 books matching {query!r}{where} (showing up to {limit})."
Três novidades, todas nos parâmetros:
Field(description=...): uma descrição por argumento que o modelo lê junto com a docstring.Field(ge=1, le=50): limites numéricos. Eles entram no schema como"minimum": 1, "maximum": 50.Literal["fiction", "non-fiction", "poetry"]: um enum. O modelo só pode escolher um desses valores.
Check
Restrições não são enfeite. Chame a ferramenta com limit=999 e o SDK responde com um
erro de ferramenta antes de a sua função executar:
Input should be less than or equal to 50
Esse erro volta para o modelo como o resultado da ferramenta, e o modelo o lê e tenta de novo com
um valor válido. Você escreveu le=50 uma vez e ganhou de graça agentes que se corrigem sozinhos.
Info
Se você já usou FastAPI ou Pydantic, já sabe tudo isso. É o mesmo Field,
o mesmo Annotated, a mesma validação. Não há nada específico de MCP para aprender aqui.
Um modelo como parâmetro
Quando uma ferramenta recebe mais do que alguns poucos argumentos, agrupe-os em um modelo Pydantic:
from pydantic import BaseModel, Field
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
class Book(BaseModel):
title: str
author: str
year: int = Field(ge=1450, description="Year of first publication.")
@mcp.tool()
def add_book(book: Book) -> str:
"""Add a book to the catalog."""
return f"Added {book.title!r} by {book.author} ({book.year})."
O schema de Book fica aninhado dentro do schema de entrada da ferramenta (como uma referência em $defs), o modelo o preenche como um objeto JSON, e sua função recebe uma instância real de Book, já validada, com os atributos .title, .author e .year.
Você pode misturar à vontade: parâmetros simples ao lado de parâmetros de modelo, modelos aninhados, listas de modelos. É Pydantic de ponta a ponta.
async def
Se uma ferramenta faz I/O (chama uma API, lê um arquivo, consulta um banco de dados), declare-a como async def e use await dentro dela. O SDK se encarrega de aguardá-la.
Uma ferramenta com def comum também funciona: o SDK a executa em uma thread, então ela nunca bloqueia o servidor.
Não há mais nada para configurar.
Nomes, títulos e anotações
Tudo o que o SDK infere, você pode sobrescrever no decorador:
from mcp.server import MCPServer
from mcp.types import ToolAnnotations
mcp = MCPServer("Bookshop")
@mcp.tool(
title="Search the catalog",
annotations=ToolAnnotations(read_only_hint=True, open_world_hint=False),
)
def search_books(query: str) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r}."
titleé um nome legível por humanos, pensado para interfaces. Os clientes mostram "Search the catalog" em vez desearch_books.annotationssão dicas de comportamento para o cliente:read_only_hint=True: esta ferramenta não altera nada.open_world_hint=False: ela opera sobre um conjunto fechado de coisas (este catálogo), não sobre a web aberta.- As outras duas,
destructive_hinteidempotent_hint, descrevem uma ferramenta que escreve: ela pode apagar alguma coisa? E chamá-la duas vezes dá no mesmo que chamá-la uma vez? A especificação define as duas apenas para ferramentas que não são somente leitura, então elas não diriam nada emsearch_books.
Um cliente bem-comportado as usa para decidir coisas como "preciso perguntar ao usuário antes de executar isto?". São dicas, não segurança. Nunca conte com um cliente respeitando-as.
Tip
@mcp.tool() também aceita name= e description= se você não quiser derivá-los
do nome da função e da docstring. Na maioria das vezes você quer.
Recapitulando
@mcp.tool()em uma função a transforma em ferramenta. O nome vem da função, a descrição vem da docstring.- As anotações de tipo são o schema de entrada. Valores padrão tornam os argumentos opcionais.
Annotated[..., Field(...)]adiciona descrições e restrições;Literaladiciona enums.- Um modelo Pydantic como parâmetro é a forma de receber um "corpo" estruturado.
- Argumentos inválidos são rejeitados para você, com um erro que o modelo consegue ler e do qual consegue se recuperar.
async defpara I/O,defcomum para todo o resto.
Saída estruturada é o que acontece com o valor que você devolve no return.