はじめに
AI エージェントが Web ページを操作するとき、従来は DOM を「目で見て」読んで操作していました。
「たぶんこれが検索フォームだろう」と推測するため、壊れやすく不正確でした。
WebMCP はこの問題を解決するための新しい Web 標準案です。
ページ側がエージェントに対して「こういうツールがありますよ」と構造化された形で宣言できます。
WebMCP には 2 種類の API がありますが、本記事では 命令型 API に特化して、JavaScript から自由にツールを定義する方法を解説します。
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 の基本
ツールの登録
registerTool() にツール定義オブジェクトを渡します。
name・description・inputSchema・execute の 4 つが基本構成です。
const modelContext = document.modelContext ?? navigator.modelContext;
modelContext.registerTool({
name: 'toggleDarkMode',
description: 'ダークモードの ON / OFF を切り替える',
inputSchema: {
type: 'object',
properties: {
enabled: { type: 'boolean', description: 'true で ON、false で OFF' }
},
required: ['enabled']
},
execute: async ({ enabled }) => {
document.body.classList.toggle('dark', enabled);
return `ダークモード: ${enabled ? 'ON' : 'OFF'}`;
}
});
execute の戻り値
execute が返した文字列がそのままエージェントへの応答になります。
エージェントが結果を正しく理解できるよう、具体的な内容を文字列で返すことを意識してください。
execute: async ({ query }) => {
const results = await fetchData(query);
// オブジェクトは JSON 文字列化して返す
return JSON.stringify({ count: results.length, items: results });
}
ツールの削除(AbortSignal)
AbortController を使うと、登録したツールを後から削除できます。
ログアウト時やページの状態変化に応じてツールを出し入れするときに使います。
const controller = new AbortController();
modelContext.registerTool(
{
name: 'addTodo',
description: 'To-Do リストに項目を追加する',
inputSchema: {
type: 'object',
properties: { text: { type: 'string', description: '追加するテキスト' } },
required: ['text']
},
execute: async ({ text }) => {
addItem(text);
return `追加しました: ${text}`;
}
},
{ signal: controller.signal }
);
// ツールを削除するときは abort() を呼ぶ
controller.abort();
ツールリストの変更を監視(toolchange)
ツールが追加・削除されると toolchange イベントが発火します。
modelContext.addEventListener('toolchange', () => {
console.log('ツールリストが変わりました');
});
アノテーション
ツール定義に annotations を追加すると、エージェントにツールの性質を伝えられます。
modelContext.registerTool({
name: 'getWeather',
description: '現在地の天気を取得する',
inputSchema: { /* ... */ },
execute: async () => { /* ... */ },
annotations: {
readOnlyHint: true, // 副作用なし(データ取得のみ)
untrustedContentHint: false // 外部の信頼できないコンテンツを扱わない
}
});
| アノテーション | 説明 |
|---|---|
readOnlyHint: true |
ページの状態を変更しない読み取り専用のツール |
untrustedContentHint: true |
外部の信頼できないコンテンツを扱う可能性がある |
実装デモ
命令型 API の特徴が出やすい 3 種類のツールを実装します。
HTML 全体
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>WebMCP 命令型 API デモ</title>
<style>
body { font-family: system-ui, sans-serif; padding: 2rem; }
body.dark { background: #1a1a1a; color: #eee; }
#todo-list { list-style: none; padding: 0; }
#todo-list li { padding: 0.4rem 0; border-bottom: 1px solid #eee; }
#todo-list li.done { text-decoration: line-through; color: #999; }
</style>
</head>
<body>
<h1>命令型 API デモ</h1>
<h2>ダークモード</h2>
<p id="theme-status">現在: ライトモード</p>
<h2>To-Do リスト</h2>
<ul id="todo-list"></ul>
<h2>イベントログ</h2>
<pre id="log" style="background:#111;color:#0f0;padding:1rem;height:120px;overflow-y:auto;font-size:0.8rem;"></pre>
<script>
const modelContext = document.modelContext ?? navigator.modelContext;
function log(msg) {
const el = document.getElementById('log');
el.textContent += `[${new Date().toLocaleTimeString('ja-JP')}] ${msg}\n`;
el.scrollTop = el.scrollHeight;
}
const todos = [];
// ---- ツール 1: ダークモード切り替え ----
modelContext.registerTool({
name: 'toggleDarkMode',
description: 'ダークモードの ON / OFF を切り替える',
inputSchema: {
type: 'object',
properties: {
enabled: { type: 'boolean', description: 'true で ON、false で OFF' }
},
required: ['enabled']
},
annotations: { readOnlyHint: false },
execute: async ({ enabled }) => {
document.body.classList.toggle('dark', enabled);
document.getElementById('theme-status').textContent =
`現在: ${enabled ? 'ダーク' : 'ライト'}モード`;
log(`toggleDarkMode: ${enabled}`);
return `ダークモード: ${enabled ? 'ON' : 'OFF'}`;
}
});
// ---- ツール 2: To-Do 追加 ----
modelContext.registerTool({
name: 'addTodo',
description: 'To-Do リストに新しい項目を追加する',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string', description: '追加するタスクのテキスト' }
},
required: ['text']
},
annotations: { readOnlyHint: false },
execute: async ({ text }) => {
const id = Date.now();
todos.push({ id, text, done: false });
const li = document.createElement('li');
li.id = `todo-${id}`;
li.textContent = text;
document.getElementById('todo-list').appendChild(li);
log(`addTodo: "${text}"`);
return JSON.stringify({ id, status: 'added' });
}
});
// ---- ツール 3: To-Do 完了 ----
modelContext.registerTool({
name: 'completeTodo',
description: 'To-Do リストの項目を完了済みにする',
inputSchema: {
type: 'object',
properties: {
id: { type: 'number', description: 'addTodo で返された id' }
},
required: ['id']
},
annotations: { readOnlyHint: false },
execute: async ({ id }) => {
const todo = todos.find(t => t.id === id);
if (!todo) return JSON.stringify({ error: 'not found' });
todo.done = true;
document.getElementById(`todo-${id}`)?.classList.add('done');
log(`completeTodo: id=${id}`);
return JSON.stringify({ id, status: 'completed' });
}
});
// ---- ツールリストの変更を監視 ----
modelContext.addEventListener('toolchange', () => {
log('toolchange: ツールリストが更新されました');
});
log('命令型 API ツールを登録しました');
if (!modelContext) {
log('⚠ modelContext が未定義です。Chrome フラグを確認してください。');
}
</script>
</body>
</html>
DevTools からツールを確認・実行する
// ツール一覧を確認
console.log(await navigator.modelContext.getTools());
// ダークモードを ON にする
const tools = await navigator.modelContext.getTools();
const tool = tools.find(t => t.name === 'toggleDarkMode');
await navigator.modelContext.executeTool(tool, JSON.stringify({ enabled: true }));
// To-Do を追加してから完了にする
const tools = await navigator.modelContext.getTools();
const addTool = tools.find(t => t.name === 'addTodo');
const result = await navigator.modelContext.executeTool(
addTool,
JSON.stringify({ text: '記事を書く' })
);
const { id } = JSON.parse(result);
const completeTool = tools.find(t => t.name === 'completeTodo');
await navigator.modelContext.executeTool(completeTool, JSON.stringify({ id }));
executeTool() の第 1 引数はツール名の文字列ではなく、getTools() が返した RegisteredTool オブジェクトです。
文字列を渡すと TypeError: The provided value is not of type 'RegisteredTool' が発生します。
クロスオリジン対応
iframe を使って別オリジンのツールを公開・取得できます。
公開側(iframe 内のページ)
// https://partner.example.com 側で登録
document.modelContext.registerTool(
{
name: 'sharedTool',
description: '別オリジンに公開するツール',
inputSchema: { /* ... */ },
execute: async () => { /* ... */ }
},
{ exposedTo: ['https://main.example.com'] } // 公開先を指定
);
取得側(親ページ)
<!-- Permissions Policy で tools を許可 -->
<iframe src="https://partner.example.com" allow="tools"></iframe>
// クロスオリジンのツールを明示的に取得
const tools = await document.modelContext.getTools({
fromOrigins: ['https://partner.example.com']
});
AI エージェントからの操作
ここまでは DevTools コンソールで手動実行して動作確認しましたが、
実際に AI エージェントからツールを呼び出す方法は主に 2 つあります。
Model Context Tool Inspector(Chrome 拡張機能)
Google が提供する公式の検証ツールです。Gemini を使ってページのツールを実際に呼び出せます。
Chrome ウェブストアで「Model Context Tool Inspector」を検索してインストールし、
ページを開いた状態で拡張機能を起動すると、登録されているツールの一覧確認・手動実行・Gemini 経由での実行ができます。
拡張機能の説明に「信頼できない Web サイトでは使用しないこと」と明記されています。あくまで開発・検証用のツールです。
Gemini の Auto-Browse 機能
Chrome に組み込まれた Gemini がページの WebMCP ツールを自動的に認識して操作します。
現在は限定公開中の機能で、今後より広く使えるようになる予定です。
おわりに
WebMCP の命令型 API は JavaScript で自由にツールを定義できるため、
フォームのない操作も含めてあらゆる機能をエージェントに公開できます。
-
registerTool()でツールを登録しexecuteで処理を記述 -
AbortControllerでツールを動的に削除 -
annotationsでツールの性質をエージェントに伝える - クロスオリジン対応で iframe 間のツール共有も可能
宣言型 API と組み合わせることで、フォームベースの操作と JS ベースの操作を両方カバーできます。
参考
最後まで読んでくださってありがとうございます!
普段はデザインやフロントエンドを中心にQiitaに記事を投稿しているので、ぜひQiitaのフォローとX(Twitter)のフォローをお願いします。