管理画面を作るのはもうやめる。業務ツールは MCP 起点で十分だった
BtoB の営業支援ツールを作っています。企業を集めて、サイトを解析して、営業文面を作って、人が承認して送る。よくある業務システムです。
そして、よくある管理画面を作りました。一覧と詳細、絞り込み、モーダル、一括操作のチェックボックス。ひと通り揃っています。
途中から操作を MCP 経由に移したところ、その画面がほとんど使われなくなりました。
最初は「日常操作だけがチャットへ移り、重要な操作は画面に残る」と考えていました。承認や送信は人が画面で押すべきだ、と。しかし運用してみると、そう思っていた根拠のほうが崩れました。画面でなければ満たせないと思っていた要件は、画面を要求していませんでした。
この記事は、そこに至るまでの記録です。結論から言うと、業務ツールに管理画面はいりません。
画面を磨いても解決しない問題があった
前提として、この業務には構造的な壁がありました。
対象業種の企業のうち、公式サイトの URL を確認できたのは母集団の数 % です。多くの企業はメールアドレスを公開していません。つまり 到達できる相手の数は、画面をどれだけ改善しても増えません。
この状況で開発者が時間を使うべき場所は、一覧のソート順やモーダルの挙動ではありません。データソースを増やすこと、判定の精度を上げること、1 人あたりの処理件数を上げることです。
管理画面は、その時間を静かに食っていました。
入口を増やすと、必ず食い違う
MCP を足すとき、最初に事故が起きました。設計の話をする前に、こちらを先に書きます。
このツールには「企業除外リスト」「送信抑止リスト」「送信根拠の有効期限」「日次上限」「静穏時間」といったガードがあります。承認の処理は当時 3 か所にありました。画面の Route Handler、MCP のツール、そして一括承認の Route Handler です。それぞれが似たコードを持っていました。
問題は一括承認でした。1 件ずつの承認はガードを通していたのに、一括承認だけが通していなかったのです。
| 操作 | 企業除外の確認 | 抑止リストの確認 | 送信根拠の鮮度 |
|---|---|---|---|
| 1 件ずつ承認 | ✅ | ✅ | ✅ |
| まとめて承認 | ✅ | ❌ | ❌ |
「まとめて承認」を選ぶだけで検査が緩くなる。担当者から見れば、どちらも同じ「承認」です。
直し方は素朴でした。状態遷移をアプリケーションサービスへ集約し、入口は Actor を組み立てて呼ぶだけの薄い層にしました。
/**
* 操作者。画面(セッション)と MCP(API トークン)の違いを、ここで 1 つに揃える。
*
* アプリケーションサービスは Actor しか受け取らない。入口ごとに別の型を渡せてしまうと、
* 「画面では効くが MCP では効かない検査」が生まれる。それを型で防ぐのがこの層の役割。
*/
export interface Actor {
organizationId: string
userId: string | null
role: UserRole
/** 操作経路。監査ログへ残して画面操作と MCP 操作を区別する */
via: "ui" | "mcp"
}
そして一括処理は、1 件ずつの処理をそのまま順に呼びます。
/**
* まとめて処理するときだけ検査を省く、ということをしないための作り。
*/
for (const draftId of draftIds) {
try {
succeeded.push(await approveDraft(actor, { draftId }))
} catch (error) {
failed.push({ id: draftId, error: message(error) })
}
}
ここまでは、よくある「サービス層を切りましょう」という話です。しかし運用しながら、この事故の教訓はもう一段先にあると思うようになりました。
入口が増えるほど、検査は食い違う。なら入口は増やさないほうがいい。
管理画面は入口です。しかも、いちばん実装が重い入口です。
「承認は画面で押すべき」を疑う
とはいえ、当初は承認と送信だけは画面に残すつもりでした。要件の絶対条件として「送信の最終判断は必ず人間が行う」と決めており、法令上の証跡としても、誰がいつ承認・送信したかを記録する必要があるからです。
しばらくして気づきました。この要件は 「人間が判断すること」を求めていて、「画面で押すこと」は求めていません。
証跡として必要なのは次の 3 つです。
- 操作者が実在の人物として特定できること
- 何を承認・送信したかが記録されていること
- 承認から送信までの間に、人の意思が挟まっていること
MCP 経由でも、3 つとも満たせます。
1 について。 MCP の認証は API トークンですが、トークンは必ず実在の利用者へ紐づけます。失効・期限切れ・利用停止は弾きます。ここを緩めると「誰が承認・送信したか」を追跡できなくなり、証跡として成立しません。逆に言えば、ここさえ守れば画面のセッションと同じ強度です。
2 について。 これは画面もチャットも変わりません。同じサービス層が同じ監査ログを書きます。むしろ via を残しているぶん、画面より情報が多いくらいです。
3 について。 ここが本題でした。送信ツールの入力スキーマを、こうしています。
confirmed: z
.literal(true)
.describe("送信対象と件数を利用者へ提示し、承諾を得た場合にだけ true を指定します")
エージェントは宛先と本文と件数を利用者へ提示し、承諾を得るまで実行できません。画面のボタンがやっていたのは、まさにこれです。 「内容を見せて、人が承知したうえで実行する」という手続きが本体であって、ボタンという形は本体ではありませんでした。
しかも、チャットのほうが提示できる情報は多い。ボタンの隣に注記を並べるより、「この 12 社へ送ります。うち 2 社は前回送信から 45 日しか空いていません」と書くほうが、判断材料としてよほど親切です。
私たちは長いあいだ、「人間の最終判断」と「画面のクリック」を同じものだと思っていました。分けて考えたら、画面が要る理由は残りませんでした。
「一覧を俯瞰したい」を疑う
次に残ると思っていたのが、送信キューや操作ログの一覧です。50 件並んだ中から赤い行を 1 件見つけるのは目のほうが速い。これは今でもそう思います。
ただし、それは 作り込んだ画面が必要 という意味ではありませんでした。
必要なのは「そのとき見たい切り口のビュー」です。作り込んだ一覧は、作った時点で切り口が固定されます。列を増やしたければ実装が要る。ソート順を変えたければ実装が要る。運用で本当に見たい切り口は、たいてい後から分かります。
チャットの戻り値には、その制約がありません。たとえば設定変更のツールは、変更後の設定に加えて「その設定で 1 日に実際に送れる件数」を必ず返します。
return {
...settings,
// 上限を上げただけでは届かない場合があるため、実際に送れる件数を必ず返す
sendCapacity: calculateSendCapacity(settings),
}
日次上限を 500 に上げても、送信間隔 60 秒と静穏時間の組み合わせで実際には 541 件が上限になる。この手の「設定の相互作用」は、画面だと小さな注記になりがちです。戻り値なら毎回目に入ります。
さらに言えば、表が見たければその場で描かせればいい。集計軸を変えたければ言い直せばいい。使い捨てのビューが即座に手に入るなら、恒久的な一覧を実装して保守する理由は薄いです。
「全員が Claude を持っているわけではない」を疑う
最後の反論がこれでした。人を増やすとき、全員に環境を配れるとは限らない。
これは正しい指摘ですが、設計の問題ではなく配布の問題 です。そして配布は時間で解決します。3 年前なら決定的な反論でしたが、今は違う。少なくとも「だから管理画面を作り込む」という結論にはなりません。
正直に書くと、私たちのプロダクトには今も画面が残っています。ただしそれは過渡期の都合であって、設計上の要請ではありません。同じサービス層を通るので、消しても業務は止まりません。消せる状態にあることと、消してあることは違いますが、方向は決まっています。
残っている本当の課題
きれいな話ばかり書いたので、いま困っていることも書きます。
新しい設定項目を追加してデプロイした直後、同じ Claude セッションから設定を変えようとすると、こう返ってきました。
Input validation error: formFallbackToEmail: Invalid input: expected boolean, received string
サーバーは新しい引数を認識しています。「boolean を期待した」と言えているので、コードは新しい。ではなぜ文字列が届くのか。
MCP クライアントは、接続した時点のツール一覧と入力スキーマを保持し続けるから です。手元のスキーマにその項目が無いため、値が boolean として型付けされずに送られていました。同じ理由で、新しく追加したツールはセッションのツール一覧に存在すらしませんでした。
MCP で機能を追加しても、「デプロイしたら使える」にはならない。開いているセッションを開き直すまで、新機能は存在しない。
Web アプリならリロードで済む話が、MCP ではセッションの再接続になります。本人には「使えない」としか見えないので、サーバー側の不具合と誤診しやすい。
これは痛い制約です。ただし プロトコルとクライアントの未成熟さであって、設計の否定ではありません。Web も昔はキャッシュの罠だらけでした。ここは近いうちに埋まると思っています。
結論
管理画面に何か月もかける時代は終わったと思っています。
私たちが画面に求めていたものを分解すると、こうでした。
| 画面に求めていたもの | 本当に必要だったもの |
|---|---|
| 承認ボタンを押す | 内容を提示され、人が承知すること |
| 誰が押したかの記録 | 操作者を実在の人物として特定できること |
| 一覧で俯瞰する | そのとき見たい切り口のビュー |
| 全員が使える入口 | 全員に環境が配られていること |
右の列は、どれも画面を要求していません。左の列を実装していた工数は、判定の精度とデータソースの拡張へ回せます。 私たちの場合、そちらのほうが事業に効きました。
今から同じものを作るなら、こうします。
- 状態遷移とガードをアプリケーションサービスへ集約する
- 入口は MCP だけにする
- 画面は、作らない
3 が言い過ぎに聞こえるなら、こう言い換えます。画面を作りたくなったら、それは 1 で集約し損ねたガードがあるか、ツールの戻り値の情報が足りていないかのどちらかです。 私たちの場合、振り返ると全部そうでした。
Discussion