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

設定

設定はどれもフラグ 1 つで表され、どのフラグにも同じ意味の環境変数があります。コマンドラインに収まらないほど構造化された少数の設定は、1 つの JSON ファイルに置きます。このページはそのすべてです。

同じことを指定する 2 つの方法

引数を何も付けずに mcpd を実行しても動きます。0.0.0.0:4317 で 待ち受け、データを ./mcpd.db に置き、機密値を秘匿化し、 すべてのトレースを無期限に保存します。以下の設定は、いずれもその判断の どれか 1 つを変えるものです。

フラグは常に対応する環境変数より優先されます。どちらもグローバルなので、 サブコマンドの前でも後ろでも受け付けます。

リファレンス

フラグ 環境変数 既定値
--listen MCPD_LISTEN 0.0.0.0:4317
--data MCPD_DATA ./mcpd.db
--project MCPD_PROJECT default
--retention-days MCPD_RETENTION_DAYS 未設定:すべて保持
--ingest-key MCPD_INGEST_KEYS なし:取り込みは開放
--read-key MCPD_READ_KEYS なし:読み取りは開放
--write-key MCPD_WRITE_KEYS なし:取り込みキーを受け付ける
--admin-key MCPD_ADMIN_KEYS なし:読み取りキーを受け付ける
--execute-key MCPD_EXECUTE_KEYS なし:書き込みキーを受け付ける
--worker-key MCPD_WORKER_KEYS なし:書き込みキーを受け付ける
--runs-data MCPD_RUNS_DATA --data のパスの拡張子の前に .runs を挟んだもの
--trusted-identity-header MCPD_TRUSTED_IDENTITY_HEADER なし:主体を記録しない
--trusted-proxy-cidr MCPD_TRUSTED_PROXY_CIDRS なし:そのヘッダーを設定できるアドレスはない
--sample MCPD_SAMPLE 未設定:すべてのトレースを保持
--quota-per-minute MCPD_QUOTA_PER_MINUTE 未設定:上限なし
--no-redact MCPD_NO_REDACT オフ:秘匿化は有効
--redact-key MCPD_REDACT_KEY 組み込みのキー。公開されています
--redact-key-contains MCPD_REDACT_KEY_CONTAINS なし:組み込みルールのみ
--redact-max-string MCPD_REDACT_MAX_STRING 128 KiB
--cors-origin MCPD_CORS_ORIGINS なし:同一オリジンのみ
--request-timeout-seconds MCPD_REQUEST_TIMEOUT_SECONDS 30
--max-body-bytes MCPD_MAX_BODY_BYTES 33554432、つまり 32 MiB
--max-batch-signals MCPD_MAX_BATCH_SIGNALS 10000
--log-format MCPD_LOG_FORMAT text
--clickhouse-url MCPD_CLICKHOUSE_URL オフ:組み込みファイルを使う
--clickhouse-database MCPD_CLICKHOUSE_DATABASE mcpd
--clickhouse-user MCPD_CLICKHOUSE_USER なし
--clickhouse-password MCPD_CLICKHOUSE_PASSWORD なし
--postgres-url MCPD_POSTGRES_URL オフ:組み込みファイルを使う
--config-file MCPD_CONFIG_FILE なし
--forward-endpoint MCPD_FORWARD_ENDPOINT オフ:何も転送しない
--forward-header MCPD_FORWARD_HEADERS なし
なし MCPD_LOG mcpd=info,mcpd_server=info,tower_http=warn

--listen

バインドするアドレスです。OTLP エンドポイント、ネイティブの取り込み エンドポイント、クエリ API、プローブ、メトリクス、インターフェースを 1 つのポートで提供するので、mcpd が持つアドレスはこれ 1 つだけです。

ループバックアドレスにバインドするのが静かな選択です。mcpd が取り込みの 開放を警告するのは、バインドしたアドレスがループバックでないときだけです。 キーを何も設定せずに 0.0.0.0 で動かすと、起動時に取り込みに ついてと読み取りについて、二度警告します。

--data

SQLite ファイルのパスです。親ディレクトリが無ければ作成します。 :memory: を渡すと何も残さないストアになります。デモやテスト には適していますが、明日も見返したいものには向きません。

--project

送信側がプロジェクトを名指ししなかったとき、シグナルが入るプロジェクト です。プロジェクトはテナントの境界であり、どのクエリも 1 つのプロジェクト に閉じます。保持期間の削除処理は、このプロジェクトと projects ブロックにあるすべてのプロジェクトに対して動きます。どちらにも現れない プロジェクトは一度も削除されません。送信側が自分でプロジェクト名を決めて いる場合は、ここが効いてきます。

--retention-days

この日数より古いシグナルを削除します。未設定ならすべて残ります。ノート PC ではそれが正しい既定ですが、それ以外の場所では意識して決める価値のある 設定です。

期間は 1 から 36500 日までで、プロジェクトの retention_days も同じです。 0 は保持期間なしとは読まれず、起動時に拒否されます。0 は切り捨ての基準を現在にし、 最初の削除処理は起動時に走るので、そのプロジェクトのシグナルをすべて消してしまうからです。 すべて残すには、この設定を書かないでください。

削除処理はクエリ時ではなく 1 時間ごとのタイマーで動くので、大きな削除が 利用者のリクエストの中に入り込むことはありません。スパン、トレース、 ログ、メトリクスは mcpd が受け取った時刻で切られ、スコア、フィードバック、 観点ノートはそれぞれ自身のタイムスタンプで切られます。行を削除してもファイルは 小さくなりません。運用 を参照してください。

--ingest-key、--read-key、--write-key、--admin-key、--execute-key、--worker-key

スコープが 6 つあるのは、それぞれ抱えるリスクが本当に違うからです。どの エンドポイントも必要なスコープを spec/mcpd.spec.json で 宣言し、ハンドラー側にも同じ印が付いていて、make check が 両者を突き合わせます。ここに書かれたスコープは、動いているサーバーが 実際に強制するスコープです。

スコープ 設定するフラグ 開くもの
ingest --ingest-key 4 つの取り込みエンドポイント。読み取りは一切できません。
read --read-key クエリ API。トレース、スパン、ログ、メトリクス、スコア、分析。
write --write-key スコア、フィードバック、データセット、実験、確認応答、そして実行を データセットに取り込む操作。
admin --admin-key トレースの削除、監査ログの読み取り、および下の 2 つのスコープが開くものすべて。
execute --execute-key 実行の開始、取り消し、シグナル送信、および実行へのイベント発行。
worker --worker-key 定義の登録、ステップの取得、結果の報告。トレースは読めません。

もっとも重要な分割は、ingest とそれ以外の分割です。取り込み キーはスパンを出すすべてのプロセスに置かれます。その中には、あなたが 書いていないコードを動かしているプロセスも含まれます。その集団に、 保存済みのプロンプトをすべて読み返せる資格情報まで渡すのは間違いです。 mcpd はその間違いを犯しにくくしてあり、取り込みキーをクエリ API に 出せば拒否されます。

write を ingest から分けているのも、規模は 小さいながら同じ理由です。実行をデータセットに取り込む操作は、その実行の 入力と出力を読みます。書き込みの姿をした読み取りなので、フリート全体が 持ち歩くキーに載せるべきものではありません。

admin を read から分けているのは、実行を削除する ことが実行を読むことではないからです。--admin-key を設定する までは、読み取りキーは読み取りだけでなく削除もでき、mcpd は起動時に そのことを伝えます。

execute と worker は実行まわりの 2 つのスコープ です。実行を開始すると誰かのワーカーが働き、費用が発生します。トレースに 注釈を付けることではそうならないので、これは write では ありません。ワーカーキーはステップの本体を動かすプロセスに置かれ、それは たいていプロバイダーの資格情報を持つプロセスでもあります。だからこの スコープが開くのはリース処理だけで、保存済みのプロンプトを読むものは 何も開きません。2 つのフラグを設定するまでは、どちらも書き込みキーを 受け付け、管理キーは常に受け付けられます。

無停止でのアップグレード

--write-key が未設定なら取り込みキーを受け付け、 --admin-key が未設定なら読み取りキーを受け付けます。この 2 つのスコープが存在する前に構成されたデプロイは、そのため以前と まったく同じ範囲を保ち、アップグレードで壊れるものはありません。 2 つのフラグを設定すれば、より狭い権限に切り替わります。どちらの状態に いるかは mcpd config が教えてくれます。そのスコープが自前の キーを持つときは key required と表示し、他から継承している ときは key required (ingest keys) または key required (read keys) と表示します。

6 つのフラグはいずれも繰り返し指定でき、複数のキーを持てます。これが 無停止でのキーの入れ替え方です。環境変数はカンマ区切りのリストを取ります。 呼び出し側はキーを Authorization: Bearer KEY または X-Mcpd-Key: KEY として提示し、比較は定数時間なので、長さも 最初に食い違った位置も外からは分かりません。

mcpd が知らないキーは 401 です。mcpd が知っているキーを、 そのスコープが及ばないエンドポイントで使えば 403 です。 この 2 つを区別する価値はあります。401 が返ると、まったく正しいキーの 中に打ち間違いを探し始めてしまうからです。

キーを 1 つも設定しなければ、すべてのスコープが開いています。これは 意図した挙動です。最初の 1 回にまで資格情報を要求すべきではないからで、 起動時の警告がある理由もそこにあります。

キーが及ばない範囲にも注意してください。このビルドでは /healthz、/readyz、/metrics、そして インターフェース自体は、キー無しで提供されます。それが問題になるなら、 自分の ingress の後ろに置いてください。

空のエントリーは、何かがリストを読む前に捨てられます。MCPD_EXECUTE_KEYS= や Kubernetes の Secret の空の値のように、設定されているのにキーを 1 つも含まないリストは、デーモンの起動を止め、拒否のメッセージはフラグと変数を示します。スコープを未設定にしたいなら、変数そのものを置かないでください。空のキーヘッダーを送るリクエストは、キーを提示していない扱いです。deploy/ の例のファイルに残っているプレースホルダー、つまり大文字小文字を問わず REPLACE_WITH か replace-me で始まる値は、デーモンの起動を止めます。拒否のメッセージはフラグかプロジェクトのリストを示し、値そのものは示しません。--redact-key も同じです。キーはそれぞれ openssl rand -hex 32 で生成してください。--write-key、--execute-key、--worker-key のどれも設定していないと、取り込みキーで実行の開始とステップの取得もでき、mcpd は起動時と mcpd doctor でそれを警告します。

監査ログ

ペイロードを返すハンドラーは、誰が何を読んだかを記録します。対象は GET /v1/api/traces/{id}、スパンとログの検索、 フィードバック、データセットの項目、取り込み、削除です。各行には時刻、 操作したキーの短いダイジェスト、操作、プロジェクト、リソース、そして 認可が許可したかどうかが入ります。キー自体が書き残されることはありません。

ログは GET /v1/api/audit で読みます。管理スコープが必要です。 このログは --retention-days では削除されません。説明対象の データと同じ日程で自分自身を消してしまうログでは、すでに消えたデータを 誰が読んだのかを答えられないからです。行の書き込みに失敗しても、その行が 説明する読み取りが失敗することはありません。書けなかった行はログに出て、 /metrics の mcpd_audit_write_failures_total に 数えられます。アラートを付ける価値があります。

--sample

0.0 から 1.0 までの割合です。この範囲を外れた値は、黙って丸めるのでは なく起動エラーになります。--sample must be between 0.0 and 1.0, got 2 のように出ます。

判定はトレース id のハッシュで行うので、ロードバランサーの後ろに並んだ mcpd 群は、互いに通信しなくても同じトレースを残し、同じトレースを捨てます。 判定はトレースごとに一度だけ行われ、バッチ内のそのトレースのすべての スパンに引き継がれます。半分だけ記録されたトレースは、観測対象の システムの不具合のように見えてしまうからです。

ハッシュはトレース id の末尾のバイトを読みます。先頭のバイトを タイムスタンプから導く実装があり、そちらを見るサンプラーは時刻と相関して しまい、まるごと数時間分を静かに捨てることになります。

ポリシー型には「エラーは常に残す」と「これより遅いものは常に残す」も ありますが、これらはトレースが終わってから下すテール側の判定に適用され ます。このビルドはヘッド側でのみサンプリングし、テールサンプリングを 有効にするフラグはありません。どちらを取るかは サンプリングで扱います。

--no-redact、--redact-key、--redact-key-contains、--redact-max-string

秘匿化は既定で有効で、何かが書き込まれる前に走ります。そのため、 そもそも保存されるべきでなかった値は一度も保存されません。後から削除 するのは同じ保証ではありません。組み込みのルールセットは 3 つあり、 いずれもキー名の小文字化した部分文字列で照合します。

ルール キーが含むとき 保存される内容
マスク api_key, apikey, api-key, secret, password, passwd, access_token, refresh_token, id_token, bearer, authorization, auth_token, private_key, client_secret, session_token, credential, signature, x-api-key [redacted]
部分マスク email, phone, ssn, credit_card, card_number, iban 先頭 2 文字と末尾 2 文字だけ、ab…yz の形。6 文字以下の 値は大半を漏らすことになるので、全体をマスクします
ハッシュ user_id, userid sha256: と鍵付きダイジェストの 12 バイト。利用者を 保存せずに、利用者ごとの集計は続けられます

照合したキーがオブジェクトや配列を持っていれば、その下にあるものもすべて 秘匿化されます。{"credentials": {"token": "..."}} が入れ子の名前だけをたよりにすり抜けることはありません。128 KiB を超える 単一の文字列は、バイト数を書いた印とともに切り詰められます。暴走した プロンプトがディスクに与える影響は、これで頭打ちになります。

--redact-key はハッシュルールの鍵です。1 つの鍵のもとでは 同じ入力は同じハッシュになり、それが集計を成り立たせています。鍵を 入れ替えれば、その結び付きは意図的に切れます。ハッシュを使うデプロイ では必ず設定してください。組み込みの既定値はソースにあるので、鍵無しの ダイジェストは推測できます。

--redact-key-contains は、組み込みの 3 つに加えて自分の マスクルールを足します。組み込みのルールは資格情報が普通どの名前で 保存されるかは知っていますが、自社のツールが顧客の口座番号を入れている フィールド名までは知りません。値の照合は組み込みルールと同じで、キーの 小文字化した部分文字列として扱います。そのため --redact-key-contains ticket は ticket_id も supportTicket も同じようにマスクし、照合したキーの下に 入れ子になったものも一緒にマスクします。複数指定するにはフラグを 繰り返すか、MCPD_REDACT_KEY_CONTAINS にカンマ区切りの リストを設定します。

--redact-max-string は、組み込みの 128 KiB という長さの 上限を下げます。上限を超えた文字列は、保存される前にそこで切られ、 何バイト落としたかを書いた印が付きます。

--no-redact は長さの上限も含めて、この処理全体を止めます。 環境変数としては MCPD_NO_REDACT=true です。上流ですでに 秘匿化している場合にだけ意味があり、上の 2 つの追加設定をこれと 並べて指定するとデーモンは起動を拒否します。適用する先が何も残らない からです。

--runs-data

実行ストアの置き場所です。定義、ステップ試行の記録、すべての期限と すべての待ちが入ります。シグナルのストアが何であっても、これは組み込み ファイルです。Postgres や ClickHouse を使っていても、実行はデーモンの そばの 1 つのファイルに残り、実行まわりは 1 台のレプリカが提供します。 既定では --data のパスの拡張子の前に .runs を 挟んだもので、既定値なら ./mcpd.runs.db、--data が :memory: ならこちらも :memory: です。 --runs-data off にすると実行はまったく提供されません。実行 まわりのエンドポイントはすべてこのフラグを名指しした 503 を 返し、デーモンのそれ以外は変わりません。

このストアの時間設定は、設定ファイルの runs の下にミリ秒 単位の整数で置きます。lease_ttl_ms、 reclaim_window_ms、stall_window_ms、 idempotency_window_ms、park_ttl_ms、 max_depth です。既定値である 30 秒のリースと 30 秒の再取得 枠は、1 台のマシンで 1 つのデーモンを動かす場合に適した値で、ワーカーが 登録時に読み返す値でもあります。

--cors-origin

どのブラウザーオリジンが、別のサイトからクエリ API を呼べるかを決めます。 何も設定しなければ、クロスオリジンのリクエストには access-control-allow-origin ヘッダーが付かず、ブラウザーは レスポンスをページに渡しません。これが既定であり、読み取り API が開放 されたままのノート PC 上のデーモンが、同じブラウザーで開いている どのウェブページからも読めてしまう事態を防いでいます。同梱の インターフェースはデーモン自身のオリジンから配信されるので、このフラグ は必要ありません。

このフラグがあるのは、インターフェースを別の場所、CDN や社内ポータルから 配信する場合のためです。そのオリジンをスキームとホストの形で指定します。 たとえば --cors-origin https://console.example です。複数 指定するにはフラグを繰り返すか、MCPD_CORS_ORIGINS に カンマ区切りのリストを設定します。オリジンになっていない値、たとえば console.example や末尾にスラッシュが付いたものは、フラグ名を 示して起動を拒否します。接続できないインターフェースと理由の分からない 状態を残すよりましだからです。--cors-origin '*' はすべての オリジンを許可します。以前のビルドは何も尋ねずにこう振る舞っていました。

/healthz、/readyz、/metrics はこの フラグの管轄外です。保存データを何も含まず、インターフェースは他の何より 先に /healthz を叩くので、どのオリジンからも読める ままにしてあります。

--request-timeout-seconds

デーモンが応答するまでに、リクエストがボディの読み取りを含めてかけて よい最長時間です。期限を過ぎたハンドラーには、本文に timeout とこのフラグ名を載せた 503 が返ります。既定は 30 秒で、 大きなバッチを読んで保存するには十分に長く、接続は受け付けるのに応答を 返さないストレージバックエンドを、待ち続けずに報告できる程度には短い 値です。無効にはできません。本当に長い時間が要る操作があるなら、値を 上げてください。

--max-body-bytes と --max-batch-signals

1 つのリクエストに対する 2 つの上限です。--max-body-bytes は デーモンが読むボディの最大サイズで、既定は 32 MiB です。デコードされる 前の段階で、1 つのリクエストがどれだけメモリーを使えるかを決めます。 これを超えるボディは、両方の数値を示した 413 で拒否され、 圧縮されたものは展開後に同じ数値で判定されます。1000 倍に膨れるボディは、 展開されるのではなく拒否されます。

--max-batch-signals は、デコード後に 1 つのリクエストが 運べるシグナルの数で、既定は 10,000 です。リクエスト全体をまたいで 数えるので、バッチの配列も NDJSON のストリームも、単一のバッチと同じ 数で判定されます。これを超えたリクエストでは何も保存されません。拒否は 最初の書き込みより前に起き、半分書いた後ではありません。

どちらも設定ファイルのキー max_body_bytes と max_batch_signals でもあり、両方に書かれていればフラグが 勝ちます。どちらも無効にはできず、ゼロは「上限なし」と読むのではなく 起動時に拒否します。ボディの上限がゼロならすべてのリクエストが拒否され、 寛大に見えるほうの解釈が取り込みを止めてしまうからです。

--log-format と MCPD_LOG

端末には text、ログ基盤には json です。 MCPD_LOG は tracing のフィルター文字列を取り、 対応するフラグはありません。デプロイの決定というより、デバッグのための つまみだからです。リクエストごとの詳細を見たいときは mcpd_server=debug に設定します。

2 つのリクエスト上限が効く場所

ボディの上限は、クエリ API も含めてボディを読むすべてのルートに効きます。 シグナルの上限は取り込みの入口に効き、それぞれの入口で数えるものは次の とおりです。

入口 シグナル上限が数えるもの
POST /v1/ingest リクエストに入っているスパン、ログ、メトリクス、スコア、フィードバック、観点ノートのすべてを合計した数。単一のバッチで届いたか、バッチの配列で届いたか、NDJSON の行の並びで届いたかは問いません
POST /v1/traces 送信側が送ったスパンの数。変換される前に数えるので、送信側が対処できる数になります
POST /v1/logs エクスポートに入っているログレコードの数
POST /v1/metrics エクスポートに入っているすべてのメトリクスにわたるデータポイントの数

サブコマンド

コマンド 動作
mcpd serve デーモンを動かします。サブコマンドを何も付けないときもこれになります
mcpd demo 現実的なサンプルの実行をストアに書き込んでから提供を始めます。最初にインターフェースを開いたときに中身があります
mcpd config この呼び出しで使われる設定を表示して終了します
mcpd mcp Model Context Protocol を stdio で提供します。エージェントが動いているデーモンから自分のトレースを読めます
mcpd doctor このデプロイを採点し、設定の誤りを報告します。障害があれば終了コード 2 で終わるので、デプロイの門番にできます。運用を参照してください

mcpd config は、これからコンテナと始める言い争いのために あります。フラグ、環境変数、既定値はすべて一か所に落ち着きます。この コマンドは、サーバーを起動せずデータファイルにも触れずに、その一か所を 表示します。

mcpd config
$ MCPD_INGEST_KEYS=k1,k2 mcpd --retention-days 30 --sample 0.1 configlisten          0.0.0.0:4317storage         sqlite at ./mcpd.dbconfig file     nonelog format      textproject         defaultprojects        noneretention       30 daysingest auth     key requiredread auth       openwrite auth      key required (ingest keys)admin auth      openexecute auth    key required (ingest keys)worker auth     key required (ingest keys)runs            sqlite at ./mcpd.runs.dbrun settings    lease_ttl_ms 30000, reclaim_window_ms 30000, stall_window_ms 300000, idempotency_window_ms 86400000, park_ttl_ms 86400000, max_depth 8, max_step_repeats 16identity        offsampling        deterministic 0.1quota           unlimitedredaction       onredact key      built in, which is public; set --redact-keycors            same-origin onlytimeout         30s per requestlimits          32 MiB per request, 10000 signals per batchderived         duration and token metricsaspects         offmonitors        noneobjectives      nonejudge           offforwarding      off

何かをデプロイする前に、認証の 6 行を読んでください。open は そのポートに届く相手なら誰にでも開いている、という意味です。括弧付きの 行は、そのスコープがどの広いリストを継承しているかを示します。古い リリースからアップグレードしたデプロイは、この状態から始まります。

設定済みのデプロイ

ここに mcpd 固有のものはありません。変数はただの環境変数なので、Compose でも Kubernetes のマニフェストでも systemd のユニットでも同じように 働きます。

compose

services:
  mcpd:
    image: ghcr.io/mcpd/mcpd
    ports: ['4317:4317']
    volumes: ['mcpd-data:/data']
    environment:
      MCPD_LISTEN: 0.0.0.0:4317
      MCPD_DATA: /data/mcpd.db
      MCPD_PROJECT: checkout
      MCPD_RETENTION_DAYS: '30'
      MCPD_INGEST_KEYS: write-key-one,write-key-two
      MCPD_READ_KEYS: read-key
      MCPD_REDACT_KEY: a-long-random-string
      MCPD_LOG_FORMAT: json
      MCPD_LOG: mcpd=info,mcpd_server=warn

volumes:
  mcpd-data:

同じ設定をフラグで書くと、シェルやユニットファイル向けにはこうなります。

mcpd --listen 0.0.0.0:4317 --data /data/mcpd.db --retention-days 30 --ingest-key $KEY --log-format json

SDK が読む変数

これらの変数を読むのはデーモンではなく、あなたのエージェントのプロセス です。ここに載せているのは、2 つの組が名前を共有していて、エージェントと mcpd を同居させたコンテナは 1 つの環境で設定されるからです。

環境変数 既定値
MCPD_ENDPOINThttp://localhost:4317
MCPD_API_KEY未設定
MCPD_PROJECTdefault
MCPD_ENVIRONMENT未設定
MCPD_SERVICE_NAME未設定
MCPD_SERVICE_VERSION未設定
MCPD_SAMPLE1.0
MCPD_ENABLEDtrue
MCPD_CAPTURE_PAYLOADStrue
MCPD_LOG_LEVELdebug

MCPD_PROJECT と MCPD_SAMPLE は両側で同じ意味を 持ちます。たいていはそれが望みどおりで、一度設定すればエージェントと デーモンのプロジェクトが揃います。考えどころはサンプリングです。SDK は 送信する前にトレースを捨てるので、ネットワークが節約できます。デーモンは 保存する前に捨てるので、こちらは自分の管理下にない送信元にも割合を強制 できる唯一の場所です。割合はどちらか一方で設定してください。両方で 設定すると、すでに絞られた流れをさらに絞ることになります。

MCPD_ENABLED=false にすると、SDK の呼び出しはすべて何もせず、 それでも正しいオブジェクトを返します。コードを変えずにテストで mcpd を 切れます。MCPD_CAPTURE_PAYLOADS=false は、プロンプトと 出力をプロセスの中に留めます。スパンは時間、使用量、モデル、エラーを 伴って届きますが、input と output はキューに 入る前に落とされるので、ネットワークには出ません。デーモン側の 秘匿化は同じ線の反対側の端であり、そもそもバイトを送ってはいけない チームにとっては誤った端です。言語ごとの詳細は各 SDK のページに あります。

--config-file

コマンドラインに収まらない設定を持つ JSON ファイルです。モニター、 サービスレベル目標とその評価間隔、転送、ジャッジ、実行ストアの時間設定、 2 つのリクエスト上限、そして下のプロジェクトごとのブロックが入ります。 YAML でも TOML でもなく JSON なのは、mcpd が他のすべてで既に JSON を 解析しているからで、依存も新しい文法も増えません。Kubernetes の ConfigMap としてそのままマウントできます。

max_body_bytes と max_batch_signals は コマンドラインだけでなくここにもあります。フリートが、一緒にデプロイ するモニターのそばにこれらを置いておけるようにするためです。両方に 書かれていればフラグが勝ちます。共有ファイルを全員のために書き換え なくても、1 つのインスタンスだけ別の上限にできます。

知らないキーは無視されるのではなく起動を失敗させます。打ち間違いのせいで 何も監視していない状態が静かに続くのが、起こりうる最悪の結果だからです。

バックグラウンドのタイマーが扱えない値も、同じく起動を失敗させます。0 以上 1 未満でない objective、 1 から 365 の外にある目標の window_days、 86400 を超える monitor_interval_seconds、 1 から 31536000 の外にある aspects.window_seconds、 604800 を超える aspects.interval_seconds、 1 から 300 の外にある judge.timeout_seconds です。 どの拒否も、キーと範囲を示します。

{
  "max_body_bytes": 33554432,
  "max_batch_signals": 10000,
  "monitor_interval_seconds": 60,
  "monitors": [
    {
      "id": "error-rate",
      "name": "Error rate above 5%",
      "project": "checkout",
      "watch": "error_rate",
      "compare": "above",
      "threshold": 0.05,
      "window_seconds": 300,
      "min_samples": 20,
      "webhook": "https://hooks.example.com/mcpd"
    }
  ],
  "forward": {
    "endpoint": "https://otlp.example.com",
    "headers": [["Authorization", "Bearer sk-..."]],
    "batch_size": 256,
    "enabled": true
  }
}

プロジェクトと、そこに届くキー

プロジェクトはテナントの境界です。それを飾りではなく本物にしているのが、 プロジェクトの下にキーを並べる仕組みです。プロジェクトのブロックにある キーはそのプロジェクトにだけ届き、他には届きません。だから 1 つの デーモンで複数のチームを受け持っても、互いのプロンプトは読めません。

{
  "projects": {
    "checkout": {
      "ingest_keys": ["ck-write-..."],
      "read_keys": ["ck-read-..."],
      "admin_keys": ["ck-admin-..."],
      "retention_days": 30,
      "sample": 0.25
    },
    "support": {
      "ingest_keys": ["sp-write-..."],
      "read_keys": ["sp-read-..."],
      "retention_days": 90
    }
  }
}

6 つのリストはすべてプロジェクトごとに存在し、ingest_keys、 read_keys、write_keys、admin_keys、 execute_keys、worker_keys という名前で、継承の仕方も同じです。 write_keys を持たないプロジェクトは自分の取り込みキーを 受け付け、admin_keys を持たないプロジェクトは自分の読み取り キーを受け付けます。上の support ブロックはどちらも設定して いないので、2 つのスコープが存在する前とまったく同じに振る舞います。

トップレベルの 6 つのフラグで設定したキーは、すべてのプロジェクトに 届きます。これがシングルテナントでの使い方で、こちらは簡単なままです。 このブロックを一度も開かなければ、ここにあるものには出会いません。

retention_days と sample は、プロジェクトが 設定していなければグローバルのフラグに戻ります。どちらもプロジェクト ごとにあるのは、キーと同じ理由です。あるチームの 90 日という要件が 他の全員のデータまで残させるべきではありませんし、あるチームの トラフィックが別のチームのサンプリング率を決めるべきでもありません。

1 つのバッチが複数のプロジェクトを名指しすることもあります。範囲を 絞った取り込みキーは、届く範囲のシグナルを書き、残りを拒否します。 拒否は黙って捨てるのではなくレスポンスで報告されるので、間違った プロジェクトに向いたフリートは、すでに読んでいる返信から分かります。

キーが届かないプロジェクトへの読み取りリクエストは、そのプロジェクトが 存在するかどうかを明かさずに拒否されます。共有デーモンでは、 プロジェクト名がチーム名や顧客名であることが多いからです。

モニターとサービスレベル目標は モニターとアラートで 扱います。目標は monitors の隣の slos 配列に 書き、同じタイマーで評価されます。judge ブロックはモデルに よる採点を有効にするもので、 回帰テストで扱います。 設定するまでは無効で、ジャッジを使うスコアラーはリクエストが名指し しない限り動きません。採点は読んだ実行ごとに費用を使うからです。

{
  "judge": {
    "endpoint": "http://localhost:11434/v1",
    "model": "qwen2.5:14b",
    "api_key": "",
    "criteria": {
      "answer_relevance": "Does the answer address the question that was asked?"
    }
  }
}

monitor_interval_seconds はモニターと目標を評価する間隔です。既定値は 60 で、5 未満の値は 5 に引き上げられます。

derived_metrics

デーモンが保存した span から導き出すメトリクスです。ブロックがなければ両方とも有効です。durations は、終了したエージェント、ツール、ワークフロー、モデル生成、プロセッサーの span ごとに所要時間の点を 1 つ書きます。tokens は、使用量を報告したモデル生成の span ごとに最大 13 個のトークンカウンターを書きます。どちらかを false にするとその書き込みが止まり、省いたフィールドは有効のままです。これに対応するフラグはありません。導出した点はプロジェクトごとの上限の後に加えられ、取り込みの応答が返す accepted の数には含まれません。

{
  "derived_metrics": { "durations": true, "tokens": false }
}

aspects

デーモンが観点ノートをテーマにまとめるかどうかと、その範囲です。ブロックがなければ無効です。enabled でパスが有効になり、モデルも資格情報も要りません。outcome 観点はデーモン自身が書くからです。interval_seconds はパスの間隔で、既定値は 300、30 未満の値は 30 に引き上げられます。window_seconds は 1 回のパスがノートをさかのぼって読む範囲で、既定値は 86400 です。disabled にはどのパスもまとめない観点を名前で並べます。すでに記録されたノートは読めるままです。

{
  "aspects": {
    "enabled": true,
    "interval_seconds": 300,
    "window_seconds": 86400,
    "disabled": ["sentiment"]
  }
}

--forward-endpoint

保存したスパンをすべて、別の OTLP/HTTP エンドポイントに複製します。 ベース URL を渡してください。/v1/traces は自動で付きます。 ローカルへの書き込みが常に先なので、宛先が落ちていても損はありません。

このフラグは設定ファイルの forward ブロックより優先される ので、共有の ConfigMap を編集せずに 1 つのインスタンスだけ別の宛先に 向けられます。転送を 参照してください。

--forward-header

転送するバッチごとに送るヘッダーで、Name: value の形で 書きます。複数指定するにはフラグを繰り返します。環境変数はカンマ区切りの リストを取ります。

コロンのない値は捨てられるのではなく、正しい書き方を示して起動時に拒否されます。 OpenTelemetry 流の key=value のヘッダーは、 ここでは key: value と書きます。

--clickhouse-url

ClickHouse サーバーの HTTP エンドポイントです。たとえば http://clickhouse:8123 です。これを設定するとストアが 組み込みファイルから ClickHouse に切り替わり、--data は 読まれなくなります。スキーマは起動時に作られます。

組み込みストアにある「書き込み手は 1 つ」「レプリカは 1 台」という制限を 外すのがこの設定です。保持期間が本番トラフィックの数か月分で測られる ようになってから選んでください。それより早くは要りません。どちらを取るかは 運用で扱います。

--clickhouse-database、--clickhouse-user、--clickhouse-password

テーブルを作るデータベースと、そこに届くための資格情報です。パスワードの 無いユーザーはパスワード無しで送られます。プライベートなネットワーク上の コンテナには適していますが、それ以外の場所では適していません。

--postgres-url

Postgres の接続文字列です。たとえば postgres://mcpd:secret@db:5432/mcpd です。これを設定すると ストアが組み込みファイルから Postgres に切り替わり、--data は読まれなくなります。スキーマは起動時に作られます。

これが中間の選択肢です。ClickHouse と同じく「書き込み手は 1 つ」 「レプリカは 1 台」という制限を外しますが、まだ持っていないデータベース を新たに運用する必要はありません。これと --clickhouse-url を 両方指定すると、どちらかが選ばれるのではなく起動が失敗します。

マイグレーションはアドバイザリーロックを取るので、複数のレプリカが同時に 起動しても安全です。適用するものが無い起動では、スキーマには一切 触れません。

TLS を使うには、接続文字列に sslmode=require を加えてください。mcpd は接続を暗号化し、サーバーの証明書を必ず検証します。 公開の認証局と照合し、sslrootcert=/path/to/ca.pem を指定すればそのファイルの認証局と照合します。独自の認証局を持つマネージドサービスにはこれが必要です。 verify-ca と verify-full は require として扱います。既定の prefer と disable では、接続は暗号化されません。

どのキーかだけでなく、誰が操作したかの記録

監査ログは、操作したキーのダイジェストを記録します。これは「ある管理 キーがこの実行を削除した」には答えますが、「どの人が」には答えません。 mcpd は ID プロバイダーを持たないので、身元は すでに手前に立っている ingress から来る必要があります。

プロキシが設定するヘッダーと、それを設定してよいアドレスを渡してください。

信頼する ingress から身元を受け取る

mcpd serve \ --trusted-identity-header x-forwarded-user \ --trusted-proxy-cidr 10.42.0.0/16

両方が必要で、この仕組みが安全である理由はアドレスのリストにあります。 ヘッダーは、そのポートにソケットを開ける相手なら誰でも送れる文字列です。 だから mcpd は、あなたが名指ししたアドレスからしかそれを読みません。 それ以外から来たリクエストは普通に処理され、その監査行には主体が入り ません。推測するより、そのほうが正直な答えです。値には長さの上限が あり、制御文字も除かれます。プロキシが攻撃者でなくても、ヘッダーは 攻撃者の影響を受けうるからです。

どちらのフラグも設定しなければ何も変わらず、主体は一度も記録されません。 それが既定です。

デーモン全体を 1 つのプロジェクトが使い切らないための上限

mcpd にあふれるほど送るプロジェクトは、同じデーモンの他のすべての プロジェクトに損をさせます。ディスクは埋まり、保持期間の処理は追いつかず、 何も悪いことをしていないチームが実行を失います。 --quota-per-minute は、それを止める上限です。

すべてのプロジェクトに上限を置き、1 つにはより大きな上限を置く

mcpd serve --quota-per-minute 10000

未設定なら上限はありません。一度も設定しないデプロイは、クォータが存在 する前とまったく同じに振る舞います。設定すると、3 つのものを覆います。 projects ブロックにあって自分の quota_per_minute を持たないすべてのプロジェクト、 --project が名指しするプロジェクト、そして 設定に一度も現れないすべてのプロジェクトです。

最後の 1 つが、これを提案ではなく上限にしている部分です。プロジェクト名は リクエストのボディの中で届くので、どの設定ファイルにも無い名前を選ぶ 送信元は、以前はどこにも課金されず、際限なく保存できました。文字列を 思い付くだけで抜けられる制限は、制限ではありません。

列挙されていないプロジェクトは、それぞれ 1 つずつではなく、全体で 1 つの カウンターを共有します。名前ごとにカウンターを作れば、送信元が名前を 思い付くだけで割り当てを増やせてしまい、同じ穴が形を変えて残ります。 共有するということは、1000 個の作り出された名前に分散した洪水も合計では この数に収まるということであり、自分の枠に値するプロジェクトは projects に書くべきだということでもあります。そこに書けば 自分の枠がもらえます。

上限が実際に抑えるもの

カウンターは 2 つの窓に重みを付ける方式です。この 1 分の数に、直前の 1 分の 数を、いま終わる窓がその 1 分をどれだけ覆っているかで重み付けして足します。 持続的な流量を上限に抑えつつ、その上に上限 1 つ分までのバーストを許します。 キューをまとめて吐き出す SDK は 1 つのバーストとして届くので、それを拒む カウンターは普通のトラフィックまで拒んでしまうからです。

この 2 つを合わせると、隠さずに書いておくべき算術上の帰結があります。 1 つのプロジェクトは、60 秒の区間の中で上限の二倍弱まで 保存できます。ある窓のいちばん最後で上限いっぱい、次の窓のいちばん最後で また上限いっぱい、というかたちです。これは近似の副作用ではありません。 上限を一度に使い切れて、その後も毎分上限を使えるどんなリミッターでも、 境界をまたぐときにはこれが起こります。取り除く唯一の方法はバーストを 拒むことです。

窓は 1 分で、変更はできません。フラグ名が単位を含んでいるので、「毎分」の 意味を変える 2 つ目の設定があると、最初の数字が読めなくなります。

超過分の扱い

上限を超えたシグナルは拒否され、報告されます。黙って捨てられることは ありません。バッチが丸ごと拒否されるわけでもありません。mcpd は収まる 分を保存し、収まらなかった分を伝えます。

  • スパンはトレース単位で扱います。半分だけの実行は、誰も保存しなかった 親を持つスパンを含むことになり、欠けた実行ではなく壊れた実行に見えます。 残りの枠に収まらないトレースは飛ばして次のトレースを試すので、大きな 実行が 1 つあるだけで、残りの枠が何も保存しないまま使われることは ありません。
  • ログ、メトリクス、スコア、フィードバックはそれぞれ独立しているので、 残った枠を到着順に埋めます。
  • 課金されるのは保存されたものだけです。切り詰めはトレースを丸ごと残す ので、許された数より少ないシグナルしか保存しないことがあり、その差は 返されます。そうしなければ、実行が残りの枠より大きいプロジェクトは、 何も保存しないまま枠をゼロまで使い果たしてしまいます。
  • 同じリクエストに入っている他のプロジェクトは影響を受けません。上限を 超えた 1 つのプロジェクトのせいで、隣で送られた別のプロジェクトの シグナルが失われることはありません。

レスポンスには、その数が rejected に入り、プロジェクト名と その上限を書いた行が errors に入ります。設定に一度も現れて いないプロジェクトには別の文が返ります。analytics-v2 という プロジェクトが上限を超えたと言われると、存在しない設定を探しに行って しまいますが、正しい答えはそのプロジェクトを追加することだからです。

誰も設定していないプロジェクトへの 8 件のシグナル、共有の上限は 5
$ curl -sX POST "$MCPD/v1/ingest" -H content-type:application/json -d @batch.json | jq{  "accepted": 5,  "rejected": 3,  "duplicate": 0,  "errors": [    {      "signal": "span",      "index": 0,      "message": "3 signal(s) named project analytics-v2, which                  no configuration lists. Every unlisted project                  shares one ceiling of 5 per minute and it was                  full, so they were not stored. Add analytics-v2                  to the `projects` block to give it an allowance                  of its own, or raise --quota-per-minute."    }  ]}

拒否は mcpd_signals_rejected_total だけでなく mcpd_signals_over_quota_total にも数えられるので、上限で 絞られているフリートと、不正なデータを送っているフリートを見分けられます。 運用を参照してください。

設定するときに確認する 2 点

上限は、シグナルを保存するすべての入口を覆います。バッチの入口である /v1/ingest、/v1/traces、/v1/logs、 /v1/metrics と、単一レコードの入口である POST /v1/api/scores、POST /v1/api/feedback です。 上限を超えたプロジェクトに対する答え方は、この 2 種類で意図的に違います。 バッチの入口は収まる分を保存し、残りを理由付きで rejected として報告します。バッチは複数のプロジェクトを名指しできるので、 リクエスト全体を拒むと上限内のプロジェクトまで巻き添えになるからです。 単一レコードの入口には部分的な答えがないので、429 と、 そのプロジェクトのカウンターが次のシグナルを受け入れるまでの秒数を載せた Retry-After ヘッダーで拒否します。この数はカウンター自身が 出すので、これに従うクライアントは、固定の待ち時間が明けたときではなく 空きができたときに戻ってきます。

書き込みの入口のうち 1 つだけは課金されません。保存済みの実行に対して スコアラーを走らせる POST /v1/api/score です。生成した スコアをそのままストレージへ渡すので、上限を使い切ったプロジェクトでも その処理の結果はディスクに載り、その行は mcpd_signals_over_quota_total にも mcpd_signals_unbounded_total にも数えられません。これは フリートが向ける入口ではなく、自分で開ける入口です。上限は送信元が 押し込める量の上限であって、ディスクに届きうるすべての上限ではないと 考えてください。

上限が --max-batch-signals、既定では 10,000 より小さいと、 正当な 1 つのバッチだけで 1 分ぶんの枠を超えることがあり、どれほど行儀の よい送信元でも切り詰められます。mcpd doctor はこれを limits として採点し、当てはまるプロジェクトを名指しします。 プロジェクトごとに上限が設定されていて、その横にトップレベルの上限が 設定されていない場合も警告します。それは、列挙されていないすべての プロジェクトを無制限のままにする設定だからです。

mcpd config は上限のかかったプロジェクトを名前で表示するので、 設定が何を覆っているかを推測せずに読み返せます。表示されるのは名前付きの 上限だけなので、共有の上限はフラグ自体で確認してください。