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 標準案です。
ページ側がエージェントに対して「こういうツールがありますよ」と構造化された形で宣言できます。

本記事では 宣言型 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 フラグを有効化

  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 の基本

ツールの登録

HTML フォームに toolnametooldescription を追加するだけでツールが使えるようになります。

<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>

ブラウザはフォーム要素のnametoolparamdescription から 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)のフォローをお願いします。

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?