Phase 13 - Lesson 08

Construcción de un Cliente MCP — Descubrimiento, Invocación, Gestión de Sesiones

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

La mayoría del contenido de MCP ofrece tutoriales sobre servidores e ignora el cliente. El código del cliente es donde reside la orquestación compleja: inicialización de procesos, negociación de capacidades, fusión de la lista de herramientas en múltiples servidores, devoluciones de llamada (callbacks) de muestreo, reconexión y resolución de colisiones de nombres de espacio (namespaces). Esta lección construye un cliente multi-servidor que eleva tres servidores MCP diferentes a un espacio de nombres de herramientas plano para el modelo.

Tipo: Build Idiomas: Python (stdlib, cliente MCP multi-servidor) Prerrequisitos: Phase 13 · 07 (building an MCP server) Tiempo: ~75 minutos

Objetivos de Aprendizaje

  • Inicializar un servidor MCP como un subproceso, completar initialize y enviar un notifications/initialized.
  • Mantener el estado de la sesión por servidor (capacidades, lista de herramientas, ids de las últimas notificaciones vistas).
  • Fusionar listas de herramientas de múltiples servidores en un único espacio de nombres con manejo de colisiones.
  • Enrutar una llamada de herramienta al servidor propietario y volver a ensamblar la respuesta.

El Problema

Un host de agente real (Claude Desktop, Cursor, Goose, Gemini CLI) carga múltiples servidores MCP a la vez. Un usuario puede tener un servidor de sistema de archivos, un servidor Postgres y un servidor GitHub ejecutándose simultáneamente. El trabajo del cliente es:

  1. Inicializar cada servidor.
  2. Realizar el handshake con cada uno de forma independiente.
  3. Llamar a tools/list en cada uno y unificar el resultado.
  4. Cuando el modelo emita notes_search, buscarlo en el espacio de nombres fusionado y enrutarlo al servidor correcto.
  5. Manejar notificaciones de cualquier servidor (tools/list_changed) sin bloquear.
  6. Reconectar en caso de fallo de transporte.

Desarrollar todo eso manualmente es lo que separa un proyecto de juguete de uno utilizable. Los SDK oficiales envuelven esto, pero el modelo mental debe ser tuyo.

El Concepto

Inicialización de subprocesos

subprocess.Popen con stdin=PIPE, stdout=PIPE, stderr=PIPE. Establece bufsize=1 y usa el modo de texto para lecturas línea por línea. Cada servidor es un proceso; el cliente mantiene un controlador Popen por servidor.

Estado de la sesión por servidor

Un objeto Session por servidor almacena:

  • process — el controlador del Popen.
  • capabilities — lo que el servidor declaró en initialize.
  • tools — el último resultado de tools/list.
  • pending — mapeo del id de solicitud a una promesa/futuro esperando la respuesta.

Las solicitudes son asíncronas por naturaleza; un tools/call enviado al servidor A mientras el servidor B está a mitad de una llamada no debe bloquear. Usa hilos con colas o asyncio.

Espacio de nombres fusionado

Cuando el cliente ve la lista agregada de herramientas, los nombres pueden colisionar. Dos servidores pueden exponer search. El cliente tiene tres opciones:

  1. Prefijar por nombre de servidor. notes/search, files/search. Claro pero estéticamente poco atractivo.
  2. Sustitución silenciosa (primero en llegar). La herramienta search del servidor más reciente sobrescribe la del anterior. Riesgoso; oculta colisiones.
  3. Rechazo de colisión. Negarse a cargar el segundo servidor; notificar al usuario. Lo más seguro para hosts sensibles a la seguridad.

Claude Desktop utiliza prefijado por servidor. Cursor utiliza rechazo de colisiones con un error claro. VS Code MCP también adopta el prefijado por servidor.

Enrutamiento

Después de la fusión, una tabla de despacho mapeia tool_name -> session. El modelo emite una llamada por nombre; el cliente encuentra la sesión y escribe un mensaje tools/call en el stdin de ese servidor, y luego espera la respuesta.

Callback de muestreo (sampling)

Si el servidor declaró la capacidad sampling en initialize, puede enviar sampling/createMessage solicitando al cliente que ejecute su LLM. El cliente debe:

  1. Bloquear nuevas solicitudes a ese servidor hasta que se resuelva la muestra, o procesar en pipeline si su implementación admite concurrencia.
  2. Llamar a su proveedor de LLM.
  3. Enviar la respuesta de vuelta al servidor.

La Lección 11 cubre el muestreo de extremo a extremo. Esta lección lo esboza por completitud.

Manejo de notificaciones

notifications/tools/list_changed significa volver a llamar a tools/list. notifications/resources/updated significa volver a leer el recurso si está en uso. Las notificaciones no deben generar respuestas; no intentes enviar acks para ellas.

Un error común del cliente: bloquear el bucle de lectura en un tools/call mientras una notificación está en el flujo. Usa un hilo lector en segundo plano que empuje cada mensaje a una cola; el hilo principal retira los elementos de la cola y realiza el despacho.

Reconexión

El transporte puede fallar: el servidor falló, el SO mató el proceso o la tubería (pipe) de stdio se rompió. El cliente detecta EOF en stdout y trata la sesión como muerta. Opciones:

  • Reiniciar silenciosamente el servidor y volver a realizar el handshake. Aceptable para servidores de solo lectura puros.
  • Mostrar el fallo al usuario. Aceptable para servidores con estado con sesiones visibles para el usuario.

La Phase 13 · 09 cubre la semántica de reconexión HTTP Streamable; stdio es más simple.

Keepalive e id de sesión

HTTP Streamable utiliza una cabecera Mcp-Session-Id. Stdio no tiene id de sesión; la identidad del proceso ES la sesión. Los pings de keepalive son opcionales; las tuberías de stdio no se rompen por inactividad.

Utilización

code/main.py inicializa tres servidores MCP simulados como subprocesos, realiza el handshake de cada uno, fusiona sus listas de herramientas y enruta las llamadas de herramientas al correcto. Los "servidores" son en realidad otros procesos de Python que ejecutan respondedores de juguete (sin LLM real). Ejecútalo para ver:

  • Tres inicializaciones, cada una con su propio conjunto de capacidades.
  • Tres resultados de tools/list fusionados en un espacio de nombres de 7 herramientas.
  • Una decisión de enrutamiento basada en el nombre de la herramienta.
  • Una colisión prevenida por prefijación del espacio de nombres.

Qué observar:

  • La clase de datos Session mantiene el estado por servidor de manera limpia.
  • El hilo lector en segundo plano extrae cada línea de la cola en stdout sin bloquear el hilo principal.
  • La tabla de despacho es un simple dict[str, Session].
  • El manejo de colisiones es explícito: cuando dos servidores declaran el mismo nombre, el segundo se renombra con un prefijo.

Producción

Esta lección produce outputs/skill-mcp-client-harness.md. Dada una lista declarativa de servidores MCP (nombre, comando, argumentos), la habilidad produce una estructura de ejecución (harness) que los inicializa, fusiona listas de herramientas y ofrece una función de enrutamiento con resolución de colisiones.

Ejercicios

  1. Ejecuta code/main.py y observa el registro de inicialización de los servidores. Detén uno de los procesos de servidor simulados con un SIGTERM y observa cómo el cliente detecta el EOF y marca esa sesión como muerta.

  2. Implementa el prefijado de espacios de nombres. Cuando dos servidores expongan search, renombra el segundo como <server>/search. Actualiza la tabla de despacho y verifica que las llamadas a herramientas se enruten correctamente.

  3. Añade una estrategia de retroceso (backoff) estilo pool de conexiones para el reinicio del servidor: retroceso exponencial en fallos consecutivos, con un límite de 30 segundos, y emite una notificación al usuario después de tres fallos.

  4. Esboza un cliente que admita 100 servidores MCP concurrentes. ¿Qué estructura de datos reemplaza al diccionario de despacho simple? (Pista: trie para espacio de nombres con prefijos, más una métrica de cantidad de herramientas por servidor).

  5. Realiza la portabilidad del cliente al SDK oficial de Python para MCP. El SDK envuelve stdio_client and ClientSession. El código debería reducirse de aproximadamente 200 líneas a unas 40 líneas, preservando el enrutamiento multi-servidor.

Términos Clave

Término Lo que la gente dice Lo que realmente significa
Cliente MCP "El host del agente" Proceso que inicializa servidores y orquesta llamadas a herramientas
Sesión "Estado por servidor" Control de capacidades, lista de herramientas y registro de solicitudes pendientes
Espacio de nombres fusionado "Una lista de herramientas" Conjunto plano de nombres de herramientas de todos los servidores activos
Colisión de espacio de nombres "Dos servidores misma herramienta" El cliente debe prefijar, rechazar o aplicar sustitución simple al duplicado
Enrutamiento "¿Quién recibe esta llamada?" Despacho desde el nombre de la herramienta al servidor propietario
Lector en segundo plano "Stdout no bloqueante" Hilo o tarea que drena la salida estándar (stdout) del servidor a una cola
Callback de muestreo "LLM como servicio" Controlador del cliente para sampling/createMessage desde el servidor
notifications/*_changed "Primitiva mutada" Señal de que el cliente debe redescobrir o volver a leer
Política de reconexión "Cuando el servidor muere" Semántica de reinicio cuando falla el transporte
Sesión de Stdio "Proceso = sesión" Sin id de sesión; el ciclo de vida del proceso hijo es la sesión

Lecturas Adicionales

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).