WebMCPの実装方法|自社サイトにAIエージェント用ツールを公開する手順と落とし穴
目次
WebMCPの実装とは、ページ内で document.modelContext.registerTool() を呼び、自社サイトの機能を「AIエージェントが呼び出せるツール」として登録することです。 サーバーもMCPサーバーも不要で、必要なのはJavaScriptだけです。この記事は、当サイト(87本の無料ツールと記事を持つAstro製の静的サイト)にサイト内検索ツール2本を実装した実物のコードをもとに、最小実装・安全な設計・動作確認の方法・実装して初めて分かった落とし穴までを解説します。CDNのスイッチで有効化する方法はCloudflare WebMCPの解説で扱っており、この記事はその「自分で書く」版です。
自力実装とCDNのスイッチの違い
先に使い分けを整理します。両者は排他ではなく、目的が違います。
| CDNのスイッチ(Cloudflare等) | 自力実装(registerTool) | |
|---|---|---|
| 必要な作業 | ダッシュボードで有効化するだけ | JavaScriptを書いてデプロイ |
| 公開できるツール | 提供されるツールパックのみ | 自サイト固有の機能を自由に |
| 反映の速さ | 即時。コード変更もデプロイも不要 | 通常のリリースと同じ |
| 向いている場面 | まず対応の受け皿を作る | サイトの中核機能をエージェントに開放する |
サイト固有の価値をエージェントに届けたいなら、自力実装以外の選択肢はありません。 ツールパックが提供するのは汎用機能(画像の来歴確認など)であり、「このサイトで何ができるか」はサイトにしか書けないためです。
最小実装:ツールは4点セット
登録するツールは、名前・自然言語の説明・入力スキーマ・実行関数の4点で構成します。仕様(W3C Web Machine Learning CGのドラフト)ではこう定義されています。
dictionary ModelContextTool {
required DOMString name;
USVString title;
required DOMString description;
object inputSchema;
required ToolExecuteCallback execute;
ToolAnnotations annotations;
};
最小のコードは次の通りです。
await document.modelContext.registerTool({
name: "search_site",
description: "サイト内をキーワードで検索し、該当ページのURLを返す",
inputSchema: {
type: "object",
properties: { query: { type: "string", description: "検索キーワード" } },
required: ["query"],
},
annotations: { readOnlyHint: true },
async execute({ query }) {
return `「${query}」の検索結果:\n- ...`;
},
});
execute の戻り値は、仕様上の型は任意ですが、エージェントには文字列化されて渡ります。エージェントはこの文字列をそのまま読むため、人間向けの整形ではなく、機械が解釈しやすい構造(1件1行・URLを必ず含める)で返すのが実務的です。登録の解除は registerTool の第2引数のオプション({ signal })に AbortSignal を渡し、controller.abort() で行います。
APIの名前には注意が必要です。仕様上の正式名は document.modelContext ですが、この提案は window.agent から navigator.modelContext を経てここに落ち着いた経緯があり、navigator.modelContext はChrome 150で非推奨になりました。日本語の解説記事にはまだ navigator 表記のものが多く残っています。当サイトでは移行期の現実に合わせ、両方を探す書き方にしています。
var mc = (document.modelContext) || (navigator.modelContext) || null;
if (!mc || typeof mc.registerTool !== 'function') return;
当サイトに実装した2つのツール
公開したのは読み取り専用の2本です。どちらも既存のサイト内検索インデックス(/search-index.json)を再利用しており、検索ロジックは画面の検索窓と同一です。
| ツール名 | 入力 | 返すもの |
|---|---|---|
search_digital_sales |
query(必須)、limit | ツール・記事・用語・業務フロー・指標・固定ページの横断検索結果。種別・タイトル・要約・絶対URL |
list_digital_sales_tools |
group、limit | ジャンル(営業・マーケティング・AI・DX・CS・お金・便利ツール)で絞り込んだ無料ツール一覧 |
実行結果は次の形式の文字列です(実際の出力)。
「NRR 計算」の検索結果 17件中 2件:
- [記事] NRR(売上維持率)とは?計算式・GRRとの違い・100%を超えるための実務 — ...
https://digital-sales-job.com/nrr-basics/
- [ツール] NRR計算ツール(売上維持率) — 期初MRRと増減からNRR・GRRを算出
https://digital-sales-job.com/tools/nrr-calculator/
実装で意識した設計判断は5つです。
- 読み取り専用に限定する: 状態を変える操作・送信する操作は一切公開しない。エージェントができることを、訪問者が画面から行えることの範囲内に収める
- 既存の検索資産を再利用する: インデックスとスコアリング式を画面の検索と揃え、同じキーワードで同じ結果が返るようにする(当サイトの実装はインデックスを共有し、スコアリングは画面側から複製している。変更時は両方を直す必要がある)
- 相対URLではなく絶対URLを返す: エージェントは受け取ったURLを別の文脈で使う。
location.originを付けて単体で辿れる形にする - インデックスは初回実行時に遅延ロードする: 通常の閲覧では1バイトも余計に読ませない
- 未対応ブラウザでは即座に何もしない:
modelContextがなければ即return。表示にも速度にも影響を与えない
description の書き方は、実は実装の中で最も重要です。ツールを選ぶのはLLMであり、判断材料はこの説明文だけです。そのため、関数のドキュメントではなく「どんなときに使う道具か」を書きます。当サイトの検索ツールの説明文には「『粗利率の計算』『稟議書の書き方』『NRRとは』のような業務上の調べものに使う」という具体例を入れています。
安全に公開するための3つの仕組み
仕様には、エージェントに渡す権限を絞るための仕組みが用意されています。
| 仕組み | 書く場所 | 意味 |
|---|---|---|
readOnlyHint |
annotations の中 |
状態を変えないツールであることの宣言。エージェントがユーザー確認の要否を判断する材料になる |
untrustedContentHint |
annotations の中 |
戻り値にユーザー生成コンテンツ等の信用できないデータが含まれる宣言。プロンプトインジェクション対策 |
exposedTo |
registerTool の第2引数 |
公開先のオリジンを配列で限定する。既定ではクロスオリジンのiframeからは見えない |
当サイトの2ツールはどちらも annotations: { readOnlyHint: true } を付けています。戻り値は自サイトが書いた文章のみで外部からの投稿を含まないため、untrustedContentHint は付けていません。
副作用のある操作(投稿・購入・送信・設定変更)は、当面公開すべきではありません。 公式が明示的に推奨しているのは exposedTo による信頼できるオリジンへの限定公開です。加えて実務では、実行前にユーザーの操作を挟む設計を併用したいところです。掲示板やレビューのようにユーザー生成コンテンツを返すツールを作る場合は、untrustedContentHint を必ず付けてください。付けないと、他人が投稿した文章がそのままエージェントへの指示として解釈される余地が残ります。
動作確認の方法
WebMCPは実験段階のため、確認の手順そのものが変わります。2026年8月時点では次の通りです。
- Chrome 146以降で
chrome://flags/#enable-webmcp-testing(WebMCP for testing)を Enabled にし、Chromeを再起動する - 対象ページを開き、開発者ツールのコンソールで
document.modelContextが存在することを確認する - 本番サイトで一般ユーザーに対して試すには、Chrome 149からのOrigin Trialに登録する
フラグを有効にできない環境でも、登録処理と実行処理そのものはスタブで検証できます。当サイトではこの方法で全ツールの動作を確認しました。ページ内のインラインスクリプトを取り出し、偽の modelContext を差し込んで再実行するだけです。
// 開発者ツールのコンソールで実行する検証用スニペット
const src = [...document.querySelectorAll('script:not([src])')]
.map(s => s.textContent).find(t => t.includes('search_digital_sales'));
const registered = [];
document.modelContext = { registerTool: t => { registered.push(t); return Promise.resolve(); } };
eval(src);
await registered[0].execute({ query: '粗利率', limit: 3 });
これで「ツールが登録されるか」「入力スキーマ通りに動くか」「0件・空入力でも壊れないか」を、ブラウザの対応状況と無関係に確かめられます。実験仕様に依存する機能を実装するときは、仕様の実装部分と自前のロジックを分離して、後者だけを先にテストできる形にしておくのが有効です。
実装して分かった落とし穴
外部のエージェント対応度診断(Cloudflareのisitagentready.com)で、実装の前後を実測しました。実装前の記録はこうです。
Check imperative WebMCP API
→ No tools registered via navigator.modelContext
実装後は Found 2 WebMCP tools via imperative_api に変わり、登録した2本のツールが、名前・タイトル・説明文・スキーマの有無まで外部から読み取られていました。ここから分かることが3つあります。
第一に、診断の項目名は非推奨の navigator.modelContext のままですが、document.modelContext を優先して登録する書き方でも検出されました。 navigator.modelContext は同じ登録先を指す後方互換の別名なので、どちらに登録しても同じ場所に載ります。診断の項目名が古いことを理由に、非推奨のAPIへ書き直す必要はありません。
第二に、ツールの説明文は外部に丸ごと公開されます。 診断ツールが取得できるということは、エージェントも、エージェントを作る側も読めるということです。社内向けのメモや未整理の文言を書く場所ではありません。
第三に、診断はサイトのトップページ1枚しか読み込みません。 特定のページにだけツールを実装すると、外部からは存在しないのと同じです。エージェント経由の発見可能性まで考えるなら、ツールの登録は個別ページではなく共通レイアウトに置き、トップページを含む全ページで有効にするのが安全です。当サイトも、登録処理をコンポーネント1つに切り出して共通レイアウトから読み込む形にしています。
もう1つ、CDNのスイッチと併用する場合の注意があります。Cloudflareのブリッジは挿入されたscriptタグの data-packs 属性でツールパックを解決するため、この属性が付いていないとツールは0本のまま待機します。「有効化した」ことと「ツールが公開されている」ことは別です。
よくある失敗
navigator.modelContextだけに書く: Chrome 150で非推奨。仕様準拠のdocument.modelContextを優先し、無ければnavigatorを見る書き方にする- 説明文を内輪の言葉で書く: 名前・タイトル・説明文は外部から丸ごと読める。公開前提の文章として書く
- 個別ページだけに実装する: 外部の診断も多くのエージェントもトップページから入る。共通レイアウトに置く
- 説明文を関数のドキュメントとして書く: エージェントは説明文で道具を選ぶ。「どんなときに使うか」を具体例つきで書く
- 副作用のある操作をいきなり公開する: 投稿・送信・購入はまだ早い。読み取り専用から始める
- 戻り値を人間向けに整形する: 表組みや装飾はエージェントには不要。1件1行・URL必須の素直な文字列にする
- 未対応ブラウザでの挙動を考えない: 例外を投げたり重い処理を走らせたりしない。存在チェックで即returnする
まとめ
- WebMCPの自力実装は
document.modelContext.registerTool()にツールの4点セットを渡すだけ。サーバーもMCPサーバーも不要 - 正式名は
document.modelContext。navigator.modelContextはChrome 150で非推奨だが、診断ツールはまだそちらを見ることがあるため移行期は両方を探す - 公開は読み取り専用から。
readOnlyHint・untrustedContentHint・exposedToで権限と信頼境界を明示する - 検証はブラウザの対応を待たなくてよい。偽の
modelContextを差し込むスタブ検証でロジックだけ先に確かめられる - ツール登録は共通レイアウトに置く。トップページで登録されていないと、外部からは実装していないのと同じに見える
AI検索・AIエージェントからの流入をどう設計するかの全体像はAEO(回答エンジン最適化)とは、クローラーへの意思表示はAIクローラーは許可か拒否か、診断で現在地を測る方法はAIエージェント対応度の実測で解説しています。プロトコルとしてのMCP、AI向けのサイト概要ファイルllms.txt、AI検索対策全体の呼称であるLLMOもあわせてどうぞ。
よくある質問
WebMCPの実装に必要なものは何ですか?
JavaScriptだけです。サーバーもMCPサーバーも不要で、ページ内で document.modelContext.registerTool() を呼び、名前・説明・入力スキーマ・実行関数の4点を渡せばツールが1本公開できます。当サイトの実装は既存のサイト内検索インデックスを再利用した約150行のスクリプト1本で、サーバー側の変更はありません。
navigator.modelContextとdocument.modelContextのどちらを使えばいいですか?
仕様上の正式名は document.modelContext で、navigator.modelContext はChrome 150で非推奨になりました。外部のエージェント対応度診断には検査項目名を navigator.modelContext のまま掲げているものがありますが、当サイトで document.modelContext を優先して登録したところ、その診断でもツール2本が正しく検出されました。両方を探して先に見つかった方へ登録すれば、将来 navigator 側が削除されても壊れません。
WebMCPのツールを公開するとセキュリティ上のリスクはありますか?
公開する操作の選び方次第です。読み取り専用のツールに限れば、エージェントができることは訪問者が画面から行えることの範囲を超えません。危険なのは投稿・購入・送信のような副作用のある操作で、仕様には限定公開のexposedToや、状態を変えないことを宣言するreadOnlyHintが用意されています。認証済みの操作や決済につながる操作は当面公開すべきではありません。