2026.08.28
コードを書かずに既存WebアプリをAI操作可能にした話
Takashi Kakizoe
エンジニアの柿添です。
今回は、以前から運用しているプロジェクト管理用のWebアプリケーションに、AI Agentから操作するための経路を追加した話です。
ちなみにですが、私は今回のこの実装でコードを一行も書いていません。
AI Agentと実行計画を壁打ちし、対象範囲・採用しない構成・検証方法・合格条件をplan.mdに落とし込みました。
計画を実装可能な粒度まで詰めた後、Codexの/goalで完了条件を渡し、実装・テスト・レビュー指摘の修正までAI Agentに進めてもらいました。
/goalは、定義した目標を保持し、完了条件を満たすまで作業を継続させるために使っています。
当初はMCPサーバーを作る案を考えていました。しかし、検討を進める中で、今回の要件には OpenAPI 3.1で記述したAPI・Rust製CLI・Skill の組み合わせが合っていると判断しました。
この記事では、MCPを採用しなかった理由だけでなく、なぜSkillだったのか、そしてなぜ認証情報をAI Agentへ渡さない構成にしたのかを書いていければなと思いいます。
プロジェクト管理の情報を一か所に集めるWebアプリケーション
このWebアプリケーションは、プロジェクトを進める中で発生する確認・記録・共有といった管理業務を支援するために作ったものです。
プロジェクトの下にシート、セクション、確認項目を持つ3階層の構造があり、業務や工程に合わせてチェックリストを組み立てられます。
単にチェックを付けるだけではないです。
シート・セクション・確認項目の好きな場所にメモを残せますし、タグもそれぞれの階層に付箋のように貼れます。
関連リンクや証跡ファイルも紐付けられるため、確認結果だけでなく、その判断に必要だった情報も同じ場所へ集められます。
主な機能は以下のような感じです!
- プロジェクト、シート、セクション、確認項目の作成・更新・並べ替え・複製
- 担当者ごとのチェック、複数者による確認、進捗率の自動集計
- ステータス、タグ、担当者、日付、進捗、優先度による整理と絞り込み
- 各階層へのメモ、関連リンク、証跡ファイルの紐付け
- 誰がいつ何を変更したかを残す操作履歴
- Excel・CSV・レポートの出力、複数シートの一括更新
- 組織管理者、スタッフ、社外パートナー、一般利用者の権限分離
もともとは人がWeb画面を開いて操作するシステムです。(APIを用意してはいなかった)
今回追加したのは、その既存機能をAI Agentとの会話から安全に利用するための別経路です。
(Web画面を置き換えたわけではないです)
たとえば、次のような依頼を扱います。
- 「プロジェクトの一覧を見せて」
- 「このシートをツリーで見せて」
- 「一番上の項目をチェックして」
- 「このセクションに判断メモを追記して」
- 「この確認項目に要対応タグを付けて」
- 「このシートに関連資料のリンクを追加して」
- 「証跡ファイルをこの項目に紐付けて」
- 「変更履歴を確認して」
APIの本数を先に決めたのではなく、管理画面が持つ機能から「人はAI Agentへどのような変更を頼むか」を洗い出し、必要な操作へ分解しました。
最終的にOpenAPIには58個のoperationIdが定義されています。
私がやったことはコーディングではなく完了条件の設計
履歴を確認すると、検討は「APIを作ってMCPで操作できないか」という壁打ちから始まっています。
そこから管理画面の機能を調べ、操作一覧、認可、データ移行、既存Web画面への影響、認証情報の流れまでをdocs/plan.mdへまとめました。
計画作成中は、APIとデータ整合性、CLIとSkillの使いやすさ、アプリケーション層のセキュリティという3観点でレビューし、
同じ回で3観点すべての指摘が0件になるまで修正を続ける条件も入れています。
3観点レビューは技術スタックを固定するまで収束しなかった
最初の計画では「3観点のレビューを指摘0件まで回す」と決めたものの、変更できない技術スタックを固定していませんでした。
その結果、セキュリティレビューがLaravel・PHP・MySQLの更新やインフラ刷新まで問題として拾い始め、修正するほど新しい論点が増える状態になりました...。
レビューが収束せず、完全にスコープ外へ暴走していました。
(自動レビューが終わってないな...と途中確認して原因に気づきました)
そこで、技術スタックを並べ、"変更しない","できない" と明記し、セキュリティレビューも認証・認可・入力検証・ログへの秘密情報混入といったアプリケーション層に限定しました。
このアプリにMFAはもともと存在しないため今回追加しない、破壊的操作には事前確認を必須にする、不明な権限は許可しない、といった判断もここで固定しました。
レビューを繰り返す指示だけでは足りなかったです。
何をレビューし、何をレビュー対象にしないかまで決めて、初めて完了条件として機能します。
これは気をつけてないと頻発するだろうことです、学びでしたね。
夜に壁打ちを始め、最初の/goalを開始したのは深夜3時頃で、主要実装のコミットは同日19時頃なので、計画確定後の主要実装は約16時間でした。
「合格条件を定めた計画を渡してから、AI Agentが主要実装を完了するまで」という意味では一日かかっていません。
この間、私はコードを書く代わりに、次の確認を担当しました。
- 実行計画を壁打ちし、実装範囲と対象外を決める
- 3観点レビューの指摘がなくなった計画を確認する
- 既存Web画面の回帰、データ移行、CLI配布まで合格条件へ入れる
- 本番由来データをローカル環境へ複製し、移行前後の状態をAI Agentに検証させ、結果レポートを確認する
- 実際のSkillを使い、一覧取得・チェック・メモ・リンク追加を試す
- 管理画面との操作履歴の差など、見つかった問題を追加の
/goalで是正する
実データに近いローカル検証では、古いチェックデータを残したまま新しいテーブルへ移行する処理に問題が見つかりました。
また、Skillからの操作自体は成功しても、既存Web画面の操作履歴と保存先が揃っていない箇所もありました。
そこで、APIが動くことだけを合格にはしませんでした。
- 既存Web画面と同じ業務結果になること
- 移行途中でも集計値を壊さないこと
- 管理画面とAPIの操作履歴が揃うこと
までを確認し、再度3観点レビューの未解決指摘が0件になるまで修正しています。
AI Agentへ「実装して」とだけ頼んだのではありません。そんな頼み方をしたら、未確定事項を勝手な想像や推定で実装し始め、ただのバイブコーディングになってしまいます。
plan.mdを正本にし、各機能の振る舞い、失敗時の扱い、実行するテスト、レビューの終了条件を先に決めました。
私が担ったのは、コードの入力ではなく 何をもって完成とするかの設計と確認 です。
当初のMCP案からSkillへ切り替えた
最初は、OpenAPIでAPI層を作り、その上にローカルのMCPサーバーを置く構成を考えていました。
RustでMCPサーバーを作る案も検討しました。
ただ、整理してみると、今回解きたかった問題は「AIクライアントへツールを公開すること」だけではありませんでした。
一つの依頼を安全に処理するには、先にプロジェクトを検索し、対象を一意に決め、現在の状態とバージョンを読み、適切な操作を選び、実行後に履歴を確認する必要があります。
削除や並べ替えなら、影響範囲をプレビューして人の確認を待たなければなりません。
必要だったのは、単発のツール呼び出しを公開する仕組みよりも、既存アプリケーションの操作手順をAI Agentへ教え、その手順を型付きの実行経路へ接続することでした。
そこで、役割を次のように分けました。
利用者
-> AI Agent
-> Skill 操作の選び方、確認手順、停止条件
-> Rust CLI 認証情報、型付き入力、通信、ローカルファイル
-> OpenAPI / API 許可する操作の契約
-> Laravel 認証、認可、業務ルール、永続化、操作履歴MCPサーバーを追加しても、認証・認可・競合制御・データ移行・既存Web画面との整合はLaravel側に実装する必要があります。
つまり、今回必要な安全性の大半はMCPが担うものではありません。
そのうえ対象は一つの既存アプリケーションです。
OpenAPIをLaravelとRust CLIの共通契約にすれば、操作定義の正本を一つにできます。
MCPを加える場合は、ツール定義の同期、サーバーの配布と起動、クライアントごとの接続確認も必要です。
今回は、その追加コストに見合う要件がありませんでした。
なぜSkillだったのか
Skillを選んだ一番の理由は、今回の操作が 判断を含む複数手順の業務フロー だったからです。
たとえば「一番上の項目をチェックして」という依頼を、そのまま更新APIへ送ることはできません。
Skillには、次の手順を持たせています。
- 認証済みか確認する
- 利用できる操作一覧を確認する
- プロジェクトとシートを検索する
- ツリーを取得し、「一番上」が指す対象を確認する
- 現在のチェック状態とバージョンを読む
- 目的の状態を
trueまたはfalseで指定する - 実行結果と操作履歴を確認する
同じ名前の候補が複数あれば、Skillは勝手に選ばず停止します。
削除・並べ替え・一括更新ではプレビューを表示し、人が明示的に確認するまで実行しません。
通信結果を受け取れなかった場合には、新しい更新としてやり直さず、同じ操作を復旧する手順を使います。
Skillは単なる説明文ではありません。
Rust CLIのバイナリ、実行スクリプト、入出力スキーマ、安全規則、コマンド例を一つの配布物にまとめています。
受け取ったチームメンバーは、Skillを所定の場所に配置して認証設定を行えば利用できます。配布物とバイナリはチェックサムで検証します。
一方で、Skillの文章だけを安全装置にはしていません。
AI Agentが手順を誤っても、CLIはOpenAPIに登録された58操作以外を呼べず、Laravel側でも権限と対象の所属関係を再確認します。
任意のURLへアクセスできる汎用HTTPコマンドも用意していません。
つまり、今回のSkillはMCPの簡易版ではありません。
SkillはAI Agentが業務依頼をどう進めるかを担当し、CLIとAPIは実行してよい範囲を強制する。 この分担が今回の用途に合っていました。
Skillの作り方や、私がどんな考え方で設計しているかは、Agent Skillsの作り方 設計から評価と運用まで https://labs.eastbraver.com/blog/agent-skills-design-evaluationにも書いていますので、あわせて参考にしていただけますと!
将来、複数の外部サービスを横断してツールを動的に公開したい、遠隔のAIクライアントから共通プロトコルで接続したい、といった要件が出ればMCPを再検討できます。
しかし、単一の既存アプリケーションをローカルのAI Agentから操作する今回の条件では、MCPサーバーを一層追加する理由がありませんでした。
認証情報をAI Agentへ渡してはいけない理由
今回、私が特に重要視したのが認証情報の扱いです。
AI AgentへログインIDやパスワード、APIトークンを渡せば、認証処理を組み立てることはできます。
しかし、AI Agentが認証情報を知る必要はありません。
AI Agentの役割は「どの操作を、どの対象へ、どの順番で実行するか」を判断することです。
ログインに成功したか、現在の利用者に何が許可されているかが分かればよく、パスワードやトークンそのものを推論材料にする必要はありません。
それにもかかわらず認証情報をコンテキストへ入れると、秘密情報が次の経路へ広がる可能性があります。
- 会話や実行履歴
- ツールの引数や生成されたコマンド
- 標準出力と標準エラー
- 例外、デバッグ情報、アプリケーションログ
- AI Agentが作る一時ファイルや操作記録
- 別のツールへ渡す入力
問題は、AI Agentを信用するかどうかではありません。
秘密情報を必要としない構成にできるのであれば、AI Agentに「絶対に出力しないで」と頼むより、そもそも見せない方が安全です!
一度コンテキストへ入った値について、「この後どの出力にも絶対に再掲しない」とアプリケーション側で強制するのは難しくなります。
ならば最初から渡さない。これは権限管理以前の、データ最小化の考え方です。
Rust CLIを認証境界にした
認証情報はCLI専用の設定ディレクトリに保存し、親ディレクトリを0700、ファイルを0600に制限します。
Skillのスクリプトはこのファイルをsourceせず、IDやパスワードを引数へ展開しません。
Rust CLI自身が決められた場所から直接読み取る構成です。
AI Agentが受け取るもの
- 認証済みかどうか
- 安全な利用者参照値
- 実行できる操作
- 操作の成功、失敗、再確認の要否
Rust CLIだけが扱うもの
- ログインID
- パスワード
- Personal Access Token
- macOS KeychainCLIはIDとパスワードでログインし、取得したPersonal Access TokenをmacOS Keychainへ保存します。
保存済みトークンは、使う前にGET /meを呼んで想定した利用者と一致することを確認します。
期限切れや無効化を検出した場合だけ、CLI内部で再ログインします。
ログインID、パスワード、トークンはargv、標準入力、標準出力、標準エラー、例外、操作履歴へ出しません。
テストでは秘密を模した値を流し、これらの経路へ残らないことを検査しています。
もちろん、これで端末上のあらゆる攻撃を防げるわけではありません。
同じOS利用者としてファイルを読み取れる悪意あるプロセスまでは対象外です。
平文の認証情報ファイルを置くこと自体にもリスクがあります。
今回守りたかった境界は、通常のAI操作経路で認証情報をプロンプトやツール呼び出しへ混ぜないことです。
利用しやすさとのトレードオフを明示したうえで、AI Agentから秘密を分離しました。
Skillの指示が破られても止まるようにした
認証情報を分離しても、正規の権限を持つAI Agentが対象を取り違えれば事故は起こります。
そこで、最終的な安全性はSkillの指示ではなく、CLIとAPIで強制しています。
- 名前だけで更新せず、候補を一意な参照値へ解決する
- リクエストの親IDを信用せず、データベース上の所属関係をたどる
Idempotency-Keyとリクエストハッシュで二重実行を判別する- バージョンを使って、他の利用者による先行更新との競合を止める
- 削除、並べ替え、一括更新はプレビュー後に対象を再検証する
- ファイル本体や署名付きURLをAI Agent向けJSONへ返さない
- APIを初期状態では無効にし、データ移行の完了後にだけ開放する
既存Web画面とAPIで業務ルールが分かれないように、両方から使う処理をLaravelのApplication層へ抽出しました。
APIを無効にした状態でも、従来のWeb操作が変わらないことを回帰テストしています。
(何件か見落としがあったので、今後どうやっていくべきか、もっとやり方を工夫したいと思います)
ローカル事前検証では、ログイン処理の問題と操作履歴の表示差が見つかりました。
反映後にも、ファイル未選択時の既存Web操作に回帰が見つかり、同日中に追加修正しています。
まとめ AI Agentへ渡したのは実装依頼ではなく完了の定義
今回、MCPサーバーを作らずSkillを選んだのは、流行している技術を避けたかったからではありません。
必要だったのは、AI Agentへ多数のツールを見せることではなく、既存の管理業務をどの順番で安全に進めるかを伝えることでした。Skillが手順と確認条件を持ち、Rust CLIが認証情報とローカル処理を引き受け、OpenAPIとLaravelが許可された操作だけを強制する。この役割分担なら、MCPサーバーを追加しなくても要件を満たせます。
そして私はコードを書かず、plan.mdと/goalを使ってAI Agentへ次を渡しました。
- 実装する機能と実装しない機能
- 既存Web画面で維持する挙動
- 認証情報を通してはいけない経路
- 失敗時と競合時の扱い
- 必ず通すテストと3観点レビュー
- 固定する技術スタックとレビュー対象外の領域
- 何をもって「完了」とするか
AI Agentに任せる範囲が大きいほど、指示を短くするのではなく、完了条件を具体的にする必要があると思います。
コードを書かなかったことよりも、コードが正しいと判断するための材料を先に揃えたこと。今回の開発で一番重要だったのは、そこでした。