要点まとめ
このガイドでやることは、次の3つです。
document.modelContextはブラウザAPIなので、クライアントコンポーネントから触る- ツールの登録は
useEffectで1回だけ行い、アンマウント時に解除する - 未対応環境では、通常UIがそのまま動く設計にする
クライアントコンポーネントに分離する
Next.jsのApp Routerでは、ページのコンポーネントは既定でサーバーで実行されます。documentはサーバーに存在しないため、ツール登録は"use client"を付けた コンポーネントに分けて書きます。
TypeScriptuse client
"use client";
import { useEffect } from "react";
export function WebMcpRegister() {
useEffect(() => {
const modelContext = (
document as Document & { modelContext?: ModelContext }
).modelContext;
if (!modelContext) return; // 未対応環境: 通常UIが使われる
const controller = new AbortController();
modelContext
.registerTool(
{
name: "search_products",
title: "商品を検索",
description: "販売中の商品を検索する。購入は行わない。",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "商品キーワード" },
},
required: ["query"],
},
annotations: { readOnlyHint: true },
execute: async ({ query }) => searchProducts(query),
},
{ signal: controller.signal },
)
.catch(() => undefined);
return () => controller.abort(); // アンマウント時に登録解除
}, []);
return null; // UIは持たない
}SSRとハイドレーションの注意点
document.modelContextの有無は、サーバーでは判定できません。 「登録済み/未対応」の表示は、ハイドレーション後に確定する値として扱います。
初期表示で分岐を書かない
サーバーとクライアントで異なるJSXを出力すると、ハイドレーション不一致が起きます。 登録状態の表示はuseEffectで状態を更新してから切り替えます。
このサイトでの実例
webmcp.jpでは、共通フックuseWebMcpToolに登録処理を集約しています。 各サンプルページはツールの中身だけを定義し、登録・解除・状態表示はフックに任せています。 実装は サンプル集 の各ページで確認できます。
チェックリスト
- ツール登録は
"use client"コンポーネント内 - 登録は
useEffectで1回、解除はAbortController - 登録状態のUI分岐はハイドレーション後に行う
- 未対応環境でも通常UIが完全に動く
次に読む
サンプル集へ ライブサンプルで動きを確認する
業種別のサンプルで、実際のツール登録を試せます。