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
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)
El servidor del principio solo está ahí para que tengas algo a lo que conectarte. El cliente son las cinco líneas resaltadas.
Client(mcp)recibe el propio objeto servidor. Ese es el transporte en memoria: sin subproceso, sin puerto, sin HTTP. Así se conectan todos los ejemplos de esta página y todas las pruebas que escribas.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 instancia de
MCPServer(o delServerde bajo nivel): se conecta en el mismo proceso. - Una cadena con una URL (
Client("http://localhost:8000/mcp")): Streamable HTTP, el camino de producción. - Un transporte: cualquier cosa que puedas usar con
async with ... as (read, write), comostdio_client(...)envolviendo un subproceso.
Todo lo demás en esta página es idéntico en los tres 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
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() 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:
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.
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.
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)
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
ValueError. 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 de la excepción 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. 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.
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()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
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() 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.
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)
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.
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])
Este bucle es correcto 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
Client(mcp), sin proceso y sin puerto, ya es un banco de pruebas para tu servidor.
Hay una opción del constructor pensada para eso: Client(mcp, raise_exceptions=True). Solo tiene efecto en conexiones en memoria, y Pruebas es la página que la explica y construye todo el patrón a su alrededor.
Resumen
Client(x)se conecta en memoria a un objeto servidor, por Streamable HTTP a una cadena con una URL, y por cualquier otra cosa mediante un transporte.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.