はじめに
AI エージェントが Web ページを操作するとき、従来は DOM を「目で見て」読んで操作していました。
「たぶんこれが検索フォームだろう」と推測するため、壊れやすく不正確でした。
WebMCP はこの問題を解決するための新しい Web 標準案です。
ページ側がエージェントに対して「こういうツールがありますよ」と構造化された形で宣言できます。
本記事では 宣言型 API に絞って、実際に動くデモを作りながら使い方を解説します。
WebMCP とは
WebMCP は MCP(Model Context Protocol)の Web ブラウザ版という位置づけです。Web ページ自体が MCP サーバーになるイメージで、エージェントは getTools() でページのツール一覧を取得し、executeTool() で実行します。
ブラウザ操作系 MCP との違い
「Puppeteer MCP」や「DevTools MCP」のようなブラウザ操作系のツールは、外部からページを力づくで操作します。
エージェント → 外部 MCP サーバー → ブラウザを遠隔操作 → DOM を推測してクリック・入力
ページは何も知らず、エージェントが「たぶんこれが検索フォームだろう」と推測しながら操作するため、壊れやすいです。
WebMCP は発想が逆で、ページ自身がツールを宣言して公開します。
エージェント → ページに「何ができる?」と聞く → ページが構造化された形でツールを返す
主導権がエージェント(外部)からページ(内部)に移ることで、推測に頼らない正確なやり取りが実現します。
宣言型 API と命令型 API
WebMCP には 2 種類の API があります。
宣言型 API は、既存の HTML フォームに属性を追加するだけでツールを公開できます。
JSON Schema はブラウザが自動生成するため、追加の JavaScript はほぼ不要です。
既存フォームにエージェント対応を後付けするときに向いています。
命令型 API は、JavaScript で明示的にツールを登録します。
フォームを持たない操作(トグル、アニメーション、API 呼び出しなど)をツール化したいときや、スキーマを細かく制御したいときに使います。
本記事では宣言型 API について解説します。
セットアップ
Chrome フラグを有効化
- Chrome で
chrome://flags/#enable-webmcp-testingを開く - Enabled に変更
- Chrome を再起動
Chrome バージョンによる注意点
WebMCP は開発中の API のため、Chrome のバージョンによってプロパティの置き場所が異なります。
-
Chrome 149:
navigator.modelContextに実装 -
Chrome 150 以降:
document.modelContextに移動(navigator.modelContextは非推奨)
両バージョンに対応するには ?? 演算子でフォールバックします。
// document.modelContext があれば使い、なければ navigator.modelContext にフォールバック
const modelContext = document.modelContext ?? navigator.modelContext;
宣言型 API の基本
ツールの登録
HTML フォームに toolname と tooldescription を追加するだけでツールが使えるようになります。
<form
toolname="searchBooks"
tooldescription="キーワードで本を検索する。ジャンルで絞り込むこともできる。"
>
<input type="text" name="query" placeholder="例: JavaScript">
<button type="submit">検索</button>
</form>
たったこれだけで、エージェントは searchBooks というツールを認識できるようになります。
パラメータの説明を追加する
フォーム要素に toolparamdescription を追加すると、エージェントへの説明をより細かく伝えられます。
<select
name="genre"
toolparamdescription="絞り込むジャンル。指定しない場合は all を使う。"
>
<option value="all">すべて</option>
<option value="programming">プログラミング</option>
<option value="design">デザイン</option>
</select>
ブラウザはフォーム要素の型・name・toolparamdescription から JSON Schema を 自動生成し、getTools() で確認すると以下のようになります。
{
"name": "searchBooks",
"description": "キーワードで本を検索する。ジャンルで絞り込むこともできる。",
"inputSchema": "{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"},\"genre\":{\"type\":\"string\",\"enum\":[\"all\",\"programming\",\"design\"],\"description\":\"絞り込むジャンル。指定しない場合は all を使う。\"}},\"required\":[\"query\"]}"
}
toolautosubmit:エージェントによる自動送信
toolautosubmit を追加すると、エージェントがフィールドを埋めた後に自動でフォームを送信します。
<form
toolname="searchBooks"
tooldescription="キーワードで本を検索する"
toolautosubmit
>
付けない場合: エージェントが値を入力するが、送信はユーザーが手動で行う
付けた場合: エージェントが値を入力して自動送信まで行う
agentInvoked と respondWith()
フォームの submit イベントで e.agentInvoked を確認すると、エージェントからの呼び出しかユーザー操作かを判別できます。
エージェントからの呼び出しの場合は e.respondWith() で結果を返します。
form.addEventListener('submit', (e) => {
e.preventDefault();
const data = new FormData(e.target);
const results = search(data.get('query'), data.get('genre'));
// エージェントには respondWith() で結果を返す
if (e.agentInvoked) {
e.respondWith(Promise.resolve(JSON.stringify({ results })));
}
});
実装デモ
実際に 2 つのフォームを持つデモを作ります。
HTML 全体
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>WebMCP 宣言型 API デモ</title>
<style>
/* エージェントがツールを操作中に青枠で強調表示 */
form:tool-form-active {
outline: 2px dashed #4f8ef7;
outline-offset: 4px;
}
/* エージェントが送信しようとしているボタンをオレンジで強調 */
button:tool-submit-active {
outline: 2px solid #f97316;
}
</style>
</head>
<body>
<!-- フォーム 1: 本の検索 -->
<form
id="search-form"
toolname="searchBooks"
tooldescription="キーワードで本を検索する。ジャンルで絞り込むこともできる。"
toolautosubmit
>
<input type="text" name="query" placeholder="キーワード" required>
<select name="genre"
toolparamdescription="絞り込むジャンル。指定しない場合は all を使う。">
<option value="all">すべて</option>
<option value="programming">プログラミング</option>
<option value="design">デザイン</option>
</select>
<button type="submit">検索</button>
</form>
<!-- フォーム 2: サポートリクエスト(toolautosubmit あり) -->
<form
id="support-form"
toolname="createSupportRequest"
tooldescription="サポートリクエストを送信する。氏名・メール・カテゴリ・詳細が必要。"
toolautosubmit
>
<input type="text" name="name" placeholder="氏名" required>
<input type="email" name="email" placeholder="メール" required>
<select name="category"
toolparamdescription="カテゴリ。billing=請求, technical=技術的問題, other=その他。"
required>
<option value="billing">請求について</option>
<option value="technical">技術的な問題</option>
<option value="other">その他</option>
</select>
<textarea name="detail" placeholder="詳細"
toolparamdescription="問題の詳細な説明。できるだけ具体的に書く。"
required></textarea>
<button type="submit">送信</button>
</form>
<script>
// Chrome 149: navigator.modelContext / Chrome 150+: document.modelContext
const modelContext = document.modelContext ?? navigator.modelContext;
const books = [
{ title: 'JavaScript 入門', genre: 'programming' },
{ title: 'React ハンズオン', genre: 'programming' },
{ title: 'UX デザインの原則', genre: 'design' },
];
document.getElementById('search-form').addEventListener('submit', (e) => {
e.preventDefault();
const data = new FormData(e.target);
const results = books.filter(b =>
b.title.includes(data.get('query')) &&
(data.get('genre') === 'all' || b.genre === data.get('genre'))
);
if (e.agentInvoked) {
e.respondWith(Promise.resolve(JSON.stringify({ results })));
}
});
document.getElementById('support-form').addEventListener('submit', (e) => {
e.preventDefault();
const ticketId = `TKT-${Math.floor(Math.random() * 9000) + 1000}`;
if (e.agentInvoked) {
e.respondWith(Promise.resolve(JSON.stringify({ ticketId, status: 'created' })));
}
});
// ツールが呼ばれたときのイベント
window.addEventListener('toolactivated', (e) => {
console.log(`ツール "${e.toolName}" が呼ばれました`);
});
</script>
</body>
</html>
DevTools からツールを確認・実行する
ページを HTTP サーバーで開いた状態で DevTools コンソールを使います。
// ツール一覧を取得
const tools = await navigator.modelContext.getTools();
console.log(tools);
// → [{name: 'searchBooks', description: '...', inputSchema: '...'}, ...]
// ツールを実行
const tools = await navigator.modelContext.getTools();
const searchTool = tools.find(t => t.name === 'searchBooks');
await navigator.modelContext.executeTool(
searchTool, // ← ツール名文字列ではなくオブジェクトを渡す
JSON.stringify({ query: 'React', genre: 'programming' })
);
// → '{"results":[{"title":"React ハンズオン","genre":"programming"}]}'
executeTool() の第 1 引数はツール名の文字列ではなく、getTools() が返した RegisteredTool オブジェクトです。文字列を渡すと TypeError: The provided value is not of type 'RegisteredTool' が発生します。
AI エージェントからの操作
ここまでは DevTools コンソールで手動実行して動作確認しましたが、
実際に AI エージェントからツールを呼び出す方法は主に 2 つあります。
Model Context Tool Inspector(Chrome 拡張機能)
Google が提供する公式の検証ツールです。
Gemini を使ってページのツールを実際に呼び出せます。
Chrome ウェブストアで「Model Context Tool Inspector」を検索してインストールし、
ページを開いた状態で拡張機能を起動すると、
登録されているツールの一覧確認・手動実行・Gemini 経由での実行ができます。
Gemini の Auto-Browse 機能
Chrome に組み込まれた Gemini がページの WebMCP ツールを自動的に認識して操作します。
現在は限定公開中の機能で、今後より広く使えるようになる予定です。
ビジュアルフィードバック
CSS 疑似クラスを使うと、エージェントがツールを操作中であることをユーザーに視覚的に伝えられます。
/* エージェントがフォームを操作中 */
form:tool-form-active {
outline: 2px dashed blue;
outline-offset: 4px;
}
/* エージェントが送信ボタンを押す直前 */
button:tool-submit-active {
outline: 2px solid orange;
}
おわりに
WebMCP の宣言型 API は HTML 属性を数行追加するだけで既存フォームをエージェント対応にできます。
-
toolname/tooldescriptionでツールを公開 -
toolparamdescriptionで JSON Schema を補完 -
toolautosubmitで自動送信を許可 -
e.agentInvoked+respondWith()で結果を返す
まだ実験段階の API ですが、Web ページとエージェントの関係を根本から変える可能性を感じました。ぜひ試してみてください。
参考
最後まで読んでくださってありがとうございます!
普段はデザインやフロントエンドを中心にQiitaに記事を投稿しているので、ぜひQiitaのフォローとX(Twitter)のフォローをお願いします。