Creación de un cliente MCP personalizado con el Agent SDK
Cada servidor MCP necesita algo en el otro extremo de la conexión.
Ese algo es un cliente, y el mismo SDK de Python de MCP que proporciona primitivas para la creación de servidores, también proporciona primitivas para clientes: conectar a un servidor, listar lo que ofrece, llamar a una herramienta, leer un recurso.
Cuando ese cliente vive dentro de una aplicación construida sobre el Agent SDK de Claude, las herramientas que expone un servidor MCP se convierten en herramientas a las que Claude mismo puede recurrir durante una conversación.
Resumen
Un cliente MCP personalizado se conecta a un servidor a través de un transporte, comúnmente stdio_client para un proceso de servidor local.
La conexión produce una ClientSession, el objeto que utilizas para negociar capacidades y luego enviar solicitudes.
list_tools(), call_tool() y read_resource() son las tres primitivas que cubren el flujo de trabajo diario: descubrir qué está disponible, invocarlo y leer datos.
Un cliente personalizado es útil siempre que necesites acceso programático a un servidor MCP fuera de Claude Desktop o Claude Code, por ejemplo, integrando las herramientas de un servidor MCP en una aplicación construida sobre el Agent SDK.
Esta página recorre el lado del cliente del mismo ciclo de solicitud cubierto en la página explicativa sobre el manejo de solicitudes.
Receta
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
server_params = StdioServerParameters(command="python", args=["server.py"])
async def main():
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print([t.name for t in tools.tools])
asyncio.run(main())Cuándo usar esto:
- Estás creando una aplicación que necesita llamar a herramientas MCP directamente, fuera de Claude Desktop o Claude Code.
- Quieres integrar las herramientas de un servidor MCP en una aplicación Claude Agent SDK como parte de un bucle de agente más grande.
- Necesitas escribir pruebas de integración que ejerciten un servidor real a través de la misma ruta de código que utiliza un cliente de producción.
- Estás depurando un servidor y quieres ver exactamente qué informa durante la negociación de capacidades.
Ejemplo de trabajo
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
server_params = StdioServerParameters(
command="python",
args=["tickets_server.py"],
)
async def run_client() -> None:
async with stdio_client(server_params) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
tools_result = await session.list_tools()
print("Herramientas disponibles:")
for tool in tools_result.tools:
print(f" - {tool.name}: {tool.description}")
create_result = await session.call_tool(
"create_ticket",
arguments={"title": "Printer offline", "priority": "low"},
)
ticket_id = create_result.content[0].text
print(f"Ticket creado: {ticket_id}")
resource_result = await session.read_resource("tickets://open")
print("Tickets abiertos:")
print(resource_result.contents[0].text)
close_result = await session.call_tool(
"close_ticket",
arguments={"ticket_id": ticket_id},
)
print(close_result.content[0].text)
if __name__ == "__main__":
asyncio.run(run_client())Lo que esto demuestra:
- El ciclo completo de conexión-negociación-intercambio desde el lado del cliente:
stdio_clientabre el transporte,ClientSessionnegocia,initialize()completa el handshake. list_tools()utilizado para descubrir lo que ofrece el servidor antes de asumir que existe alguna herramienta.call_tool()invocado dos veces, una para crear un ticket y otra para cerrarlo, mostrando cómo un cliente impulsa un flujo de trabajo de varios pasos.read_resource()utilizado para obtener datos de solo lectura por URI, distinto de las llamadas a acciones orientadas a herramientas que lo rodean.
Análisis en profundidad
Cómo funciona
stdio_client(server_params)inicia el servidor como un subproceso utilizando el comando y los argumentos dados, y devuelve un par de flujos para leer y escribir en él.ClientSession(read_stream, write_stream)envuelve esos flujos en la capa de protocolo, proporcionándole las primitivas de solicitud/respuesta en lugar de bytes sin procesar.session.initialize()realiza la negociación de capacidades, el cliente y el servidor intercambian versiones compatibles, y el servidor devuelve su manifiesto de herramientas, recursos y prompts.- Cada llamada después de
initialize(),list_tools(),call_tool(),read_resource(), es un único ciclo de solicitud/respuesta sobre la sesión ya negociada. - Ambos bloques
async withmanejan la limpieza: cerrar la sesión y terminar el subproceso cuando el bloque sale, incluso si se genera una excepción dentro de él.
Primitivas del cliente de un vistazo
| Primitiva | Propósito | Devuelve |
|---|---|---|
list_tools() | Descubrir herramientas disponibles | Una lista de definiciones de herramientas con nombre, descripción y esquema de entrada |
call_tool(name, arguments) | Invocar una herramienta | Un resultado con una lista content |
read_resource(uri) | Obtener contenido del recurso | Un resultado con una lista contents |
list_resources() | Descubrir recursos disponibles | Una lista de definiciones de recursos |
Manejo de errores de herramientas
from mcp import McpError
try:
result = await session.call_tool("close_ticket", arguments={"ticket_id": "T-999"})
except McpError as exc:
print(f"la llamada a la herramienta falló: {exc}")Una llamada a una herramienta que genera una excepción en el lado del servidor se presenta al cliente como un error estructurado en lugar de un fallo, captúralo de la misma manera que capturarías cualquier modo de fallo esperado en tu aplicación.
Notas de Python
# Siempre llama a session.initialize() antes que cualquier otro método de sesión.
# Llamar a list_tools() o call_tool() antes de que se complete la inicialización
# fallará, ya que el cliente aún no ha negociado lo que ofrece el servidor.
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
# la sesión es ahora segura de usarParámetros y valores de retorno
| Método | Argumento Clave | Notas |
|---|---|---|
call_tool(name, arguments) | arguments: dict | Debe coincidir con el esquema de entrada declarado de la herramienta |
read_resource(uri) | uri: str | Debe coincidir con una URI que el servidor haya registrado realmente |
list_tools() | ninguno | Seguro de llamar repetidamente, refleja el manifiesto actual del servidor |
Trampas
- Llamar a
list_tools()ocall_tool()antes deinitialize(). La sesión aún no ha negociado las capacidades, por lo que el servidor no tiene contexto para la solicitud. Solución: siempreawait session.initialize()como la primera llamada dentro del bloque de sesión. - Pasar argumentos que no coinciden con el esquema de una herramienta. Una clave con error tipográfico o un tipo incorrecto falla la validación en el servidor y devuelve un error que el cliente debe manejar. Solución: comprueba la salida de
list_tools()para ver el esquema exacto antes de codificar de forma rígida una carga útil decall_tool(). - No manejar
McpErroralrededor decall_tool(). Un error de herramienta no manejado se propaga como una excepción inesperada en el código de la aplicación que no espera un fallo a nivel de protocolo. Solución: envuelve las llamadas a herramientas que puedan fallar (IDs inválidos, registros faltantes) en un try/except paraMcpError. - Filtrar el subproceso al omitir el gestor de contexto
async with. Llamar astdio_client()sin el gestor de contexto puede dejar el proceso del servidor en ejecución después de que tu script salga. Solución: usa siempreasync with stdio_client(...)para que la limpieza se ejecute automáticamente. - Asumir que un recurso siempre existe.
read_resource()en una URI que el servidor nunca registró, o una cuyo dato subyacente fue eliminado, genera un error al igual que una llamada a herramienta inválida. Solución: llama alist_resources()primero si no estás seguro de que la URI todavía es válida. - Reutilizar una
ClientSessionen tareas concurrentes no relacionadas sin coordinación. Una sesión no es automáticamente segura para uso concurrente arbitrario de múltiples corrutinas que emiten llamadas superpuestas. Solución: serializa las llamadas a través de una tarea, o abre una sesión por unidad lógica de trabajo.
Alternativas
| Alternativa | Usar cuando | No usar cuando |
|---|---|---|
| Claude Desktop / Claude Code como cliente | Solo necesitas que Claude use el servidor interactivamente, sin lógica de aplicación personalizada | Necesitas control programático, orquestación personalizada o pruebas automatizadas |
| Un cliente HTTP genérico contra un servidor HTTP/SSE | El servidor está desplegado remotamente y no necesitas el modelo de subproceso stdio | El servidor es solo local y stdio es más simple |
El Inspector MCP (npx @modelcontextprotocol/inspector) | Estás explorando manualmente un servidor durante el desarrollo | Necesitas este comportamiento incrustado en una aplicación en ejecución |
Preguntas frecuentes
¿Cuál es el código mínimo necesario para conectarse a un servidor MCP?
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()Esa es la secuencia completa de conexión y negociación; todo lo demás se basa en una sesión inicializada.
¿Por qué necesito tanto stdio_client como ClientSession?
stdio_clientmaneja el transporte, iniciando el subproceso y proporcionándote flujos de lectura/escritura sin procesar.ClientSessionmaneja la capa de protocolo sobre esos flujos, la negociación, las solicitudes y las respuestas.- Separarlos permite que la misma API
ClientSessionfuncione sobre otros transportes, como HTTP/SSE, sin cambiar tu código a nivel de protocolo.
¿Cómo sé qué argumentos espera una herramienta?
Llama a list_tools() e inspecciona el esquema de entrada de cada herramienta. Es el mismo esquema que el servidor generó a partir de las sugerencias de tipo de su manejador, por lo que es la fuente autorizada, no el código fuente del servidor.
¿Qué sucede si llamo a una herramienta que no existe?
La llamada falla con un McpError. Captúralo explícitamente en lugar de asumir que cada invocación de call_tool() tiene éxito, especialmente si el nombre de la herramienta provino de la entrada del usuario o de la configuración.
¿Puede un cliente conectarse a varios servidores a la vez?
Sí, abre un par stdio_client/ClientSession separado por servidor. Cada sesión rastrea sus propias capacidades negociadas de forma independiente, no hay estado compartido entre ellas.
¿Cómo se relaciona esto con el Claude Agent SDK?
Una aplicación construida sobre el Agent SDK puede usar estas mismas primitivas de cliente para conectarse a un servidor MCP y exponer sus herramientas como parte del conjunto de herramientas disponibles del agente, permitiendo que Claude las llame durante una conversación de la misma manera que lo haría con cualquier otra herramienta.
¿Necesito llamar a list_resources() antes de read_resource()?
No estrictamente, si ya conoces la URI. Es útil cuando no sabes de antemano qué recursos existen, de la misma manera que list_tools() es útil antes de un call_tool() desconocido.
¿Qué devuelve exactamente call_tool()?
Un objeto de resultado con una lista content, que típicamente contiene bloques de texto. Extrae el valor real con algo como result.content[0].text, coincidiendo con la forma que devolvió el manejador del servidor.
¿La conexión del cliente es síncrona o asíncrona?
Asíncrona. Cada primitiva, initialize(), list_tools(), call_tool(), read_resource(), es un método async y debe ser esperado dentro de un bucle de eventos asyncio.
¿Qué sucede con el proceso del servidor cuando mi script cliente sale?
Si usaste async with stdio_client(...), el gestor de contexto termina el subproceso como parte de la limpieza cuando el bloque sale, incluso cuando se genera una excepción.
¿Puedo probar un servidor con este cliente en lugar de una implementación real?
Sí, ese es un patrón común. Conectar un cliente de prueba a tu servidor a través de stdio y llamar a sus herramientas directamente es la base para las pruebas de integración, cubierto con más detalle en la página dedicada a pruebas.
Relacionado
- Cómo los servidores MCP manejan las solicitudes - el ciclo de vida que este cliente impulsa desde el otro lado
- Conceptos básicos de los servidores MCP - el servidor mínimo al que este cliente puede conectarse
- Creación de un servidor MCP con el SDK de Python - los manejadores del lado del servidor que este cliente llama
- Pruebas de servidores MCP antes del despliegue - uso de una sesión de cliente dentro de pruebas automatizadas
- Comparación de despliegue MCP stdio vs HTTP/SSE - conexión a un servidor a través de un transporte diferente
Versiones de la pila: Escrito contra la línea de modelos de Claude actual a partir de ~junio de 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (el predeterminado) y Claude Haiku 4.5 - y los SDK actuales de Model Context Protocol para Python/TypeScript. Los nombres de los modelos, las versiones de los SDK y las especificaciones de MCP cambian rápidamente; verifica los detalles actuales en platform.claude.com/docs y modelcontextprotocol.io antes de confiar en ellos.