Pular para conteúdo

O cliente

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.

Um Client é como um programa Python conversa com um servidor MCP.

É um objeto com um ciclo de vida: construa, entre no async with, chame os métodos. Cada verbo do protocolo (listar as ferramentas, chamar uma, ler um recurso, renderizar um prompt) é um método async nele que retorna um resultado tipado.

Seu primeiro cliente

client.py
from mcp import Client
from mcp.server import MCPServer

mcp = MCPServer("Bookshop", instructions="Search the catalog before recommending a book.")


@mcp.tool()
def search_books(query: str) -> str:
    """Search the catalog by title or author."""
    return f"Found 3 books matching {query!r}."


async def main() -> None:
    async with Client(mcp) as client:
        print(client.server_info)
        print(client.server_capabilities)
        print(client.protocol_version)
        print(client.instructions)

O servidor no topo só está ali para você ter algo a que se conectar. O cliente são as cinco linhas destacadas.

  • Client(mcp) recebe o próprio objeto servidor. Esse é o transporte em memória: sem subprocesso, sem porta, sem HTTP. É assim que todo exemplo nesta página, e todo teste que você escrever, se conecta.
  • async with é o ciclo de vida. Entrar nele conecta e negocia; sair dele desconecta. Não há um par connect() / close(), e um Client não pode ser reutilizado depois que o bloco termina.
  • Dentro do bloco, os fatos da conexão já estão ali como propriedades comuns.

O que você pode passar para Client

Client recebe um argumento posicional e resolve o transporte a partir do tipo dele:

  • Uma instância de MCPServer (ou do Server de baixo nível): conectada no mesmo processo.
  • Uma string de URL (Client("http://localhost:8000/mcp")): Streamable HTTP, o caminho de produção.
  • Um transporte: qualquer coisa com que você possa fazer async with ... as (read, write), como stdio_client(...) encapsulando um subprocesso.

Todo o resto desta página é idêntico entre os três. Cabeçalhos, subprocessos, timeouts e o protocolo Transport têm sua própria página: Transportes do cliente.

O que há em um cliente conectado

Quatro propriedades somente leitura, preenchidas no instante em que você entra no bloco:

  • client.server_info: a identidade do servidor, ou None para um servidor da era 2026 que não informa uma (servidores do python-sdk informam por padrão). server_info.name aqui é "Bookshop", server_info.version é o que o servidor informar.
  • client.server_capabilities: o que o servidor sabe fazer (tools, resources, prompts, completions, ...). Uma capacidade que o servidor não tem é None.
  • client.protocol_version: a versão do protocolo em que os dois lados concordaram. Aqui é "2026-07-28".
  • client.instructions: a string instructions= do servidor, ou None se ele não definiu uma.

Você nunca escolheu uma versão do protocolo. Por padrão, o Client sonda o servidor e recorre ao handshake clássico nos mais antigos, então um único cliente funciona contra servidores de qualquer era. Quando você precisar controlar isso, Versões do protocolo tem a história completa.

Tip

client.session é a ClientSession subjacente, a saída de emergência de baixo nível. Você não vai precisar dela para nada nesta página.

Listando ferramentas

client.py
from mcp import Client
from mcp.server import MCPServer

mcp = MCPServer("Bookshop")


@mcp.tool(title="Search the catalog")
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})."


async def main() -> None:
    async with Client(mcp) as client:
        result = await client.list_tools()
        for tool in result.tools:
            print(tool.name)
            print(tool.title)
            print(tool.description)
            print(tool.input_schema)

list_tools() retorna um ListToolsResult; as ferramentas estão em .tools. Cada uma é a definição completa que um host entregaria a um modelo:

tool.name          # 'search_books'
tool.title         # 'Search the catalog'
tool.description   # 'Search the catalog by title or author.'

e tool.input_schema é o JSON Schema que o servidor derivou das anotações de tipo da função:

{
  "type": "object",
  "properties": {
    "query": {"title": "Query", "type": "string"},
    "limit": {"default": 10, "title": "Limit", "type": "integer"}
  },
  "required": ["query"],
  "title": "search_booksArguments"
}

Esse schema é tudo o que uma UI precisa para renderizar um formulário de argumentos, e tudo o que um modelo precisa para produzir argumentos válidos.

Tip

title é opcional, então uma UI que mostra ferramentas a um humano tem que escolher: o title se houver um, o name se não. from mcp.shared.metadata_utils import get_display_name faz exatamente isso, para ferramentas, recursos, templates de recurso e prompts.

Chamando uma ferramenta

call_tool(name, arguments) executa a ferramenta e devolve um CallToolResult.

client.py
from pydantic import BaseModel

from mcp import Client
from mcp.server import MCPServer
from mcp.types import TextContent

mcp = MCPServer("Bookshop")


class Book(BaseModel):
    title: str
    author: str
    year: int


@mcp.tool()
def lookup_book(title: str) -> Book:
    """Look up a book by its exact title."""
    if title != "Dune":
        raise ValueError(f"No book titled {title!r} in the catalog.")
    return Book(title="Dune", author="Frank Herbert", year=1965)


async def main() -> None:
    async with Client(mcp) as client:
        result = await client.call_tool("lookup_book", {"title": "Dune"})

        for block in result.content:
            if isinstance(block, TextContent):
                print(block.text)

        print(result.structured_content)
        print(result.is_error)

O lookup_book do servidor retorna um Book do Pydantic. Eis o que o cliente vê:

result.content             # [TextContent(type='text', text='{\n  "title": "Dune",\n  "author": "Frank Herbert",\n  "year": 1965\n}')]
result.structured_content  # {'title': 'Dune', 'author': 'Frank Herbert', 'year': 1965}
result.is_error            # False

Um valor de retorno, três coisas para ler. Cada uma tem um consumidor diferente.

content: o que o modelo lê

content é uma list de blocos de conteúdo, e um bloco de conteúdo é uma união: TextContent, ImageContent, AudioContent, ResourceLink ou EmbeddedResource. Uma ferramenta pode retornar vários, de tipos diferentes.

É por isso que main faz o narrowing com isinstance(block, TextContent) antes de tocar em block.text. Repare que não há .text fora do isinstance: o verificador de tipos não permite, porque ImageContent tem .data, não .text. A união é honesta sobre o que uma ferramenta pode enviar a você; seu código também deve ser.

structured_content: o que sua aplicação lê

structured_content é o valor de retorno da ferramenta como JSON, correspondendo ao output_schema declarado pela ferramenta. Sem parsing de strings, sem adivinhação.

Quando ambos estão presentes, eles dizem a mesma coisa duas vezes de propósito: content é para um modelo, structured_content é para código. De onde vem a metade estruturada, e como controlá-la, é a página Saída estruturada.

is_error: se a ferramenta falhou

Uma ferramenta que lança uma exceção não lança no seu cliente. Ela volta como um resultado comum com is_error=True.

Check

Peça "Solaris" ao lookup_book (um título que não está no catálogo) e a função lança ValueError. A chamada ainda retorna normalmente:

result.is_error            # True
result.content             # [TextContent(type='text', text="Error executing tool lookup_book: No book titled 'Solaris' in the catalog.")]
result.structured_content  # None

A mensagem da exceção foi parar em content, onde o modelo pode lê-la e tentar de novo. Isso é proposital: um erro de ferramenta faz parte da conversa, não é um crash. Sempre olhe is_error antes de confiar em structured_content.

Warning

is_error=True cobre mais do que o seu próprio raise. Peça uma ferramenta que o servidor nem tem (call_tool("does_not_exist", {})) e nada lança exceção. Você recebe o mesmo formato de volta, is_error=True com Unknown tool: does_not_exist em content. Um método de Client lança MCPError apenas quando o servidor responde com um erro JSON-RPC em vez de um resultado, e Tratando erros cobre quando um servidor produz cada um.

Recursos

Os verbos de recurso vêm em pares: duas formas de listar, uma forma de ler.

client.py
from mcp import Client
from mcp.server import MCPServer
from mcp.types import TextResourceContents

mcp = MCPServer("Bookshop")


@mcp.resource("catalog://genres")
def genres() -> list[str]:
    """The genres the catalog is organised by."""
    return ["fiction", "non-fiction", "poetry"]


@mcp.resource("catalog://genres/{genre}")
def books_in_genre(genre: str) -> str:
    """Every title we stock in one genre."""
    return f"3 books filed under {genre}."


async def main() -> None:
    async with Client(mcp) as client:
        listed = await client.list_resources()
        print([resource.uri for resource in listed.resources])

        templates = await client.list_resource_templates()
        print([template.uri_template for template in templates.resource_templates])

        result = await client.read_resource("catalog://genres/poetry")
        for contents in result.contents:
            if isinstance(contents, TextResourceContents):
                print(contents.text)
  • list_resources() retorna os recursos concretos, os que têm uma URI fixa. Aqui: ['catalog://genres'].
  • list_resource_templates() retorna os parametrizados. Aqui: ['catalog://genres/{genre}']. São duas listas diferentes porque um template não pode ser lido até você preenchê-lo.
  • read_resource(uri) recebe uma URI str comum e funciona com ambos: passe "catalog://genres/poetry" e o servidor a casa com o template.

read_resource retorna contents, uma lista de TextResourceContents ou BlobResourceContents. Mesma ideia do conteúdo de ferramenta: faça o narrowing com isinstance, depois leia .text (ou .blob).

Um cliente também pode ser avisado quando um recurso muda. Em conexões da era 2025 isso é subscribe_resource(uri) / unsubscribe_resource(uri) - um par de métodos que o MCPServer não implementa, então no protocolo 2026-07-28 (onde esses verbos não existem mais) a requisição responde -32601, Method not found. O substituto de 2026 é um stream subscriptions/listen, que o MCPServer serve sim - server_capabilities.resources.subscribe é True ali - e consumi-lo com client.listen(...) é a página Assinaturas desta seção.

Prompts

client.py
from mcp import Client
from mcp.server import MCPServer

mcp = MCPServer("Bookshop")


@mcp.prompt(title="Recommend a book")
def recommend(genre: str) -> str:
    """Ask for a recommendation in a genre."""
    return f"Recommend one {genre} book from the catalog and say why."


async def main() -> None:
    async with Client(mcp) as client:
        listed = await client.list_prompts()
        print(listed.prompts)

        result = await client.get_prompt("recommend", {"genre": "poetry"})
        for message in result.messages:
            print(message.role, message.content)

list_prompts() diz o que o servidor oferece e do que cada prompt precisa:

prompt.name        # 'recommend'
prompt.title       # 'Recommend a book'
prompt.arguments   # [PromptArgument(name='genre', required=True)]

get_prompt(name, arguments) o renderiza. O dict de argumentos é str -> str: argumentos de prompt são sempre strings. O resultado é messages, uma lista de PromptMessage, cada uma com um role e um bloco content:

message.role     # 'user'
message.content  # TextContent(type='text', text='Recommend one poetry book from the catalog and say why.')

Um host entrega essas mensagens direto ao modelo. A funcionalidade inteira é essa.

Completions

Um servidor com um handler de completion pode autocompletar argumentos de prompts e de templates de recurso enquanto o usuário digita.

client.py
from mcp import Client
from mcp.server import MCPServer
from mcp.types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference

mcp = MCPServer("Bookshop")

GENRES = ["fiction", "non-fiction", "poetry"]


@mcp.prompt()
def recommend(genre: str) -> str:
    """Ask for a recommendation in a genre."""
    return f"Recommend one {genre} book from the catalog and say why."


@mcp.completion()
async def complete_genre(
    ref: PromptReference | ResourceTemplateReference,
    argument: CompletionArgument,
    context: CompletionContext | None,
) -> Completion | None:
    return Completion(values=[genre for genre in GENRES if genre.startswith(argument.value)])


async def main() -> None:
    async with Client(mcp) as client:
        result = await client.complete(
            ref=PromptReference(type="ref/prompt", name="recommend"),
            argument={"name": "genre", "value": "p"},
        )
        print(result.completion.values)
  • ref diz qual prompt ou template você está preenchendo: uma PromptReference ou uma ResourceTemplateReference.
  • argument é {"name": ..., "value": ...}: o argumento e o que o usuário digitou até agora.

A resposta está em result.completion.values. Digite "p" e o servidor volta com ['poetry']. O lado do servidor, e como um handler usa os outros argumentos já preenchidos para refinar as sugestões, é a página Completions.

Paginação

Todo método list_* aceita um argumento nomeado cursor= e todo resultado carrega um next_cursor. Quando next_cursor é None, você tem tudo.

client.py
from mcp import Client
from mcp.server import MCPServer
from mcp.types import Tool

mcp = MCPServer("Bookshop")


@mcp.tool()
def search_books(query: str) -> str:
    """Search the catalog by title or author."""
    return f"Found 3 books matching {query!r}."


@mcp.tool()
def reserve_book(title: str) -> str:
    """Put a book on hold."""
    return f"Reserved {title!r}."


async def main() -> None:
    async with Client(mcp) as client:
        tools: list[Tool] = []
        cursor: str | None = None
        while True:
            page = await client.list_tools(cursor=cursor)
            tools.extend(page.tools)
            if page.next_cursor is None:
                break
            cursor = page.next_cursor
        print([tool.name for tool in tools])

Esse loop está correto contra qualquer servidor. O MCPServer retorna tudo em uma página só, então next_cursor é None e o loop roda uma vez, e é por isso que a maioria do código nunca o escreve. Servidores que paginam de verdade, e as regras que os cursores obedecem, estão em Paginação.

Em testes

Client(mcp), sem processo e sem porta, já é um harness de teste para o seu servidor.

Existe uma flag do construtor feita para isso: Client(mcp, raise_exceptions=True). Ela só tem efeito em conexões em memória, e Testes é a página que a explica e constrói todo o padrão em torno dela.

Recapitulando

  • Client(x) conecta em memória a um objeto servidor, via Streamable HTTP a uma string de URL, e por qualquer outra coisa via um transporte.
  • async with é o ciclo de vida inteiro. Dentro dele, server_capabilities e protocol_version já estão preenchidos; server_info e instructions também, quando o servidor os fornece.
  • list_tools() dá a você o name, title, description e input_schema de cada ferramenta.
  • call_tool() retorna content para o modelo, structured_content para o seu código e is_error. Uma ferramenta que lança exceção é um resultado, não uma exceção.
  • content é uma união de tipos de bloco; faça o narrowing com isinstance antes de ler.
  • list_resources / list_resource_templates / read_resource, list_prompts / get_prompt e complete completam os verbos.
  • Todo list_* aceita cursor=; itere até next_cursor ser None.

As coisas que um servidor pode pedir ao cliente, e como você as responde, são os Callbacks do cliente.