0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WebMCP 命令型 API で始める AI エージェント対応 Web ページの作り方

0
Last updated at Posted at 2026-06-21

はじめに

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 フラグを有効化

  1. Chrome で chrome://flags/#enable-webmcp-testing を開く
  2. Enabled に変更
  3. 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() にツール定義オブジェクトを渡します。
namedescriptioninputSchemaexecute の 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)のフォローをお願いします。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?