jev-mcp というMCPサーバーを作った。
TypeSafe AIの判断モデルJev を、Codexの作業中に呼び出すためのもの。Rustで実装していて、Codexとは標準入出力で通信する。
コードを書く作業そのものを別のエージェントへ渡すのではなく、調査中に出てくる小さな判断を切り出したい。
例えば「このファイルは今回の不具合に関係しているか」「次にどのサブシステムを調べるか」といった部分。調査・編集・テストはCodexが続け、その途中の分類や評価にJevを使う形にした。
まだ実験段階なので、これで開発が何倍速くなったとか、トークンが何割減ったという話ではない。まず接続と使い方を用意したところ。
以下は9月20日時点、b1688a68の実装についてのメモ。
作業全部ではなく、一つの判断を渡す
コーディングエージェントの作業には、ファイルを読む、編集する、テストを動かすという操作の間に、次の行動を選ぶ判断が入る。
その全部を同じモデルで処理してもよい。ただ、候補の分類や、渡された証拠に対する関連性の判定なら、質問を小さくして別のモデルへ渡せるのでは、というのが今回の出発点。
Codexがコード・差分・ログを調べる
-> 判断に必要な情報と質問をまとめる
-> jev-mcpからTypeSafe APIへ送る
-> 型付きの回答と確率分布を受け取る
-> Codexが追加調査・編集・テストを進める
「この不具合を分析して直して」と丸ごと渡す使い方にはしていない。
変更との関連性、調査先の選択、影響の大きさは、それぞれ別の質問にする。その結果を見て、次に何を実行するかは呼び出し側で決める。
単純な文字列検索や、テストを実行すれば確定することまでモデルへ聞く必要はない。Jevを呼ぶこと自体が目的になると、通信が増えるだけになってしまう。
用意したのは4つのツール
Jevは、状態と型を指定した質問を受け取り、構造化された判断を返すモデル。今回のサーバーでは、その入口を4つのMCPツールとして公開した。
| ツール | 用途 | 主な回答 |
|---|---|---|
jev.noul | 一つの条件が成立するか | Yesの確率noul |
jev.choice | 名前付きの選択肢から一つ選ぶ | choice、probabilities、confidence |
jev.score | 一つの観点を段階評価する | score、legend、probabilities、confidence |
jev.batch | 同じ材料に対する独立した質問をまとめる | 質問IDごとの回答 |
ここで、Choiceは一つを選ぶためのもの。「この5ファイルのうち、関係がありそうなものを全部知りたい」なら、ファイルごとのNoulをまとめる方が質問に合っている。
Scoreも、何となく100点満点で採点するのではなく、低い段階から順番に具体的な評価基準を渡す。現在の入力では2〜10段階を扱う。
3段階ならscoreの範囲は0〜2で、小数も返る。これは段階番号の加重平均であって、障害が起こる確率ではない。
判断材料は呼び出し側で集める
jev-mcp自体には、リポジトリを探索する機能も、エージェントの実行履歴を自動収集する機能もない。
渡すのはstateと質問。ファイルのパスだけを送っても、その中身を読んでくれるわけではない。
例えばjev.batchには次のような引数を渡す。これは説明用の例で、実際の障害ログではない。
{
"state": {
"diff": "ワーカーの再試行上限を3回から0回に変更した。",
"failure": "worker_retries_transient_failure: expected 3 attempts, got 1"
},
"questions": {
"related": {
"type": "noul",
"instructions": "failureはdiffに関係していますか?"
},
"investigate": {
"type": "choice",
"instructions": "diffとfailureから、最初に調べる箇所を選んでください。",
"criteria": {
"worker": "ワーカーの再試行制御",
"api": "HTTPリクエストの処理",
"unknown": "判断材料が不足している"
}
}
}
}
質問IDのrelatedやinvestigateは回答を対応付けるための名前。質問そのものはinstructionsへ書く。
選択肢にunknownを入れたのは、証拠が足りなくても何かを選ばせる形にしないため。もちろん、これを入れれば誤判断がなくなるわけではない。
Jevが関連ありと返しても、原因を確認したことにはならない。その結果を受けて再試行制御を読み、テストを動かして確かめるところまでが作業になる。
同じ情報への質問はまとめる
同じ差分と失敗ログについて何度も別々に問い合わせるのは、あまりうれしくない。そのため、独立した質問はjev.batchでまとめられるようにした。
一回のAPIリクエストに共通のstateと複数の質問を入れ、回答を質問IDで受け取る。
ただし、バッチ内の質問は前の質問の回答を使わない。「関連ありなら、その理由を踏まえて次を選ぶ」という順次処理ではない。
前の判断を受けて追加のログを読んだり、候補を変えたりする必要があるなら、その時点で新しいstateを作って次の呼び出しにする。
サーバー内部では、単発ツールもresultという質問IDのバッチへ変換している。HTTPリクエストと回答検証の経路を共通化し、単発の結果はanswers.result、バッチはanswers.<質問ID>へ入る。
confidenceを正解率として扱わない
ChoiceとScoreにはconfidenceが付くが、これは回答の確率分布から算出される指標。
TypeSafeの説明
でも、選択肢に確率が集中しているか、分散しているかを表すものとして扱われている。
confidence: 0.9だから、その判断が90%の確率で正しいと検証されたわけではない。
今回、記事の構成を考えるときにも、実装の要点を渡してJevへ一度問い合わせた。返ってきたモデル名はjev-1.13.0で、主題を選ぶChoiceは次の結果だった。
| 候補 | probabilities |
|---|---|
| 通信実装を中心にする | 0.48 |
| 判断の役割分担を中心にする | 0.45 |
| 判断材料不足 | 0.06 |
| 節約・高速化の実績を中心にする | 0.01 |
選ばれたのは通信実装だが、confidenceは0.32。上位二つがかなり近い。
そこで選択結果だけを採用せず、役割分担を導入に置いて、通信実装と検証を続ける構成にした。判断を丸投げするというより、迷っている箇所がどこかを見る使い方になった。
これは一回の利用例で、モデルの精度評価ではない。また、この問い合わせのusageは入力807 tokens、出力65 tokensだったが、Codex側の調査や質問作成まで含む総使用量ではない。この値だけで節約できたとは言えない。
Rust側では入力と回答の両方を検証する
実装は、MCPを扱うserver.rs、HTTP通信のclient.rs、入力・回答の型を定義したtypes.rsに分けた。MCPにはrmcp、HTTPにはreqwest、JSONとスキーマにはserde・schemarsを使っている。
型と検証の実装 では、知らない入力フィールド、空の質問集合、Choiceの選択肢数、Scoreの段階数などを確認する。不正な入力はAPIへ送る前にエラーにする。
回答についても、JSONとして読めればよい、とはしていない。
要求した質問IDが揃っているか、質問と回答の型が一致するか、Choiceの回答が渡した選択肢に含まれるかを確認する。確率が0〜1に収まっているか、確率分布の合計が許容誤差内で1になるかも見る。
Scoreなら段階に対応するキーと値域を確認する。ただし、この検証が保証するのは回答の形式や数値範囲であって、判断内容の正しさではない。
成功時はmodel・answers・usageをMCPの構造化結果として返す。エラー時はisError: trueとして返し、評価失敗をNoや低スコアへ置き換えない。
外部APIなので待ち時間と送信内容を意識する
MCPサーバーはローカルプロセスだが、判断はローカルだけで完結しない。渡した情報と質問はhttps://api.typesafe.ai/v1/systemoneへ送られる。
ソースやログを使うなら、その内容が外部サービスへ渡ることを前提にする。必要な部分だけをまとめ、認証情報や無関係なデータを含めない。サーバーが自動で秘密情報を除去するわけではない。
通信処理には次の制限を入れた。
| 項目 | 現在の実装 |
|---|---|
| 接続タイムアウト | 10秒 |
| 1リクエストのタイムアウト | 30秒 |
| 再試行・待機を含む全体 | 60秒 |
| 429/529 | 最大2回再試行 |
| 成功応答のサイズ | 最大8 MiB |
| リダイレクト | 追従しない |
再試行の標準待機は0.5秒、1秒。秒数形式のRetry-Afterがあれば従うが、長い待機を指示されても全体の60秒は超えないようにしている。他のHTTPエラーや通信エラーは自動再試行しない。
APIが返したエラー本文も、そのままツール結果へ流さない。送信した情報や認証情報を含む可能性があるので、ステータスとこちらで用意した説明へ置き換える。
MCPサーバーとSkillは別
リポジトリには jev-decisionsスキル も入れている。
MCPサーバーは呼び出し口を提供するもの。Skillは、どの判断を渡すか、質問をどう分けるか、回答を次の調査へどう使うかを整理したもの。
例えば、同じ証拠に対する複数の関連性判定はバッチ化する。Scoreは一つの観点に絞る。確率が拮抗したら選択結果だけで原因を確定しない。失敗したAPI呼び出しを同じ引数で繰り返し続けない。
このあたりを使い方として書いた。サーバーに接続できたからといって、良い質問が自動で作られるわけではない。
Skillだけ入れてもサーバー接続や認証は行われない。利用には別途TypeSafe APIキーとMCP接続が必要になる。セットアップは README にまとめた。
テストで確認するものと、まだ分からないもの
通信まわりは、ローカルのモックHTTPサーバーを使って検証している。リクエスト内容、型の違う回答、不正JSON、再試行、タイムアウト、過大な応答などをテストする。
さらに、実際にjev-mcpを子プロセスとして起動するstdioテストもある。初期化、4ツールの列挙、キー未設定時のツールエラー、ping、標準入力を閉じたあとの正常終了を確認する。標準出力へMCP以外の文字列を混ぜないことも、この経路で検査している。
これらは実際の有料APIを呼ばずに実行できる。ただし、モックが期待どおり返答したことは、実モデルがよい判断をする証拠にはならない。
実際に減らしたいのは、不要な調査や重複した判断に使う時間。一方で、証拠をまとめる手間、APIの待ち時間、別モデルの利用料も増える。
今後は同じような作業をJevあり・なしで比較し、総使用量や所要時間だけでなく、必要なファイルを見落としていないか、修正やテストの品質を維持できているかまで確認する必要がある。
とりあえず、小さな判断を外へ渡して、その結果を作業へ戻せるところまではできた。まず使ってみて、切り出す意味がある判断と、自分でそのまま調べた方が早いものを分けていく。