Ir al contenido
mcpd
sin daemon
Código

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.

RutaQué esClave
/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:

¿está ahí?
$ 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 contenidoCuerpo
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

trace_id Store Search Export

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_key hace 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 …, o X-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-bytes y --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ía content-encoding: gzip para 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 traceparent de 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',
})

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

A conformance panel with one row per language and one column per family of check. Every cell passes. The families are: the root span is the agent run; every child hangs off the root; a failed step makes the whole run an error, and only that step; the error carries a message, on the step that failed; the model call records its model; token usage and cost survive the wire; the scores land, with their scale; the feedback lands; ids are OpenTelemetry-shaped. The suite asserts 37 checks in each of the 13 languages.

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.

HerramientaQué 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ónValor
ComandoEl 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 clave ingest. 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