Next.jsでWebMCPツールを登録する

App Routerでツールを登録する場所と、サーバー側の描画や画面更新で注意する点を、コード例に沿って説明します。

画面表示時にツールを登録し、利用中は画面と同期し、終了時に登録を解除する流れ
ブラウザ上の登録処理をクライアントコンポーネントに分け、終了時に解除します。図を大きく見る(新しいタブ)

要点まとめ

このガイドでやることは、次の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が完全に動く
次に読む

ライブサンプルで動きを確認する

業種別のサンプルで、実際のツール登録を試せます。

サンプル集へ