Aller au contenu
mcpd
aucun daemon
Source

Intégration

Un daemon tourne quelque part. Voici les trois façons de l’atteindre depuis l’extérieur, et comment choisir entre elles.

mcpd a une seule porte et trois façons de la franchir. Tout ce qui suit s’adresse à la même surface HTTP : les SDK sont une commodité posée au-dessus, et le serveur MCP en est un client.

Un port, quatre surfaces

Un seul processus sert l’ensemble sur un seul port, 4317 par défaut. Pas de sidecar, pas de collecteur à faire tourner à côté, pas de serveur de fichiers statiques pour l’interface.

CheminCe que c’estClé
/v1/traces, /v1/logs, /v1/metrics Ingestion OpenTelemetry. N’importe quel SDK OTel, sans une ligne de code propre à mcpd dans votre application. Ingestion
/v1/ingest Ingestion native. Un lot transporte n’importe quel mélange des six signaux et tout le modèle de données, et une requête transporte un flux de lots. Ingestion
/v1/api/… L’API de requête. L’interface et les SDK lisent par là, et par rien d’autre. Lecture, écriture, admin, execute ou worker, déclarée endpoint par endpoint
/ L’interface d’exploitation, compilée dans le binaire. Aucune pour le balisage ; ses données passent par l’API de requête

Deux autres chemins, /healthz et /readyz, sont sans authentification pour qu’une sonde puisse les atteindre. Commencez par là quand vous cherchez à savoir si un endpoint est joignable du tout :

est-il là
$ curl -s "$MCPD/healthz"{"status":"ok","version":"0.1.0","spec_version":"1.0.0",…}$ curl -s "$MCPD/readyz"storage is not reachable: …# readyz interroge le stockage : un 503 ici veut dire que le daemon# est debout et que la base derrière lui ne l’est pas.

L’API HTTP, sans SDK

Si votre langage a un client HTTP, il peut parler à mcpd. Rien à déclarer, aucun projet à créer au préalable, aucune poignée de main. Chaque SDK n’est qu’une commodité au-dessus des requêtes ci-dessous, et prendre cette voie vous prive du regroupement en lots, des nouvelles tentatives et de la génération d’identifiants, dont vous vous chargez alors vous-même.

Écrire une exécution

POST /v1/ingest accepte un lot. Un lot peut transporter ensemble des spans, des logs, des métriques, des scores, du feedback et des notes d’aspect, et c’est la raison d’être de la porte native à côté de celle d’OTLP : les six signaux partagent les identifiants de trace et de span, donc les envoyer dans une seule requête les garde corrélés sans seconde recherche.

Trois formes arrivent à cet endpoint, et c’est le type de contenu qui dit laquelle. Rien n’est deviné à partir des octets : ajouter un saut de ligne à l’intérieur d’une chaîne ne peut jamais changer le sens d’un corps.

Type de contenuCorps
application/json Un objet lot, ce qu’envoie chaque SDK
application/json Un tableau d’objets lot, pour un émetteur qui en a plusieurs sous la main
application/x-ndjson Un lot par ligne, lu au fil de l’arrivée du corps plutôt que retenu en entier. application/jsonl veut dire la même chose

Chaque élément est un lot à part entière : il est stocké dans sa propre transaction lorsqu’il porte un ingest_key, si bien qu’un flux renvoyé est reconnu ligne par ligne et non comme un bloc, tandis que les éléments sans clé sont stockés ensemble. La requête entière est refusée, sans rien stocker, si une ligne est illisible ou si la requête dans son ensemble dépasse l’une des deux limites. La réponse est un seul IngestResult qui couvre le tout.

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

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

Quatre choses que la requête doit faire correctement :

  • Les identifiants ont la forme OpenTelemetry. 32 caractères hexadécimaux minuscules pour une trace, 16 pour un span, jamais tous à zéro. Toute autre forme casse la corrélation avec les autres outils que vous faites tourner.
  • ingest_key rend un renvoi sans danger. C’est une clé d’idempotence stable pour un lot : un renvoi est reconnu et compté comme doublon au lieu d’être écrit deux fois.
  • La clé va dans un en-tête. Authorization: Bearer …, ou X-Mcpd-Key. L’ingestion demande une clé d’ingestion, et seulement quand le daemon en a de configurées.
  • Les lots ont des limites. 32 MiB de corps et 10 000 signaux par défaut, réglables l’un et l’autre avec --max-body-bytes et --max-batch-signals. La limite de signaux compte la requête entière : répartir les mêmes signaux sur plusieurs lignes ne permet pas de la contourner. Envoyez content-encoding: gzip pour tout ce qui est volumineux.

La réponse indique ce qui s’est passé pour chaque signal : {"accepted":2,"rejected":0,"duplicate":0}, avec un tableau errors qui nomme l’indice et la raison de chaque refus. Les endpoints OTLP répondent plutôt dans la forme propre à OTLP, avec un objet partialSuccess qui porte le nombre de refus, parce que c’est ce que lit un émetteur OpenTelemetry.

Relire ce qui est stocké

lire

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

Les filtres se combinent, la pagination se fait par curseur, et project retombe sur le projet par défaut du daemon quand vous l’omettez. La recherche plein texte sur les payloads stockés passe par le paramètre q : c’est ce qui fait de « trouver l’exécution où le modèle a dit ceci » une requête et non un grep.

Une erreur que mcpd lève lui-même revient en JSON, avec un code error stable et lisible par une machine, un message écrit pour celui qui devra corriger, et un field quand un paramètre précis est en cause. Branchez sur le code, affichez le message.

Une catégorie d’erreur n’a pas cette forme, et un client qui la suppose n’arrivera pas à analyser la réponse. Un corps de requête qui ne peut pas être décodé dans le type attendu par l’endpoint est refusé par le framework web avant que la moindre ligne de mcpd ne s’exécute : il répond 422 avec content-type: text/plain et un message du genre missing field `scorer_id`. Une syntaxe de corps qui n’est pas du JSON du tout répond 400, en texte également. Dans les deux cas, il n’y a aucun code error sur lequel brancher. Cela vaut pour tous les endpoints qui prennent un corps typé : POST /v1/api/scores, /v1/api/feedback, /v1/api/score, /v1/api/datasets, ses routes items et capture, et /v1/api/experiments. Les portes d’ingestion lisent le corps elles-mêmes, donc /v1/ingest et les routes OTLP renvoient bien la forme JSON. Traitez un corps non-JSON sur un 4xx comme une requête malformée venue de votre propre code, et lisez-le comme du texte.

Les SDK

Six langages : TypeScript, Python, Rust, Go, Kotlin et Elixir. Chacun est idiomatique là où il peut l’être et identique là où cela compte. Qui a lu la prise en main TypeScript peut deviner celle de Kotlin.

Ce que vous gagnez par rapport au HTTP brut :

  • Le regroupement en lots et un worker en arrière-plan : enregistrer un span ajoute à une file et rend la main.
  • Les nouvelles tentatives avec backoff sur 429 et sur les codes 5xx qui valent la peine, jamais sur un 4xx qui restera malformé.
  • La génération d’identifiants, la propagation du contexte, et l’en-tête W3C traceparent lu à l’entrée et émis à la sortie, pour qu’une trace survive au passage d’un service à l’autre.
  • Des types générés depuis la même spécification que ceux du daemon : un champ ne peut pas vouloir dire une chose dans le SDK et une autre dans le stockage.

Trois promesses tiennent dans tous les langages, et ce sont elles qui rendent un SDK sûr sur un chemin critique. L’instrumentation ne lève jamais d’exception : un daemon injoignable est consigné au plus une fois par intervalle, puis absorbé. Elle ne bloque jamais : le travail réseau se fait sur le worker, donc la latence de votre agent ne dépend pas de la disponibilité de mcpd. Et sa file est bornée : quand elle se remplit, les spans les plus anciens sont abandonnés et un compteur monte, parce qu’un tampon sans limite transforme une panne réseau en arrêt pour mémoire épuisée.

se connecter

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 construit sans argument fonctionne face à un daemon local. Tout se règle dans le code ou par variable d’environnement, le code l’emportant, et les noms de variables sont les mêmes dans tous les langages : une valeur copiée d’un service à l’autre continue de fonctionner même quand les deux services ne sont pas écrits dans le même langage. MCPD_ENDPOINT, MCPD_API_KEY, MCPD_PROJECT, MCPD_ENVIRONMENT, MCPD_SERVICE_NAME, MCPD_SERVICE_VERSION, MCPD_SAMPLE et MCPD_ENABLED. Mettre MCPD_ENABLED=false transforme chaque appel en une opération sans effet qui rend malgré tout des objets valides, de sorte qu’une suite de tests peut couper mcpd sans toucher au code.

Ils sont interchangeables, et cela se vérifie

Les tests unitaires prouvent que chaque SDK fait ce que ses propres auteurs avaient en tête. Ils ne prouvent rien de l’accord entre les six. La suite de conformité écrit donc une exécution canonique depuis chaque langage vers un même daemon en fonctionnement, plus un septième canal qui envoie la même exécution avec curl et rien d’autre, et vérifie que ce qui revient est structurellement identique :

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 vous choisissez un langage pour un nouveau service, voici la réponse à « les traces ressembleront-elles à celles que nous avons déjà ».

Si votre langage n’est pas l’un des six

Pointez un SDK OpenTelemetry vers /v1/traces et n’écrivez pas une ligne de plus. mcpd est un récepteur OTLP, et il fait correspondre aussi bien la convention OpenTelemetry gen_ai.* que la convention OpenInference llm.*, si bien que l’instrumentation que vous avez déjà arrive correctement. Voir OpenTelemetry.

Depuis un agent IA, par MCP

La troisième voie s’adresse à un agent plutôt qu’à un service : au lieu que votre code lise les traces, un agent de développement ou de support les lit pour vous et répond à une question à leur sujet. « Pourquoi la dernière exécution a-t-elle échoué », « montre-moi les exécutions qu’un scorer a mal notées cette semaine », « combien a coûté cette session ». mcpd expose cela par le Model Context Protocol.

Les trois outils

Trois, pas trente. Ce sont les trois gestes d’une même enquête : quelle exécution, ce qui s’y est passé, et si le problème tient à une exécution ou à toutes.

OutilCe à quoi il répond
find_runs Quelle exécution. Une ligne chacune : identifiant, âge, statut, durée, coût, étapes. Filtrez par statut, modèle, utilisateur, session ou texte libre ; triez par durée pour obtenir les plus lentes. Porte sur les dernières 24 heures par défaut.
get_run Ce qui s’est passé. La cascade, l’étape qui a échoué et pourquoi, les prompts et les complétions, les logs, les scores. Les payloads sont plafonnés pour qu’une seule exécution ne puisse pas remplir une fenêtre de contexte.
run_stats Une exécution ou toutes. Coût, taux d’échec, latence et tokens sur une fenêtre, groupés par modèle, nom, statut, environnement ou utilisateur. Un taux d’échec de 40 % signifie que l’exécution que vous lisez n’est pas le problème.

Les résultats reviennent en texte plutôt qu’en JSON : un choix délibéré, parce que c’est le contexte de l’agent que cela dépense. Quarante objets span, c’est surtout de la ponctuation ; la même exécution dessinée en cascade tient en quelques centaines de tokens, et l’agent en voit la forme.

Comment il est fait

Le serveur MCP est une sous-commande du même binaire, pas un second service. Il se lance avec mcpd mcp, parle JSON-RPC sur l’entrée et la sortie standard, et ne détient aucune donnée propre. Aucun port à ouvrir, aucun processus à maintenir en vie, rien à déployer : l’hôte de l’agent le démarre comme processus enfant quand une conversation en a besoin, puis l’arrête.

Ce qu’il détient, en revanche, c’est une connexion à un daemon. Chaque appel d’outil devient une requête vers l’API de requête décrite plus haut, avec la même clé de lecture et le même cadrage par projet : un agent ne peut pas atteindre un projet que la clé qu’on lui a donnée n’atteint pas. Confier ce serveur à un agent vous expose exactement autant que lui confier cette clé, et c’est la propriété à vérifier avant d’en configurer un.

Le jeu d’outils est volontairement restreint. Une grande surface fait mal choisir un modèle, et la plupart des questions sur le comportement d’un agent se ramènent à lister des exécutions, en ouvrir une, et poser une question de chiffres sur une fenêtre. Un client découvre le jeu courant avec l’appel tools/list du protocole lui-même, plutôt qu’avec une liste écrite ici qui pourrait prendre du retard.

Configurer un client

Un hôte MCP lance un serveur à partir d’une commande, d’arguments et d’un environnement. Le fichier exact et la clé exacte appartiennent à l’hôte, pas à mcpd : reportez-vous à sa documentation. Les trois valeurs qu’il attend de vous sont partout les mêmes.

Ce dont l’hôte a besoinValeur
CommandeLe binaire mcpd, sur un chemin que l’hôte peut exécuter.
Arguments La sous-commande mcp. Elle prend --endpoint, le daemon à interroger, et --api-key, une clé de lecture pour celui-ci, à l’intention d’un hôte qui passe plus volontiers des arguments qu’un environnement.
Environnement Les deux mêmes valeurs, sous les noms MCPD_ENDPOINT et MCPD_API_KEY. L’endpoint vaut http://127.0.0.1:4317 par défaut, et la clé peut être omise quand le daemon n’a aucune clé définie.

ce que lance l’hôte

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

Trois choses à retenir quand vous émettez cette clé.

  • Donnez à l’agent une clé read, pas une clé ingest. Les portées existent pour qu’un secret qui sert à écrire des spans ne serve pas aussi à lire tous les prompts stockés.
  • Limitez-la à un projet. Une clé configurée au niveau supérieur atteint tous les projets du daemon.
  • Sachez qu’une clé de lecture supprime aussi des traces tant que vous n’avez pas configuré de liste admin, car la portée admin retombe sur la liste de lecture quand elle n’est pas définie. Un agent qui détient une clé de lecture sur un daemon auquel on n’a jamais donné de liste admin peut supprimer une exécution.

Les trois sont traitées sur Sécurité.

Laquelle choisir

VoieÀ prendre quand
HTTP Votre langage n’a pas de SDK, vous envoyez depuis un shell ou une tâche, ou vous ne voulez aucune dépendance.
Un SDK Vous instrumentez une application, et le regroupement en lots, les nouvelles tentatives et la garantie de ne jamais lever d’exception valent plus que la dépendance.
OpenTelemetry Vous émettez déjà de l’OTel et vous préférez changer une URL plutôt qu’ajouter une bibliothèque.
MCP Une personne pose à un agent des questions sur les exécutions stockées, plutôt qu’un code qui les lit à intervalle régulier.

Elles se mélangent. Une flotte qui émet par le SDK TypeScript, une tâche nocturne qui poste des scores avec curl, et un agent qui relit le tout par MCP, cela fait un seul déploiement et non trois.

Ensuite