El cliente
Traducción automática
Esta página se tradujo automáticamente a partir de la documentación en inglés, y la página en inglés es la versión de referencia. Si algo no se lee bien, Traducciones explica cómo avisarnos.
Un Client es la forma en que un programa de Python se comunica con un servidor MCP.
Es un solo objeto con un solo ciclo de vida: lo construyes, entras en async with, llamas a sus métodos. Cada verbo del protocolo (listar las herramientas, llamar a una, leer un recurso, renderizar un prompt) es un método async del objeto que devuelve un resultado tipado.
Tu primer cliente
Un cliente necesita un servidor con el que hablar. Este Bookshop es al que se conectan todos los fragmentos de esta página. Guárdalo como server.py y déjalo ejecutándose por HTTP:
from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference
mcp = MCPServer("Bookshop", instructions="Search the catalog before recommending a book.")
GENRES = ["fiction", "non-fiction", "poetry"]
class Book(BaseModel):
title: str
author: str
year: int
@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})."
@mcp.tool()
def lookup_book(title: str) -> Book:
"""Look up a book by its exact title."""
if title != "Dune":
raise ToolError(f"No book titled {title!r} in the catalog.")
return Book(title="Dune", author="Frank Herbert", year=1965)
@mcp.resource("catalog://genres")
def genres() -> list[str]:
"""The genres the catalog is organised by."""
return GENRES
@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}."
@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."
@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)])
uv run mcp run server.py --transport streamable-http
Con eso queda disponible en http://localhost:8000/mcp. El cliente es un programa aparte. Guárdalo como client.py y ejecuta python client.py en una segunda terminal:
import anyio
from mcp import Client
async def main() -> None:
async with Client("http://localhost:8000/mcp") as client:
print(client.server_info)
print(client.server_capabilities)
print(client.protocol_version)
print(client.instructions)
if __name__ == "__main__":
anyio.run(main)
Client("http://localhost:8000/mcp")recibe una URL, así que se conecta por Streamable HTTP al servidor que acabas de iniciar.async withes el ciclo de vida. Al entrar se conecta y negocia; al salir se desconecta. No hay un parconnect()/close(), y unClientno se puede reutilizar una vez que termina el bloque.- Dentro del bloque, los datos de la conexión ya están ahí como propiedades simples.
Qué puedes pasarle a Client
Client recibe un solo argumento posicional y resuelve el transporte a partir de su tipo:
- Una cadena con una URL (
Client("http://localhost:8000/mcp")): Streamable HTTP, el transporte con el que despliegas. - Un
StdioServerParameters: el comando que se lanza como subproceso local, con el que se habla a través de su stdin y su stdout. - Un transporte: cualquier cosa que puedas usar con
async with ... as (read, write), comostreamable_http_client(url, http_client=...)envolviendo tu propio cliente HTTP. - Una instancia de
MCPServer(o delServerde bajo nivel): se conecta en el mismo proceso, sin subproceso y sin puerto. Ese caso es para las pruebas, y Pruebas se construye sobre él.
Todo lo demás en esta página es idéntico en los cuatro casos. Los encabezados, los subprocesos, los timeouts y el protocolo Transport tienen su propia página: Transportes del cliente.
Qué hay en un cliente conectado
Cuatro propiedades de solo lectura, que se rellenan en cuanto entras en el bloque:
client.server_info: la identidad del servidor, oNonepara un servidor de la generación 2026 que no la informa (los servidores de python-sdk lo hacen por defecto). Aquíserver_info.namees"Bookshop"yserver_info.versiones lo que el servidor informe.client.server_capabilities: lo que el servidor puede hacer (tools,resources,prompts,completions, ...). Una capacidad que el servidor no tiene esNone.client.protocol_version: la versión del protocolo que acordaron las dos partes. Aquí es"2026-07-28".client.instructions: la cadenainstructions=del servidor, oNonesi no definió una.
Nunca elegiste una versión del protocolo. Por defecto, el Client sondea el servidor y recurre al handshake clásico con los más antiguos, así que un mismo cliente funciona contra servidores de cualquier generación. Cuando necesites controlar eso, Versiones del protocolo tiene todos los detalles.
Tip
client.session es la ClientSession subyacente, la vía de escape de bajo nivel.
No la necesitarás para nada de esta página.
Listar herramientas
import anyio
from mcp import Client
async def main() -> None:
async with Client("http://localhost:8000/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)
if __name__ == "__main__":
anyio.run(main)
list_tools() devuelve un ListToolsResult; las herramientas están en .tools. Cada una es la definición completa que un host le entregaría a un modelo. Esta es la primera:
tool.name # 'search_books'
tool.title # 'Search the catalog'
tool.description # 'Search the catalog by title or author.'
y tool.input_schema es el JSON Schema que el servidor derivó de las anotaciones de tipo de la función:
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"}
},
"required": ["query"],
"title": "search_booksArguments"
}
Ese esquema es todo lo que una UI necesita para renderizar un formulario de argumentos, y todo lo que un modelo necesita para producir argumentos válidos.
La segunda herramienta, lookup_book, se registró sin title=, así que su tool.title es None.
Tip
title es opcional, así que una UI que muestra herramientas a una persona tiene que elegir: el title si lo hay,
el name si no. from mcp.shared.metadata_utils import get_display_name hace exactamente eso,
para herramientas, recursos, plantillas de recursos y prompts.
Llamar a una herramienta
call_tool(name, arguments) ejecuta la herramienta y te devuelve un CallToolResult.
import anyio
from mcp import Client
from mcp.types import TextContent
async def main() -> None:
async with Client("http://localhost:8000/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)
if __name__ == "__main__":
anyio.run(main)
El lookup_book del servidor devuelve un Book de Pydantic. Esto es lo que ve el cliente:
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
Un solo valor de retorno, tres cosas que leer. Cada una tiene un consumidor distinto.
content: lo que lee el modelo
content es una list de bloques de contenido, y un bloque de contenido es una unión: TextContent, ImageContent, AudioContent, ResourceLink o EmbeddedResource. Una herramienta puede devolver varios, de distintos tipos.
Por eso main acota el tipo con isinstance(block, TextContent) antes de tocar block.text. Fíjate en que no hay ningún .text fuera del isinstance: el verificador de tipos no lo permite, porque ImageContent tiene .data, no .text. La unión es honesta sobre lo que una herramienta puede enviarte; tu código también debería serlo.
structured_content: lo que lee tu aplicación
structured_content es el valor de retorno de la herramienta en JSON, conforme al output_schema que declara la herramienta. Sin analizar cadenas, sin adivinar.
Cuando ambos están presentes dicen lo mismo dos veces a propósito: content es para un modelo, structured_content es para el código. De dónde sale la mitad estructurada, y cómo controlarla, está en la página Salida estructurada.
is_error: si la herramienta falló
Una herramienta que lanza una excepción no la lanza en tu cliente. Vuelve como un resultado normal con is_error=True.
Check
Pídele "Solaris" a lookup_book (un título que no está en el catálogo) y la función lanza
ToolError. Aun así, la llamada devuelve un resultado normal:
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
El mensaje del ToolError acabó en content, donde el modelo puede leerlo y volver a intentarlo. Es
deliberado: un error de herramienta es parte de la conversación, no un fallo fatal. (Si la herramienta hubiera
fallado con alguna otra excepción, content diría solo Error executing tool lookup_book.) Mira siempre
is_error antes de confiar en structured_content.
Warning
is_error=True cubre más que tu propio raise. Pide una herramienta que el servidor ni siquiera tiene
(call_tool("does_not_exist", {})) y no se lanza nada. Recibes la misma forma de vuelta,
is_error=True con Unknown tool: does_not_exist en content. Un método de Client lanza
MCPError solo cuando el servidor responde con un error JSON-RPC en lugar de un resultado, y
Manejo de errores explica cuándo un servidor produce cada cosa.
Recursos
Los verbos de recursos vienen en pares: dos formas de listar, una de leer.
import anyio
from mcp import Client
from mcp.types import TextResourceContents
async def main() -> None:
async with Client("http://localhost:8000/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)
if __name__ == "__main__":
anyio.run(main)
list_resources()devuelve los recursos concretos, los que tienen una URI fija. Aquí:['catalog://genres'].list_resource_templates()devuelve los parametrizados. Aquí:['catalog://genres/{genre}']. Son dos listas distintas porque una plantilla no se puede leer hasta que la rellenas.read_resource(uri)recibe una URI comostrsimple y funciona con ambos: pasa"catalog://genres/poetry"y el servidor la hace coincidir con la plantilla.
read_resource devuelve contents, una lista de TextResourceContents o BlobResourceContents. La misma idea que con el contenido de las herramientas: acota con isinstance y luego lee .text (o .blob).
A un cliente también se le puede avisar cuando cambia un recurso. En conexiones de la generación 2025 eso es subscribe_resource(uri) / unsubscribe_resource(uri), un par de métodos que MCPServer no implementa, así que con el protocolo 2026-07-28 (donde esos verbos ya no existen) la solicitud responde -32601, Method not found. El reemplazo de 2026 es un stream subscriptions/listen, que MCPServer sí sirve (allí server_capabilities.resources.subscribe es True), y cómo consumirlo con client.listen(...) es la página Suscripciones de esta sección.
Prompts
import anyio
from mcp import Client
async def main() -> None:
async with Client("http://localhost:8000/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)
if __name__ == "__main__":
anyio.run(main)
list_prompts() te dice qué ofrece el servidor y qué necesita cada prompt:
prompt.name # 'recommend'
prompt.title # 'Recommend a book'
prompt.arguments # [PromptArgument(name='genre', required=True)]
get_prompt(name, arguments) lo renderiza. El diccionario de argumentos es str -> str: los argumentos de un prompt siempre son cadenas. El resultado es messages, una lista de PromptMessage, cada uno con un role y un bloque content:
message.role # 'user'
message.content # TextContent(type='text', text='Recommend one poetry book from the catalog and say why.')
Un host le entrega esos mensajes directamente al modelo. Esa es toda la funcionalidad.
Autocompletado
Un servidor con un handler de autocompletado puede autocompletar argumentos de prompts y de plantillas de recursos mientras el usuario escribe.
import anyio
from mcp import Client
from mcp.types import PromptReference
async def main() -> None:
async with Client("http://localhost:8000/mcp") as client:
result = await client.complete(
ref=PromptReference(type="ref/prompt", name="recommend"),
argument={"name": "genre", "value": "p"},
)
print(result.completion.values)
if __name__ == "__main__":
anyio.run(main)
refdice qué prompt o plantilla estás rellenando: unPromptReferenceo unResourceTemplateReference.argumentes{"name": ..., "value": ...}: el argumento y lo que el usuario ha escrito hasta ahora.
La respuesta está en result.completion.values. Escribe "p" y el servidor devuelve ['poetry']. El lado del servidor, y cómo un handler usa los otros argumentos ya rellenados para acotar sus sugerencias, es la página Autocompletado.
Paginación
Cada método list_* acepta un argumento nombrado cursor= y cada resultado trae un next_cursor. Cuando next_cursor es None, ya lo tienes todo.
import anyio
from mcp import Client
from mcp.types import Tool
async def list_all_tools(client: Client) -> list[Tool]:
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:
return tools
cursor = page.next_cursor
async def main() -> None:
async with Client("http://localhost:8000/mcp") as client:
tools = await list_all_tools(client)
print([tool.name for tool in tools])
if __name__ == "__main__":
anyio.run(main)
list_all_tools es correcta contra cualquier servidor. MCPServer devuelve todo en una sola página, así que next_cursor es None y el bucle se ejecuta una vez; por eso la mayoría del código nunca lo escribe. Los servidores que realmente paginan, y las reglas que siguen los cursores, están en Paginación.
En las pruebas
Cada client.py de esta página llegó a server.py por HTTP. En una prueba te saltas la red y le pasas a Client el propio objeto servidor: from server import mcp y luego Client(mcp). Sin proceso, sin puerto, y todos los métodos anteriores funcionan igual.
Hay una opción del constructor pensada para eso: Client(mcp, raise_exceptions=True). Solo tiene efecto en conexiones en el mismo proceso, y Pruebas es la página que la explica y construye todo el patrón a su alrededor.
Resumen
Client(x)se conecta por Streamable HTTP a una cadena con una URL, lanza un subproceso para unStdioServerParameters, entra directamente en un transporte y, en las pruebas, recibe el propio objeto servidor.async withes todo el ciclo de vida. Dentro,server_capabilitiesyprotocol_versionya están rellenas;server_infoeinstructionstambién, cuando el servidor las proporciona.list_tools()te da elname,title,descriptioneinput_schemade cada herramienta.call_tool()devuelvecontentpara el modelo,structured_contentpara tu código, eis_error. Una herramienta que lanza una excepción es un resultado, no una excepción.contentes una unión de tipos de bloque; acota conisinstanceantes de leer.list_resources/list_resource_templates/read_resource,list_prompts/get_promptycompletecompletan los verbos.- Cada
list_*aceptacursor=; itera hasta quenext_cursorseaNone.
Lo que un servidor puede pedirle al cliente, y cómo le respondes, está en Callbacks del cliente.