Framework Guide · WebMCP · Next.js

WebMCP in Next.js: registering tools across the server/client boundary

Ask a WebMCP question in a Next.js codebase and you hit the framework’s founding tension within ten seconds: tools register in the browser, while the App Router defaults your entire route tree to components whose code never reaches it. This reference article, written for React architects, maps the boundary precisely — where registration legally lives, how streaming and Suspense interact with tool lifecycles, the origin-trial wiring in next.config, and the placement patterns that keep your client surface small.

Fact-checked and updated on September 2, 2026 (Next.js 16.x documentation, August 2026 edition). Primary sources are cited at the end of the article.


The one paragraph that reorganizes everything

The App Router compiles two module graphs. The server graph renders Server Components into an RSC payload — HTML, serialized props, and placeholders — and the client graph hydrates only the islands behind a 'use client' directive; state, effects, event handlers and browser-only APIs (window, navigator.*) require Client Components, and “a Server Component’s code never reaches the browser” ([Next.js docs — The Server and Client Boundary](https://nextjs.org/docs/app/guides/server-and-client-boundary)). WebMCP is a browser-only, secure-context, top-level-document interface ([W3C specification](https://webmachinelearning.github.io/webmcp)). Therefore: every tool an in-tab agent can call is registered from hydrated client code on the page the agent is viewing — and a registerTool call in a Server Component is a no-op waiting to become a build error.

The upside deserves to be stated as a design principle: your tool registry hydrates exactly where your interactivity hydrates. A product page with a client-side cart widget owns add_to_cart; a docs page with a client search box owns search-docs; a Server-Component-only marketing page exposes only its declarative forms. Tool availability mirrors UI capability — automatically, with no separate permission system to drift.

Implementation status: the 2026 rollout you are coding against

  • Chrome 146 (February 2026) — early preview behind chrome://flags/#enable-webmcp-testing; local development only.
  • Chrome 149 (June 2026) — public origin trial begins (runs through Chrome 156); production exposure requires your registered token ([Chrome for Developers](https://developer.chrome.com/docs/ai/webmcp)).
  • Chrome 150+ — stabilization around document.modelContext as the canonical root, with navigator.modelContext as compatibility alias. Feature-detect both.

Edge has co-authored the spec without shipping it; Firefox and Safari have no announced timeline. Desktop MCP clients — Claude Desktop, Cursor, Windsurf — do not render pages at all and cannot call in-browser tools: your Next.js app’s server-side twin (an MCP endpoint on your API routes) remains mandatory for that traffic, sharing the same tool schemas.

Key takeaway: in Next.js, WebMCP is not “an agent feature” — it is the client-side projection of decisions you already made component by component. The framework did the architectural work; you are only naming it.

The registration, done correctly

'use client';

import { useEffect } from 'react';

export function SearchTools({ collectionId }: { collectionId: string }) {
  useEffect(() => {
    const modelContext = document.modelContext ?? navigator.modelContext;
    if (!modelContext || !('registerTool' in modelContext)) return;

    const controller = new AbortController();

    modelContext.registerTool(
      {
        name: 'search-products',
        description:
          'Searches the product catalog. Returns up to 10 matches with name, slug and price.',
        inputSchema: {
          type: 'object',
          properties: {
            query: { type: 'string', description: 'Free-text keywords' },
            maxResults: { type: 'integer', minimum: 1, maximum: 10, default: 5 },
          },
          required: ['query'],
          additionalProperties: false,
        },
        annotations: { readOnlyHint: true },
        async execute({ query, maxResults = 5 }) {
          const res = await fetch(`/api/search?c=${collectionId}&q=${encodeURIComponent(query)}`);
          const data = await res.json();
          return { content: [{ type: 'text', text: JSON.stringify(data.slice(0, maxResults)) }] };
        },
      },
      { signal: controller.signal },
    );

    return () => controller.abort();
  }, [collectionId]);

  return null;
}

Every line of ceremony is load-bearing. Registration lives in useEffect, cleanup in the returned abort — the specification removed unregisterTool(); the AbortController signal is the lifecycle ([GoogleChrome modern-web-guidance](https://github.com/GoogleChrome/modern-web-guidance/blob/main/skills/modern-web-guidance/guides/webmcp/agentic-javascript-tools.md)). Under React 18 Strict Mode’s double-invoked effects, effect-less module-body registration throws “tool already registered” in development, and a missing dependency array leaves a stale-closure tool serving yesterday’s props. The result shape — { content: [{ type: "text", text }] } — is MCP’s content envelope, deliberately shared with server-side MCP so agent code transfers between surfaces (see also our Declarative API reference).

Origin trial wiring: the header, not the meta tag

The trial token is origin-specific. In App Router, prefer the HTTP header over DOM injection — it cannot be stripped by streaming retries, cached HTML variants, or next/headers edge manipulation, and it stays out of your RSC payload:

// next.config.js
module.exports = {
  async headers() {
    return [{
      source: '/:path*',
      headers: [{ key: 'Origin-Trial', value: process.env.WEBMCP_OT_TOKEN }],
    }];
  },
};

For local development, the #enable-webmcp-testing flag removes the token requirement entirely — which also means your CI never depends on trial plumbing.

Placement: feature layouts, not the root layout

Where tool components mount is a real architecture decision in App Router, because marking a layout 'use client' expands the client boundary and forfeits Server Component optimization for everything beneath it. The guidance from the MCP-B project is exactly right: register per route-group (app/(dashboard)/layout.tsx owning account tools; app/(marketing) owning search and lead forms), keeping the root layout server-rendered ([MCP-B framework docs](https://docs.mcp-b.ai/how-to/frameworks)).

Auth-gated routes get a free security property: if the Server Component tree redirects the visitor, the tools below it never render and never register. An agent that cannot reach /checkout cannot see place_order. The inverse holds and should keep you sober: anything a logged-in user’s browser registers, an agent riding that session can call — which is why write-tools need server-side capability checks, never client-side trust.

Feed tools from the boundary: server data, zero extra fetch

Tools that only read server data should not refetch it — let the RSC payload deliver it. The Server Component fetches once; the props cross the boundary; the tool answers from memory:

// app/products/[slug]/page.tsx — Server Component
import { VariantTools } from './variant-tools';

export default async function Page({ params }) {
  const { variants } = await getProduct(params.slug);
  return <VariantTools variants={variants} />;
}

// variant-tools.tsx — marked 'use client'
// registers get_available_sizes() reading from the `variants` prop: zero IO at call time.

This collapses one of the most common agent-latency complaints — tools that trigger cold-cache API calls — into an instant answer, using the mechanism Next.js already built for hydration.

Streaming and the race you must design for

With streaming SSR, client scripts execute as their chunks arrive; a tool whose props arrive later through a Suspense boundary must be re-registered when they land. Registration inside an effect keyed on the props handles it (the pattern above re-runs on commit with fresh data); module-level registration captures the fallback render and silently serves defaults forever. This is the Next-specific face of a general draft-spec truth: tool lifetime is component lifetime, and React’s rendering model now includes a new consumer — the agent — that acts at machine speed on whatever the last commit exposed.

Frequent pitfall: next/font, next/image and WebMCP share nothing — but next/script tempts people into global-scope registration with strategy="afterInteractive". Module-scope <script> in a Client Component gives you the same timing with bundle-hashing and dead-code elimination; reach for next/script only for third-party tooling, and never for auth-sensitive tool bodies.
Governance pattern that scales: keep one tools.ts module exporting typed tool definitions; the browser bundle imports it for registerTool, and a Route Handler app/api/mcp/route.ts re-exports the same array for tools/list. Browser-WebMCP and server-MCP then cannot drift, and one unit test suite covers both transports.

Verification: 30 seconds per page

Run pnpm dev, flag enabled, and on each template:

await document.modelContext.getTools();
await document.modelContext.executeTool('search-products', '{"query":"wool socks"}');

For full validation — synthesized schema, manual invocation, Gemini-simulated routing — use the Model Context Tool Inspector extension (François Beaufort, Chrome team). The invariant to assert in integration tests: the tool list on a route equals the registry slice assigned to that route group.

FAQ

Can I register tools from middleware or a Server Action?

No. Both run server-side; WebMCP registration requires a live document in a supporting browser. Server Actions can back a tool’s execute (the fetch target), which is the correct division of labor.

Do the tools work in Edge or Safari today?

No — Chrome 149–156 origin trial only. Feature detection keeps every other browser untouched, and your MCP endpoint covers non-browser clients.

What breaks when Chrome 156 ends and GA ships?

Prior history says token plumbing disappears and the canonical root object settles (document.modelContext). Centralized feature detection plus one header constant makes the migration a one-line deploy. Sites hand-scattering checks face an audit instead.

Is declarative WebMCP still relevant in Next.js?

Very: server-rendered forms in RSC output take toolname/tooldescription attributes with zero client JS — the same three-attribute pattern as any stack, covered in depth in our Declarative API article.

Conclusion

Next.js spent six years teaching its community one habit: explicitly deciding, file by file, what runs in the browser. WebMCP is simply the newest reason to keep that list accurate — your tool surface is that decision, spelled one level louder. Model the boundary first, register inside it with abort-scoped effects, feed tools through props, mirror the registry on the server, and your app becomes the rare thing in the agentic web: an interface agents can use and your security model already recognises.


Primary sources

  1. nextjs.org — The Server and Client Boundary (v16.3, updated 2026-08).
  2. nextjs.org — Server and Client Components.
  3. nextjs.org — the use client directive.
  4. webmachinelearning.github.io/webmcp — W3C CG specification.
  5. developer.chrome.com/docs/ai/webmcp — origin trial and API guidance.
  6. GoogleChrome/modern-web-guidance — lifecycle and root-object migration.
  7. docs.mcp-b.ai — framework registration patterns.
  8. Model Context Tool Inspector — validation extension.

Leave a Reply

Your email address will not be published. Required fields are marked *