CodeGym /Cursos /ChatGPT Apps /Handshake y capabilities: cómo el cliente averigua qué sa...

Handshake y capabilities: cómo el cliente averigua qué sabe hacer el servidor

ChatGPT Apps
Nivel 6 , Lección 2
Disponible

1. Para qué sirve el handshake

Si los endpoints REST son un conjunto de puertas separadas a las que se llama por URL, MCP es más bien un diálogo continuo por un único canal. El cliente no envía peticiones aisladas: primero establece una sesión. El handshake es el momento de presentación al inicio de esa sesión.

En MCP este momento se implementa como una solicitud especial initialize, que el cliente envía inmediatamente después de establecer el transporte (STDIO, HTTP/stream, WebSocket — da igual). En la solicitud informa: «Hablo esta versión de MCP, esto es lo que puedo hacer y esto es quién soy». El servidor responde: «Yo admito esta versión y estas capacidades, encantado de conocerte».

Tras el intercambio exitoso, el cliente envía la notificación notifications/initialized y solo después empieza la vida “laboral”: tools/list, resources/list, tools/call y otras cosas útiles.

Si queremos una analogía, el handshake de MCP es como un contrato de alquiler antes de llevar servidores a un centro de datos. Mientras no se acuerdan las reglas (formato del protocolo, qué servicios presta el data center, quién paga) — mover servidores no tiene sentido.

Desde un punto de vista práctico, el handshake resuelve tres tareas:

  1. Comprueba la compatibilidad de las versiones del protocolo.
  2. Declara qué «primitivos» de MCP admite el servidor: tools, resources, prompts, logging, notificaciones, etc.
  3. Da metainformación sobre el cliente y el servidor — nombre y versión de la implementación.

2. Ciclo de vida de la conexión MCP: dónde vive el handshake

Para que no resulte algo abstracto, veamos un escenario típico (flow) de conexión, bastante simplificado:

sequenceDiagram
    participant C as Cliente (ChatGPT/Inspector)
    participant S as Servidor MCP

    C->>S: (1) Establecemos el transporte (STDIO/HTTP-stream)
    C->>S: (2) Solicitud: "initialize"
    S-->>C: (3) Resultado: "initialize" (capabilities, serverInfo)
    C->>S: (4) Notificación: "notifications/initialized"
    C->>S: (5) Solicitud: "tools/list" / "resources/list"
    S-->>C: (6) Resultado: listas de herramientas/recursos
    C->>S: (7) Solicitud: "tools/call" y otros

Técnicamente los pasos son así:

  1. Transporte establecido: por ejemplo, ChatGPT inicia tu servidor como subproceso y se conecta por STDIO, o Inspector hace una solicitud HTTP/stream a /mcp.
  2. El cliente envía la solicitud JSON-RPC initialize.
  3. El servidor responde con un resultado JSON-RPC con los campos protocolVersion, capabilities y serverInfo.
  4. El cliente envía la notificación notifications/initialized — señal: «ya lo he leído todo, podemos trabajar».
  5. El cliente invoca los métodos de discovery (tools/list, resources/list, prompts/list) en función de lo que haya visto en las capabilities del servidor.
  6. El servidor devuelve metadatos de herramientas/recursos/prompts.
  7. Después ya vienen las solicitudes “de trabajo”: tools/call, resources/read y otras.

Es importante notar que el handshake no es más que una llamada JSON-RPC normal initialize. Nada de magia. Tras la lección sobre el formato de mensajes MCP ya sabes cómo analizar estas solicitudes; la única diferencia es que aquí el método siempre es uno y “especial”, y se ejecuta primero.

3. Qué envía el cliente en initialize

Desglosamos la solicitud initialize por partes. Aproximadamente así puede verse una solicitud mínima (simplificada para la lección):

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "chatgpt-gift-client",
      "version": "2.3.0"
    }
  }
}

Este ejemplo es cercano a lo que muestra la documentación oficial de MCP. Los campos principales en params:

protocolVersion

Cadena con la versión de la especificación MCP, normalmente en formato de fecha, por ejemplo "2025-06-18". No es la versión de tu aplicación, sino la del propio protocolo. El cliente dice: «espero hablar esta versión de MCP». El servidor debe o bien confirmarla en la respuesta, o bien devolver un error si no la conoce.

Es una protección ante la situación «el cliente piensa una cosa, el servidor implementa otra». Si no se encuentra una versión común, es mejor cortar la conexión honestamente que intercambiar mensajes incompatibles.

capabilities del cliente

Objeto en el que el cliente declara qué capacidades de MCP admite él mismo. Por ejemplo, el cliente de ChatGPT suele indicar la clave elicitation, señalando que puede gestionar solicitudes al usuario (entrada adicional, confirmaciones, etc.).

Ejemplo:

"capabilities": {
  "elicitation": {},
  "sampling": {}
}

El servidor puede usar esta información para entender qué capacidades extendidas del protocolo tiene sentido utilizar. Por ejemplo, elicitation significa que el cliente (ChatGPT) puede hacer preguntas aclaratorias al usuario y solicitar datos adicionales.

clientInfo

Metainformación sencilla: nombre y versión del cliente.

"clientInfo": {
  "name": "ChatGPT",
  "version": "2.0.0"
}

Desde el punto de vista del desarrollador del servidor, esto es oro para los logs: siempre puedes ver qué cliente se ha conectado — ChatGPT, MCP Inspector, tu propio cliente de pruebas — y qué número de versión tiene.

4. Qué responde el servidor: resultado de initialize

La respuesta a initialize es un resultado JSON-RPC normal con el mismo id, pero en el campo result se coloca la descripción de lo que el servidor sabe hacer.

En la solicitud miramos las capabilities desde el lado del cliente — lo que él soporta. Ahora veamos el objeto espejo en la respuesta: las capabilities del servidor, es decir, lo que puede hacer él. Esquemáticamente:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": {
        "listChanged": true
      },
      "resources": {},
      "prompts": {},
      "logging": {}
    },
    "serverInfo": {
      "name": "gift-genius-backend",
      "version": "0.1.0"
    }
  }
}

Verás una estructura similar en la descripción oficial del protocolo y/o en la descripción del SDK. Partes principales:

protocolVersion en la respuesta

El servidor o bien repite la versión propuesta por el cliente, o (teóricamente) podría elegir otra versión común si hay varias. En implementaciones típicas simplemente se confirma la versión del cliente si el servidor la soporta. Si no — el servidor debe devolver un error y terminar la comunicación.

serverInfo

Metainformación sobre el servidor: nombre y versión.

"serverInfo": {
  "name": "gift-genius-backend",
  "version": "0.1.0"
}

Suena aburrido, pero son justamente estos datos los que usarás luego para filtrar y buscar en los logs: «por qué ChatGPT con la versión X no llega a un acuerdo con nuestro servidor versión Y».

capabilities del servidor

El campo más interesante. Aquí el servidor declara qué primitivos y extensiones MCP soporta: si puede manejar tools/*, resources/*, prompts/*, si sabe enviar notificaciones de cambios en las listas, etc.

Si en capabilities no está la sección tools, ningún cliente correctamente implementado intentará llamar a tools/list o tools/call. Del mismo modo, la ausencia de resources significa que el cliente no enviará resources/list ni resources/read.

Así, las capabilities funcionan como un contrato ligero: «qué se puede y qué no se puede hacer con este servidor».

5. Capabilities como «lista de superpoderes»

A partir de aquí nos interesan solo las capabilities del servidor — el objeto que llega en la respuesta a initialize y que determina qué primitivos MCP soporta ese servidor.

Veamos su estructura con más detalle. Ejemplo (simplificado, pero cercano a la especificación):

 {
"capabilities": {
  "tools": {
    "listChanged": true
  },
  "resources": {
    "subscribe": true,
    "listChanged": true
  },
  "prompts": {
    "listChanged": false
  },
  "logging": {}
}

Este ejemplo se analiza en la arquitectura oficial de MCP. Lo descodificamos por secciones.

Capabilities.tools

La presencia de la clave tools dice: el servidor sabe responder a los métodos tools/list y tools/call. Si además hay el flag listChanged: true, significa que el servidor en el futuro puede enviar notificaciones tools/list_changed cuando cambie el conjunto de herramientas.

Para ChatGPT esto es útil: se puede cachear la lista de herramientas y, al recibir list_changed, actualizarla sin reconectar por completo.

Capabilities.resources

La sección resources declara que el servidor soporta el trabajo con recursos: resources/list, resources/read, a veces búsqueda. Flags dentro:

  • subscribe: true — el cliente puede suscribirse a cambios de recursos (por ejemplo, para logs en vivo o actualizaciones de archivos).
  • listChanged: true — el servidor puede enviar la notificación resources/list_changed si se añaden o desaparecen recursos.

Esto es especialmente importante para grandes catálogos o datos «vivos» que cambian continuamente.

Capabilities.prompts

Si el servidor registra prompts predefinidos (por ejemplo, plantillas de llamadas al modelo ligadas a tu dominio), en capabilities aparece la clave prompts. También puede tener el flag listChanged.

Al ver esta sección, el cliente entiende que está disponible el método prompts/list y posiblemente prompts/get.

Capabilities.logging y otras

Algunas implementaciones de servidores también anuncian logging — significa que el servidor puede enviar logs estructurados al cliente por MCP, por ejemplo, para depuración.

También pueden aparecer otras secciones (por ejemplo, sampling u otras extensiones específicas). Lo importante es que el protocolo se diseñó desde el principio para ser extensible: puedes añadir nuevas claves en capabilities, y los clientes antiguos simplemente las ignorarán si no las conocen.

Insight

Se ha comprobado experimentalmente que ChatGPT App ignora los mensajes listChanged que se le envían. A día de hoy, al escribir una aplicación no puedes anunciar un conjunto de tools y después añadir o quitar más tools. Aunque el protocolo MCP lo permita.

En el momento de escribir este curso la situación es: en el momento del registro de tu aplicación en ChatGPT Store, ChatGPT solicita a tu aplicación la lista de tools y resources y la cachea para siempre. La probabilidad de que la situación cambie a lo largo de 2026 es alta; la probabilidad de que cambie durante el primer trimestre de 2026 es baja.

6. Discovery tras el handshake: cómo obtener la lista de herramientas y recursos

El handshake responde a la pregunta «qué sabe hacer el servidor». El siguiente paso es el llamado discovery: el cliente ya extrae por métodos concretos los detalles — qué herramientas hay exactamente, qué recursos están disponibles, qué prompts están definidos.

Para ello se usan los métodos de discovery: en esencia tools/list, resources/list, prompts/list. En la documentación de la arquitectura MCP se propone contarlo así: handshake → discovery → llamadas a herramientas.

Ejemplo de solicitud tools/list:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}

La respuesta del servidor contiene un array de herramientas: nombres, descripciones, JSON Schema de argumentos y a veces metadatos, como categorías o iconos.

Después de esto, ChatGPT (u otro cliente) cachea la lista y ya durante el diálogo la utiliza para:

  • elegir la herramienta adecuada para la tarea del usuario;
  • comprobar que el nombre de la herramienta existe;
  • validar los argumentos antes de enviar tools/call.

Con los recursos la historia es parecida, solo que resources/list a menudo soporta paginación vía cursores, para no traer de golpe un millón de registros. Esto también está descrito en la especificación MCP y se trata como un caso típico para grandes catálogos.

7. Handshake y capabilities con el ejemplo de nuestra aplicación GiftGen

En módulos anteriores construimos una aplicación de aprendizaje que ayuda a seleccionar regalos. Ya tenemos un widget, una herramienta suggest_gifts en el backend y algún catálogo de regalos. Ahora imaginemos cómo se ve el handshake para el servidor MCP gift-genius.

Ejemplo de handshake para GiftGen

Solicitud del cliente:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "ChatGPT",
      "version": "2.1.0"
    }
  }
}

Respuesta de nuestro servidor:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": { "listChanged": true },
      "resources": { "listChanged": true },
      "prompts": {},
      "logging": {}
    },
    "serverInfo": {
      "name": "gift-genius-backend",
      "version": "0.2.0"
    }
  }
}

En esencia, casi repetimos los ejemplos de la arquitectura oficial de MCP, simplemente adaptando los nombres a nuestra aplicación.

Qué aprende el cliente de esta respuesta:

  • Hay herramientas (tools) y la lista puede cambiar dinámicamente (listChanged: true).
  • Hay recursos (nuestro catálogo de regalos, posiblemente almacenado en archivos o en una BD).
  • Hay prompts (por ejemplo, una plantilla «Formula una descripción breve del regalo para el usuario N»).
  • El servidor puede enviar logs (útil para inspectores y depuración).

Luego el cliente hace tools/list y ve, por ejemplo, una herramienta así:

{
  "name": "suggest_gifts",
  "description": "Sugiere ideas de regalo según el perfil del destinatario.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "age": { "type": "integer" },
      "relationship": { "type": "string" },
      "budget": { "type": "number" }
    },
    "required": ["age", "relationship"]
  }
}

Y ahora, cuando el usuario escribe algo como: «Sugiere un regalo para mi hermana, 25 años, presupuesto hasta 50 dólares», el modelo ya sabe: existe la herramienta suggest_gifts con tal conjunto de argumentos, y se puede invocar mediante tools/call.

8. Cómo el SDK oculta el handshake (pero por qué sigue siendo importante entenderlo)

En el SDK de TypeScript para MCP (el que usaremos en la siguiente lección) toda esta historia con initialize y notifications/initialized está oculta en el método connect. Código aproximado:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "gift-genius",
  version: "1.0.0",
});

// Registro del instrumento: el SDK configurará capabilities.tools en base a esto
server.tool(
  "suggest_gifts",
  {
    description: "Sugiere ideas de regalo.",
    inputSchema: {
      type: "object",
      properties: {
        age: { type: "integer" },
        relationship: { type: "string" },
        budget: { type: "number" },
      },
      required: ["age", "relationship"],
    },
  },
  async (input) => {
    // ... lógica para seleccionar regalos ...
    return { suggestions: [] };
  },
);

const transport = new StdioServerTransport();

// Aquí el SDK:
// 1) recibe initialize del cliente,
// 2) responde con serverInfo y capabilities,
// 3) espera notifications/initialized,
// 4) después empieza a procesar las llamadas tools/*.
await server.connect(transport);

El SDK recopila automáticamente las capabilities en función de lo que hayas registrado: si hay al menos un server.tool(...), añadirá la sección tools a capabilities. Si registras recursos o prompts, aparecerán resources y prompts.

Entender el handshake y las capabilities no es para escribir JSON a mano (nunca lo hagas), sino para:

  • leer los logs de MCP y comprender por qué el cliente «no ve» tus herramientas;
  • diagnosticar incompatibilidades de versiones del protocolo;
  • implementar, si es necesario, un servidor personalizado o un transporte no estándar.

9. Versiones del protocolo y evolución de las capacidades

El campo protocolVersion en el handshake no es decorativo. En la especificación MCP se subraya claramente: es la forma de acordar una versión compatible del protocolo; si no se encuentra una versión común, es mejor finalizar la conexión.

Escenario típico:

  1. Despliegas un servidor MCP en producción con un SDK que implementa MCP versión "2025-06-18".
  2. Con el tiempo sale una nueva versión de MCP, actualizas el cliente, pero el servidor sigue siendo el antiguo.
  3. El cliente envía protocolVersion: "2026-02-01", el servidor no conoce esa versión y devuelve el error invalid protocol version (o similar).

La práctica demuestra: los desarrolladores a menudo ignoran este campo y luego se sorprenden de por qué no se establece la conexión.

Actitud correcta ante las versiones:

  • Saber siempre qué versión de MCP soporta tu SDK (normalmente en la documentación/notas de versión).
  • Al actualizar el SDK — actualizar conscientemente la versión del protocolo.
  • Los logs y el monitorizado deben mostrar explícitamente los errores de inicialización por discrepancia en protocolVersion.

La ampliación de capacidades a través de capabilities también está ligada a la evolución: las funciones nuevas de MCP se añaden como claves nuevas en capabilities. Los clientes antiguos las ignoran y los nuevos pueden usarlas. Este patrón se describe precisamente en la documentación oficial de MCP como forma de mantener compatibilidad hacia atrás.

10. El handshake visto por ChatGPT y por el inspector

Qué hace ChatGPT al conectarse a MCP

Cuando en Dev Mode vinculas un servidor MCP a ChatGPT, la plataforma entre bastidores hace aproximadamente lo siguiente:

  1. Abre el transporte (generalmente HTTP/stream en /mcp).
  2. Envía initialize con protocolVersion, capabilities y clientInfo (algo como «ChatGPT Enterprise, tal versión»).
  3. Recibe la respuesta y cachea las capabilities del servidor.
  4. Hace tools/list, resources/list, prompts/list según las capabilities vistas.
  5. Ya durante el diálogo, cuando el modelo decide llamar a una herramienta, consulta esta caché: si existe esa tool, cuál es su esquema de argumentos y cómo formatear la llamada.

Si las capabilities del servidor no contienen tools, ChatGPT ni siquiera intentará proponer tu App como herramienta. Si en capabilities hay resources, pero no tienen el flag listChanged, ChatGPT puede cachear la lista de recursos y no esperar notificaciones de cambios.

Cómo ayudan los inspectores y MCP Jam a la depuración

Herramientas como MCP Jam / MCP Inspector hacen prácticamente lo mismo: establecen la conexión, realizan el handshake, te muestran las capabilities del servidor y te permiten invocar manualmente tools/list, tools/call y demás.

Desde el punto de vista del desarrollador, es imprescindible:

  • se ve qué protocolVersion ha devuelto realmente el servidor;
  • se ve de inmediato si en capabilities están tools, resources, prompts;
  • se puede entender por qué ChatGPT no ve las herramientas (capabilities no declaradas o handshake fallido).

En la última lección de este módulo usarás estas herramientas con más profundidad, pero ya ahora viene bien entender que funcionan justo encima del handshake que estamos analizando.

11. Errores típicos al trabajar con handshake y capabilities

En teoría todo parece bastante directo, pero en la práctica precisamente el handshake y la declaración de capabilities suelen convertirse en la fuente de bugs muy primitivos — especialmente en Dev Mode o MCP Inspector. A continuación — varios errores típicos con los que casi seguro te toparás, ya sea en tu código o en los logs de tus compañeros.

Error n.º 1: Formato incorrecto de la solicitud initialize.
Un problema muy frecuente al implementar a mano un servidor MCP sin SDK es perder algún campo obligatorio de JSON-RPC. Por ejemplo, olvidar jsonrpc: "2.0", confundir method (escribir "init" en vez de "initialize") o convertir capabilities en un booleano en vez de un objeto. La especificación MCP espera un formato estricto; cualquier desviación lleva a errores de parsing y a romper la conexión. La documentación y las guías prácticas recomiendan asegurarse primero de que initialize cumple estrictamente la especificación antes de mirar cualquier otra cosa.

Error n.º 2: Ignorar protocolVersion.
A veces los desarrolladores simplemente copian un ejemplo de la documentación y ponen una cadena arbitraria sin mirar el soporte en el SDK. Como resultado, cliente y servidor hablan versiones distintas de MCP y la conexión no se establece. El error puede disfrazarse de «el cliente no se conecta». Debe tratarse protocolVersion como un contrato real: acordar esa versión entre el equipo de frontend/plataforma de agentes y el equipo que escribe el servidor MCP.

Error n.º 3: Capabilities olvidadas.
Situación clásica: registraste una herramienta en el servidor, pero al implementar a mano el handshake olvidaste añadir "tools": {} en las capabilities de la respuesta initialize. En el inspector ves que hay herramientas, pero ChatGPT muestra «No tools available» — porque confía en capabilities y no hace tools/list si la sección tools no está. Las guías de resolución de problemas del Apps SDK subrayan: si ChatGPT no ve las herramientas, lo primero es revisar las capabilities.

Error n.º 4: Intentar usar métodos no declarados en capabilities.
A veces los alumnos experimentan y, por ejemplo, envían resources/list a un servidor cuyo capabilities no tiene sección resources. Formalmente el servidor podría responder Method not found, pero lo correcto es no invocar esos métodos. MCP introduce capabilities precisamente como protección ante intentos así. El cliente debe mirar primero si existe la sección correspondiente en capabilities y solo entonces invocar los métodos.

Error n.º 5: El servidor empieza a “hablar” antes de notifications/initialized.
Si el servidor, nada más enviar la respuesta a initialize, empieza a mandar logs o notificaciones sin esperar notifications/initialized, algunos clientes pueden ignorar esos mensajes o incluso cortar la conexión. En la arquitectura oficial de MCP se subraya que primero debe terminar el handshake y solo después de la notificación de inicialización empieza la vida “laboral”.

Error n.º 6: Cambiar el esquema de herramientas sin señal de cambio de lista.
Cuando cambias el JSON Schema de una herramienta (vuelves un campo obligatorio, renombras un argumento), pero no reinicias el servidor o no envías la notificación de que la lista de herramientas ha cambiado, la caché del cliente puede contener la versión antigua del esquema. Esto produce errores de validación extraños. La especificación propone usar el flag listChanged y las notificaciones tools/list_changed y resources/list_changed para ayudar al cliente a actualizar su caché a tiempo.

Error n.º 7: Optimización prematura y “magia” en torno a capabilities.
A veces los desarrolladores empiezan a idear esquemas complejos con generación dinámica de capabilities, versionado por cliente y otra exotrería, sin haber entendido los mecanismos básicos. Al principio basta con declarar honestamente qué sabe hacer el servidor: tools, resources, prompts, logging. Amplía las capabilities a medida que haya una necesidad real, no “por si acaso”. Es más un antipatrón organizativo que un error puramente de protocolo, pero en proyectos en producción se ve muy a menudo.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION