Ir para o conteúdo
mcpd
sem daemon
Código

Integração

Um daemon está rodando em algum lugar. Estas são as três formas de qualquer outra coisa chegar até ele, e como escolher entre elas.

O mcpd tem uma porta só e três formas de atravessá-la. Tudo o que vem abaixo fala com a mesma superfície HTTP: os SDKs são uma conveniência por cima dela, e o servidor MCP é um cliente dela.

Uma porta, quatro superfícies

Um único processo serve tudo isso em uma porta, a 4317 por padrão. Não há sidecar, nem coletor para rodar ao lado, nem servidor de arquivos estáticos para a interface.

CaminhoO que éChave
/v1/traces, /v1/logs, /v1/metrics Ingestão OpenTelemetry. Qualquer SDK do OTel, sem nenhum código específico do mcpd na sua aplicação. De ingestão
/v1/ingest Ingestão nativa. Um lote carrega qualquer mistura dos seis sinais e do modelo de dados inteiro, e uma requisição carrega um fluxo de lotes. De ingestão
/v1/api/… A API de consulta. A interface e os SDKs leem por aqui e por mais nada. De leitura, escrita, administração, execute ou worker, declarada endpoint a endpoint
/ A interface do operador, compilada dentro do binário. Nenhuma para a marcação; os dados dela passam pela API de consulta

Outros dois caminhos, /healthz e /readyz, não pedem autenticação, para que uma sonda os alcance. Comece por eles quando estiver descobrindo se um endpoint sequer responde:

está de pé?
$ curl -s "$MCPD/healthz"{"status":"ok","version":"0.1.0","spec_version":"1.0.0",…}$ curl -s "$MCPD/readyz"storage is not reachable: …# readyz toca no armazenamento, então um 503 aqui diz que o daemon# está de pé e que o banco atrás dele não está.

A API HTTP, sem SDK nenhum

Se a sua linguagem tem um cliente HTTP, ela fala com o mcpd. Não há nada para registrar, nenhum projeto para criar de antemão e nenhum aperto de mão. Todo SDK é uma conveniência por cima das requisições abaixo, e escolher este caminho custa a você o agrupamento em lotes, a repetição e a geração de ids, que passam a ser trabalho seu.

Escrever uma execução

POST /v1/ingest recebe um lote. Um lote pode carregar spans, logs, métricas, notas, feedback e notas de aspecto de uma vez, e é por isso que a porta nativa existe ao lado da porta OTLP: os seis sinais compartilham os ids de trace e de span, então mandá-los em uma requisição os mantém correlacionados sem uma segunda consulta.

Três formatos chegam a esse endpoint, e o content type diz qual é. Nada é deduzido dos bytes, então acrescentar uma quebra de linha dentro de uma string nunca muda o que um corpo significa.

Content typeCorpo
application/json Um objeto de lote, que é o que todo SDK manda
application/json Um array de objetos de lote, para quem tem vários em mãos
application/x-ndjson Um lote por linha, lido conforme o corpo chega em vez de guardado inteiro. application/jsonl quer dizer o mesmo

Cada elemento é o lote que ele é: fica gravado na própria transação quando carrega um ingest_key, então um fluxo reenviado é reconhecido linha a linha e não como um bloco só, e os elementos sem chave ficam gravados juntos. A requisição inteira é recusada, sem nada gravado, se alguma linha não puder ser lida ou se a requisição como um todo passar de qualquer um dos dois limites. A resposta é um único IngestResult que cobre tudo.

  • 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

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

Quatro coisas que a requisição precisa acertar:

  • Os ids têm o formato do OpenTelemetry. 32 caracteres hexadecimais minúsculos para um trace, 16 para um span, nunca todos zero. Qualquer outra coisa quebra a correlação com as demais ferramentas que você roda.
  • ingest_key torna uma repetição segura. É uma chave de idempotência estável para um lote, então um reenvio é reconhecido e contado como duplicata em vez de gravado duas vezes.
  • A chave vai em um cabeçalho. Authorization: Bearer …, ou X-Mcpd-Key. A ingestão pede uma chave de ingestão, e só quando o daemon tem alguma configurada.
  • Lotes têm limites. 32 MiB de corpo e 10.000 sinais por padrão, ambos ajustáveis com --max-body-bytes e --max-batch-signals. O limite de sinais conta a requisição inteira, então dividir os mesmos sinais em várias linhas não passa por ele. Mande content-encoding: gzip para qualquer coisa grande.

A resposta relata o que houve por sinal: {"accepted":2,"rejected":0,"duplicate":0}, com um array errors que nomeia o índice e o motivo de tudo o que foi recusado. Os endpoints OTLP respondem no formato do próprio OTLP, com um objeto partialSuccess carregando a contagem de recusados, porque é isso que um emissor do OpenTelemetry lê.

Ler de volta

ler

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

Os filtros se combinam, a paginação é por cursor, e project assume o projeto padrão do daemon quando você o deixa de fora. A busca em texto completo sobre os payloads gravados é o parâmetro q, que é o que faz de “encontre a execução em que o modelo disse isto” uma consulta e não um grep.

Uma falha que o próprio mcpd levanta volta como JSON com um código error estável e legível por máquina, uma message escrita para quem vai consertar, e um field quando um parâmetro é o culpado. Ramifique pelo código, mostre a mensagem.

Uma classe de falha não tem esse formato, e um cliente que supõe que tem não vai conseguir interpretar a resposta. Um corpo de requisição que não pode ser decodificado no tipo que o endpoint espera é recusado pelo framework web antes de qualquer código do mcpd rodar, então ele responde 422 com content-type: text/plain e uma mensagem como missing field `scorer_id`. Uma sintaxe de corpo que nem JSON é responde 400, também como texto. Não há código error pelo qual ramificar em nenhum dos dois casos. Isso vale para todo endpoint que recebe um corpo tipado: POST /v1/api/scores, /v1/api/feedback, /v1/api/score, /v1/api/datasets, suas rotas items e capture, e /v1/api/experiments. As portas de ingestão leem o corpo elas mesmas, então /v1/ingest e as rotas OTLP devolvem sim o formato JSON. Trate um corpo que não é JSON em um 4xx como uma requisição malformada vinda do seu próprio código, e leia-o como texto.

Os SDKs

Seis linguagens: TypeScript, Python, Rust, Go, Kotlin e Elixir. Cada um é idiomático onde dá e idêntico onde importa. Quem leu o início rápido do TypeScript consegue prever o do Kotlin.

O que você ganha em relação ao HTTP puro:

  • Agrupamento em lotes e um worker em segundo plano, de modo que registrar um span acrescenta a uma fila e retorna.
  • Repetição com backoff em 429 e nos códigos 5xx que valem a pena repetir, e nunca em um 4xx que vai seguir malformado.
  • Geração de ids, propagação de contexto, e o traceparent do W3C lido na entrada e emitido na saída, para que um trace sobreviva à fronteira entre serviços.
  • Tipos gerados da mesma especificação que a do daemon, de modo que um campo não possa dizer uma coisa no SDK e outra no armazenamento.

Três promessas valem em todas as linguagens, e são o motivo de um SDK ser seguro no caminho quente. A instrumentação nunca lança exceção: um daemon inalcançável é anotado no máximo uma vez por intervalo e engolido. Ela nunca bloqueia: o trabalho de rede acontece no worker, então a latência do seu agente não depende de o mcpd estar de pé. E a fila dela tem limite: quando enche, os spans mais antigos são descartados e um contador sobe, porque um buffer sem limite transforma uma queda de rede em uma morte por falta de memória.

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',
})

Um cliente construído sem argumento nenhum funciona contra um daemon local. Tudo se define em código ou por variável de ambiente, e o código vence, e os nomes das variáveis são os mesmos em todas as linguagens, então um valor copiado entre dois serviços continua funcionando quando eles estão escritos em linguagens diferentes: MCPD_ENDPOINT, MCPD_API_KEY, MCPD_PROJECT, MCPD_ENVIRONMENT, MCPD_SERVICE_NAME, MCPD_SERVICE_VERSION, MCPD_SAMPLE e MCPD_ENABLED. Definir MCPD_ENABLED=false transforma cada chamada em uma operação nula que ainda devolve objetos válidos, então uma suíte de testes desliga o mcpd sem tocar em código algum.

Eles são intercambiáveis, e isso é testado

Os testes de unidade provam que cada SDK faz o que os próprios autores dele quiseram. Não provam nada sobre os seis concordarem entre si. Por isso a suíte de conformidade escreve uma execução canônica por cada linguagem contra um mesmo daemon vivo, mais um sétimo canal que manda a mesma execução com nada além de curl, e afirma que o que volta é estruturalmente 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.

Se você está escolhendo uma linguagem para um serviço novo, esta é a resposta para “os traces vão sair iguais aos que já temos”.

Se a sua linguagem não é uma das seis

Aponte um SDK do OpenTelemetry para /v1/traces e não escreva código novo. O mcpd é um receptor OTLP, e mapeia tanto a convenção gen_ai.* do OpenTelemetry quanto a llm.* do OpenInference, então a instrumentação que você já tem chega no lugar certo. Veja OpenTelemetry.

De um agente de IA, por MCP

O terceiro caminho é para um agente, não para um serviço: em vez de o seu código ler os traces, um agente de programação ou de suporte os lê por você e responde a uma pergunta sobre eles. “Por que a última execução falhou”, “me mostre as execuções que um avaliador rebaixou nesta semana”, “quanto custou esta sessão”. O mcpd expõe isso pelo Model Context Protocol.

As três ferramentas

Três, não trinta. São os três movimentos de uma mesma investigação: qual execução, o que aconteceu dentro dela, e se é uma execução só ou todas.

FerramentaO que ela responde
find_runs Qual execução. Uma linha para cada: id, idade, status, duração, custo, etapas. Filtre por status, modelo, usuário, sessão ou texto livre; ordene por duração para achar as mais lentas. O padrão são as últimas 24 horas.
get_run O que aconteceu. A cascata, a etapa que falhou e por quê, os prompts e as respostas do modelo, os logs, as notas. Os payloads têm teto, para que uma execução não encha uma janela de contexto.
run_stats Uma execução ou todas. Custo, taxa de falha, latência e tokens ao longo de uma janela, agrupados por modelo, nome, status, ambiente ou usuário. Uma taxa de falha de 40% quer dizer que a execução que você está lendo não é o problema.

Os resultados voltam como texto e não como JSON, o que é uma escolha deliberada sobre o contexto de quem isso gasta. Quarenta objetos de span são quase só pontuação; a mesma execução desenhada como cascata são algumas centenas de tokens, e o agente enxerga o formato dela.

Como ele é montado

O servidor MCP é um subcomando do mesmo binário, não um segundo serviço. Ele roda como mcpd mcp, fala JSON-RPC pela entrada e pela saída padrão, e não guarda dado nenhum próprio. Não há porta para abrir, processo para manter vivo nem nada para implantar: o host do agente o inicia como processo filho quando uma conversa precisa dele e o encerra depois.

O que ele guarda é uma conexão com um daemon. Cada chamada de ferramenta vira uma requisição para a mesma API de consulta descrita acima, com a mesma chave de leitura e o mesmo recorte por projeto, então um agente não alcança um projeto que a chave dada a ele não alcança. O alcance de entregar este servidor a um agente é exatamente o alcance de entregar a ele aquela chave, e é essa a propriedade a conferir antes de configurar um.

O conjunto de ferramentas é pequeno de propósito. Uma superfície grande faz um modelo escolher mal, e a maior parte das perguntas sobre o comportamento de um agente se resume a listar execuções, abrir uma, e fazer uma pergunta numérica sobre uma janela. Um cliente descobre o conjunto atual com a própria chamada tools/list do protocolo, e não por uma lista escrita aqui que poderia ficar para trás.

Configurar um cliente

Um host MCP levanta um servidor a partir de um comando, alguns argumentos e um ambiente. O arquivo exato e a chave exata são do host, não do mcpd, então consulte a documentação dele; os três valores que ele precisa de você são os mesmos em toda parte.

O que o host precisaValor
ComandoO binário mcpd, em um caminho que o host consiga executar.
Argumentos O subcomando mcp. Ele aceita --endpoint, o daemon a consultar, e --api-key, uma chave de leitura para ele, para um host que passa argumentos com mais facilidade do que um ambiente.
Ambiente Os mesmos dois valores, como MCPD_ENDPOINT e MCPD_API_KEY. O endpoint tem http://127.0.0.1:4317 por padrão, e a chave pode ficar de fora quando o daemon não tem chave alguma definida.

o que o host executa

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

Três coisas para segurar quando você emitir essa chave.

  • Dê ao agente uma chave read, não uma chave ingest. Os escopos existem para que uma credencial de escrita de spans não seja também uma credencial para todo prompt gravado.
  • Limite-a a um projeto. Uma chave configurada no nível de cima alcança todos os projetos do daemon.
  • Saiba que uma chave de leitura também apaga traces enquanto você não configurar uma lista admin, porque o escopo de administração recai sobre a lista de leitura quando não está definido. Um agente com uma chave de leitura em um daemon que nunca recebeu uma lista de administração consegue apagar uma execução.

Os três estão em Segurança.

Qual deles usar

CaminhoUse quando
HTTP Sua linguagem não tem SDK, você está mandando de um shell ou de um job, ou não quer dependência nenhuma.
Um SDK Você está instrumentando uma aplicação, e ali o agrupamento em lotes, a repetição e o nunca lançar exceção valem mais do que a dependência.
OpenTelemetry Você já emite OTel e prefere mudar uma URL a acrescentar uma biblioteca.
MCP Uma pessoa está perguntando a um agente sobre execuções gravadas, em vez de um código lê-las em horário marcado.

Eles se misturam. Uma frota que emite pelo SDK de TypeScript, um job noturno que manda notas com curl, e um agente lendo tudo de volta por MCP são uma implantação só, não três.

A seguir