Integración
Hay un daemon en ejecución en alguna parte. Estas son las tres formas de llegar hasta él desde fuera, y cómo elegir entre ellas.
mcpd tiene una sola puerta y tres formas de cruzarla. Todo lo que sigue habla con la misma superficie HTTP: los SDK son una comodidad encima de ella, y el servidor MCP es un cliente suyo.
Un puerto, cuatro superficies
Un único proceso sirve todo eso en un solo puerto, el 4317 por defecto. No hay sidecar, ni colector que ejecutar al lado, ni servidor de archivos estáticos para la interfaz.
| Ruta | Qué es | Clave |
|---|---|---|
/v1/traces, /v1/logs, /v1/metrics | Ingesta de OpenTelemetry. Cualquier SDK de OTel, sin código específico de mcpd en tu aplicación. | De ingesta |
/v1/ingest | Ingesta nativa. Un lote lleva cualquier mezcla de las seis señales y todo el modelo de datos, y una solicitud lleva un flujo de lotes. | De ingesta |
/v1/api/… | La API de consulta. La interfaz y los SDK leen por aquí y por ningún otro sitio. | De lectura, escritura, administración, execute o worker, declarada endpoint por endpoint |
/ | La interfaz del operador, compilada dentro del binario. | Ninguna para el marcado; sus datos pasan por la API de consulta |
Otras dos rutas, /healthz y /readyz, no piden
autenticación para que una sonda pueda alcanzarlas. Empieza por ahí cuando
estés averiguando si un endpoint responde siquiera:
$ curl -s "$MCPD/healthz"{"status":"ok","version":"0.1.0","spec_version":"1.0.0",…}$ curl -s "$MCPD/readyz"storage is not reachable: …# readyz touches the store, so a 503 here means the daemon is up# and the database behind it is not. La API HTTP, sin SDK
Si tu lenguaje tiene un cliente HTTP, puede hablar con mcpd. No hay nada que registrar, ningún proyecto que crear de antemano y ningún saludo inicial. Cada SDK es una comodidad sobre las solicitudes de abajo, y elegir esta vía te cuesta la agrupación en lotes, los reintentos y la generación de ids, que pasas a hacer tú.
Escribir una ejecución
POST /v1/ingest acepta un lote. Un lote puede llevar spans,
logs, métricas, puntuaciones, feedback y notas de aspecto a la vez, y esa es
la razón de que la puerta nativa exista junto a la de OTLP: las seis señales
comparten los ids de traza y de span, así que enviarlas en una sola
solicitud las mantiene correlacionadas sin una segunda búsqueda.
A ese endpoint llegan tres formas, y el tipo de contenido dice cuál es. Nada se deduce de los bytes, así que añadir un salto de línea dentro de una cadena no puede cambiar nunca lo que significa un cuerpo.
| Tipo de contenido | Cuerpo |
|---|---|
application/json | Un objeto de lote, que es lo que envía cada SDK |
application/json | Un array de objetos de lote, para un emisor que tiene varios a mano |
application/x-ndjson | Un lote por línea, leído según llega el cuerpo en vez de retenerlo entero. application/jsonl significa lo mismo |
Cada elemento es el lote que es: se almacena en su propia transacción
cuando lleva un ingest_key, de modo que un flujo reenviado se
reconoce línea a línea y no como un bloque, y los elementos sin clave se
almacenan juntos. Se rechaza la solicitud entera, sin almacenar nada, si
alguna línea no se puede leer o si la solicitud en conjunto supera
cualquiera de los dos límites. La respuesta es un solo
IngestResult que cubre todo.
-
Spans
What ran, nested, with timings
-
Logs
Lines the run emitted
-
Metrics
Numbers, with exemplars back to a run
-
Scores
Machine evaluation
-
Feedback
What a person thought
-
Aspect notes
A sentence a model wrote about the run
un lote
# One batch carries any mix of the five signals. Send the whole run at once.
curl -s -X POST "$MCPD/v1/ingest" \
-H 'content-type: application/json' \
-H "authorization: Bearer ${MCPD_API_KEY:-}" \
-d @- <<JSON
{
"ingest_key": "run-$TRACE",
"spans": [
{
"trace_id": "$TRACE",
"span_id": "$ROOT",
"name": "support_agent",
"kind": "agent_run",
"status": "ok",
"start_time": "$NOW", "end_time": "$NOW",
"is_root": true, "is_event": false, "received_at": "$NOW",
"input": { "question": "Where is order 4417?" },
"output": { "answer": "It is in the Leeds depot." },
"correlation": { "project": "default", "environment": "production" }
},
{
"trace_id": "$TRACE",
"span_id": "$GEN",
"parent_span_id": "$ROOT",
"name": "chat",
"kind": "model_generation",
"status": "ok",
"start_time": "$NOW", "end_time": "$NOW",
"is_root": false, "is_event": false, "received_at": "$NOW",
"model": "gpt-4o",
"provider": "openai",
"usage": { "input_tokens": 1284, "output_tokens": 210 },
"correlation": { "project": "default" }
}
]
}
JSON
# → {"accepted":2,"rejected":0,"duplicate":0}
#
# mcpd prices the model span from its own price book on the way in, so cost is
# stored with the run rather than recomputed later from rates that may have
# changed since. Cuatro cosas que la solicitud tiene que acertar:
- Los ids tienen la forma de OpenTelemetry. 32 caracteres hexadecimales en minúscula para una traza, 16 para un span, nunca todo ceros. Cualquier otra cosa rompe la correlación con las demás herramientas que ejecutas.
-
ingest_keyhace seguro un reintento. Es una clave de idempotencia estable para un lote, así que un reenvío se reconoce y se cuenta como duplicado en vez de escribirse dos veces. - La clave va en una cabecera.
Authorization: Bearer …, oX-Mcpd-Key. La ingesta necesita una clave de ingesta, y solo cuando el daemon tiene alguna configurada. - Los lotes tienen límites. 32 MiB de cuerpo y 10,000
señales por defecto, ambos ajustables con
--max-body-bytesy--max-batch-signals. El límite de señales cuenta la solicitud entera, así que repartir las mismas señales en varias líneas no lo esquiva. Envíacontent-encoding: gzippara cualquier cosa grande.
La respuesta informa de lo que pasó con cada señal:
{"accepted":2,"rejected":0,"duplicate":0}, con un array
errors que nombra el índice y el motivo de todo lo rechazado.
Los endpoints de OTLP responden en la forma propia de OTLP, con un objeto
partialSuccess que lleva el número de rechazos, porque es lo
que lee un emisor de OpenTelemetry.
Volver a leerlo
leer
# Read it back. Any filter in the API works here.
curl -s "$MCPD/v1/api/traces?project=default&limit=10"
# The whole run, with its logs, scores and feedback in one request.
curl -s "$MCPD/v1/api/traces/$TRACE?include=all"
# Find the run by something the model actually said.
curl -s --get "$MCPD/v1/api/traces" \
--data-urlencode 'project=default' \
--data-urlencode 'q=Leeds depot'
# Find the runs a scorer marked down. This is the query that finds bad answers.
curl -s --get "$MCPD/v1/api/traces" \
--data-urlencode 'project=default' \
--data-urlencode 'has_score=answer_relevance' \
--data-urlencode 'score_below=0.5'
# Where the money went, over the last week, by model.
curl -s --get "$MCPD/v1/api/analytics/cost" \
--data-urlencode 'project=default' \
--data-urlencode 'from=-7d' \
--data-urlencode 'group_by=model'
Los filtros se combinan, la paginación es por cursor, y
project toma el proyecto por defecto del daemon cuando lo
omites. La búsqueda de texto completo sobre los payloads almacenados es el
parámetro q, y es lo que convierte «encontrar la ejecución
donde el modelo dijo esto» en una consulta y no en un grep.
Un fallo que levanta el propio mcpd vuelve como JSON con un código
error estable y legible por una máquina, un
message escrito para quien tenga que arreglarlo, y un
field cuando la culpa es de un parámetro concreto. Ramifica
por el código, muestra el mensaje.
Hay una clase de fallo que no tiene esa forma, y un cliente que dé por
hecho que sí no conseguirá interpretar la respuesta. Un cuerpo de solicitud
que no se puede decodificar al tipo que el endpoint espera lo rechaza el
framework web antes de que se ejecute ningún código de mcpd, así que
responde 422 con content-type: text/plain y un
mensaje como missing field `scorer_id`. Una sintaxis de cuerpo
que ni siquiera es JSON responde 400, también como texto. En
ninguno de los dos casos hay un código error por el que
ramificar. Esto vale para todos los endpoints que reciben un cuerpo
tipado:
POST /v1/api/scores, /v1/api/feedback,
/v1/api/score, /v1/api/datasets, sus rutas
items y capture, y
/v1/api/experiments. Las puertas de ingesta leen el cuerpo
ellas mismas, así que /v1/ingest y las rutas de OTLP sí
devuelven la forma JSON. Trata un cuerpo que no es JSON en un 4xx como una
solicitud mal formada de tu propio código, y léelo como texto.
Los SDK
Seis lenguajes: TypeScript, Python, Rust, Go, Kotlin y Elixir. Cada uno es idiomático donde puede serlo e idéntico donde importa. Quien haya leído el inicio rápido de TypeScript puede predecir el de Kotlin.
Lo que ganas frente al HTTP a secas:
- Agrupación en lotes y un worker en segundo plano, así que registrar un span añade a una cola y vuelve.
- Reintentos con backoff ante un 429 y ante los códigos 5xx que merecen reintento, y nunca ante un 4xx que seguirá mal formado.
- Generación de ids, propagación del contexto, y el
traceparentde W3C analizado a la entrada y emitido a la salida, para que una traza sobreviva al límite entre dos servicios. - Tipos generados a partir de la misma especificación que los del daemon, para que un campo no pueda significar una cosa en el SDK y otra en el almacén.
Tres promesas se cumplen en todos los lenguajes, y son la razón de que un SDK se pueda poner en una ruta crítica. La instrumentación nunca lanza una excepción: un daemon inalcanzable se anota como mucho una vez por intervalo y el error se traga ahí. Nunca bloquea: el trabajo de red ocurre en el worker, así que la latencia de tu agente no depende de que mcpd esté levantado. Y su cola tiene un tope: cuando se llena, se descartan los spans más antiguos y sube un contador, porque un búfer sin límite convierte un corte de red en una muerte por falta de memoria.
conectar
import { Mcpd } from '@mcpd/sdk'
// Every field here also reads an environment variable, so the same code runs
// unchanged in development and in production. With no arguments at all this
// talks to a daemon on http://localhost:4317.
const mcpd = new Mcpd({
project: 'support',
environment: 'development',
serviceName: 'support-agent',
serviceVersion: '1.4.0',
}) from mcpd import FeedbackKind, Mcpd, SpanKind
# Everything here has an MCPD_* environment variable behind it, so the same code
# runs in development and in production with no edit. `Mcpd()` with no arguments
# talks to a local daemon.
mcpd = Mcpd(
endpoint=os.environ.get("MCPD_ENDPOINT", "http://localhost:4317"),
project="support",
environment="development",
service_name="support-agent",
service_version="1.4.2",
) // Nothing here is required. `Mcpd::new()` reads MCPD_ENDPOINT, MCPD_API_KEY
// and the rest from the environment and talks to a local mcpd by default.
// Anything set in code wins over the environment.
let mcpd = Mcpd::with_config(
Config::from_env()
.with_project("support")
.with_environment("local")
.with_service("support-agent", Some("1.0.0")),
); m := mcpd.New(
mcpd.WithEndpoint("http://localhost:4317"),
mcpd.WithProject("support"),
mcpd.WithEnvironment("development"),
mcpd.WithService("support-agent", "1.4.2"),
)
// Shutdown flushes what is still queued and stops the background worker.
// A short-lived process that skips it loses whatever had not been sent.
defer m.Shutdown(ctx) // Every field also reads an environment variable (MCPD_ENDPOINT,
// MCPD_PROJECT, MCPD_API_KEY, ...), so `Mcpd()` on its own works too.
val mcpd = Mcpd(
McpdConfig(
endpoint = "http://localhost:4317",
project = "support-desk",
environment = "development",
serviceName = "support-agent",
serviceVersion = "1.4.0",
)
) # Every option also reads an environment variable (MCPD_ENDPOINT, MCPD_PROJECT,
# MCPD_API_KEY, ...), so `Mcpd.start_link()` on its own works too.
{:ok, mcpd} =
Mcpd.start_link(
endpoint: "http://localhost:4317",
project: "support-desk",
environment: "development",
service_name: "support-agent",
service_version: "1.4.0"
) # mcpd needs no registration and no project to be created up front. Check that
# the daemon is up and which spec version it implements.
curl -s "$MCPD/healthz"
# → {"status":"ok","version":"0.1.0","spec_version":"1.0.0", ...}
Un cliente construido sin argumentos funciona contra un daemon local. Todo
se puede fijar en el código o por variable de entorno, y gana el código, y
los nombres de las variables son los mismos en todos los lenguajes, así que
un valor copiado entre dos servicios sigue funcionando cuando están
escritos en lenguajes distintos:
MCPD_ENDPOINT, MCPD_API_KEY,
MCPD_PROJECT, MCPD_ENVIRONMENT,
MCPD_SERVICE_NAME, MCPD_SERVICE_VERSION,
MCPD_SAMPLE y MCPD_ENABLED. Poner
MCPD_ENABLED=false convierte cada llamada en una operación
vacía que aun así devuelve objetos válidos, así que una suite de pruebas
puede apagar mcpd sin tocar código.
Son intercambiables, y eso está probado
Las pruebas unitarias demuestran que cada SDK hace lo que quisieron sus
autores. No demuestran nada sobre si los seis coinciden. Por eso la suite
de conformidad escribe una misma ejecución canónica desde cada lenguaje
contra un daemon vivo, más un séptimo canal que envía esa misma ejecución
solo con curl, y comprueba que lo que vuelve es
estructuralmente idéntico:
Cross-language conformance
tests/e2e/run.mjs
| Channel | root | tree | status | error | model | tokens | score | feedback | ids |
|---|---|---|---|---|---|---|---|---|---|
| TypeScript | |||||||||
| Python | |||||||||
| Go | |||||||||
| Elixir | |||||||||
| Kotlin | |||||||||
| Rust | |||||||||
| cURL | |||||||||
| TypeScript | |||||||||
| Python | |||||||||
| Go | |||||||||
| Elixir | |||||||||
| Kotlin | |||||||||
| Rust |
13 languages, one canonical run, one daemon. Every trace comes back the same shape.
481 checks
Si estás eligiendo lenguaje para un servicio nuevo, esta es la respuesta a «¿las trazas se verán igual que las que ya tenemos?».
Si tu lenguaje no es uno de los seis
Apunta un SDK de OpenTelemetry a /v1/traces y no hace falta
código nuevo. mcpd es un receptor OTLP, y mapea tanto el convenio
gen_ai.* de OpenTelemetry como el llm.* de
OpenInference, así que la instrumentación que ya tienes aterriza donde
debe. Consulta OpenTelemetry.
Desde un agente de IA, por MCP
La tercera vía es para un agente y no para un servicio: en lugar de que tu código lea las trazas, un agente de programación o de soporte las lee por ti y responde a una pregunta sobre ellas. «Por qué falló la última ejecución», «enséñame las ejecuciones que un evaluador puntuó bajo esta semana», «cuánto costó esta sesión». mcpd expone eso a través del Model Context Protocol.
Las tres herramientas
Tres, no treinta. Son los tres movimientos de una misma investigación: qué ejecución, qué pasó dentro de ella, y si es una ejecución o son todas.
| Herramienta | Qué responde |
|---|---|
find_runs | Qué ejecución. Una línea por cada una: id, antigüedad, estado, duración, costo, pasos. Filtra por estado, modelo, usuario, sesión o texto libre; ordena por duración para sacar las más lentas. Por defecto, las últimas 24 horas. |
get_run | Qué pasó. La cascada, el paso que falló y por qué, los prompts y las completions, los logs, las puntuaciones. Los payloads tienen un tope para que una sola ejecución no llene una ventana de contexto. |
run_stats | Una ejecución o todas. Costo, tasa de fallos, latencia y tokens en una ventana, agrupados por modelo, nombre, estado, entorno o usuario. Una tasa de fallos del 40% significa que la ejecución que estás leyendo no es el problema. |
Los resultados vuelven como texto y no como JSON, una decisión deliberada sobre el contexto de quién se gasta aquí. Cuarenta objetos de span son sobre todo signos de puntuación; la misma ejecución dibujada como cascada son unos cientos de tokens y el agente ve su forma.
Cómo está montado
El servidor MCP es un subcomando del mismo binario, no un segundo servicio.
Se ejecuta como mcpd mcp, habla JSON-RPC por la entrada y la
salida estándar, y no guarda datos propios. No hay puerto que abrir, ni
proceso que mantener vivo, ni nada que desplegar: el anfitrión del agente
lo arranca como proceso hijo cuando una conversación lo necesita y lo para
después.
Lo que sí mantiene es una conexión con un daemon. Cada llamada a una herramienta se convierte en una solicitud a la misma API de consulta descrita arriba, con la misma clave de lectura y el mismo ámbito de proyecto, así que un agente no puede llegar a un proyecto al que no llegue la clave que le diste. El radio de impacto de dar a un agente este servidor es exactamente el de darle esa clave, y esa es la propiedad que hay que comprobar antes de configurar uno.
El conjunto de herramientas es pequeño a propósito. Una superficie grande
hace que un modelo elija mal, y casi todas las preguntas sobre el
comportamiento de un agente se reducen a listar ejecuciones, abrir una y
hacer una pregunta numérica sobre una ventana. Un cliente descubre el
conjunto actual con la llamada tools/list del propio
protocolo, y no con una lista escrita aquí que podría quedarse atrás.
Configurar un cliente
Un anfitrión MCP lanza un servidor a partir de un comando, unos argumentos y un entorno. El archivo exacto y la clave exacta son del anfitrión y no de mcpd, así que consulta su documentación; los tres valores que necesita de ti son los mismos en todas partes.
| Qué necesita el anfitrión | Valor |
|---|---|
| Comando | El binario mcpd, en una ruta que el anfitrión pueda ejecutar. |
| Argumentos |
El subcomando mcp. Acepta --endpoint, el
daemon a consultar, y --api-key, una clave de lectura
para él, pensados para un anfitrión que pasa argumentos con más
facilidad que un entorno.
|
| Entorno |
Los mismos dos valores, como MCPD_ENDPOINT y
MCPD_API_KEY. El endpoint por defecto es
http://127.0.0.1:4317, y la clave se puede omitir cuando
el daemon no tiene claves configuradas.
|
lo que ejecuta el anfitrión
MCPD_ENDPOINT=https://mcpd.internal MCPD_API_KEY=$READ mcpd mcp Tres cosas que tener presentes cuando emitas esa clave.
-
Dale al agente una clave
read, no una claveingest. Los ámbitos existen para que una credencial que escribe spans no sea además una credencial para todos los prompts almacenados. - Limítala a un proyecto. Una clave configurada en el nivel superior llega a todos los proyectos del daemon.
-
Ten en cuenta que una clave de lectura también borra trazas hasta que
configures una lista
admin, porque el ámbito de administración recurre a la lista de lectura cuando no está definido. Un agente con una clave de lectura, en un daemon al que nunca se le dio una lista de administración, puede borrar una ejecución.
Las tres están en Seguridad.
Cuál usar
| Vía | Úsala cuando |
|---|---|
| HTTP | Tu lenguaje no tiene SDK, envías desde una shell o desde un trabajo programado, o no quieres ninguna dependencia. |
| Un SDK | Estás instrumentando una aplicación, y ahí los lotes, los reintentos y no lanzar nunca una excepción valen más que la dependencia. |
| OpenTelemetry | Ya emites OTel y prefieres cambiar una URL antes que añadir una biblioteca. |
| MCP | Una persona hace preguntas a un agente sobre ejecuciones almacenadas, en vez de que sea código quien las lee de forma programada. |
Se mezclan. Una flota que emite por el SDK de TypeScript, un trabajo
nocturno que publica puntuaciones con curl y un agente que lo
lee todo por MCP son un despliegue, no tres.
A continuación
- Qué se almacena y quién puede leerlo: Seguridad.
- Apuntar a mcpd una instalación de OTel que ya existe: OpenTelemetry.
- Todos los endpoints, generados a partir de la especificación: API HTTP.
- Las consultas que conviene conocer: Encontrar una ejecución.