本文へスキップ
mcpd
デーモンなし
ソース

エージェントが踏むすべてのステップを、最後まで保持し、記録に残す。

mcpd はデーモンであって、フレームワークではありません。ステップ間の実行を保持するのは mcpd です。リトライも、タイマーも、承認を待つ 1 週間もそこに含まれます。ステップの中身は、あなた自身のコードがどの言語でも担当します。すべてのステップは、かかったコストとともにトレースに残ります。

モデルのレイテンシ
A strip chart recording model latency over the last hour: a baseline near 420 milliseconds with five occasional excursions towards 800 milliseconds.
A strip chart recording model latency over the last hour: a baseline near 420 milliseconds with five occasional excursions towards 800 milliseconds.

ビルドして実行

git clone https://github.com/mcpd/mcpd && cd mcpd && make build && ./target/release/mcpd demo

あとは localhost:4317 を開くだけです。アカウントも、立ち上げるデータベースも、署名するものもありません。リリースはまだありません。イメージとパッケージは最初のリリースと同時に公開します。5 分のクイックスタートを読む。

読み解ける実行。

エージェントが失敗する場所は、エラーが書かれたログ行ではありません。失敗はその 3 ステップ前、モデルが誤ったツールを選び、もう一度同じツールを選んだところにあります。

mcpd は実行全体をツリーとして復元します。モデル呼び出し、ツール呼び出し、検索の一つひとつに、そのときのプロンプトと補完、それぞれのレイテンシ、そしてそれぞれのコストが付きます。

インターフェースを開く

Trace b3d9e71a Fault
0ms 2.1s 4.2s 6.3s 8.4s
  1. agent support_agent fail
  2. model chat
  3. tool returns_policy
  4. model chat
  5. tool returns_policy
  6. model chat
  7. tool returns_policy

The model called one tool three times and never answered. That is visible in four seconds here, and invisible in a log file.

実行ドラム

同じ実行を記録ドラムに巻いたものです。時間は周に沿って進み、スパンごとに 1 本のトラックがあります。回すと、ペンが真下のスパンを読みます。

ドラッグしてドラムを回すか、矢印キーを使います 2.64 s · tool returns_policy · 90 ms

起動したプロセスより長く生きる実行。

ステップ間の実行はデーモンが持ちます。ステップの中身はすべてあなたのプロセスが持ち、何も渡しません。モデルも、プロンプトも、資格情報もです。

リース盤

デーモンはステップごとに 1 枚のタグを盤に掛けています。ワーカーがタグを持てるのはハートビートが届いている間だけです。ワーカーを止めると、リース期間と回収ウィンドウ 1 回分の後、タグは盤に戻り、もう一方のワーカーが取ります。

クリックするか Space キーでワーカーを止めます draft · worker-a · started · lease 25 s

グラフはデータ、本体はあなたのもの

ワークフローはコードで一度だけ書きます。グラフは定義としてデーモンに渡り、そこで検証されてバージョンが付きます。ステップの本体は 6 言語のいずれかであなたのプロセスに残り、ワーカーがそれを実行します。

リトライ、タイマー、待機は、すべてストアの行

一時的な失敗は、そのステップ自身の予算でリトライします。スリープや承認待ちの間はプロセスが 1 つも生きていなくても実行が保たれ、デプロイも、クラッシュも、1 週間も越えられます。イベントは名前とキーで待機に応え、同じイベントが再び届いても重複であって、2 回目の実行にはなりません。

すべての実行がトレース

各ステップは、入力、出力、報告された使用量を持つスパンです。価格は、ほかのスパンに付けるのと同じ料金表で付きます。ステップ内部のモデル呼び出しはその下にぶら下がるので、検索もコストもスコアラーも、実行をそのまま扱えます。

ワークフローについて読む

すでに使っている言語で計装。

6 つの SDK、振る舞いは 1 つです。スパンの記録がブロックすることも、例外を投げることも、際限なく増えることもありません。mcpd に届かなくても、あなたのエージェントは気づきません。可観測性の失敗の仕方として、許されるのはそれだけです。どの SDK もワーカーを備えているので、永続実行のステップは、それを記録するのと同じプロセスから提供されます。

どれも必須ではありません。OTEL_EXPORTER_OTLP_ENDPOINT を mcpd に向ければ、すでに動かしている OpenTelemetry SDK がそのまま届きます。あなたのアプリケーションに私たちのコードは 1 行も入りません。6 つの SDK は HTTP の面の上に置いた便利な層であって、記録される条件ではありません。

フレームワークに付属してくる可観測性が記録できるのは、そのフレームワークで作ったエージェントだけです。mcpd は OTLP エンドポイントに届いたものを何でも受け取ります。エージェントフレームワークでも、自分で書いたループでも、そのどちらかが呼び出すサービスでもかまいません。

実行を計装する

/**
 * One ticket, one trace.
 *
 * `withSpan` opens the span, keeps it active for everything the callback
 * starts, and ends it when the callback settles. Nothing inside has to be
 * passed a handle to be nested under it.
 */
async function handleTicket(ticket: Ticket): Promise<{ answer: string; traceId: string }> {
  let traceId = ''
  const answer = await mcpd.withSpan('support_agent', async (run) => {
    // Keep the id: a score or a piece of feedback that arrives later attaches
    // to this run by it.
    traceId = run.traceId
    // Tags label the whole run, so the trace list can filter on them.
    run.setTags(['support', 'tier-1'])
    run.setCorrelation({ sessionId: ticket.id, userId: ticket.customerId })
    run.setInput({ question: ticket.question })

    const order = await lookUpOrder(ticket)
    const answer = await draftAnswer(ticket, order)

    run.setOutput({ answer })
    return answer
  })
  return { answer, traceId }
}

6 つのシグナル、結び付けるのはそれを生んだ実行。

多くのツールはトレースをここに、ログを別のどこかに置き、タイムスタンプの突き合わせは目視に任せます。mcpd は 6 つのシグナルすべてを同じ trace id と span id に対して保存します。だからグラフ上のスパイクからは、それを起こした実行が開き、実行からは、そこで出力されたログ行が見えます。

仕分け台

6 種類のシグナルが 1 本のベルトに混ざって届きます。リーダーが各片の trace id を読み、同じ実行の片はすべてその実行の引き出しに入ります。

引き出しを指すか、矢印キーを使います trace 4bf9 · span 3 · log 1 · metric 1 · score 1 · feedback 1

振る舞いが一致する 13 言語。

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.

どの SDK も単体でテストしていますが、それで分かるのは作者が意図したとおりに動くことだけで、互いに一致するかどうかは何も分かりません。そこで、各 SDK が 1 つのデーモンに対して同じ実行を書き込み、そのトレースを読み戻して互いに比較します。

ある言語で成り立ち、別の言語で成り立たない振る舞いは、そこでしか表に出ません。変更のたびに CI で実行します。他の選択肢との比較。

誰が何を読んだか、断られた人も含めて。

セキュリティレビューを決めるのは、そのデータが安全かどうかという問いではありません。あとから、誰がそこに届いたかを示せるかどうかです。

Audit log

GET /v1/api/audit

Time Principal Action Project Result
14:02:11 a3f9c2e1b7d4 get_trace checkout ok
14:02:11 a3f9c2e1b7d4 list_spans checkout ok
14:04:38 7c1e08b53a9f list_dataset_items billing refused
14:07:02 d52a4f8c0e63 capture_into_dataset checkout ok
14:11:55 d52a4f8c0e63 delete_trace checkout ok
14:19:20 7c1e08b53a9f list_feedback billing ok

A refusal is recorded like a read. The principal is a digest of the key, never the key. Retention never trims this table.

18 actions

An audit log with six entries, each showing the time, a truncated digest of the key that acted, the action, the project and the result. One entry, a read of another project's dataset items, is recorded as refused.

6 つのキースコープ

テレメトリーを送ること、プロンプトを読むこと、保存済みのデータを変えること、それを破棄すること、実行を開始すること、ステップを取得することは、別々の権限です。write を設定しなければ ingest キーを受け付け、admin を設定しなければ read キーを受け付けます。execute と worker を設定しなければ write キーを受け付けます。アップグレードで締め出されることはありません。

プロジェクトが境界

キーも保持期間もサンプリングも、すべてプロジェクト単位です。あるプロジェクトに登録されたキーが届くのはそのプロジェクトだけで、デーモンはデータの持ち主を確かめてから、その中の 1 件を返します。

外部への自動通信はなし

デーモン全体で外向きのリクエストを出す箇所は 4 つだけで、そのすべてをあなたが設定します。フォワーダー、2 つの webhook、そしてジャッジです。更新チェックも、テレメトリーも、ライセンスサーバーもありません。

どのキーで、どの人物か

監査台帳にはキーが残ります。前段のイングレスがすでに付けているヘッダーと、それを付けてよいアドレスを mcpd に伝えれば、人物も残ります。それ以外の場所から送られたヘッダーは誰でも名乗れる主張にすぎないので、その欄は推測を書かずに空のままにします。

削除の依頼は 1 回の呼び出し

1 回の DELETE で、ある人物に対して記録された実行がすべて消え、そこにぶら下がるスパン、ログ、メトリクス、スコア、フィードバックも一緒に消えます。応答はシグナルごとの件数です。そのキーで開けないプロジェクトの行は、触られることも、数に入ることもありません。

信頼する前に点検

mcpd doctor は動いている構成を採点します。ストレージが書き込みを受け取って返せるか、開いたままのスコープがないか、設定済みのプロジェクトがスパンを一度でも受け取ったか。対処が必要な項目には、それを直すフラグの名前が付きます。

mcpd がやらないこと。

どのツールにもこういう一覧があります。多くは社内に留められていて、あなたが気づくのは 2 週目です。ここに出しておきます。添えるのは日付ではなく理由です。

gRPC 上の OTLP はなし。
HTTP は両方のエンコーディングでトレースを、JSON でログとメトリクスを運びます。gRPC はまったく対応していません。
ベンダー名の付いたエクスポーターはなし。
mcpd が転送するのは OTLP です。1 つの実装でほとんどの送り先に届き、mcpd はデータが留まらなければならない場所ではなく、途中の 1 ホップになります。自社のプロトコルしか話さない送り先には届きません。
SSO も SCIM もなし。
キーのスコープはプロジェクトで、これはテナントの境界であって、ID プロバイダーではありません。その人が誰であるかは、mcpd の前段でイングレスが確立したものがすべてです。
マルチリージョンの取り込みはなし。
Postgres や ClickHouse の上では、1 つのサービスの背後で複数のレプリカとして動きます。それはリージョン間のフェイルオーバーではありません。

既存製品のソースを直接監査した結果と突き合わせた完全な内訳は PARITY.md にあります。更新するのは何かが入ったときであって、何かが計画されたときではありません。

最初のトレースまで 5 分。

デーモンを起動し、エージェントをそこへ向け、インターフェースを開きます。それより時間がかかるなら不具合です。報告をお待ちしています。

起動

docker run -p 4317:4317 ghcr.io/mcpd/mcpd