Vai al contenuto
mcpd
nessun daemon
Codice

Integrazione

Un daemon gira da qualche parte. Ecco i tre modi in cui tutto il resto lo raggiunge, e come scegliere fra loro.

mcpd ha un solo ingresso e tre modi per attraversarlo. Tutto quello che segue parla alla stessa superficie HTTP: gli SDK sono una comodità posata sopra di essa, e il server MCP ne è un client.

Una porta, quattro superfici

Un solo processo serve tutto quanto su una sola porta, la 4317 come impostazione predefinita. Nessun sidecar, nessun collector da tenere acceso accanto, nessun server di file statici per l’interfaccia.

PercorsoChe cos’èChiave
/v1/traces, /v1/logs, /v1/metrics Ingestione OpenTelemetry. Qualsiasi SDK OTel, senza una riga di codice specifico per mcpd nella tua applicazione. Ingestione
/v1/ingest Ingestione nativa. Un batch porta qualunque mescolanza dei sei segnali e tutto il modello dei dati, e una richiesta porta un flusso di batch. Ingestione
/v1/api/… L’API di query. L’interfaccia e gli SDK leggono da qui e da nient’altro. Lettura, scrittura, admin, execute o worker, dichiarata endpoint per endpoint
/ L’interfaccia operativa, compilata dentro il binario. Nessuna per il markup; i suoi dati passano dall’API di query

Altri due percorsi, /healthz e /readyz, non chiedono autenticazione, così una sonda arriva a interrogarli. Parti da lì quando stai cercando di capire se un endpoint sia raggiungibile o no:

c’è o no
$ curl -s "$MCPD/healthz"{"status":"ok","version":"0.1.0","spec_version":"1.0.0",…}$ curl -s "$MCPD/readyz"storage is not reachable: …# readyz tocca lo storage: un 503 qui vuol dire che il daemon è# attivo e che il database che ha dietro non lo è.

L’API HTTP, senza SDK

Se il tuo linguaggio ha un client HTTP, può parlare con mcpd. Non c’è niente da registrare, nessun progetto da creare in anticipo e nessuna stretta di mano. Ogni SDK è una comodità sopra le richieste qui sotto, e prendere questa strada ti toglie il raggruppamento in batch, i nuovi tentativi e la generazione degli identificatori, di cui poi ti occupi tu.

Scrivere un’esecuzione

POST /v1/ingest prende un batch. Un batch può portare insieme span, log, metriche, punteggi, feedback e note di aspetto, ed è la ragione per cui l’ingresso nativo esiste accanto a quello OTLP: i sei segnali condividono gli id di traccia e di span, quindi mandarli in una sola richiesta li tiene correlati senza una seconda ricerca.

Tre forme arrivano a quell’endpoint, e il content type dice quale. Niente viene dedotto dai byte, quindi aggiungere un a capo dentro una stringa non può mai cambiare il significato di un corpo.

Content typeCorpo
application/json Un solo oggetto batch, che è quello che manda ogni SDK
application/json Un array di oggetti batch, per un mittente che ne ha parecchi sottomano
application/x-ndjson Un batch per riga, letto mentre il corpo arriva anziché tenuto per intero. application/jsonl vuol dire la stessa cosa

Ogni elemento è un batch a sé: viene archiviato in una transazione propria quando porta un ingest_key, così un flusso rimandato è riconosciuto riga per riga anziché come un blocco unico, e gli elementi senza chiave sono archiviati insieme. L’intera richiesta è rifiutata, senza archiviare nulla, se una riga non si lascia leggere o se la richiesta nel suo insieme supera uno dei due limiti. La risposta è un solo IngestResult che copre tutto quanto.

  • 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 batch

# 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.

Quattro cose che la richiesta deve fare nel modo giusto:

  • Gli identificatori hanno la forma di OpenTelemetry. 32 caratteri esadecimali minuscoli per una traccia, 16 per uno span, mai tutti zero. Qualunque altra cosa rompe la correlazione con ogni altro strumento che tieni acceso.
  • ingest_key rende sicuro un nuovo tentativo. È una chiave di idempotenza stabile per un batch, così un rinvio è riconosciuto e contato come duplicato anziché scritto due volte.
  • La chiave va in un header. Authorization: Bearer …, oppure X-Mcpd-Key. L’ingest chiede una chiave di ingest, e solo quando il daemon ne ha configurata qualcuna.
  • I batch hanno dei limiti. 32 MiB di corpo e 10.000 segnali come impostazione predefinita, entrambi regolabili con --max-body-bytes e --max-batch-signals. Il limite sui segnali conta l’intera richiesta, quindi spargere gli stessi segnali su più righe non lo aggira. Manda content-encoding: gzip per qualsiasi cosa di una certa mole.

La risposta riferisce che cosa è successo segnale per segnale: {"accepted":2,"rejected":0,"duplicate":0}, con un array errors che nomina l’indice e il motivo di ogni rifiuto. Gli endpoint OTLP rispondono invece nella forma propria di OTLP, con un oggetto partialSuccess che porta il conteggio dei rifiutati, perché è quello che legge un mittente OpenTelemetry.

Rileggerla

leggere

# 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'

I filtri si compongono, la paginazione va a cursore e project ricade sul progetto predefinito del daemon quando lo lasci fuori. La ricerca full-text sui payload archiviati è il parametro q, ed è quello che rende «trova l’esecuzione in cui il modello ha detto questo» una query e non un grep.

Un errore che mcpd solleva da sé torna indietro come JSON con un codice error stabile e leggibile da una macchina, un message scritto per chi dovrà rimediare, e un field quando è un parametro a essere in torto. Decidi in base al codice, mostra il messaggio.

Una classe di errori non ha quella forma, e un client che dà per scontato il contrario non riuscirà a leggere la risposta. Un corpo di richiesta che non si lascia decodificare nel tipo che l’endpoint si aspetta viene rifiutato dal framework web prima che giri una riga di codice di mcpd, quindi risponde 422 con content-type: text/plain e un messaggio come missing field `scorer_id`. Una sintassi del corpo che non è JSON per niente risponde 400, anche questo come testo. In nessuno dei due casi c’è un codice error su cui decidere. Vale per ogni endpoint che prende un corpo tipizzato: POST /v1/api/scores, /v1/api/feedback, /v1/api/score, /v1/api/datasets, le sue rotte items e capture, e /v1/api/experiments. Gli ingressi di ingest leggono il corpo da sé, quindi /v1/ingest e le rotte OTLP restituiscono davvero la forma JSON. Tratta un corpo non JSON su un 4xx come una richiesta malformata che arriva dal tuo codice, e leggilo come testo.

Gli SDK

Sei linguaggi: TypeScript, Python, Rust, Go, Kotlin ed Elixir. Ognuno è idiomatico dove può esserlo e identico dove conta. Chi ha letto l’avvio rapido di TypeScript sa prevedere quello di Kotlin.

Che cosa ti danno in più rispetto all’HTTP nudo:

  • Raggruppamento in batch e un worker in secondo piano, così registrare uno span accoda e ritorna.
  • Nuovi tentativi con backoff sul 429 e sui codici 5xx per cui vale la pena riprovare, e mai su un 4xx che resterà malformato.
  • Generazione degli identificatori, propagazione del contesto, e il traceparent W3C letto in entrata ed emesso in uscita, così una traccia sopravvive al confine fra due servizi.
  • Tipi generati dalla stessa specifica da cui nascono quelli del daemon, così un campo non può voler dire una cosa nell’SDK e un’altra nello storage.

Tre promesse tengono in ogni linguaggio, e sono la ragione per cui un SDK si può mettere su un percorso caldo. La strumentazione non solleva mai eccezioni: un daemon irraggiungibile finisce in un log al massimo una volta per intervallo e poi viene inghiottito. Non blocca mai: il lavoro di rete avviene sul worker, quindi la latenza del tuo agente non dipende dal fatto che mcpd sia attivo. E la sua coda è limitata: quando si riempie, gli span più vecchi vengono scartati e un contatore sale, perché un buffer senza limite trasforma un’interruzione di rete in un processo ucciso per memoria esaurita.

connettersi

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 client costruito senza argomenti funziona contro un daemon locale. Tutto si imposta nel codice o per variabile d’ambiente, e il codice vince; i nomi delle variabili sono gli stessi in ogni linguaggio, così un valore copiato fra due servizi continua a funzionare quando i due sono scritti in linguaggi diversi: MCPD_ENDPOINT, MCPD_API_KEY, MCPD_PROJECT, MCPD_ENVIRONMENT, MCPD_SERVICE_NAME, MCPD_SERVICE_VERSION, MCPD_SAMPLE e MCPD_ENABLED. Impostare MCPD_ENABLED=false trasforma ogni chiamata in un’operazione a vuoto che restituisce comunque oggetti validi, così una suite di test spegne mcpd senza toccare una riga di codice.

Sono intercambiabili, ed è una cosa verificata

I test unitari dimostrano che ogni SDK fa quello che intendevano i suoi autori. Non dimostrano niente sul fatto che i sei vadano d’accordo. Perciò la suite di conformità scrive una sola esecuzione canonica attraverso ogni linguaggio contro un unico daemon vivo, più un settimo canale che manda la stessa esecuzione con nient’altro che curl, e verifica che quello che torna indietro sia strutturalmente identico:

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.

Se stai scegliendo un linguaggio per un servizio nuovo, questa è la risposta a «le tracce verranno uguali a quelle che abbiamo già».

Se il tuo linguaggio non è uno dei sei

Punta un SDK OpenTelemetry su /v1/traces e non scrivere una riga in più. mcpd è un ricevitore OTLP, e mappa sia la convenzione gen_ai.* di OpenTelemetry sia la llm.* di OpenInference, così la strumentazione che hai già atterra al posto giusto. Vedi OpenTelemetry.

Da un agente IA, tramite MCP

La terza strada è per un agente e non per un servizio: invece che sia il tuo codice a leggere le tracce, le legge per tuo conto un agente di programmazione o di assistenza, e risponde a una domanda su di esse. «Perché è fallita l’ultima esecuzione», «mostrami le esecuzioni che uno scorer ha penalizzato questa settimana», «quanto è costata questa sessione». mcpd espone tutto ciò attraverso il Model Context Protocol.

I tre strumenti

Tre, non trenta. Sono le tre mosse di una sola indagine: quale esecuzione, che cosa è successo dentro, e se è una sola esecuzione o sono tutte.

StrumentoA che cosa risponde
find_runs Quale esecuzione. Una riga per ciascuna: id, età, stato, durata, costo, passi. Filtra per stato, modello, utente, sessione o testo libero; ordina per durata per avere le più lente. Se non dici altro, le ultime 24 ore.
get_run Che cosa è successo. Il waterfall, il passo fallito e il perché, i prompt e i completion, i log, i punteggi. I payload hanno un tetto, perché una sola esecuzione non riempia una finestra di contesto.
run_stats Una sola esecuzione o tutte. Costo, tasso di fallimento, latenza e token su una finestra, raggruppati per modello, nome, stato, ambiente o utente. Un tasso di fallimento del 40% vuol dire che l’esecuzione che stai leggendo non è il problema.

I risultati tornano come testo e non come JSON, ed è una scelta presa apposta su chi paga questo contesto. Quaranta oggetti span sono per lo più punteggiatura; la stessa esecuzione disegnata come waterfall sono poche centinaia di token, e l’agente ne vede la forma.

Com’è fatto

Il server MCP è un sottocomando dello stesso binario, non un secondo servizio. Gira come mcpd mcp, parla JSON-RPC su standard input e standard output, e non tiene dati suoi. Non c’è nessuna porta da aprire, nessun processo da tenere in vita e niente da mettere in deploy: l’host dell’agente lo avvia come processo figlio quando una conversazione ne ha bisogno e lo ferma dopo.

Quello che tiene, invece, è una connessione a un daemon. Ogni chiamata a uno strumento diventa una richiesta alla stessa API di query descritta sopra, con la stessa chiave di lettura e lo stesso ambito di progetto, quindi un agente non arriva a un progetto a cui non arriva la chiave che gli hai dato. Il raggio d’azione di consegnare a un agente questo server è esattamente il raggio d’azione di consegnargli quella chiave, ed è la proprietà da controllare prima di configurarne uno.

L’insieme degli strumenti è piccolo di proposito. Una superficie grande fa scegliere male un modello, e quasi ogni domanda sul comportamento di un agente si riduce a elencare le esecuzioni, aprirne una e fare una domanda numerica su una finestra. Un client scopre l’insieme di oggi con la chiamata tools/list del protocollo stesso, e non da un elenco scritto qui che potrebbe restare indietro.

Configurare un client

Un host MCP avvia un server a partire da un comando, qualche argomento e un ambiente. Il file preciso e la chiave precisa sono dell’host, non di mcpd, quindi guarda la sua documentazione; i tre valori che vuole da te sono gli stessi dappertutto.

Che cosa vuole l’hostValore
ComandoIl binario mcpd, su un percorso che l’host può eseguire.
Argomenti Il sottocomando mcp. Prende --endpoint, il daemon da interrogare, e --api-key, una chiave di lettura per esso, per un host che passa argomenti più volentieri di un ambiente.
Ambiente Gli stessi due valori sotto forma di MCPD_ENDPOINT e MCPD_API_KEY. L’endpoint vale http://127.0.0.1:4317 quando non dici altro, e la chiave si può lasciare fuori quando il daemon non ha chiavi impostate.

che cosa esegue l’host

MCPD_ENDPOINT=https://mcpd.internal MCPD_API_KEY=$READ mcpd mcp

Tre cose da tenere a mente quando emetti quella chiave.

  • Dai all’agente una chiave read, non una chiave ingest. Gli ambiti esistono perché una credenziale per scrivere span non sia anche una credenziale per ogni prompt archiviato.
  • Legala a un progetto. Una chiave configurata al livello più alto arriva a ogni progetto sul daemon.
  • Sappi che una chiave di lettura cancella anche le tracce finché non configuri un elenco admin, perché l’ambito admin ricade sull’elenco di lettura quando nessuno lo imposta. Un agente che tiene una chiave di lettura su un daemon a cui non è mai stato dato un elenco admin può cancellare un’esecuzione.

Tutte e tre stanno su Sicurezza.

Quale usare

StradaUsala quando
HTTP Il tuo linguaggio non ha un SDK, stai mandando da una shell o da un job, oppure non vuoi nessuna dipendenza.
Un SDK Stai strumentando un’applicazione, dove il batch, il nuovo tentativo e il non sollevare mai eccezioni valgono più della dipendenza.
OpenTelemetry Emetti già OTel e preferisci cambiare un URL anziché aggiungere una libreria.
MCP È una persona a fare domande a un agente sulle esecuzioni archiviate, e non del codice che le legge a orario.

Si mescolano. Una flotta che emette tramite l’SDK TypeScript, un job notturno che manda punteggi con curl e un agente che rilegge tutto tramite MCP sono un solo deploy, non tre.

Poi