導入
デーモンがどこかで動いています。ほかのものがそこへ到達する経路は 3 つです。その選び方もここで説明します。
mcpd の入口は 1 つで、そこを通る道が 3 つあります。以下で説明するものはすべて 同じ HTTP 面に対して通信します。SDK はその上に載せた便利な層であり、MCP サーバーはその面のクライアントです。
1 つのポート、4 つの面
単一のプロセスが、既定では 4317 番の 1 つのポートですべてを提供します。 サイドカーはなく、横で動かすコレクターもなく、インターフェース用の静的 ファイルサーバーも要りません。
| パス | 内容 | キー |
|---|---|---|
/v1/traces、/v1/logs、/v1/metrics | OpenTelemetry の取り込み口。どの OTel SDK でも使え、アプリケーションに mcpd 固有のコードは要りません。 | ingest |
/v1/ingest | ネイティブの取り込み口。1 つのバッチが 6 種類のシグナルを任意に混ぜて運び、データモデル全体を扱えます。1 つのリクエストはバッチの連なりを運べます。 | ingest |
/v1/api/… | クエリ API。インターフェースも SDK も、読み取りはここだけを通ります。 | read、write、admin、execute、worker のいずれか。エンドポイントごとに宣言されます |
/ | 運用インターフェース。バイナリに組み込まれています。 | ページ自体には不要。そのデータはクエリ API を通ります |
もう 2 つ、/healthz と /readyz は認証なしで、
プローブが到達できるようにしてあります。エンドポイントにそもそも到達できるのか
を調べるときは、ここから始めてください。
$ curl -s "$MCPD/healthz"{"status":"ok","version":"0.1.0","spec_version":"1.0.0",…}$ curl -s "$MCPD/readyz"storage is not reachable: …# readyz はストアに触れるので、ここで 503 が返るのはデーモンが生きていて# その背後のデータベースが生きていない、という意味です。 SDK を使わない HTTP API
あなたの言語に HTTP クライアントがあれば、mcpd と通信できます。登録するものは なく、あらかじめ作っておくプロジェクトもなく、ハンドシェイクもありません。 どの SDK も以下のリクエストの上に載せた便利な層にすぎず、この経路を選ぶと バッチ処理、リトライ、id の生成を失い、それらは自分で書くことになります。
実行の書き込み
POST /v1/ingest は 1 つのバッチを受け取ります。バッチはスパン、
ログ、メトリクス、スコア、フィードバック、観点ノートをまとめて運べます。
ネイティブの入口が OTLP の入口と並んで存在する理由はここにあります。6 種類の
シグナルはトレース id
とスパン id を共有するので、1 つのリクエストで送れば、二度目の照合なしに
対応づいたままになります。
このエンドポイントには 3 つの形が届き、どれであるかは content type が決めます。 バイト列から推測されるものは何もないので、文字列の中に改行を足しても本文の 意味が変わることはありません。
| Content type | 本文 |
|---|---|
application/json | バッチオブジェクト 1 つ。どの SDK もこれを送ります |
application/json | バッチオブジェクトの配列。手元に複数ある送信側のためのもの |
application/x-ndjson | 1 行につき 1 バッチ。全体を抱えずに本文が届いた順に読みます。application/jsonl も同じ意味です |
それぞれの要素はそれ自体が 1 つのバッチです。ingest_key を持つ
要素は自分のトランザクションで保存されるので、再送されたストリームは一塊
としてではなく行ごとに認識されます。キーを持たない要素はまとめて保存され
ます。いずれかの行が読めない場合、またはリクエスト全体がどちらかの上限を
超えた場合は、何も保存せずにリクエスト全体を拒否します。応答は全体をまとめた
1 つの IngestResult です。
-
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
1 つのバッチ
# 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. リクエストが正しく満たすべきことが 4 つあります。
- id は OpenTelemetry の形です。トレースは小文字の 16 進 32 文字、スパンは 16 文字で、すべてゼロは不可です。それ以外は、あなたが動かして いる他のあらゆるツールとの対応づけを壊します。
-
ingest_keyがリトライを安全にします。これは バッチに対して安定した冪等キーで、再送は重複として認識され数えられ、二重に 書き込まれることはありません。 - キーはヘッダーに入れます。
Authorization: Bearer …かX-Mcpd-Keyです。 取り込みには ingest キーが必要で、必要になるのはデーモンにキーが設定されて いるときだけです。 - バッチには上限があります。既定で本文 32 MiB、シグナル
10,000 件で、どちらも
--max-body-bytesと--max-batch-signalsで変えられます。シグナルの上限はリクエスト 全体を数えるので、同じシグナルを複数行に分けても回避できません。大きなものはcontent-encoding: gzipを付けて送ってください。
応答はシグナルごとに何が起きたかを報告します。
{"accepted":2,"rejected":0,"duplicate":0} の形で、拒否された
ものについては添字と理由を挙げる errors 配列が付きます。OTLP の
エンドポイントは代わりに OTLP 自身の形で答え、拒否件数を運ぶ
partialSuccess オブジェクトを返します。OpenTelemetry の送信側が
読むのはそちらだからです。
読み返し
読み取り
# 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'
フィルターは重ねられ、ページ送りはカーソル方式で、project を
省くとデーモンの既定プロジェクトになります。保存されたペイロードに対する
全文検索は q パラメーターで、これがあるおかげで「モデルがこう
言った実行を探す」が grep ではなくクエリになります。
mcpd 自身が出す失敗は JSON で返り、機械が読める安定した error
コード、直す人に向けて書かれた message、そして 1 つの
パラメーターに原因があるときは field を含みます。コードで分岐し、
メッセージを表示してください。
1 種類の失敗だけはこの形を取らないので、この形だと決めてかかるクライアントは
応答の解析に失敗します。エンドポイントが期待する型に復号できないリクエスト
本文は、mcpd のコードが動く前に Web フレームワークが拒否します。したがって
422 と content-type: text/plain で、
missing field `scorer_id` のようなメッセージが返ります。そもそも
JSON ですらない構文の本文には 400 が、これもテキストで返ります。
どちらの場合も分岐できる error コードはありません。これは型付きの
本文を取るすべてのエンドポイントに当てはまります。
POST /v1/api/scores、/v1/api/feedback、
/v1/api/score、/v1/api/datasets とその
items および capture のルート、そして
/v1/api/experiments です。取り込み口は本文を自分で読むので、
/v1/ingest と OTLP のルートは JSON の形を返します。4xx で本文が
JSON でないときは、自分のコードから出た不正なリクエストだとみなし、テキスト
として読んでください。
SDK
6 つの言語があります。TypeScript、 Python、 Rust、 Go、 Kotlin、 Elixir です。 それぞれ、できるところではその言語らしく、重要なところでは同一です。 TypeScript のクイックスタートを読んだ人は、Kotlin のそれを予想できます。
生の HTTP に対して得られるものは次のとおりです。
- バッチ処理とバックグラウンドワーカー。スパンの記録はキューへの追加で終わり、すぐ戻ります。
- 429 とリトライする価値のある 5xx でのバックオフ付きリトライ。不正なままになる 4xx では決してリトライしません。
- id の生成、コンテキストの伝播、そして W3C の
traceparentを受信時に解析し送信時に出力すること。これによりトレースはサービス境界を越えても続きます。 - デーモンと同じ仕様から生成された型。あるフィールドが SDK とストアで別の意味になることはありません。
どの言語でも成り立つ約束が 3 つあり、これが SDK をホットパスに置いても安全な 理由です。計装は例外を投げません。デーモンに到達できないことは間隔ごとに多くて 一度だけ記録され、あとは飲み込まれます。ブロックもしません。ネットワークの処理は ワーカーで行われるので、あなたのエージェントのレイテンシが mcpd の稼働に 左右されることはありません。そしてキューには上限があります。いっぱいになると 古いスパンから捨てられ、カウンターが増えます。上限のないバッファーは、 ネットワーク障害をメモリ不足による強制終了に変えてしまうからです。
接続
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", ...}
引数なしで作ったクライアントは、ローカルのデーモンに対して動きます。すべては
コードでも環境変数でも設定でき、コードが勝ちます。変数名はどの言語でも同じなので、
2 つのサービスの間でコピーした値は、それらが別々の言語で書かれていても効き続けます。
MCPD_ENDPOINT、MCPD_API_KEY、
MCPD_PROJECT、MCPD_ENVIRONMENT、
MCPD_SERVICE_NAME、MCPD_SERVICE_VERSION、
MCPD_SAMPLE、MCPD_ENABLED です。
MCPD_ENABLED=false にすると、すべての呼び出しは有効な
オブジェクトを返したまま何もしなくなるので、テストではコードに触れずに mcpd を
切れます。
6 つが交換可能であることの検証
ユニットテストが証明するのは、それぞれの SDK が自分の作者の意図どおりに動く
ことだけです。6 つが一致するかどうかについては何も証明しません。そこで適合
スイートが、1 つの生きたデーモンに対して各言語から基準となる同じ実行を書き込み、
さらに curl だけで同じ実行を送る 7 つ目の経路も加えて、返ってくる
ものが構造として同一であることを確かめます。
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
新しいサービスの言語を選んでいるなら、これが「トレースは今あるものと同じに 見えるか」への答えです。
あなたの言語が 6 つに入っていないとき
OpenTelemetry の SDK を /v1/traces に向ければ、新しいコードは
要りません。mcpd は OTLP の受信側であり、OpenTelemetry の
gen_ai.* 規約と OpenInference の llm.* 規約の両方を
対応づけるので、すでにある計装がそのまま正しく届きます。
OpenTelemetry を
参照してください。
AI エージェントから、MCP 経由で
3 つ目の経路は、サービスではなくエージェントのためのものです。あなたのコードが トレースを読むのではなく、コーディングやサポートのエージェントがあなたに 代わって読み、それについての質問に答えます。「直近の実行はなぜ失敗したのか」、 「今週スコアラーが低く付けた実行を見せて」、「このセッションはいくらかかったか」。 mcpd はこれを Model Context Protocol で提供します。
3 つのツール
30 ではなく 3 つです。これは 1 つの調査の 3 つの動きにあたります。どの実行か、 その中で何が起きたか、そしてそれが 1 つの実行の問題かすべての問題か、です。
| ツール | 答えるもの |
|---|---|
find_runs | どの実行か。1 件 1 行で、id、経過時間、状態、所要時間、コスト、ステップ数 を出します。状態、モデル、ユーザー、セッション、自由文で絞り込み、所要 時間で並べ替えれば最も遅いものが出ます。既定は直近 24 時間です。 |
get_run | 何が起きたか。ウォーターフォール、失敗したステップとその理由、プロンプトと 補完、ログ、スコア。ペイロードには上限があるので、1 つの実行が コンテキストウィンドウを埋め尽くすことはありません。 |
run_stats | 1 つの実行か、全体か。ある期間のコスト、失敗率、レイテンシ、トークンを、 モデル、名前、状態、環境、ユーザーで分けて出します。失敗率が 40% なら、 いま読んでいる実行は問題ではありません。 |
結果は JSON ではなくテキストで返ります。誰のコンテキストを使うかを考えた、 意図的な選択です。スパンオブジェクト 40 個はほとんどが記号であり、 同じ実行をウォーターフォールとして描けば数百トークンで済み、エージェントは その形を見て取れます。
その姿
MCP サーバーは 2 つ目のサービスではなく、同じバイナリのサブコマンドです。
mcpd mcp として動き、標準入出力で JSON-RPC を話し、自分のデータは
持ちません。開けるポートもなく、生かし続けるプロセスもなく、デプロイするものも
ありません。エージェントのホストが、会話で必要になったときに子プロセスとして
起動し、終わったら止めます。
持っているのはデーモンへの接続です。ツールの呼び出しはすべて、上で説明した同じ クエリ API へのリクエストになり、同じ read キーと同じプロジェクトの範囲が 適用されます。したがってエージェントは、渡されたキーが届かないプロジェクトには 届きません。このサーバーをエージェントに渡すことの影響範囲は、そのキーを渡す ことの影響範囲とちょうど同じです。設定する前に確かめるべきはそこです。
ツールの数は意図的に少なくしてあります。選べるものが多いとモデルは選び方を誤りますし、
エージェントの挙動についての質問はたいてい、実行を並べ、1 つを開き、ある期間に
ついて数値を尋ねる、という形に行き着きます。クライアントは、ここに書き下ろされて
古びるかもしれない一覧ではなく、プロトコル自身の tools/list
呼び出しで現在の一式を知ります。
クライアントの設定
MCP のホストは、コマンド、いくつかの引数、環境からサーバーを起動します。 どのファイルでどのキーを使うかはホスト側の事情であって mcpd の事情ではないので、 ホストのドキュメントを確認してください。ホストがあなたから必要とする 3 つの値は どこでも同じです。
| ホストが必要とするもの | 値 |
|---|---|
| コマンド | ホストが実行できるパスに置かれた mcpd のバイナリ。 |
| 引数 | mcp サブコマンド。環境より引数のほうが渡しやすいホストの
ために、問い合わせ先のデーモンを指す --endpoint と、その
read キーである --api-key を取ります。
|
| 環境 | MCPD_ENDPOINT と MCPD_API_KEY という同じ 2 つの
値。エンドポイントの既定は http://127.0.0.1:4317 で、
デーモンにキーが設定されていなければキーは省けます。
|
ホストが実行するもの
MCPD_ENDPOINT=https://mcpd.internal MCPD_API_KEY=$READ mcpd mcp そのキーを発行するときに押さえておくことが 3 つあります。
-
エージェントには
ingestキーではなくreadキーを 渡してください。スコープが分かれているのは、スパンを書くための資格情報が、 保存されたすべてのプロンプトを読むための資格情報にならないようにするため です。 - キーはプロジェクトに絞ってください。最上位で設定したキーは、デーモン上の すべてのプロジェクトに届きます。
-
adminの一覧を設定するまでは、read キーでトレースを削除できる ことを知っておいてください。admin スコープは未設定のとき read の一覧に 戻るからです。admin の一覧を一度も与えられていないデーモンでは、read キーを 持つエージェントが実行を削除できます。
3 つともセキュリティにあります。
どれを使うか
| 経路 | 選ぶとき |
|---|---|
| HTTP | あなたの言語に SDK がない、シェルやジョブから送っている、あるいは依存を一切増やしたくないとき。 |
| SDK | アプリケーションに計装を入れていて、バッチ処理、リトライ、例外を投げないことが依存の重さに見合うとき。 |
| OpenTelemetry | すでに OTel を出していて、ライブラリを足すより URL を変えるほうがよいとき。 |
| MCP | コードが定期的に読むのではなく、人がエージェントに保存済みの実行について尋ねるとき。 |
これらは混ざります。TypeScript の SDK で送信する多数のサービス、
curl でスコアを投げる夜間ジョブ、そしてそのすべてを MCP 経由で
読み返すエージェントは、3 つではなく 1 つのデプロイです。
次に
- 何が保存され、誰が読めるか。セキュリティ。
- 既存の OTel の設定を mcpd に向ける。OpenTelemetry。
- 仕様から生成された全エンドポイント。HTTP API。
- 知っておく価値のあるクエリ。実行を探す。