MCP por dentro y su observabilidad profunda: el LSP de los agentes IA y cómo verlo todo con OpenTelemetry
TL;DR
Model Context Protocol (MCP) es el estándar que Anthropic publicó a finales de 2024 y que se ha convertido en 2026 en el protocolo dominante para conectar agentes IA con herramientas y datos externos. Su valor —el motivo por el que toda la industria lo ha adoptado en menos de 18 meses— es que resuelve un problema combinatorio: antes de MCP, integrar M apps IA con N herramientas requería M×N integraciones ad-hoc; con MCP, M + N. Es el mismo movimiento que hizo el Language Server Protocol en 2016 para los editores de código. La arquitectura es tres roles bien definidos —Host (la app IA), Cliente (la conexión, uno por servidor) y Servidor (la pieza que expone capacidades)—; las primitivas son seis —tres del lado servidor (Tools, Resources, Prompts), tres del lado cliente (Sampling, Roots, Elicitation)—; el protocolo es JSON-RPC sobre dos transportes —stdio para procesos locales, Streamable HTTP para remoto—. El reto operacional aparece cuando hay 10-20 servers MCP corriendo simultáneamente, cada uno con varias tools, conectados a un agente que encadena llamadas multistep: observar qué pasa, dónde fallan las cosas, cuánto cuesta cada tool, qué tenant invoca qué se vuelve crítico. La respuesta del ecosistema en 2026: las nuevas OpenTelemetry GenAI semantic conventions for MCP (ya estables), trace context propagation vía params._meta (porque JSON-RPC no lo trae nativo), FastMCP con instrumentación OTel built-in, MCP Gateways como capa centralizada (Traefik Hub, MintMCP, OpenObserve), y MCP Inspector para debugging interactivo. Este artículo recorre la arquitectura desde fuera hacia dentro, sitúa cada concepto en su lugar exacto, y baja al detalle de la observabilidad: trazas, métricas RED, casos de uso reales y trampas.
Este es el tercer post de la serie post-tracing. Posts previos: Evals y Guardrails. Aquí bajamos al protocolo que conecta agentes con herramientas, y cómo verlo en producción.
La analogía maestra (en tres versiones)
MCP es un protocolo de comunicación. Como cualquier protocolo, se entiende mejor con la analogía adecuada. Voy a darte tres porque cada una ilumina una faceta distinta y la combinación te deja entendiéndolo mejor que cualquier definición técnica.
Versión 1 — El USB-C de las apps IA (la oficial)
Es la analogía que Anthropic adoptó al presentarlo. Antes de USB-C, cada dispositivo electrónico tenía su propio conector. Tu móvil llevaba microUSB o Lightning, tu portátil un puerto propietario para alimentación, tus auriculares un jack 3.5mm, tu disco externo USB-A en una punta y mini-USB en la otra. Resultado: tres cajas llenas de cables específicos que se perdían, ninguno servía para dos cosas, comprar un dispositivo nuevo significaba comprar accesorios nuevos.
USB-C cambió eso. Un único conector físico que muchos protocolos atraviesan: datos (USB 3, USB 4, Thunderbolt), vídeo (DisplayPort), alimentación (Power Delivery), audio. Conectas cualquier cosa a cualquier cosa y funciona; los protocolos negocian arriba.
MCP juega el mismo rol para apps IA. Antes de MCP, cada aplicación que quería integrar herramientas con un LLM —Claude Desktop, Cursor, Continue, custom agents propios— inventaba su propia forma de hacerlo. Cada vendor de tools tenía que escribir N integraciones distintas, una por app. Resultado: fragmentación masiva, mucho código duplicado, integraciones que se rompían cuando una app cambiaba su API interna.
Con MCP, el conector es uno: cualquier app que hable MCP puede usar cualquier herramienta MCP. Igual que tu USB-C habla a impresoras, monitores y discos sin que la impresora “sepa” que el cable está conectado a un Mac o a un Linux.
Versión 2 — El LSP de los editores de código (la más técnicamente precisa)
Esta es mi preferida porque la analogía es estructuralmente idéntica, no solo metafórica.
Hasta 2016, si querías que tu editor de código soportara un lenguaje nuevo —Rust, Go, TypeScript— alguien tenía que escribir un plugin específico para tu editor concreto. VSCode tenía su plugin de Rust, IntelliJ otro distinto, Vim otro, Emacs otro. Cada feature decente (go-to-definition, autocompletado, refactoring) era una implementación duplicada N veces. M editores × N lenguajes = M·N integraciones.
Microsoft propuso en 2016 el Language Server Protocol (LSP): cada lenguaje implementa un único “language server” (un proceso que entiende ese lenguaje); cada editor implementa un único cliente LSP; cuando trabajas con código Rust en VSCode, VSCode lanza rust-analyzer como subproceso y le habla LSP por stdio. Cualquier editor LSP + cualquier servidor LSP = funciona. M + N.
MCP es literalmente este patrón, trasladado de “editor + language server” a “app IA + tool provider”. Y comparte hasta el detalle técnico: ambos pasan JSON-RPC sobre stdio (entre otros transportes). Cuando Anthropic diseñó MCP, miraron a LSP. Quien venga del mundo de editores e IDEs encontrará MCP familiar.
Versión 3 — El driver del sistema operativo (la operativa)
Por último, una analogía que ayuda a entender lo que hace un MCP server concreto.
Un sistema operativo no sabe directamente cómo hablar con tu impresora HP LaserJet específica. Lo que sabe es una interfaz genérica: “imprimir documento”, “consultar estado”, “cancelar tarea”. El driver de impresora es la pieza que traduce esa interfaz genérica a los comandos propietarios de tu impresora específica.
Un MCP server hace exactamente lo mismo:
- Tu agente IA sabe una interfaz genérica: invocar una tool con un schema definido, leer un resource por URI, pedir un prompt template por nombre.
- El MCP server es el driver: traduce esas operaciones genéricas a las API concretas del sistema underlying —tu base de datos PostgreSQL, tu filesystem, tu API GitHub, tu Stripe—.
Esto deja al agente IA libre de saber cómo se autentica con GitHub, qué SQL exacto usa PostgreSQL, qué endpoints tiene Stripe. Habla MCP; el server se encarga de los detalles.
Con las tres analogías combinadas: MCP es la capa entre el LLM y el mundo, un USB-C estándar implementado como LSP en JSON-RPC, con cada server actuando de driver para un sistema underlying concreto.
Qué problema concreto resuelve MCP
Antes de bajar a la arquitectura, conviene fijar el problema específico que MCP resuelve, porque sin eso muchas decisiones de diseño parecen arbitrarias.
El problema es el coste cuadrático de las integraciones.
Imagina que tienes M aplicaciones que usan LLMs (Claude Desktop, Cursor, Continue, ChatGPT Desktop, tu propio agente custom, …) y N herramientas externas que esos LLMs podrían usar (filesystem, GitHub, Slack, PostgreSQL, Jira, Notion, …). Sin un estándar:
- Cada par (aplicación, herramienta) requiere una integración específica.
- Cada vez que la aplicación cambia su API interna, hay que actualizar N integraciones.
- Cada vez que la herramienta cambia su API, hay que actualizar M.
- Para que tu herramienta nueva sea adoptada, tienes que escribir M integraciones.
- Para que tu aplicación nueva soporte el ecosistema, tienes que escribir N.
Resultado real en 2023-2024: fragmentación masiva. Function calling de OpenAI no era compatible con tool use de Anthropic; cada framework (LangChain, LlamaIndex, dspy) tenía su propio wrapper; los plugins de Claude Desktop no funcionaban en Cursor; etc.
MCP rompe la cuadratura. Cada aplicación implementa el protocolo una vez; cada herramienta implementa el protocolo una vez; cualquier par funciona. M + N.
Es exactamente lo que pasó con USB-C, con LSP, con SQL (antes había APIs propietarias por base de datos), con POSIX (antes había APIs propietarias por sistema operativo). El patrón se repite porque resuelve siempre el mismo tipo de problema.
La arquitectura: tres roles, situados con claridad
Vamos a fijar dónde vive cada cosa, porque mezclar los roles es la fuente número uno de confusión en MCP.
Tres roles. Vamos a fijar qué hace cada uno y dónde vive físicamente.
Host: la aplicación IA
El Host es la aplicación que el usuario abre. Claude Desktop, Cursor, Continue, ChatGPT Desktop, un agente custom que tu equipo construye, una extensión de VSCode. Lo que el usuario percibe como “el producto”.
El Host es el responsable de:
- Decidir qué servidores MCP conectar (configurados por el usuario en un archivo o vía UI).
- Lanzar o conectar con cada servidor MCP.
- Crear un Cliente MCP por servidor (es 1:1, no comparten).
- Embeber el LLM (o llamarlo vía API) que toma las decisiones de qué herramientas usar.
- Mediar la autorización del usuario para acciones sensibles (mostrarle al humano “el agente quiere ejecutar X tool, ¿permites?”).
Importante: el LLM vive dentro del Host, no en los servidores. Los servidores son tontos; ejecutan operaciones cuando se les pide. El razonamiento ("¿debería llamar a esta tool ahora?") vive en el LLM del host.
Cliente: la conexión, una por servidor
Un Cliente MCP es una conexión específica entre el Host y un Servidor. Si tu Host tiene 5 servidores MCP configurados, tiene 5 clientes, no uno compartido. Cada cliente:
- Mantiene su socket o stdio pipe con el servidor.
- Negocia capacidades en el handshake inicial (qué versión del protocolo, qué primitivas soportan ambos).
- Serializa requests JSON-RPC al servidor y deserializa respuestas.
- Es el punto donde el Host invoca operaciones del servidor.
La separación 1:1 cliente-servidor es importante porque permite que cada server tenga su propio estado de sesión, sus permisos específicos y su contexto autenticado independiente. No hay multiplexación en el cliente.
Servidor: la pieza que expone capacidades
El Servidor MCP es la pieza que implementa el lado tool-provider del protocolo. Recibe JSON-RPC del cliente, lo procesa, ejecuta la acción contra el sistema underlying y devuelve respuesta.
Hay dos sabores físicamente:
- Servidor local: arranca como subproceso del Host, comunica por stdio. Su ciclo de vida es el del Host (cuando cierras Claude Desktop, los servidores locales mueren). Modelo típico: tu Host lanza
node filesystem-mcp-server.jscomo hijo. - Servidor remoto: corre como servicio independiente, accesible por HTTP. Multi-tenant, autenticado, escalable. Modelo típico: una empresa publica
https://mcp.acme.com/v1y muchos hosts se conectan.
Esta diferencia tiene consecuencias enormes en observabilidad (volveremos en breve).
Resumen del lugar de cada cosa
| Componente | Vive en | Hay cuántos | Habla qué con quién |
|---|---|---|---|
| Host | Máquina del usuario | 1 (la app abierta) | UI con usuario; lanza clientes |
| LLM | Embebido en Host (o cloud API) | 1 (el principal) | Razona; pide tools |
| Cliente | Host | 1 por servidor | JSON-RPC con su servidor |
| Servidor local | Subproceso del Host | 1 por integración local | stdio con su cliente |
| Servidor remoto | Servicio externo | 1 por servicio | HTTP/SSE con sus clientes |
| Sistema underlying | Externo | Depende | API/DB/FS, no MCP |
Si te confundes en discusión, vuelve a esta tabla. La fuente número uno de errores en MCP es decir “el servidor” cuando se quiere decir “el host”.
Las dos capas del protocolo
MCP separa data layer y transport layer. Esta separación es la que permite que el protocolo funcione por stdio local y por HTTP remoto sin cambiar nada en las primitivas.
Data Layer: JSON-RPC con extensiones MCP
La capa de datos define el vocabulario de los mensajes. Es JSON-RPC 2.0. Cada mensaje es un JSON con jsonrpc: "2.0", un method (eg tools/call, resources/read), params, e id para correlar request con response.
Encima de JSON-RPC, MCP añade:
- Lifecycle: el handshake inicial (
initialize,initialized) que negocia capacidades. - Las primitivas (siguiente sección):
tools/*,resources/*,prompts/*,sampling/*, etc. - Notifications: mensajes sin respuesta (eg
notifications/cancelledpara abortar una tool en curso). - Meta-information: el campo
params._metapor convención lleva metadata transversal (trace context, request IDs).
Transport Layer: cómo se mueven los mensajes
La capa de transporte define cómo viajan los mensajes JSON-RPC. Dos transportes oficiales:
stdio: el cliente lanza el servidor como subproceso y se comunican por sus stdin/stdout/stderr con JSON-RPC. Un mensaje por línea, separados por newline. Sin red, sin handshake TLS, sin auth (la confianza se hereda del propio sistema operativo: si lanzas el subproceso, le confías). Latencia mínima (~100 μs round-trip), ancho de banda máximo (memcpy, no socket).
Caso de uso: servidores locales que viven en la misma máquina que el host. La mayoría de servidores MCP que ves en directorios públicos son stdio.
Streamable HTTP: el cliente envía POST a un endpoint HTTP del servidor; el servidor responde con JSON, opcionalmente abre un stream Server-Sent Events para enviar notificaciones asíncronas o respuestas largas. Auth por bearer token, API key o headers custom.
Introducido en la spec de noviembre 2025, sustituye al transporte SSE puro de versiones anteriores que tenía limitaciones de bidireccionalidad. Caso de uso: servidores remotos que sirven a muchos clientes simultáneos, con autenticación y multi-tenancy.
Importante: las primitivas son las mismas en ambos transportes. Un tools/call es idéntico en stdio y en HTTP. El transport es accidental, no fundamental.
Las seis primitivas: situadas en la arquitectura
Aquí está la chicha. Hay seis primitivas en MCP. Suelen confundirse porque varias parecen hacer cosas similares. La clasificación clave: tres viven del lado servidor (server expone, cliente consume) y tres del lado cliente (cliente expone, servidor consume).
Server-side: lo que el servidor le da al host
Tools son acciones que el servidor expone. Cada tool tiene un schema (parámetros tipados, descripción) y una implementación. Cuando el LLM del host decide invocar una tool, el cliente envía tools/call al servidor, este la ejecuta y devuelve resultado.
- Ejemplo: el server
github-mcpexponecreate_issue(repo, title, body). El LLM del host decide “voy a crear un issue”, llama esta tool, github-mcp habla a la API de GitHub, devuelve el issue ID al LLM. - Lugar arquitectónico: el servidor las expone, el LLM las consume.
Resources son datos contextuales que el servidor expone, direccionables por URI. No son acciones; son lecturas de contenido. Un resource tiene URI (file:///path/to/doc.md, postgres://table/users), metadata y un endpoint para leer contenido.
- Ejemplo: el server
filesystem-mcpexpone como resources los archivos de los directorios autorizados. El LLM pideresources/readcon URIfile:///docs/api.mdy obtiene el texto. - Lugar arquitectónico: el servidor las expone, el host las lee (y opcionalmente las pasa al LLM como contexto).
Diferencia clave Tools vs Resources: Tools son verbos (ejecutan, modifican estado, tienen side effects); Resources son sustantivos (existen, se leen, son idempotentes). Si tienes algo que es “buscar texto en archivos” → probablemente Tool (acción). Si es “este archivo concreto” → Resource. La distinción importa para auditoría y permisos: tools requieren más control.
Prompts son plantillas de prompt parametrizadas que el servidor expone. El usuario o el host puede invocarlas para inyectar un patrón conversacional al modelo.
- Ejemplo: un server
code-review-mcpexpone un promptreview_diff(diff_text, style="strict")que devuelve un prompt completo bien escrito para pedirle al LLM que revise código. - Lugar arquitectónico: el servidor las expone, el usuario o el host las invoca, el LLM las recibe como input.
Los prompts son la primitiva menos usada de las tres; muchos servers ni los implementan. Pero permiten que un equipo publique buenos prompts como librería reutilizable, separados del agente.
Client-side: lo que el host le da al servidor
Aquí es donde MCP se diferencia de protocolos como HTTP REST: el servidor también puede pedir cosas al host, no es solo una vía. Tres primitivas viajan en esa dirección.
Sampling: el servidor pide al host que ejecute una generación con su LLM. Es decir, el servidor toma prestado el LLM del host para razonar.
- Ejemplo: el server
search-mcprecibe una query del agente, busca en su corpus, encuentra 50 resultados y necesita resumirlos antes de devolver. En vez de tener su propio LLM, manda unsampling/createMessageal cliente; el host pasa esto a su LLM, ejecuta la generación con permisos del usuario, devuelve el resumen al servidor. - Lugar arquitectónico: el servidor lo pide, el host (con su LLM y la autorización del usuario) lo cumple.
- Por qué importa: el usuario controla qué modelo se usa, qué coste se paga, qué permisos aplican. El servidor no necesita su propia API key de OpenAI.
Roots: el host le dice al servidor dónde mirar. Roots son URIs (directorios, repositorios, namespaces) que el host autoriza al servidor a explorar.
- Ejemplo: tu Claude Desktop arranca
filesystem-mcpcon roots[file:///Users/yo/proyectos]. El servidor sabe que solo debe operar dentro de esa carpeta, no en/etc/passwd. - Lugar arquitectónico: el host las declara en el handshake, el servidor las respeta.
Elicitation: el servidor pide al host información adicional al usuario humano vía UI estructurada.
- Ejemplo: el server
stripe-mcpestá a punto de procesar un refund de 5000€. Antes de ejecutar, mandaelicitation/createMessageal cliente; el host muestra al usuario “Confirma este refund de €5000” con un botón; cuando el usuario confirma, devuelve OK al server, que entonces procede. - Lugar arquitectónico: el servidor pide, el host muestra al usuario, el usuario decide, la respuesta vuelve al servidor.
- Es la primitiva clave para human-in-the-loop en acciones sensibles.
Visualización del flujo de las seis primitivas
HOST SERVIDOR
│ │
Server-side ─────┼─────────────────────────────────────┤
│ │
tools/list ──────┼────── pregunta qué tools hay ──────▶│
│◀────── devuelve lista ──────────────│
│ │
tools/call ──────┼────── ejecuta esta tool ───────────▶│
│◀────── resultado ──────────────────│
│ │
resources/read ──┼────── lee este URI ────────────────▶│
│◀────── contenido ─────────────────│
│ │
prompts/get ─────┼────── dame este prompt ────────────▶│
│◀────── prompt compilado ──────────│
│ │
Client-side ─────┼─────────────────────────────────────┤
│ │
sampling ────────│◀────── necesito una generación ─────│
│── usa mi LLM ───┐ │
│── devuelve ─────▼──────────────────▶│
│ │
roots ───────────┼─── declarados en handshake ────────▶│
│ │
elicitation ─────│◀────── pregunta al usuario X ───────│
│── muestra UI ──┐ │
│── confirma ────▼───────────────────▶│
El JSON-RPC en acción: un ejemplo concreto
Para que la teoría se materialice, una conversación MCP real entre cliente y servidor filesystem-mcp:
// 1. Handshake inicial (cliente → servidor)
{
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2026-03-01",
"capabilities": {
"sampling": {}, // este cliente soporta sampling
"roots": { "listChanged": true }
},
"clientInfo": { "name": "ClaudeDesktop", "version": "1.2.0" }
}
}
// 2. Server responde con sus capabilities
{
"jsonrpc": "2.0", "id": 1, "result": {
"protocolVersion": "2026-03-01",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": true, "listChanged": true },
"prompts": {}
},
"serverInfo": { "name": "filesystem-mcp", "version": "0.5.2" }
}
}
// 3. Cliente pide listado de tools
{
"jsonrpc": "2.0", "id": 2, "method": "tools/list"
}
// 4. Server devuelve sus tools con schema
{
"jsonrpc": "2.0", "id": 2, "result": {
"tools": [
{
"name": "read_file",
"description": "Read a file from the filesystem",
"inputSchema": {
"type": "object",
"properties": { "path": { "type": "string" } },
"required": ["path"]
}
},
{ "name": "write_file", "description": "...", "inputSchema": {} },
{ "name": "list_directory", "description": "...", "inputSchema": {} }
]
}
}
// 5. El LLM decide llamar read_file; cliente envía tools/call
{
"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {
"name": "read_file",
"arguments": { "path": "/Users/yo/proyectos/notas.md" },
"_meta": { // ← extensión donde irá trace context
"traceparent": "00-abc123...-def456-01"
}
}
}
// 6. Server devuelve contenido del archivo
{
"jsonrpc": "2.0", "id": 3, "result": {
"content": [
{ "type": "text", "text": "# Mis notas\n\n..." }
]
}
}
Lo importante a notar: params._meta. Ese es el bag donde MCP convencionalmente pasa metadata transversal, incluyendo trace context. Volveremos en breve.
El problema de observabilidad: por qué tracing tradicional no basta
Hasta aquí la teoría. Bajemos al problema operacional: en un cluster de producción 2026, un agente típico tiene 5-15 servidores MCP conectados simultáneamente, cada uno con 5-20 tools, y cada conversación con el agente puede generar decenas de llamadas a tools encadenadas. Sin observabilidad, depurar incidencias es imposible.
Por qué el tracing genérico (Hubble, OTel sin convenciones MCP) no es suficiente:
Stdio no se ve en la red. Los servidores locales hablan por pipes del SO. Tu Hubble o tu Datadog APM no ven nada; no hay paquetes que capturar. AgentSight (visto en el post anterior de la serie eBPF) con stdiocap lo captura pero da el JSON-RPC en crudo, sin contexto semántico (qué tool es, qué resource, qué prompt).
HTTP genérico tampoco entiende MCP. Si trazas el HTTP a un servidor MCP remoto sin convenciones MCP, ves un POST a /v1 con un body JSON-RPC opaco. Pierdes “qué tool se invocó”, “qué argumentos”, “fue elicitation o sampling”. Métricas RED por endpoint no te sirven; necesitas RED por tool.
JSON-RPC no propaga trace context nativo. A diferencia de HTTP (W3C traceparent header) o gRPC (metadata), JSON-RPC no tiene un campo estándar para trace context. Si no propagas, cada llamada al servidor empieza un trace nuevo desconectado del trace del agente.
Multistep multi-server es muy difícil de seguir. Una sola conversación del usuario puede traducirse en: 1) call a github-mcp get_pr; 2) call a filesystem-mcp read_file para varios archivos; 3) llamada al LLM principal con todo el contexto; 4) call a postgres-mcp query; 5) call a slack-mcp send_message. Sin trace context propagado, son cinco traces inconexos. Con propagación, es un árbol.
La solución: OpenTelemetry semantic conventions for MCP, ya estables en 2026.
OpenTelemetry semantic conventions for MCP
Las GenAI MCP semantic conventions son el set de atributos estandarizados para spans y métricas relacionados con MCP. Se publicaron como parte del subgrupo GenAI de OpenTelemetry SIG y son la primera parte de las semantic conventions GenAI que llegó a estable.
Por qué semantic conventions específicas
Antes de tenerlas, los equipos instrumentaban MCP con las RPC semantic conventions genéricas (las que usarías para gRPC o XML-RPC). Funcionaba a medias. Las conventions MCP-específicas añaden:
- Atributos para identificar qué primitiva se ejecutó (
mcp.method.name = "tools/call"). - Atributos para identificar qué tool/resource/prompt concreto se tocó (
mcp.tool.name,mcp.resource.uri,mcp.prompt.name). - Atributos para el flujo bidireccional (sampling/elicitation requests del servidor al cliente).
- Atributos para el handshake (
mcp.protocol.version,mcp.client.name,mcp.server.name). - Métricas RED estandarizadas por tool (
mcp.tool.call.duration,mcp.tool.call.errors).
Los atributos canónicos
Los atributos que cualquier instrumentación MCP-aware debería emitir:
| Atributo | Significado | Ejemplo |
|---|---|---|
mcp.method.name | Método JSON-RPC | "tools/call" |
mcp.tool.name | Nombre de la tool | "read_file" |
mcp.resource.uri | URI del resource | "file:///docs/api.md" |
mcp.prompt.name | Nombre del prompt | "code_review" |
mcp.session.id | ID de sesión MCP | "sess-abc123" |
mcp.protocol.version | Versión del protocolo | "2026-03-01" |
mcp.client.name | Identidad del cliente | "ClaudeDesktop/1.2.0" |
mcp.server.name | Identidad del servidor | "filesystem-mcp/0.5.2" |
mcp.transport | Transporte usado | "stdio" o "http" |
mcp.error.code | JSON-RPC error code | -32602 (Invalid params) |
gen_ai.usage.input_tokens | Tokens consumidos (si sampling) | 1240 |
gen_ai.usage.output_tokens | Tokens generados (si sampling) | 512 |
Los dos últimos vienen de las semantic conventions GenAI genéricas y se aplican cuando la llamada MCP involucra sampling (servidor usando el LLM del cliente).
Métricas RED por tool
Más allá de los spans, las semantic conventions definen tres métricas core:
mcp.tool.call.duration(histograma): latencia de cada invocación.mcp.tool.call.count(counter): número total de invocaciones.mcp.tool.call.errors(counter): errores por tool.
Etiquetadas con mcp.tool.name, mcp.server.name, mcp.client.name. Pivotables en Grafana para responder “qué tool es la más lenta”, “qué tool falla más”, “qué cliente carga más a qué server”.
Trace context propagation: el truco del params._meta
JSON-RPC no tiene cabeceras como HTTP, así que MCP no puede usar traceparent header de W3C directamente. La solución que el ecosistema ha consensuado: propagar trace context en params._meta.
Cuando el cliente MCP envía un tools/call, su instrumentación OTel hace:
import json
from opentelemetry.propagate import inject
carrier = {}
inject(carrier) # rellena con traceparent/tracestate del span activo
params = {
"name": "read_file",
"arguments": {"path": "/notas.md"},
"_meta": carrier, # ← propaga trace context
}
Cuando el servidor recibe, hace lo simétrico:
from opentelemetry.propagate import extract
ctx = extract(request.params.get("_meta", {}))
with tracer.start_as_current_span("tools/call", context=ctx):
# esta span es hija de la del cliente
return execute_tool(request.params)
Resultado: el span del servidor es hijo del span del cliente en el árbol de traces. Cuando ves la trace en Tempo o Phoenix, ves toda la cadena: usuario → host → cliente → server → ejecución → respuesta → cliente → host → respuesta al usuario.
Esto requiere que ambos extremos instrumenten consistentemente. Si el server no extrae el contexto, ves spans desconectados pero al menos tienes traceability del lado cliente.
Patrones de instrumentación
Hay tres caminos para instrumentar MCP, en orden creciente de esfuerzo:
1. FastMCP con OpenTelemetry built-in
FastMCP es uno de los frameworks Python más usados para construir servidores MCP. Trae instrumentación OpenTelemetry built-in: cada tool, resource template, prompt operation genera spans automáticamente con las conventions MCP correctas.
from fastmcp import FastMCP
from opentelemetry.sdk.trace.export import OTLPSpanExporter
mcp = FastMCP("my-server", otel_endpoint="https://otel-collector:4318")
@mcp.tool()
def search_docs(query: str) -> str:
"""Search the corpus for matching documents."""
# esto genera automáticamente un span con
# mcp.tool.name=search_docs, mcp.method.name=tools/call, etc.
return run_search(query)
Cero código de instrumentación. Spans con conventions correctas. Es el patrón recomendado si arrancas un servidor MCP en Python desde cero.
2. OpenTelemetry SDK manual
Para servidores ya existentes o en otros lenguajes (TypeScript, Go), la opción es instrumentar manualmente con el SDK estándar OTel + emitir los atributos MCP convencionales:
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
async def handle_tools_call(req: JSONRPCRequest):
ctx = extract_trace_context(req)
with tracer.start_as_current_span("mcp.tools.call", context=ctx) as span:
span.set_attribute("mcp.method.name", "tools/call")
span.set_attribute("mcp.tool.name", req.params["name"])
span.set_attribute("mcp.server.name", "filesystem-mcp")
try:
result = await execute_tool(req.params)
return result
except Exception as e:
span.set_attribute("mcp.error.code", -32603)
span.record_exception(e)
raise
Más boilerplate pero funciona con cualquier servidor existente.
3. MCP Inspector para debugging interactivo
MCP Inspector (oficial) es una herramienta de debugging interactivo a nivel protocolo. Lanza un proxy local (puerto 6277) entre tu cliente y el servidor, y abre una UI web (puerto 6274) donde ves cada mensaje JSON-RPC ida y vuelta en tiempo real.
No es observabilidad de producción —es desarrollo y depuración—. Pero es insustituible durante el bring-up de un servidor nuevo: ves exactamente qué requests llegan, qué responses se devuelven, qué errores se producen. Ahorra horas de logging ad-hoc.
MCP Gateways: la pieza centralizada para enterprise
Cuando tu organización tiene muchos agentes conectándose a muchos servidores MCP, gestionar la matriz de conexiones se vuelve operacionalmente serio. La pregunta natural —"¿puede haber un proxy delante de todos los MCP servers que centralice auth, rate limiting, logging y observabilidad?"— ya tiene respuesta: MCP Gateways.
Un Gateway MCP es un proxy que:
- Acepta conexiones MCP de los hosts/agentes.
- Las enruta a los servers MCP backend correspondientes.
- Aplica autenticación y autorización centralizada (qué agente puede llamar qué tool).
- Aplica rate limiting por agente, por tool, por tenant.
- Observa: emite métricas OTel de cada operación pasante.
- Propaga identidad del agente al servidor backend (con varios modelos: token forwarding, token exchange, impersonación).
Las opciones que se han establecido en 2026:
- Traefik Hub MCP Gateway — del equipo de Traefik. Configuración declarativa, integración nativa con el ecosistema Kubernetes/Helm de Traefik.
- MintMCP — gateway con foco en observabilidad y multi-tenancy. SaaS y self-host.
- OpenObserve MCP Gateway — integrado con la plataforma de observabilidad OpenObserve.
Para deployments pequeños (un equipo, pocos agentes) un Gateway puede ser overkill. Para enterprise (decenas de agentes, decenas de servers, compliance regulado), es prácticamente obligatorio.
Casos de uso reales de la observabilidad MCP
Vamos a aterrizar con cinco casos donde la observabilidad MCP propiamente instrumentada da valor inmediato:
1. Audit por tool, por tenant, por agente
Pregunta: “¿quién ejecutó la tool delete_repo el mes pasado?”. Sin observabilidad MCP, imposible. Con conventions OTel + propagación de identidad: query en tu backend de traces filtrando por mcp.tool.name="delete_repo", agrupando por mcp.client.name o por user_id propagado en _meta. Compliance feliz.
2. Coste por tool y por tenant
Pregunta: “¿cuánto cuesta cada tool?”. Si las tools invocan APIs externas (Stripe, OpenAI sampling) o consumen recursos significativos (GPU para una tool de inferencia), saber su coste agregado importa. Con mcp.tool.call.duration + gen_ai.usage.* agregadas por tool y tenant, se construyen dashboards de cost accountability sin instrumentar nada extra.
3. Debug de cadenas multistep que fallan
Pregunta: “el agente falló al completar esta tarea, ¿dónde fue?”. El trace propagado conecta: span del usuario → span del LLM con su CoT → spans de cada tool invocada → span del LLM final. Si la cadena se rompió en la tercera tool, en Tempo se ve el span rojo con el mensaje de error específico. Reproducir el fallo es trivial.
4. Latencia y degradación de tools
Pregunta: “¿qué tool está degradando?”. Métricas RED por tool en Grafana muestran latencia p95/p99 a lo largo del tiempo. Cuando una tool empieza a subir de 200ms a 800ms (porque el servicio underlying se está colapsando), lo ves antes de que los usuarios se quejen.
5. Detección de loops y anomalías agentic
Pregunta: “¿algún agente está atascado en bucle?”. Si un agente llama tools/call read_file 80 veces en 30 segundos para el mismo path, claramente algo está mal. Alerta sobre mcp.tool.call.count agrupado por (session_id, tool_name) detecta esto. Combinado con detección de loops a nivel de razonamiento, cierra el círculo.
Trampas operativas
Falta de identity propagation
Tu Gateway autentica al agente, pero pasa requests al backend sin propagar identidad. Resultado: los logs del backend dicen “service-account” en todo, imposible auditar quién invocó qué. Elige una estrategia de propagación temprano: token forwarding (sencillo, expone tokens al backend), token exchange (más seguro), o impersonación con logging cruzado.
Servidores stdio que no aparecen en tu APM
Es la trampa nº1 del campo. Tu agente Cursor usa filesystem-mcp como stdio; no ves nada en Datadog porque no hay tráfico de red. Solución: instrumentar el servidor stdio con OTel SDK que exporta por OTLP a tu collector (vía gRPC o HTTP, OTel collector puede recibir aunque el server hable stdio con su cliente). O usar AgentSight stdiocap para capturar el JSON-RPC en crudo y procesarlo offline.
Múltiples versiones de protocolo en producción
Diferentes clientes usan distintas versiones de MCP simultáneamente. Tu metrics dashboard mezcla peras y manzanas. Etiqueta SIEMPRE con mcp.protocol.version y filtra/agrupa por ella.
_meta perdido al pasar por proxy
Tu Gateway acepta el request del cliente, lo reescribe para el backend, y se olvida de copiar params._meta. Resultado: trace roto en el Gateway, dos traces inconexos. Asegúrate de que tu Gateway preserva o re-inyecta trace context en cada hop.
Volumen de trazas con servers chatty
Algunos servers MCP emiten muchas pequeñas operaciones (filesystem listings, partial reads). Sin sampling, llenan tu backend de trazas inútiles. Aplica tail-based sampling que conserve sesiones completas o solo conserve traces con errores/latencia alta.
Cardinalidad en métricas
mcp.tool.call.duration con mcp.session.id como label explota la cardinalidad. No incluyas IDs únicos por sesión en labels; mantén la cardinalidad bajo control con labels que toman pocos valores discretos (tool name, server name, client name, error code).
Confundir spans del cliente y del servidor
Cuando ves el árbol, distingue: el cliente ve latencia total desde su perspectiva (incluye network); el servidor ve solo su trabajo. Si miras solo el span del servidor para depurar latencia percibida por el usuario, te pierdes el RTT. Usa ambos.
Lo que no hemos cubierto
- MCP transport WebSocket experimental: alternativa a Streamable HTTP, aún no estándar.
- Servidores MCP en cloud-native deployments con sidecars: patrón emergente de desplegar MCP servers como sidecars de pods.
- MCP federation: composición de varios servers como uno solo (similar a GraphQL federation).
- eBPF + MCP: cómo
stdiocapde AgentSight y los hooks de Cilium se complementan con la instrumentación nativa. - MCP testing y contract tests: cómo validar que tu servidor cumple la spec.
Referencias
Especificación y conceptos:
- Model Context Protocol — sitio oficial — entrada canónica.
- MCP architecture overview.
- Transports — MCP docs.
- MCP Inspector (GitHub) — debugging interactivo.
OpenTelemetry GenAI MCP:
- Semantic conventions for Model Context Protocol — OpenTelemetry — referencia normativa.
- Adding OpenTelemetry Trace Support to MCP (Discussion #269) — historia de la propuesta.
- How to Instrument MCP Servers with OpenTelemetry (OneUptime).
- How to trace MCP server tool calls with OpenTelemetry and Elastic APM.
- MCP Observability with OpenTelemetry (SigNoz).
- Distributed tracing for agentic workflows (Red Hat Developer).
- OpenTelemetry for AI Agents in MCP Workflows (MintMCP).
Frameworks y gateways:
- FastMCP OpenTelemetry — instrumentación built-in.
- Traefik Hub MCP Gateway — gateway de Traefik.
- MintMCP — gateway con foco en observabilidad.
- OpenObserve MCP Gateway guide.
- What is an MCP Gateway (DEV Community).
- OpenTelemetry MCP Server (Traceloop) — el patrón inverso: usar MCP para que agentes consulten traces OTel.
Cross-references:
- Post anterior: Guardrails y safety.
- AgentSight y el nuevo tracing de LLMs — donde se introdujo
stdiocappara capturar stdio de servidores MCP locales. - Evals: la capa después del tracing.