MCP サーバの品質はユニットテストでは守れない|779本のフィールドトライアルで摩擦を狩る方法論
はじめに
小さな MCP サーバを OSS で公開して、ユニットテストは緑なのにユーザが初回統合で詰まる、そんな品質に頭を抱えている方へ。
小さな MCP サーバを OSS で公開していると、ある不都合な事実に突き当たります。ユニットテストが全部グリーンでも、ユーザは詰まる。
私が保守している nene-mcp は、Composer で入る MCP ブリッジです。やることは単純で、MCP ホスト(Cursor / Claude Desktop など)からの JSON-RPC を、ローカルの HTTP API へ stdio 経由で橋渡しするだけ。ロジックの多くは純粋な関数で、テストも書けます。
それでも、価値が決まる場所はテストの外にありました。composer require して、tools.json を配線して、MCP ホストを設定して、初めて API を叩く——その「初回統合の体験」こそが製品の質そのものなのに、そこはユニットテストからは一切観測できないのです。
この記事は、その観測できない品質をどう守るかについて、nene-mcp で採用した**フィールドトライアル(Field Trial, 以下 FT)**という方法論の話です。「テストを増やそう」ではなく、「テストでは見えないものを、別の instrument でどう捕まえるか」という角度で書きます。
この記事で分かること:
- ユニットテストの射程外にある「初回統合の体験」という品質層の正体
- 摩擦ID・直列ゲート・
N % 3 == 0セキュリティレビューで摩擦を必ず精算する運用プロトコル - 「摩擦ゼロは敗北」——難易度ランプで現実の統合者が詰まるまでスコープを上げる考え方
なぜユニットテストで守れないのか
MCP サーバの不具合は、コードの内側ではなく統合の継ぎ目に出ます。nene-mcp の設計判断を記録した ADR 0001 は、「内部ユニットテストが観測できないもの」を明示的に列挙しています。
- Cursor / Claude Desktop など MCP クライアント設定の摩擦
- 絶対パスを要求する環境変数まわり
- カタログ(
tools.json)と OpenAPI 定義のズレ - 書き込み系ツールのための Bearer トークン設定
- セキュリティ事故(MCP を本番に向けてしまう、
nene_mcp_aboutがトークンを漏らす、等)
これらはどれも「コードが正しいか」ではなく、「人間や AI エージェントが、新規の環境で正しく組めるか」の問題です。単体テストは自分が書いた前提の中でしか動かないので、前提そのものが間違っているケース——ドキュメントが実挙動とズレている、設定手順に暗黙知が要る——を構造的に見逃します。
だから発想を逆にします。品質を「内側から証明する」のではなく、「外側から、実際に使って壊してみる」。それが FT です。
摩擦ID と直列ゲート
FT の運用は、想像以上に禁欲的なプロトコルで縛られています。核は 2 つ、摩擦IDと直列ゲートです。
摩擦ID(F-N)
各トライアルは、リポジトリの外の作業ディレクトリで「小さくても現実的な統合」を一から組み立てます。vendor/ を使い回さない、温まった環境を再利用しない、公開されているドキュメントだけを頼りにソースを読まない——徹底して「新規ユーザの初回」を再現する。
そこで詰まった点・驚いた点・地味に痛かった点(papercut)を、すべて F-1, F-2, … という安定した摩擦ID で記録します。この ID は使い捨てのメモではなく、後段の Issue・修正・回帰テストまで追跡できる背骨になります。摩擦は次の 3 つのいずれかに分類され、修正先が明示されます。
-
fix-in-package …
nene-mcp本体を直す - document … ドキュメントで埋める
- fix-in-host … ホスト側リポ(NeNe やアプリ側)で直す
fix-in-host は「ブリッジの外」の問題を、ブリッジのドキュメントに吸い込ませないための分類です。トライアルはパッケージ境界をまたいでよいが、修正は正しいリポに置く、という規律がここに現れています。
「FT{N+1} は FT{N} の issue を閉じるまで開始しない」
そして最も効いているのが、直列ゲートです。ADR と品質戦略ドキュメントの両方に、次の一文が明記されています。
FT{N+1} does not start until FT{N} Issues are closed.
(FT{N+1} は、FT{N} で立てた Issue が閉じるまで開始しない)
品質戦略ドキュメントはこれをさらに厳しく定義していて、「マージされた修正・検証済みの解決・follow-ups.md への明示的な defer 行のいずれか」で全 Issue が閉じない限り、次の FT に進めません。しかも defer は例外扱い(default にしない)。nene-mcp リポ側は「そのFTに紐づく open Issue がゼロ」であることが着手条件です。
これは一見すると単なる運用ルールですが、効果は本質的です。摩擦を見つけて放置することができなくなる。FT を回すほど未修正の借金が積み上がる並列運用と違い、直列ゲートは「見つけた摩擦は次に進む前に必ず精算する」ことを構造で強制します。トライアル回数がそのまま「閉じた摩擦の数」になる。
副作用として、トライアル1本のあとに Issue と PR がまとめて出ます。ADR は「1回の FT のあとに 3〜7 本の PR が出るのは想定内」と明記しており、これは異常ではなく設計どおりの挙動です。
N % 3 == 0 はセキュリティレビュー
摩擦ID と直列ゲートに加えて、周期的なセキュリティレビューが組み込まれています。ルールはシンプルで、FT 番号 N が 3 の倍数(N % 3 == 0)のとき、そのトライアルではフルのセキュリティレビューを必須にする、というもの。
これは思いつきではなく、姉妹プロジェクト由来の cadence です。ADR 0001 は、nene2-python が 190+ 本のトライアルを「3の倍数でセキュリティレビュー」という同じ周期で回していたことを参照元として挙げています。nene-mcp はそれをパッケージ規模で継承しました。
実際のレポートを見ると、この cadence はちゃんと機能しています。たとえば FT777(777 は 3 の倍数)のレポートには専用の "Security Review (required when N % 3 == 0)" セクションがあり、書き込みの fail-closed(Bearer 無しでは書けない)を再検査した記録が残っています。3 の倍数でない FT778 のレポートには、同じ欄に「N/A — security review scheduled for FT780」とだけ書かれ、次のセキュリティ回が予約されている。
「毎回フルのセキュリティレビューをやる」は理想ですが現実には形骸化します。3回に1回に絞ることで、必ず回るが重すぎないという持続可能な密度に落としているのがポイントです。書き込みツール・Bearer・新しい HTTP サーフェスといったセキュリティ影響の大きい変更は、この周期に必ず一度は正面から点検される。
FT の規模と回し方
では、この禁欲的なプロトコルを実際にどれだけ回したのか。
執筆時点で、nene-mcp のフィールドトライアルレポートは FT779 まで到達しています(docs/field-trials/ に個別レポートが 779 本)。直近の FT778 が「次は FT780 でセキュリティレビュー」と予約しているとおり、780 本目(3の倍数なのでセキュリティ回)が次に控えている状況です。パッケージ規模の小さな MCP ブリッジ1本に対して、これはかなりの反復量です。
ただし、数字の中身には注意書きが要ります。品質戦略ドキュメントは「FT の数を数えること自体が目的ではない」と繰り返し釘を刺しており、途中の一括ログ(2026-05 の FT19〜FT100 バッチ)は「自動回帰実行(ft-cycle.sh)を後からラベリングしたもの」で、個別レポートを持つ本来の FT とは別物だと明記しています。自動回帰は FT を補うもので、置き換えるものではない。だから「779」は玉石混交の総数であり、すべてが手作業のフル FT というわけではありません。ここは正直に区別しておきます。
回し方の工夫として面白いのが、ペルソナ難易度ランプ(FT201 以降)です。トライアルは「自動テストが通るまで」ではなく「現実の統合者が詰まるまで」スコープを上げ続けます。
| Tier | 例 | 業務/認証スコープ | 想定される摩擦 |
|---|---|---|---|
| L1 | FT201 | ヘルスチェック+偽の read 1本(404) | インストールの pin、ブートストラップのリンク、カタログ例 |
| L2 | FT202–203 | 書き込みの fail-closed、ロケール/ホスト設定 | セキュリティ JSON-RPC の端、i18n ドキュメントの遅れ |
| L3 | FT204 | 複数ツールの TODO モジュール(ログイン・一覧・作成・パスパラメータ・URI_ROOT) | セッション cookie と Bearer、パス規約、ベース URL |
| L4+ | FT205–211 | クロスロケール整合、Bearer ネイティブなホスト、部分的なカタログ、回帰帯 | ホスト固有の認証、運用者のミス、i18n の遅れ |
ルールも徹底していて、「L1 のチェックしか回していないのに『摩擦ゼロ』と報告してはいけない(何をまだ試していないかを書け)」「actionable な発見が2本未満で終わったら、次に進む前に tier を上げろ(ツール・認証・デプロイを増やせ)」と定めています。つまり、簡単すぎて摩擦が出ないなら、それは合格ではなく難易度不足のサインとして扱う。
学び
779 本回して見えてきたのは、次のことです。
MCP サーバは「使われ方」でしか壊れない。 ロジックのバグより先に、統合の継ぎ目——設定・パス・トークン・ドキュメントのズレ——で人は詰まる。そこはユニットテストの射程外なので、実利用の反復そのものを回帰スイートにするしかない。FT はその「反復を仕組みにしたもの」です。
摩擦を見つけることが成功。 品質戦略ドキュメントと FT の README は、はっきりこう書いています。「複雑な FT で摩擦が何も出ないのはむしろ疑わしい——スコープを広げるか、次はもっと敵対的に攻めろ」。テストの世界では「全部グリーン」が勝利ですが、FT の世界では「摩擦ゼロ」は敗北(=難易度が足りない)に近い。この価値観の反転が、方法論の中核だと思います。
規律は数ではなく直列性から来る。 779 という数字自体には意味がありません。効いているのは「見つけた摩擦を、次に進む前に必ず閉じる」という直列ゲートと、「3回に1回は必ずセキュリティを正面から見る」という周期です。回数は結果であって目標ではない。品質戦略ドキュメントが何度も「FT の数を数えるな」と書くのは、この順序を取り違えないためです。
ユニットテストを捨てろ、という話ではありません。nene-mcp も PHPUnit と CI を回帰の安全網として持っています。言いたいのは、テストで守れる品質と、外部利用でしか守れない品質は別の層にあるということ。MCP のように「他人の環境に組み込まれて初めて価値が出る」プロダクトほど、後者の層を仕組みで守る投資が効いてきます。
一次資料
-
nene-mcpADR 0001 — Adopt Field Trial Methodology(直列ゲート/N % 3 == 0セキュリティ cadence/F-N 摩擦ID/fix-in-package・document・fix-in-host の分類) -
nene-mcpdocs/field-trials/quality-strategy.md(FT complete の定義、ペルソナ難易度ランプ FT201+、「摩擦を見つけることが成功」) -
nene-mcpdocs/field-trials/README.md(FT の趣旨・批判的/敵対的スタンス) -
nene-mcpdocs/field-trials/2026-05-field-trial-777.md/-778.md(N % 3 == 0セキュリティレビュー欄の実例、FT779 まで到達・FT780 予約)
シリーズ / 相互リンク
同じ nene-mcp を題材に、別角度で書いた記事があります。本記事は「品質をどう守るか(方法論)」に絞り、DB 境界論の再説明は主役にしていません。
- Qiita: MCP サーバーに本番 DB を触らせない — AI エージェントは「文書化された API 境界」を通す — MCP と API の境界をどこに引くか
- dev.to: An MCP tool wired to your production database — the app boundary I use instead — MCP は DB を直接触るべきでない、という DB 境界の議論(英語)
こちらは「MCP がどこまで踏み込むべきか(設計境界)」の話で、本記事の「踏み込んだ実装の品質をどう担保するか」とは補完関係です。
── 筆者: 森 秀之(彩音インターナショナル)— 自己ホストの業務ツール群を実運用中。
中小企業向けの業務システムを料金公開・固定価格で開発しています。
🔗 ayane.co.jp
Discussion