Saltar a contenido

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

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)

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 with es el ciclo de vida. Al entrar se conecta y negocia; al salir se desconecta. No hay un par connect() / close(), y un Client no 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 del Server de 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), como stdio_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, o None para un servidor de la generación 2026 que no la informa (los servidores de python-sdk lo hacen por defecto). Aquí server_info.name es "Bookshop" y server_info.version es 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 es None.
  • client.protocol_version: la versión del protocolo que acordaron las dos partes. Aquí es "2026-07-28".
  • client.instructions: la cadena instructions= del servidor, o None si 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

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() 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.

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)

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.

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() 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 como str simple 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 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

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() 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.

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 dice qué prompt o plantilla estás rellenando: un PromptReference o un ResourceTemplateReference.
  • argument es {"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.

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])

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 with es todo el ciclo de vida. Dentro, server_capabilities y protocol_version ya están rellenas; server_info e instructions también, cuando el servidor las proporciona.
  • list_tools() te da el name, title, description e input_schema de cada herramienta.
  • call_tool() devuelve content para el modelo, structured_content para tu código, e is_error. Una herramienta que lanza una excepción es un resultado, no una excepción.
  • content es una unión de tipos de bloque; acota con isinstance antes de leer.
  • list_resources / list_resource_templates / read_resource, list_prompts / get_prompt y complete completan los verbos.
  • Cada list_* acepta cursor=; itera hasta que next_cursor sea None.

Lo que un servidor puede pedirle al cliente, y cómo le respondes, está en Callbacks del cliente.