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.
| Caminho | O 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:
$ 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 type | Corpo |
|---|---|
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
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_keytorna 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 …, ouX-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-bytese--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. Mandecontent-encoding: gzippara 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
traceparentdo 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',
}) 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", ...}
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
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.
| Ferramenta | O 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 precisa | Valor |
|---|---|
| Comando | O 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 chaveingest. 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
| Caminho | Use 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
- O que fica gravado e quem consegue ler: Segurança.
- Apontar para o mcpd um OTel que você já tem: OpenTelemetry.
- Todos os endpoints, gerados a partir da especificação: API HTTP.
- As consultas que vale a pena conhecer: Encontrar uma execução.