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名前付きの選択肢から一つ選ぶchoiceprobabilitiesconfidence
jev.score一つの観点を段階評価するscorelegendprobabilitiesconfidence
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のrelatedinvestigateは回答を対応付けるための名前。質問そのものは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とスキーマにはserdeschemarsを使っている。

型と検証の実装 では、知らない入力フィールド、空の質問集合、Choiceの選択肢数、Scoreの段階数などを確認する。不正な入力はAPIへ送る前にエラーにする。

回答についても、JSONとして読めればよい、とはしていない。

要求した質問IDが揃っているか、質問と回答の型が一致するか、Choiceの回答が渡した選択肢に含まれるかを確認する。確率が0〜1に収まっているか、確率分布の合計が許容誤差内で1になるかも見る。

Scoreなら段階に対応するキーと値域を確認する。ただし、この検証が保証するのは回答の形式や数値範囲であって、判断内容の正しさではない。

成功時はmodelanswersusageを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あり・なしで比較し、総使用量や所要時間だけでなく、必要なファイルを見落としていないか、修正やテストの品質を維持できているかまで確認する必要がある。

とりあえず、小さな判断を外へ渡して、その結果を作業へ戻せるところまではできた。まず使ってみて、切り出す意味がある判断と、自分でそのまま調べた方が早いものを分けていく。