「この文言を変えて」みたいな要望を、チャットで送るだけでWebサイトに反映される仕組みを作りました。
今回は、実際に作った prompt-to-app の構成と、どこを工夫したかをまとめます。
何を作ったのか
以下の体験を目指したコンセプトサイトです。
- ユーザーが自然文で変更要望を送る
- サーバー側で要望を安全なUI操作に変換する
- 画面プレビューへ反映する
- 必要に応じて
public/index.htmlに永続化し、git commit && git pushまで実行する
「会話がそのまま仕様になる」流れを、できるだけ短い導線で再現しています。
デモ画面のイメージ
フロント側にはランディングページとチャットUIを置いています。
-
public/index.html: ランディング + チャットUI -
public/app.js: チャット送信、レスポンス反映、操作ガード -
public/styles.css: UIデザイン
たとえば以下のような依頼を送る想定です。
- 「ヒーロータイトルをもっと刺さる表現に変えて」
- 「CTAを Start Free Pilot に変更して」
- 「テーマを sunrise にして」
全体アーキテクチャ
今回の構成は大きく4つです。
- Firebase Hosting
- Firebase Functions(
/api/chat) - APIサーバー(
servers/api) - MCPサーバー(
servers/mcp)
1. Functionsで自然文を受ける
functions でチャットリクエストを受け、LLM(Groq)に問い合わせます。
ここで重要なのは、レスポンスを自由テキストではなく、JSON契約で受けることです。
- 許可する操作を限定(例:
setText,setCta,setStatus,setTheme) - フロントで適用前にバリデーション
- 想定外のDOM変更を防ぐ
この設計にしておくと、「AIが何を返すかわからない問題」をかなり抑えられます。
2. フロントは安全な操作だけ適用
public/app.js 側では、受け取ったJSON操作をそのまま実行せず、許可済みコマンドだけを反映します。
つまり「チャットで何でも変更できる」のではなく、変更可能な範囲を明示的に狭める構成です。
3. API + MCPでファイル更新とGit連携
プレビューだけでなく実ファイルへ反映したい場合、MCP経由でAPIサーバーを呼びます。
-
update_index_html:public/index.htmlの対象箇所を更新 -
git_commit_push_index_html:public/index.htmlをコミットしてorigin/mainへpush
Gitフローは固定で、次の順序です。
git add public/index.htmlgit commit -m <message>git push origin main
変更がない場合はpushをスキップするようにしています。
MCPサーバーがやっていること(重要)
この構成でMCPサーバーは、AI(Functions)からの実行要求を、安全にAPIサーバーへ橋渡しするツールゲートウェイです。
言い換えると、Functionsが直接gitやファイル編集を触るのではなく、MCPが「使ってよい操作だけ」を公開します。
MCPサーバーの主な責務
- ツール定義の公開
-
update_index_html- 許可されたtargetだけ更新
- 値の文字数制限あり
-
git_commit_push_index_html- コミットメッセージ長を制限
-
public/index.html対象の固定フローのみ実行
- 入力スキーマによる検証
- toolごとにJSON Schemaを定義
-
additionalProperties: falseで余計な引数を拒否 - targetは列挙型で制限(任意DOM操作を防ぐ)
- APIサーバーへの中継
- MCPは自分でHTML編集/Git操作を持たない
- 実務処理はAPIサーバーの
/edit-index-htmlと/git-commit-push-index-htmlに委譲 - MCPは「正規化された引数で呼び出す」役割に集中
- 認証ヘッダの付与
- API向けに
x-api-keyを必ず付与 - Cloud Run構成ではID Tokenを取得して
Authorization: Bearer ...を付与可能 - HTTPモード時、MCP自身にもBearerトークン保護を付けられる
- エラー整形
- API応答をJSONとして扱い、失敗時はMCPエラーとして呼び出し元へ返却
- Functions側はその結果をユーザー向けメッセージへ反映しやすい
なんでMCPを挟むのか
MCPを入れるメリットは、操作境界を明確にできることです。
- Functionsの責務: 自然文を解釈して「何をしたいか」を決める
- MCPの責務: 実行可能な操作を定義し、安全に中継する
- APIの責務: 実際のファイル編集とGit実行を行う
この分離により、将来的にツールを増やす場合も「MCPに新しい安全なツールを足す」だけで拡張しやすくなります。
実際のリクエストフロー
- ユーザーがチャットで「文言変更して、コミットしてpushして」と入力
- FunctionsがLLM応答をJSON化し、必要なら
tools/callを発行 - MCPがツール名と引数を検証
- MCPがAPIサーバーへ認証付きPOST
- APIサーバーがHTML更新/Git操作を実行
- 結果がMCP→Functions→フロントへ返る
セキュリティ上のポイント
- MCPが公開するツールを最小にする(今回は2つ)
- ツール引数を厳密にスキーマ制約する
- APIキーとBearerトークンを分離し、漏れても影響範囲を限定する
- 「自由なコマンド実行」を作らない
この構成だと、AIに権限を丸投げせず、操作を宣言的に制御したまま自動化できます。
ディレクトリ構成(抜粋)
public/
index.html
app.js
styles.css
functions/
index.js
servers/
api/
src/
server.js
indexHtmlService.js
gitService.js
mcp/
src/
server.js
役割分離のポイントは以下です。
-
functions: LLM呼び出しとチャットAPI -
servers/api: HTML編集・Git操作の実務 -
servers/mcp: ツール公開レイヤー
ローカル起動手順
1. 依存をインストール
npm install -g firebase-tools
npm --prefix functions install
npm --prefix servers/api install
npm --prefix servers/mcp install
2. envファイルを作成
cp functions/.env.example functions/.env
cp servers/api/.env.example servers/api/.env
cp servers/mcp/.env.example servers/mcp/.env
最低限、functions/.env には GROQ_API_KEY を設定します。
3. エミュレータ起動
firebase emulators:start --only hosting,functions
必要なら別ターミナルでAPI/MCPサーバーも起動します。
npm run api:start
npm run mcp:start
実装して感じたハマりどころ
1. 「AIに自由にDOMを触らせない」設計が必須
最初は柔軟性を重視していましたが、すぐに危険だと分かりました。
- 変更対象をIDベースで限定
- 操作種別をホワイトリスト化
- レスポンスをJSON Schemaで検証
この3つを入れるだけで安定性が大きく変わります。
2. Git実行環境の差分
実行環境によっては git バイナリが存在せず、spawn git ENOENT になることがあります。
APIサーバーを動かすランタイムに git を含める(Dockerfile側で担保)設計にすると解決しやすいです。
3. push失敗時の扱い
non-fast-forward などでpushが失敗するケースは必ず起きます。
- エラーメッセージをUIに返す
- どの段階で失敗したか(add/commit/push)を返す
この情報があると運用時の復旧が速くなります。
この構成の良かった点
- 要望の入口を「自然文」に寄せられる
- 変更を「安全な操作」に落とし込める
- プレビューと永続化を分離できる
- 既存のGit運用に接続しやすい
「チャットUIを付けただけ」で終わらず、実運用に必要なガードまで含めて組めたのが収穫でした。
まとめ
Chatからの指示でサイトが変わる体験は、見た目以上に設計が重要でした。
特に、
- 自由入力をどう制約付き操作へ変換するか
- どこまでをプレビューにし、どこからを本反映にするか
- Git連携を失敗時含めてどう扱うか
この3点を押さえると、PoCから実運用に近づけやすいです。
同じように「会話を起点にUIを更新するプロダクト」を作る方の参考になればうれしいです。