Phase 13 - Lesson 07

Construyendo un Servidor MCP — SDKs de Python + TypeScript

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

La mayoría de los tutoriales de MCP solo muestran hello-worlds de stdio. Un servidor real expone herramientas, recursos y prompts, maneja la negociación de capacidades, emite errores estructurados y funciona igual en todos los SDK. Esta lección construye un servidor de notas de principio a fin: transporte stdio de la biblioteca estándar (stdlib), despacho JSON-RPC, las tres primitivas de servidor y un estilo de función pura que se puede portar directamente a FastMCP de la SDK de Python o a la SDK de TypeScript cuando decidas avanzar.

Tipo: Build Idiomas: Python (servidor MCP stdio con stdlib) Requisitos previos: Phase 13 · 06 (MCP fundamentals) Tiempo: ~75 minutos

Objetivos de Aprendizaje

  • Implementar los métodos initialize, tools/list, tools/call, resources/list, resources/read, prompts/list y prompts/get.
  • Escribir un bucle de despacho (dispatch loop) que lea mensajes JSON-RPC de stdin y escriba respuestas en stdout.
  • Emitir respuestas de error estructuradas según la especificación JSON-RPC 2.0 y los códigos adicionales de MCP.
  • Migrar una implementación con stdlib a FastMCP (SDK de Python) o al SDK de TypeScript sin reescribir la lógica de las herramientas.

El Problema

Antes de poder usar un transporte remoto (Phase 13 · 09) o una capa de autenticación (Phase 13 · 16), necesitas un servidor local limpio. Local significa stdio: el cliente inicia el servidor como un proceso hijo, y los mensajes fluyen delimitados por saltos de línea a través de stdin/stdout.

La especificación del 25-11-2025 prescribe que los mensajes stdio se codifiquen como objetos JSON con un separador \n explícito. Nada de SSE aquí; SSE era el antiguo modo remoto y se eliminará a mediados de 2026 (el servidor MCP Rovo de Atlassian lo depreció el 30 de junio de 2026; Keboola el 1 de abril de 2026). Para stdio, un objeto JSON por línea es todo el formato de transmisión.

Un servidor de notas es un buen modelo porque ejercita las tres primitivas del servidor. Las herramientas realizan mutaciones (notes_create). Los recursos exponen datos (notes://{id}). Los prompts entregan plantillas (review_note). El diseño de esta lección se puede generalizar a cualquier dominio.

El Concepto

Bucle de despacho

loop:
  line = stdin.readline()
  msg = json.loads(line)
  if has id:
    handle request -> write response
  else:
    handle notification -> no response

Tres reglas:

  • No imprimas nada en stdout que no sea un sobre JSON-RPC. Los registros de depuración (debug) van a stderr.
  • Toda solicitud DEBE tener una respuesta correspondiente que contenga el mismo id.
  • Las notificaciones NO DEBEN ser respondidas.

Implementando initialize

def initialize(params):
    return {
        "protocolVersion": "2025-11-25",
        "capabilities": {
            "tools": {"listChanged": True},
            "resources": {"listChanged": True, "subscribe": False},
            "prompts": {"listChanged": False},
        },
        "serverInfo": {"name": "notes", "version": "1.0.0"},
    }

Declara solo lo que soportes. El cliente depende del conjunto de capacidades (capabilities) para habilitar o restringir funcionalidades.

Implementando tools/list y tools/call

tools/list devuelve {tools: [...]} con cada entrada conteniendo name, description e inputSchema. tools/call recibe {name, arguments} y devuelve {content: [blocks], isError: bool}.

Los bloques de contenido (content blocks) son tipados. Los más comunes:

{"type": "text", "text": "Found 2 notes"}
{"type": "resource", "resource": {"uri": "notes://14", "text": "..."}}
{"type": "image", "data": "<base64>", "mimeType": "image/png"}

Los errores de herramientas se presentan en dos formas. Los errores a nivel de protocolo (método desconocido, parámetros inválidos) son errores JSON-RPC. Los errores a nivel de herramienta (llamada válida, pero el funcionamiento falló) se devuelven como {content: [...], isError: true}. Esto permite que el modelo vea el fallo en su contexto y pueda actuar en consecuencia.

Implementando recursos

Los recursos son de solo lectura por diseño. resources/list devuelve un manifiesto; resources/read devuelve el contenido. Las URIs pueden ser file://..., http://... o un esquema personalizado como notes://.

Cuando expones datos como un recurso en lugar de una herramienta:

  • El modelo no lo "llama"; el cliente puede inyectarlo en el contexto a petición del usuario.
  • Las suscripciones (subscriptions) permiten al servidor enviar actualizaciones cuando el recurso cambia (Phase 13 · 10).
  • La Phase 13 · 14 extiende esto con ui:// para recursos interactivos.

Implementando prompts

Los prompts son plantillas con argumentos nombrados. El host los muestra como comandos de barra diagonal (slash-commands). Un prompt review_note puede recibir un argumento note_id y producir una plantilla de prompt con múltiples mensajes que el cliente entrega a su modelo.

Sutilezas del transporte stdio

  • JSON delimitado por saltos de línea. Sin marcos prefijados por longitud (length-prefixed framing).
  • No uses búfer. Llama a sys.stdout.flush() después de cada escritura.
  • El cliente controla el tiempo de vida. Cuando stdin se cierra (EOF), sal de manera limpia.
  • No manejes SIGPIPE de forma silenciosa; registra el log y sal.

Anotaciones

Cada herramienta puede llevar annotations (anotaciones) que describen propiedades de seguridad:

  • readOnlyHint: true — lectura pura, segura para reintentar.
  • destructiveHint: true — efectos secundarios irreversibles; el cliente debe confirmar.
  • idempotentHint: true — las mismas entradas producen las mismas salidas.
  • openWorldHint: true — interactúa con sistemas externos.

El cliente usa esto para decidir la experiencia de usuario UX (diálogos de confirmación, indicadores de estado) y el enrutamiento (Phase 13 · 17).

Ruta de transición (Graduation path)

El servidor stdlib en code/main.py tiene alrededor de 180 líneas. FastMCP (Python) simplifica esta misma lógica en un estilo basado en decoradores:

from fastmcp import FastMCP
app = FastMCP("notes")

@app.tool()
def notes_search(query: str, limit: int = 10) -> list[dict]:
    ...

El SDK de TypeScript tiene un diseño equivalente. La ruta de transición es directa cuando estés listo; los conceptos (capacidades, despacho, bloques de contenido) son los mismos.

Cómo Usarlo

code/main.py es un servidor MCP de notas completo sobre stdio, usando solo la biblioteca estándar (stdlib). Maneja initialize, tools/list, tools/call para tres herramientas (notes_list, notes_search, notes_create), resources/list y resources/read para cada nota, y un prompt review_note. Puedes ejecutarlo enviando mensajes JSON-RPC a través de una tubería (pipe):

echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | python main.py

Qué observar:

  • El despachador (dispatcher) es un dict[str, Callable] que tiene el nombre del método como clave.
  • Cada ejecutor de herramienta devuelve una lista de bloques de contenido (content blocks), no una cadena de texto simple.
  • isError: true se establece cuando el ejecutor lanza una excepción.

Entrégalo

Esta lección produce outputs/skill-mcp-server-scaffolder.md. Dado un dominio (notas, tickets, archivos, base de datos), la skill genera la estructura (scaffolds) de un servidor MCP con la división correcta de herramientas / recursos / prompts y la ruta de transición de SDK correspondiente.

Ejercicios

  1. Ejecuta code/main.py e interactúa con él usando mensajes JSON-RPC creados manualmente. Prueba notes_create y luego resources/read para recuperar la nueva nota.

  2. Agrega una herramienta notes_delete con annotations: {destructiveHint: true}. Verifica si el cliente mostraría un diálogo de confirmación (esto requiere un host real; Claude Desktop funciona).

  3. Implementa resources/subscribe para que el servidor envíe notificaciones de resources/updated cada vez que se modifique una nota. Agrega una tarea de mantenimiento de conexión (keepalive).

  4. Porta el servidor a FastMCP. El archivo de Python debería reducirse a menos de 80 líneas. El comportamiento de red debe ser idéntico; verifícalo con la misma herramienta de pruebas de JSON-RPC.

  5. Lee la sección server/tools de la especificación e identifica un campo de definición de herramienta no implementado en el servidor de esta lección. (Consejo: hay varios; elige uno y agrégalo).

Términos Clave

Término Lo que la gente dice Lo que realmente significa
Servidor MCP "La cosa que expone herramientas" Proceso que habla MCP JSON-RPC a través de stdio o HTTP
transporte stdio "Modelo de proceso hijo" El servidor es iniciado por el cliente; se comunica a través de stdin/stdout
Despachador (Dispatcher) "Enrutador de métodos" Mapeo del nombre del método JSON-RPC a la función manejadora
Bloque de contenido (Content block) "Fragmento de resultado de herramienta" Elemento tipado en el arreglo content de una respuesta de herramienta
isError "Fallo a nivel de herramienta" Señala que la herramienta falló; se distingue de un error de JSON-RPC
Annotations "Indicaciones de seguridad" Flags readOnly / destructive / idempotent / openWorld
FastMCP "SDK de Python" Framework de alto nivel basado en decoradores sobre el protocolo MCP
URI del Recurso (Resource URI) "Datos direccionables" file://, db:// o esquema personalizado que identifica un recurso
Plantilla de Prompt (Prompt template) "Resumen de comando de barra" Plantilla proporcionada por el servidor con espacios para argumentos para interfaces de hosts
Declaración de capacidades (Capability declaration) "Alternancia de funciones" Flags por primitiva declaradas en initialize

Lecturas Adicionales

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