Módulo 6: Construir un cliente MCP y descubrimiento
Leer las capabilities del servidor
Descripción
La lección 02 dejó connect() guardando self.capabilities — el objeto que el servidor devuelve dentro de result en su response a initialize. Esta lección le da uso real a ese dato: un método supports() que el cliente consulta antes de llamar tools/list, resources/list o prompts/list, en vez de asumir que las tres siempre están disponibles. Vas a ver, ejecutado, por qué esta comprobación no es una formalidad — vas a llamar un método sin haberlo comprobado, y vas a ver exactamente el error que produce.
Conexión con el módulo
Hasta ahora, los cuatro servidores que construiste en M2-M5 siempre declararon las tres categorías (tools, resources, prompts) en sus capabilities, así que nunca hizo falta comprobar nada — cualquier método siempre estaba disponible. Esta lección introduce el primer servidor de la guía que no las declara todas: sunroom-cafe-mcp-server, que solo soporta tools. Es la primera vez que "leer capabilities antes de invocar" deja de ser una buena práctica abstracta y se vuelve una comprobación con consecuencias reales.
Qué son capabilities, otra vez, pero ahora desde el cliente
El Módulo 2 (lección 04) ya definió capabilities desde el lado del servidor: un objeto que declara, dentro de la response a initialize, qué categorías de primitivos soporta — {"tools": {}, "resources": {}, "prompts": {}} para un servidor que soporta las tres, con un objeto vacío en cada clave porque, en la revisión 2025-06-18, la presencia de la clave alcanza para declarar soporte (no hace falta contenido adicional dentro de cada una).
Desde el lado del cliente, capabilities es información con la que decides, no solo información que lees. El patrón central de esta lección es simple:
def supports(self, primitive: str) -> bool:
"""True si el server declaro esta categoria en capabilities de initialize."""
return primitive in self.capabilities
Con este único método, cualquier código que use MCPClient puede preguntar client.supports("resources") antes de llamar client.list_resources() — y decidir, en consecuencia, si tiene sentido seguir o no.
Qué esperar: dos servidores, dos capabilities distintas
Este es el primer momento de la guía donde comparar dos servidores lado a lado importa. reservo-mcp-server (M3-M5) declara las tres categorías; sunroom-cafe-mcp-server —el segundo proveedor de este módulo, presentado en la lección 01— declara solo una:
from mcp_client import MCPClient
print("=== reservo-mcp-server ===")
reservo = MCPClient("reservo_full_mcp_server.py", verbose=False)
reservo.connect()
print(f"[client] {reservo.server_info['name']} declaro capabilities: {reservo.capabilities}")
print(f"[client] supports('tools')={reservo.supports('tools')} "
f"supports('resources')={reservo.supports('resources')} "
f"supports('prompts')={reservo.supports('prompts')}")
reservo.close()
print()
print("=== sunroom-cafe-mcp-server ===")
cafe = MCPClient("sunroom_cafe_mcp_server.py", verbose=False)
cafe.connect()
print(f"[client] {cafe.server_info['name']} declaro capabilities: {cafe.capabilities}")
print(f"[client] supports('tools')={cafe.supports('tools')} "
f"supports('resources')={cafe.supports('resources')} "
f"supports('prompts')={cafe.supports('prompts')}")
cafe.close()
Corriendo python3.14 este_script.py:
=== reservo-mcp-server ===
[client] reservo-mcp-server declaro capabilities: {'tools': {}, 'resources': {}, 'prompts': {}}
[client] supports('tools')=True supports('resources')=True supports('prompts')=True
=== sunroom-cafe-mcp-server ===
[client] sunroom-cafe-mcp-server declaro capabilities: {'tools': {}}
[client] supports('tools')=True supports('resources')=False supports('prompts')=False
Mismo cliente, mismo método supports(), dos resultados completamente distintos — porque capabilities no es una propiedad de MCP en general, es una declaración de este servidor en particular. El servidor del café atiende reservas de café, no políticas de cancelación ni plantillas de reserva de salas — no tiene sentido que declare resources o prompts que no tiene, y el protocolo le da una forma explícita de decir "esto no lo ofrezco", en vez de forzarlo a fingir que sí.
Qué pasa si ignoras capabilities y llamas igual
Este es el experimento que justifica por qué esta comprobación importa de verdad, no solo en teoría. sunroom-cafe-mcp-server no declaró resources — ¿qué pasa si un cliente descuidado llama resources/list contra él de todas formas?
cafe2 = MCPClient("sunroom_cafe_mcp_server.py", verbose=True)
cafe2.connect()
cafe2._send({"jsonrpc": "2.0", "id": next(cafe2.request_ids), "method": "resources/list", "params": {}})
bad_response = cafe2._recv()
print(f"[client] respuesta a resources/list sin soporte: {bad_response}")
cafe2.close()
Salida real:
[client -> sunroom_cafe_mcp_server.py] {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "reservo-mcp-client", "version": "1.0.0"}}}
[sunroom_cafe_mcp_server.py -> client] {"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "2025-06-18", "capabilities": {"tools": {}}, "serverInfo": {"name": "sunroom-cafe-mcp-server", "version": "1.0.0"}}}
[client -> sunroom_cafe_mcp_server.py] {"jsonrpc": "2.0", "method": "notifications/initialized"}
[client -> sunroom_cafe_mcp_server.py] {"jsonrpc": "2.0", "id": 2, "method": "resources/list", "params": {}}
[sunroom_cafe_mcp_server.py -> client] {"jsonrpc": "2.0", "id": 2, "error": {"code": -32601, "message": "Method not found: resources/list"}}
[client] respuesta a resources/list sin soporte: {'jsonrpc': '2.0', 'id': 2, 'error': {'code': -32601, 'message': 'Method not found: resources/list'}}
---- stderr de sunroom_cafe_mcp_server.py ----
[cafe] arrancando, esperando mensajes por stdin...
[cafe] initialize <- cliente {'name': 'reservo-mcp-client', 'version': '1.0.0'}
[cafe] notifications/initialized <- el cliente confirma que ya puede operar
-32601 Method not found — el mismo código de error de protocolo que ya conoces del Módulo 2 (lección 06), para el caso de un method que el servidor no reconoce en absoluto. Desde el punto de vista del servidor del café, resources/list no es "un método que existe pero devuelve vacío" — es un método que no está en su dispatch en absoluto, exactamente como si le mandaras un method inventado. La declaración en capabilities no es decorativa: es la única forma en que un cliente sabe, de antemano, qué parte del árbol de if method == ... del servidor existe siquiera.
Nota de producción: lo que el SDK oficial hace con capabilities, del lado del cliente
En el SDK oficial mcp (PyPI; pip install "mcp<2"), la clase ClientSession guarda capabilities de la misma forma que self.capabilities en esta lección, y las expone para que el código de aplicación las consulte antes de operar — el chequeo if primitive in self.capabilities que escribiste a mano aquí es, conceptualmente, lo mismo que ofrece esa capa del SDK. Lo que el SDK no hace por ti es la decisión de negocio de qué hacer cuando una capability falta — eso, correctamente, sigue siendo responsabilidad del código que usa el cliente, no de la librería.
Errores comunes
-
Comprobar
capabilitiesen el servidor, no en el cliente. El servidor SÍ declara sus capabilities en la response deinitialize— pero es el CLIENTE quien tiene que leerlas y decidir en consecuencia. Un servidor no impide que le llamen un método no declarado (como viste, responde con un error, no con un rechazo previo) — la responsabilidad de no llamarlo está del lado de quien inicia la conexión. -
Asumir que
capabilitiesvacío ({}) para una clave específica significa "sin soporte". Es al revés: la ausencia de la clave completa ("resources"ni siquiera aparece en el diccionario) es lo que significa "sin soporte". Un valor de{}dentro de una clave presente (como enreservo-mcp-server, con"resources": {}) significa "sí lo soporto, sin extensiones adicionales" — el mismo matiz que ya viste en el Módulo 2, ahora relevante para decidir, no solo para leer. -
Pensar que
-32601en este contexto es un bug del servidor. No lo es — es el comportamiento correcto y esperado cuando un cliente llama un método fuera del contrato que el servidor anunció. El "bug", si lo hay, está del lado del cliente que no comprobócapabilitiesantes de llamar.
Ejercicios
Ejercicio 1: Predice antes de correr (Fácil)
Sin ejecutar nada, predice qué devolvería reservo.supports("prompts") y qué devolvería cafe.supports("prompts"), usando solo las capabilities que ya viste en el "Qué esperar" de esta lección. Después confírmalo corriendo el primer script de la lección.
Ver solución
reservo.supports("prompts") → True (Reservo declaró "prompts": {} en sus capabilities). cafe.supports("prompts") → False (el café solo declaró "tools": {}, la clave "prompts" ni siquiera existe en su diccionario de capabilities). Esto coincide exactamente con la salida citada en la sección "Qué esperar": supports('prompts')=True para Reservo, supports('prompts')=False para el café.
Ejercicio 2: Una función que detecta qué falta (Medio)
Escribe una función missing_capabilities(client, wanted) que reciba un MCPClient ya conectado y un conjunto de nombres de categorías deseadas ({"tools", "resources", "prompts"}), y devuelva el subconjunto de wanted que el servidor no declaró soportar. Pruébala contra Reservo y contra el café.
Ver solución
from mcp_client import MCPClient
def missing_capabilities(client: MCPClient, wanted: set[str]) -> set[str]:
return wanted - set(client.capabilities)
reservo = MCPClient("reservo_full_mcp_server.py", verbose=False)
reservo.connect()
cafe = MCPClient("sunroom_cafe_mcp_server.py", verbose=False)
cafe.connect()
wanted = {"tools", "resources", "prompts"}
print("reservo -- capabilities faltantes:", missing_capabilities(reservo, wanted))
print("cafe -- capabilities faltantes:", missing_capabilities(cafe, wanted))
Salida esperada:
reservo -- capabilities faltantes: set()
cafe -- capabilities faltantes: {'resources', 'prompts'}
Explicación: wanted - set(client.capabilities) es una resta de conjuntos — todo lo que está en wanted y NO está entre las claves de capabilities. Para Reservo, el resultado es el conjunto vacío (set(), no {} — en Python, {} es un diccionario vacío, no un conjunto vacío) porque declaró las tres. Para el café, el resultado son exactamente las dos que le faltan. Una función así es la base de un chequeo de compatibilidad más general: "¿este servidor me sirve para lo que necesito hacer?", antes incluso de intentar la primera llamada.
Ejercicio 3: Otro método, la misma pregunta (Difícil)
El "Qué pasa si ignoras capabilities" de esta lección probó resources/list contra el café. Repite el experimento con prompts/get (pidiendo, por ejemplo, plan_booking sin argumentos) contra el mismo servidor. ¿El error es exactamente el mismo tipo, o cambia algo? Ejecútalo y confirma.
Ver solución
from mcp_client import MCPClient
cafe = MCPClient("sunroom_cafe_mcp_server.py", verbose=False)
cafe.connect()
# El cafe no declaro "prompts" -- llamamos prompts/get de todas formas.
cafe._send({"jsonrpc": "2.0", "id": next(cafe.request_ids), "method": "prompts/get",
"params": {"name": "plan_booking", "arguments": {}}})
response = cafe._recv()
print("respuesta a prompts/get contra un server sin la capability prompts:")
print(response)
Salida real:
respuesta a prompts/get contra un server sin la capability prompts:
{'jsonrpc': '2.0', 'id': 2, 'error': {'code': -32601, 'message': 'Method not found: prompts/get'}}
Explicación: exactamente el mismo tipo de error, -32601 Method not found, con el method correcto (prompts/get) mencionado en el mensaje. No importa qué argumentos lleve el pedido (plan_booking es un nombre de prompt que ni siquiera existe en el café) — el servidor nunca llega a evaluar el nombre del prompt, porque el método prompts/get en sí no está en su dispatch. Esto confirma un punto general: cuando falta una capability completa, el error de protocolo ocurre en el primer nivel (el method), antes de que cualquier dato del params (como el name del prompt) tenga oportunidad de importar.
Resumen y siguiente paso
capabilities, guardado porconnect()en la lección 02, se usa ahora activamente consupports(primitive): una comprobación de una línea,primitive in self.capabilities.- Comparaste dos servidores reales, lado a lado:
reservo-mcp-serverdeclara las tres categorías;sunroom-cafe-mcp-serverdeclara solotools— el primer caso de la guía dondecapabilitiesdistingue de verdad. - Ignorar
capabilitiesy llamar un método de todas formas produce-32601 Method not found— el mismo error de protocolo del Módulo 2, ahora con una causa concreta y evitable. - La ausencia de una clave completa en
capabilitiessignifica "sin soporte"; un valor{}dentro de una clave presente significa "con soporte, sin extensiones" — la distinción que el Ejercicio 1 pone a prueba.
Siguiente lección: 04 — El flujo de descubrimiento: listar todo. Con supports() ya disponible, el cliente puede decidir, de forma automática, cuáles de los tres */list llamar contra un servidor — el primer método completo de MCPClient que arma un catálogo real.
Recursos adicionales
- Model Context Protocol — Specification 2025-06-18: Lifecycle — La sección de negociación de capabilities dentro del handshake.
- Model Context Protocol — Specification 2025-06-18: Base Protocol — El código de error
-32601, ya definido por JSON-RPC 2.0 y reutilizado por MCP. - JSON-RPC 2.0 Specification — La tabla de códigos de error estándar, incluido
-32601 Method not found. - Python — Operaciones con conjuntos (
set) — La resta de conjuntos (-) usada en el Ejercicio 2 para detectar capabilities faltantes.