Módulo 2: Arquitectura Host-Client-Server

El Server: El Proveedor de Capabilities

El Server: El Proveedor de Capabilities

Descripción de la cápsula

Llegaste a la capa que más te importa como developer: el MCP Server. El Host orquesta, el Client comunica, pero el Server es tu código — el programa que tú escribes, que expone capabilities (tools, resources, prompts), responde a requests, y ejecuta operaciones reales. Cuando en el módulo 4 construyas tu primer MCP server en TypeScript, todo lo que aprendas en esta cápsula será tu mapa.

En esta cápsula vas a entender cómo un Server se estructura internamente, qué debe hacer durante la inicialización, cómo responde a requests del Client, cómo maneja su estado, y qué patrones de diseño producen Servers robustos. No vas a escribir código todavía (eso viene en módulos 4 y 5), pero vas a entender la anatomía completa de un Server.

Volviendo a la analogía del restaurante: el Server es la cocina. Tiene su chef (tu lógica de negocio), su menú (capabilities registradas), y su forma de preparar platos (handlers de requests). El mesero (Client) trae pedidos; la cocina los prepara y retorna el resultado.


Anatomía de un MCP Server

Estructura general

Todo MCP Server tiene estos componentes:

MCP Server
│
├── Registro de capabilities
│   ├── Tools: funciones que el modelo puede ejecutar
│   ├── Resources: datos que el modelo puede leer
│   └── Prompts: templates reutilizables
│
├── Handlers
│   ├── initialize handler → Responde al handshake del Client
│   ├── tools/list handler → Retorna lista de tools disponibles
│   ├── tools/call handler → Ejecuta un tool específico
│   ├── resources/list handler → Retorna lista de resources
│   ├── resources/read handler → Lee un resource específico
│   ├── prompts/list handler → Retorna lista de prompts
│   └── prompts/get handler → Retorna un prompt específico
│
├── Lógica de negocio
│   ├── Conexiones a APIs externas
│   ├── Acceso a databases
│   ├── Operaciones con archivos
│   └── Cualquier operación que tu Server provea
│
└── Transport
    ├── stdio (lee de stdin, escribe en stdout)
    └── HTTP/SSE (escucha en un puerto)

Ciclo de vida del Server

Desde que el proceso arranca hasta que termina:

1. STARTUP
   └── El proceso inicia (lanzado por el Host)
       ├── Configura el transport (stdio o HTTP)
       └── Registra capabilities (tools, resources, prompts)

2. INITIALIZE (responde al Client)
   └── Recibe "initialize" del Client
       ├── Retorna capabilities y serverInfo
       └── Recibe "notifications/initialized"

3. READY (listo para operar)
   └── Escucha requests del Client
       ├── tools/list → Retorna lista
       ├── tools/call → Ejecuta y retorna resultado
       ├── resources/list → Retorna lista
       ├── resources/read → Lee y retorna datos
       ├── prompts/list → Retorna lista
       └── prompts/get → Retorna prompt

4. SHUTDOWN
   └── El Client cierra la conexión
       └── El proceso termina

Capabilities: qué puede exponer un Server

Las 3 primitivas

Un MCP Server puede exponer hasta 3 tipos de capabilities. Cada una tiene un propósito distinto:

Capabilities del Server:
│
├── 🔧 Tools (funciones ejecutables)
│   ├── El modelo las invoca activamente
│   ├── Pueden tener side effects (escribir, crear, modificar)
│   ├── Cada tool tiene input schema (parámetros validados)
│   └── Ejemplo: create_file, run_query, send_message
│
├── 📄 Resources (datos de lectura)
│   ├── El modelo las lee para obtener contexto
│   ├── Son read-only (no modifican nada)
│   ├── Identificadas por URI (protocol://path)
│   └── Ejemplo: db://schema, config://settings
│
└── 💬 Prompts (templates)
    ├── Templates predefinidos con parámetros
    ├── Ayudan al modelo a hacer preguntas específicas
    ├── El usuario las selecciona explícitamente
    └── Ejemplo: analyze_table(name), review_code(file)

Importante: No todo Server necesita exponer las 3. Un Server mínimo puede tener solo 1 tool. Un Server completo puede tener docenas de tools, resources, y prompts.

En esta cápsula verás los 3 tipos a nivel arquitectural. El módulo 3 profundizará en cada primitiva con detalle completo.


Ejemplo: Server de database

Veamos cómo se traducen las 3 primitivas en un Server concreto:

Database MCP Server:
│
├── 🔧 Tools:
│   ├── query(sql) → Ejecuta una query SQL
│   ├── list_tables() → Lista tablas de la database
│   ├── describe_table(name) → Schema de una tabla
│   └── insert_record(table, data) → Inserta un registro
│
├── 📄 Resources:
│   ├── db://schema → Schema completo de la database
│   ├── db://tables/users → Datos de la tabla users
│   └── db://stats → Estadísticas de la database
│
└── 💬 Prompts:
    ├── analyze_table(name) → "Analiza la estructura y
    │   datos de la tabla {name} y sugiere mejoras"
    └── optimize_query(sql) → "Revisa esta query SQL
        y sugiere optimizaciones: {sql}"

Cómo un Server registra capabilities

El proceso de registro

Cuando escribes un MCP Server, el primer paso es registrar qué capabilities expone. Cada SDK (TypeScript, Python) tiene su forma de hacerlo, pero conceptualmente es lo mismo:

Registro de un tool:
├── Nombre: "read_file"
├── Descripción: "Lee el contenido de un archivo"
├── Input Schema:
│   ├── path (string, required): "Ruta al archivo"
│   └── encoding (string, optional): "Codificación del archivo"
└── Handler: función que se ejecuta cuando el tool es invocado

En pseudocódigo (la sintaxis real la verás en módulos 4 y 5):

// Pseudocódigo - registrar un tool
server.register_tool({
  name: "read_file",
  description: "Lee el contenido de un archivo",
  input_schema: {
    path: { type: "string", required: true },
    encoding: { type: "string", required: false, default: "utf-8" }
  },
  handler: function(args) {
    content = read_from_disk(args.path, args.encoding)
    return { type: "text", text: content }
  }
})
// Pseudocódigo - registrar un resource
server.register_resource({
  uri: "config://app-settings",
  name: "Application Settings",
  description: "Current application configuration",
  mime_type: "application/json",
  handler: function() {
    settings = load_settings()
    return { type: "text", text: JSON.stringify(settings) }
  }
})
// Pseudocódigo - registrar un prompt
server.register_prompt({
  name: "analyze_logs",
  description: "Analyze application logs for errors",
  arguments: [
    { name: "timeframe", description: "Time period to analyze", required: true }
  ],
  handler: function(args) {
    return {
      messages: [
        {
          role: "user",
          content: "Analyze the application logs from the last " + args.timeframe +
                   ". Focus on errors, warnings, and unusual patterns."
        }
      ]
    }
  }
})

Input Schema: la importancia de la validación

Cada tool define un inputSchema usando JSON Schema. Esto no es decorativo — el Client usa estos schemas para:

  1. Informar al modelo qué parámetros puede pasar
  2. Validar que los argumentos sean correctos antes de enviarlos
  3. Documentar la API del tool automáticamente
{
  "name": "create_user",
  "description": "Crea un nuevo usuario en la database",
  "inputSchema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "Nombre completo del usuario"
      },
      "email": {
        "type": "string",
        "format": "email",
        "description": "Email del usuario (debe ser único)"
      },
      "role": {
        "type": "string",
        "enum": ["admin", "editor", "viewer"],
        "description": "Rol del usuario en el sistema"
      }
    },
    "required": ["name", "email"]
  }
}

El modelo (Claude) ve este schema y sabe:

  • name y email son obligatorios
  • role tiene 3 valores válidos
  • email debe tener formato de email

Esto permite que Claude construya argumentos correctos sin que el usuario tenga que especificar cada campo.


Cómo un Server responde a requests

Formato de respuesta

Todo response de un tool tiene la misma estructura:

{
  "content": [
    {
      "type": "text",
      "text": "El resultado de la operación..."
    }
  ],
  "isError": false
}

El campo content es un array que puede contener diferentes tipos:

Tipos de content:
│
├── text
│   { "type": "text", "text": "contenido textual" }
│   Uso: la mayoría de respuestas
│
├── image
│   { "type": "image", "data": "base64...", "mimeType": "image/png" }
│   Uso: capturas de pantalla, gráficos
│
└── resource
    { "type": "resource", "resource": { "uri": "...", "text": "..." } }
    Uso: referenciar un resource

Respuesta exitosa

{
  "content": [
    {
      "type": "text",
      "text": "Archivo creado exitosamente: /Users/dev/output.json\nTamaño: 1.2 KB"
    }
  ],
  "isError": false
}

Respuesta con error

{
  "content": [
    {
      "type": "text",
      "text": "Error: No se pudo conectar a la database. Verifica que PostgreSQL esté corriendo y las credenciales sean correctas.\n\nDetalles: Connection refused at localhost:5432"
    }
  ],
  "isError": true
}

El Server retorna errores de operación dentro de content con isError: true. Esto permite que el Host presente el error al usuario de forma legible.


Manejo de estado en el Server

Stateless vs Stateful

Los MCP Servers pueden ser stateless o stateful. La elección depende del caso de uso:

Stateless Server:
├── Cada request es independiente
├── No mantiene información entre requests
├── Ejemplo: Filesystem server (lee/escribe archivos, sin memoria)
├── Ventaja: Simple, predecible
└── Cuándo usarlo: operaciones atómicas

Stateful Server:
├── Mantiene estado entre requests
├── Requests anteriores afectan los siguientes
├── Ejemplo: Database server (mantiene conexión abierta)
├── Ventaja: Eficiente (no reconecta cada vez)
└── Cuándo usarlo: conexiones persistentes, caches

Ejemplos de estado

Estado que un Server puede mantener:
│
├── Conexión a database
│   └── Abrir conexión al iniciar, reutilizar en cada request
│
├── Cache de resultados
│   └── Cachear queries frecuentes para responder más rápido
│
├── Sesión de autenticación
│   └── Token de API renovable, no re-autenticar cada request
│
├── Configuración cargada
│   └── Leer config al iniciar, no releer cada request
│
└── Contadores / métricas
    └── Trackear cuántas operaciones se han ejecutado

Estado y lifecycle

El estado del Server existe mientras el proceso viva. Cuando el Host cierra la conexión (shutdown), el proceso termina y el estado se pierde:

Lifecycle del estado:
│
├── STARTUP → Estado inicial (vacío o desde config)
├── INITIALIZE → Puede cargar estado persistente
├── OPERATION → Estado se acumula (cache, conexiones)
└── SHUTDOWN → Estado se pierde (a menos que persistas)

Si necesitas estado persistente entre sesiones, tu Server debe guardarlo explícitamente (en archivo, database, etc.).


Patrones de diseño para Servers

Patrón 1: Server de API wrapper

El patrón más común — tu Server envuelve una API existente:

API Wrapper Server:
│
├── Entrada: request MCP del Client
├── Proceso: traduce a API call
├── Salida: respuesta MCP al Client
│
│   Client → Server → API Externa
│                   ← Response
│            ← MCP Response
│   ← Presenta al usuario

Ejemplo: GitHub MCP Server
├── tools/call: create_issue(repo, title, body)
│   └── Traduce a: POST https://api.github.com/repos/{repo}/issues
│   └── Retorna: { content: [{ type: "text", text: "Issue #42 created" }] }

Patrón 2: Server de database

El Server mantiene una conexión a la database y expone operaciones:

Database Server:
│
├── STARTUP: abre conexión a PostgreSQL
├── tools/call: query(sql)
│   └── Ejecuta: connection.query(sql)
│   └── Retorna: resultados formateados
├── resources/read: db://schema
│   └── Ejecuta: query information_schema
│   └── Retorna: schema de la database
└── SHUTDOWN: cierra conexión

Patrón 3: Server de filesystem

Opera directamente con el sistema de archivos:

Filesystem Server:
│
├── No mantiene conexiones (stateless)
├── tools/call: read_file(path)
│   └── Lee: fs.readFile(path)
│   └── Retorna: contenido del archivo
├── tools/call: write_file(path, content)
│   └── Escribe: fs.writeFile(path, content)
│   └── Retorna: confirmación
└── Validación: verifica que paths estén dentro de allowed directories

Patrón 4: Server de agregación

Combina múltiples fuentes de datos:

Aggregation Server:
│
├── tools/call: project_status()
│   ├── Lee: GitHub API → PRs abiertos
│   ├── Lee: Jira API → tickets activos
│   ├── Lee: CI/CD → último build
│   └── Retorna: resumen consolidado

Seguridad en el Server

Validación de inputs

Tu Server recibe argumentos del Client, pero no debes confiar ciegamente en ellos:

Validaciones esenciales:
│
├── Path traversal
│   ├── Input: path = "../../etc/passwd"
│   ├── Validación: verificar que path esté dentro del allowed directory
│   └── Sin validación: el Server lee archivos del sistema
│
├── SQL injection
│   ├── Input: sql = "'; DROP TABLE users; --"
│   ├── Validación: usar prepared statements
│   └── Sin validación: la database se corrompe
│
├── Size limits
│   ├── Input: content = (archivo de 10GB)
│   ├── Validación: limitar tamaño de input
│   └── Sin validación: el Server consume toda la memoria
│
└── Rate limiting
    ├── Input: 1000 requests por segundo
    ├── Validación: limitar requests
    └── Sin validación: la API externa te bloquea

Principio de mínimo privilegio

Tu Server debe tener solo los permisos que necesita:

✅ Bien:
├── Filesystem server: solo puede leer/escribir en /Users/dev/projects
├── Database server: solo puede ejecutar SELECT (no DROP/ALTER)
└── GitHub server: solo tiene read access a repos públicos

❌ Mal:
├── Filesystem server: puede acceder a todo el disco
├── Database server: tiene permisos de admin
└── GitHub server: tiene full access a repos privados

Server vs Host vs Client: responsabilidades claras

¿Quién hace qué?

"El usuario quiere contar archivos .py"

Host (Claude Code):
├── ✅ Recibe la petición del usuario
├── ✅ Decide que necesita el Filesystem server
├── ✅ Decide usar el tool "search_files"
└── ✅ Presenta el resultado al usuario

Client:
├── ✅ Envía tools/call al Server
├── ✅ Recibe la respuesta JSON-RPC
└── ✅ Entrega el resultado al Host

Server:
├── ✅ Recibe el request de search_files
├── ✅ Busca archivos .py en el directorio
├── ✅ Formatea el resultado
└── ✅ Retorna la respuesta al Client

¿Quién NO debe hacer qué?

Server:
├── ❌ Decidir si el usuario puede ejecutar esta operación (eso es del Host)
├── ❌ Presentar resultados al usuario (eso es del Host)
└── ❌ Hablar con otros Servers (eso lo coordina el Host)

Troubleshooting

"Mi tool no aparece en Claude Code"

Causa: El tool no está registrado correctamente en el Server, o las capabilities no incluyen tools.

Solución: Verifica:

  1. Que tu Server retorne "tools": {} en capabilities durante initialize
  2. Que el tool esté registrado con nombre, descripción, e inputSchema
  3. Que tools/list retorne el tool en la lista
// Verificar: la respuesta de tools/list debe incluir tu tool
{
  "tools": [
    {
      "name": "my_tool",
      "description": "Descripción del tool",
      "inputSchema": { "type": "object", "properties": {} }
    }
  ]
}

"El tool se ejecuta pero retorna vacío"

Causa: Tu handler retorna un formato incorrecto.

Solución: Verifica que retornes el formato correcto:

// ✅ Correcto
{
  "content": [
    { "type": "text", "text": "resultado aquí" }
  ]
}

// ❌ Incorrecto (falta content array)
{
  "text": "resultado aquí"
}

// ❌ Incorrecto (content no es array)
{
  "content": { "type": "text", "text": "resultado" }
}

"Error: Tool execution failed"

Causa: Tu handler lanzó una excepción no manejada.

Solución: Siempre envuelve tu lógica en try/catch y retorna errores como isError: true:

// Pseudocódigo
handler(args):
  try:
    result = do_operation(args)
    return { content: [{ type: "text", text: result }], isError: false }
  catch error:
    return { content: [{ type: "text", text: error.message }], isError: true }

"El Server crashea al iniciar"

Causa: Error en la configuración del transport o en la inicialización.

Solución:

# Ejecutar el server manualmente para ver errores
node ./my-server.js

# Los errores van a stderr
# Causas comunes:
# - Puerto ya en uso (HTTP transport)
# - Dependencias faltantes
# - Variables de entorno no configuradas

"Resources no se actualizan"

Causa: El Server cachea resources y no los refresca.

Solución: Implementa notifications de cambio:

// Cuando un resource cambia, el Server puede notificar al Client:
{
  "jsonrpc": "2.0",
  "method": "notifications/resources/updated",
  "params": {
    "uri": "db://users"
  }
}

Ejercicios

Ejercicio 1: Identificar capabilities de un Server (Fácil)

Para cada escenario, decide si necesitas un Tool, un Resource, o un Prompt:

  1. Mostrar la configuración actual de la aplicación
  2. Crear un nuevo usuario en la database
  3. Template para analizar logs de error
  4. Listar los endpoints de una API
  5. Enviar un mensaje a Slack
  6. Template para generar documentación de un endpoint
Ver solución
  1. Resource — config://app-settings — Es datos de lectura, no una acción
  2. Tool — create_user(name, email) — Es una acción con side effects
  3. Prompt — analyze_logs(timeframe) — Es un template reutilizable
  4. Resource — api://endpoints — Es datos de lectura
  5. Tool — send_message(channel, text) — Es una acción con side effects
  6. Prompt — document_endpoint(path, method) — Es un template reutilizable

Regla rápida:

  • ¿Modifica algo? → Tool
  • ¿Solo lee datos? → Resource
  • ¿Es un template para el modelo? → Prompt

Ejercicio 2: Diseñar el inputSchema de un tool (Medio)

Diseña el JSON Schema completo para un tool llamado send_email que acepta:

  • to (string, requerido): email del destinatario
  • subject (string, requerido): asunto del email
  • body (string, requerido): contenido del email
  • cc (array de strings, opcional): emails en copia
  • priority (string, opcional): "low", "normal", o "high"
Ver solución
{
  "name": "send_email",
  "description": "Envía un email a un destinatario con asunto y cuerpo",
  "inputSchema": {
    "type": "object",
    "properties": {
      "to": {
        "type": "string",
        "format": "email",
        "description": "Email del destinatario"
      },
      "subject": {
        "type": "string",
        "description": "Asunto del email"
      },
      "body": {
        "type": "string",
        "description": "Contenido del email (texto plano o HTML)"
      },
      "cc": {
        "type": "array",
        "items": {
          "type": "string",
          "format": "email"
        },
        "description": "Lista de emails en copia (opcional)"
      },
      "priority": {
        "type": "string",
        "enum": ["low", "normal", "high"],
        "default": "normal",
        "description": "Prioridad del email"
      }
    },
    "required": ["to", "subject", "body"]
  }
}

Puntos clave:

  • required solo incluye los 3 campos obligatorios
  • cc usa "type": "array" con "items" que define el tipo de cada elemento
  • priority usa "enum" para limitar valores válidos
  • format: "email" ayuda al modelo a generar emails válidos

Ejercicio 3: Escribir respuestas de Server (Medio)

Escribe la respuesta JSON que un Server retornaría para cada escenario:

  1. El tool count_records ejecutó exitosamente y contó 1,247 usuarios
  2. El tool delete_file falló porque el archivo no existe
  3. El resource db://schema retorna el schema de la database
Ver solución

1. Éxito de count_records:

{
  "content": [
    {
      "type": "text",
      "text": "Total de registros en la tabla users: 1,247"
    }
  ],
  "isError": false
}

2. Error de delete_file:

{
  "content": [
    {
      "type": "text",
      "text": "Error: El archivo '/Users/dev/output.log' no existe. Verifica la ruta e intenta de nuevo."
    }
  ],
  "isError": true
}

3. Resource db://schema:

{
  "contents": [
    {
      "uri": "db://schema",
      "mimeType": "application/json",
      "text": "{\n  \"tables\": [\n    {\"name\": \"users\", \"columns\": [\"id\", \"name\", \"email\"]},\n    {\"name\": \"posts\", \"columns\": [\"id\", \"title\", \"user_id\"]}\n  ]\n}"
    }
  ]
}

Nota: Los resources usan contents (plural) con uri y mimeType. Los tools usan content (singular) con type y text.

Ejercicio 4: Diseñar un Server completo conceptualmente (Difícil)

Diseña un MCP Server para un sistema de notas personales. Define:

  • Al menos 3 tools con sus inputSchemas
  • Al menos 2 resources con sus URIs
  • Al menos 1 prompt con sus argumentos
  • Qué estado mantendría el Server
  • Qué validaciones de seguridad implementarías
Ver solución

Tools:

  1. create_note(title, content, tags) — Crea una nueva nota

    {
      "inputSchema": {
        "type": "object",
        "properties": {
          "title": { "type": "string", "description": "Título de la nota" },
          "content": { "type": "string", "description": "Contenido de la nota" },
          "tags": { "type": "array", "items": { "type": "string" }, "description": "Tags para organizar" }
        },
        "required": ["title", "content"]
      }
    }
  2. search_notes(query, tags) — Busca notas por contenido o tags

    {
      "inputSchema": {
        "type": "object",
        "properties": {
          "query": { "type": "string", "description": "Texto a buscar" },
          "tags": { "type": "array", "items": { "type": "string" }, "description": "Filtrar por tags" }
        },
        "required": ["query"]
      }
    }
  3. delete_note(id) — Elimina una nota por ID

    {
      "inputSchema": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "ID de la nota a eliminar" }
        },
        "required": ["id"]
      }
    }

Resources:

  • notes://all — Lista todas las notas (título, fecha, tags)
  • notes://tags — Lista todos los tags existentes con conteo

Prompt:

  • summarize_notes(tag) — "Resume todas las notas con el tag {tag}. Identifica temas comunes y puntos clave."

Estado del Server:

  • Conexión a SQLite local (almacena notas)
  • Cache de tags (evita recalcular con cada request)
  • Índice de búsqueda en memoria (para búsquedas rápidas)

Validaciones de seguridad:

  • Sanitizar contenido de notas (prevenir script injection si se renderiza)
  • Limitar tamaño de notas (máximo 100KB)
  • Validar que IDs sean UUIDs válidos (prevenir inyección)
  • Rate limiting en create_note (máximo 100 notas por minuto)

Ejercicio 5: Identificar patrones de Server (Difícil)

Para cada MCP Server descrito, identifica qué patrón de diseño usa (API wrapper, database, filesystem, o agregación) y justifica:

  1. Un Server que consulta la API de OpenWeather para dar pronósticos
  2. Un Server que lee y escribe archivos de configuración YAML
  3. Un Server que combina datos de GitHub, Jira, y Slack para dar un "project dashboard"
  4. Un Server que ejecuta queries en MongoDB
Ver solución
  1. API Wrapper — Envuelve la API de OpenWeather. Traduce MCP requests a HTTP calls a la API, y formatea responses.

  2. Filesystem — Opera directamente con archivos YAML en disco. Es stateless, cada operación lee/escribe independientemente.

  3. Agregación — Combina múltiples fuentes (GitHub, Jira, Slack) en un resultado consolidado. Hace múltiples API calls internamente y presenta un dashboard unificado.

  4. Database — Mantiene una conexión a MongoDB, ejecuta queries, y retorna resultados formateados. Es stateful (mantiene la conexión abierta).

Punto clave: Los patrones no son exclusivos — un Server puede combinar patrones. Por ejemplo, un Server de "project dashboard" (agregación) puede también cachear resultados en un archivo local (filesystem).

Ejercicio 6: Auditoría de seguridad de un Server (Difícil)

Dado este pseudocódigo de un MCP Server, identifica al menos 3 problemas de seguridad:

server.register_tool({
  name: "run_query",
  handler: function(args) {
    result = database.execute(args.sql)
    return { content: [{ type: "text", text: result }] }
  }
})

server.register_tool({
  name: "read_file",
  handler: function(args) {
    content = fs.readFile(args.path)
    return { content: [{ type: "text", text: content }] }
  }
})
Ver solución

Problema 1: SQL Injection

  • database.execute(args.sql) ejecuta cualquier SQL sin validación
  • Un atacante podría ejecutar DROP TABLE users o SELECT * FROM passwords
  • Fix: Usar prepared statements o limitar a queries SELECT

Problema 2: Path Traversal

  • fs.readFile(args.path) lee cualquier archivo del sistema
  • args.path = "/etc/passwd" o args.path = "../../secrets.env" serían válidos
  • Fix: Validar que path esté dentro de un directorio permitido

Problema 3: Sin manejo de errores

  • Si database.execute o fs.readFile fallan, la excepción no está manejada
  • El Server crashea en vez de retornar un error limpio
  • Fix: Envolver en try/catch, retornar isError: true

Problema 4: Sin validación de input

  • No hay inputSchema definido
  • El Server acepta cualquier argumento sin validar tipo o formato
  • Fix: Definir inputSchema con tipos y restricciones

Problema 5: Sin límites de tamaño

  • read_file podría intentar leer un archivo de 10GB
  • run_query podría retornar millones de registros
  • Fix: Limitar tamaño de archivos y resultados de queries

Resumen

En esta cápsula aprendiste:

  • El MCP Server es tu código — el programa que expone capabilities al Host via el Client
  • Expone 3 tipos de capabilities: Tools (funciones), Resources (datos), Prompts (templates)
  • Cada tool tiene un inputSchema que valida parámetros y guía al modelo
  • Las respuestas siguen un formato estándar con content array y isError flag
  • Los Servers pueden ser stateless (filesystem) o stateful (database)
  • Existen 4 patrones comunes: API wrapper, database, filesystem, agregación
  • La seguridad es responsabilidad del Server: validar inputs, limitar acceso, manejar errores
  • El Server no decide qué requests atender — eso lo decide el Host

Próxima cápsula: Flujo completo request-response — unir las 3 capas en un flujo end-to-end trazando un request completo + mini-proyecto: diagramar la arquitectura de tu setup.


Recursos adicionales

  1. MCP Server Development Guide - Documentación oficial sobre MCP Servers
  2. MCP Tools Specification - Especificación de Tools
  3. MCP Resources Specification - Especificación de Resources
  4. MCP Prompts Specification - Especificación de Prompts
  5. JSON Schema Reference - Referencia para escribir inputSchemas
  6. MCP Server Examples - Repositorio oficial de MCP servers de referencia
  7. OWASP Input Validation Cheat Sheet - Guía de seguridad para validación de inputs

Siguiente cápsula: Flujo completo request-response — traza un request desde el usuario hasta el Server y de vuelta, uniendo Host, Client, y Server en un diagrama completo. Incluye el mini-proyecto del módulo.