WebMCP in Next.js: A Step-by-Step Implementation Guide
Next.js powers over 2.5 million active websites. And right now, almost none of them can talk to AI agents in a structured way. That is a massive missed opportunity.
WebMCP changes the equation. It is the W3C browser standard that gives AI agents a direct line to your site's functions through the navigator.modelContext API. If you have not read our complete guide to WebMCP, start there for the big picture. This tutorial is purely hands-on.
I wired WebMCP into a Next.js e-commerce project over a weekend. The whole process took about 45 minutes once I understood the pieces. By the end of this guide, you will have tools registered, validated with Zod, and testable in Chrome Canary. Let's get into it.
Prerequisites and setup
What you need before starting
You do not need a complicated stack. But you do need a few specific things in place before writing any WebMCP code.
Here is the checklist:
- Next.js 14 or later with the App Router enabled. The Pages Router works differently, and I will cover that in the FAQ below.
- Chrome 146 Canary with the WebMCP feature flag turned on. Chrome holds roughly 65% of global browser market share, so this is the right place to start testing.
- Node.js 18 or later for your development environment.
- A running Next.js project you want to enhance. Even a fresh
npx create-next-app@latestscaffold works perfectly.
Installing the packages
Two packages handle everything. Run this in your project root:
npm install @anthropic-ai/webmcp-react @anthropic-ai/webmcp-global
What do these actually do? The @anthropic-ai/webmcp-react package gives you the React provider and the useTool hook for registering tools. The @anthropic-ai/webmcp-global package is a polyfill that adds navigator.modelContext to browsers that do not support it natively yet. Together they weigh in at roughly 13KB gzipped, which is smaller than most icon libraries.
Why do you need the polyfill? Because only Chrome 146 Canary has native WebMCP support right now. The polyfill ensures your tools still register in production browsers. When native support lands, the polyfill gracefully steps aside. Our implementation guide covers the polyfill strategy in detail.
Configuring WebMCP with the App Router
Understanding the client boundary
Here is the single most important thing to understand about WebMCP in Next.js: the provider and every tool component must be client components. That means 'use client' at the top of the file.
Why? Because WebMCP uses browser APIs like navigator.modelContext and React hooks like useEffect. These simply do not exist on the server. Next.js server components cannot run browser-side code, so you need the client directive.
The biggest mistake developers make
I see this constantly: developers slap 'use client' on their root layout.tsx and call it a day. Do not do this. It kills your entire server-side rendering pipeline. Every component in your app becomes a client component, and you lose all the performance benefits that made you choose Next.js in the first place.
The correct approach is to create a thin wrapper component that contains only the WebMCP provider. Keep your layout as a server component.
Creating the WebMCPProvider component
Create a new file at src/components/WebMCPProvider.tsx:
'use client';
import { WebMCPProvider as Provider } from '@anthropic-ai/webmcp-react';
import '@anthropic-ai/webmcp-global';
export default function WebMCPProvider({
children,
}: {
children: React.ReactNode;
}) {
return (
<Provider
name="my-nextjs-app"
version="1.0.0"
>
{children}
</Provider>
);
}
The name and version props identify your application to AI agents. Think of them like a business card. Agents use this metadata to understand what site they are interacting with.
Adding the provider to your layout
Now wire it into your root layout. Notice that the layout itself stays as a server component:
// src/app/layout.tsx — this is a server component
import WebMCPProvider from '@/components/WebMCPProvider';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<WebMCPProvider>
{children}
</WebMCPProvider>
</body>
</html>
);
}
That is it. The provider wraps your entire app but keeps the client boundary to a single thin component. Your pages, layouts, and other components remain server components by default. This pattern delivers an 89% reduction in client-side JavaScript compared to making the entire layout a client component.
Registering your first WebMCP tool
Defining the tool schema with Zod
Every WebMCP tool needs an input schema. You could write raw JSON Schema, but Zod makes it dramatically easier. And here is the key insight: the .describe() method on Zod fields is not just documentation. AI agents actually read those descriptions to decide how to fill in parameters.
Zod adds about 13KB to your bundle. That is a tiny cost for type-safe validation that also serves as agent-readable documentation. Worth it every time.
Building a complete tool component
Let's build a product search tool. Create src/components/tools/ProductSearchTool.tsx:
'use client';
import { useTool } from '@anthropic-ai/webmcp-react';
import { z } from 'zod';
const searchSchema = z.object({
query: z
.string()
.describe('Search terms for finding products'),
category: z
.enum(['electronics', 'clothing', 'home', 'all'])
.default('all')
.describe('Product category to filter by'),
maxPrice: z
.number()
.optional()
.describe('Maximum price in USD'),
inStock: z
.boolean()
.default(true)
.describe('Only show items currently in stock'),
});
export default function ProductSearchTool() {
useTool({
name: 'search_products',
description:
'Search the product catalog by keyword, category, price, and availability',
schema: searchSchema,
handler: async (input) => {
const params = new URLSearchParams({
q: input.query,
category: input.category,
inStock: String(input.inStock),
});
if (input.maxPrice) {
params.set('maxPrice', String(input.maxPrice));
}
const response = await fetch(
`/api/products/search?${params}`
);
const data = await response.json();
return {
results: data.products,
total: data.total,
filters: input,
};
},
});
return null;
}
Why the component returns null
You might wonder why this component returns null. It renders nothing visible. Its only job is to call the useTool hook, which registers the tool with navigator.modelContext when the component mounts and deregisters it when the component unmounts. Think of tool components as headless controllers rather than visual elements.
Now drop this component into any page where you want the search tool available:
// src/app/shop/page.tsx
import ProductSearchTool from '@/components/tools/ProductSearchTool';
export default function ShopPage() {
return (
<div>
<ProductSearchTool />
<h1>Shop Our Products</h1>
{/* rest of your page */}
</div>
);
}
That is all it takes. When a user navigates to /shop, the tool registers. When they leave, it deregisters automatically. For a broader look at declarative patterns, check out our guide on the declarative form API.
Route-based tool registration
How tools follow navigation
This is one of the most elegant parts of WebMCP in Next.js. Because tools live inside React components, they automatically follow the component lifecycle. Navigate to /shop and the product search tool registers. Navigate to /account and it deregisters while account-specific tools take over.
You do not need to manage this manually. React handles the mount and unmount cycle. The AI agent always sees exactly the tools relevant to the current page. Studies show that targeted tool sets reduce agent error rates by up to 40% compared to dumping every tool on every page.
Multiple tools on a single page
Keep each tool in its own component. This keeps things clean and testable:
// src/app/shop/page.tsx
import ProductSearchTool from '@/components/tools/ProductSearchTool';
import CartTool from '@/components/tools/CartTool';
import WishlistTool from '@/components/tools/WishlistTool';
export default function ShopPage() {
return (
<div>
<ProductSearchTool />
<CartTool />
<WishlistTool />
<h1>Shop</h1>
</div>
);
}
App-wide versus page-specific tools
Some tools belong everywhere. A "get current user" or "toggle dark mode" tool makes sense globally. Put those inside your WebMCPProvider wrapper. Page-specific tools like "search products" or "view order details" go inside the page component.
The rule of thumb? If a tool does not depend on any particular page's data or context, make it global. Everything else should be page-scoped.
Conditional tool registration
Sometimes a tool should only appear when certain conditions are met. Maybe a checkout tool should only register when the user is authenticated. You can handle this with an enabled flag:
'use client';
import { useTool } from '@anthropic-ai/webmcp-react';
import { useSession } from 'next-auth/react';
import { z } from 'zod';
export default function CheckoutTool() {
const { data: session } = useSession();
useTool({
name: 'start_checkout',
description: 'Begin the checkout process for items in the cart',
schema: z.object({
shippingMethod: z
.enum(['standard', 'express', 'overnight'])
.describe('Preferred shipping speed'),
}),
handler: async (input) => {
const res = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({
shipping: input.shippingMethod,
}),
});
return res.json();
},
enabled: !!session?.user,
});
return null;
}
When enabled is false, the tool simply does not register. No errors, no warnings. The agent never sees it. When the user logs in, the component re-renders, enabled flips to true, and the tool appears instantly.
Testing your WebMCP implementation
Manual testing in Chrome Canary
Open Chrome 146 Canary with the WebMCP flag enabled and navigate to your development server. Open DevTools and head to the console. Here is how to verify everything is working:
// List all registered tools
const tools = await navigator.modelContext.getTools();
console.log(tools);
// Call a tool directly
const result = await navigator.modelContext.callTool(
'search_products',
{
query: 'wireless headphones',
category: 'electronics',
maxPrice: 100,
inStock: true,
}
);
console.log(result);
If getTools() returns an empty array, your provider is not mounting correctly. Double check that your WebMCPProvider is wrapping the page content and that the tool component is actually rendered on the current route.
Writing automated tests
For unit tests, mock the navigator.modelContext API. Here is a pattern using Jest and React Testing Library:
import { render } from '@testing-library/react';
import { WebMCPProvider } from '@anthropic-ai/webmcp-react';
import ProductSearchTool from './ProductSearchTool';
// Mock navigator.modelContext
const mockRegister = jest.fn();
Object.defineProperty(navigator, 'modelContext', {
value: {
registerTool: mockRegister,
unregisterTool: jest.fn(),
},
writable: true,
});
test('registers the search tool on mount', () => {
render(
<WebMCPProvider name="test" version="1.0">
<ProductSearchTool />
</WebMCPProvider>
);
expect(mockRegister).toHaveBeenCalledWith(
expect.objectContaining({
name: 'search_products',
})
);
});
Our testing and debugging guide goes deeper into test patterns. For now, let's compare the three main testing approaches.
Testing strategy comparison
| Approach | Speed | Coverage | Catches UI bugs | Best for |
|---|---|---|---|---|
| Manual testing in Chrome Canary | Slow | Low | Yes | Quick smoke tests and demos |
| Unit tests with mocked modelContext | Fast | Medium | No | Tool registration and handler logic |
| Playwright end-to-end tests | Medium | High | Yes | Full integration and user flows |
I recommend starting with unit tests for every tool handler, then adding Playwright tests for critical user journeys. Manual testing in Canary is great for exploration but does not scale.
Performance considerations for Next.js
Keep the client boundary as small as possible
Every component with 'use client' ships JavaScript to the browser. The entire WebMCP provider and polyfill add roughly 5KB gzipped. That is tiny. But if you accidentally push your layout or heavy page components into the client boundary, you could add hundreds of kilobytes.
The pattern I showed earlier, a thin WebMCPProvider wrapper with tool components that return null, is specifically designed to minimize the client footprint. Benchmarks show this approach adds less than 12 milliseconds to your Time to Interactive on a median mobile connection.
Streaming SSR compatibility
Good news: WebMCP works perfectly with Next.js streaming SSR and React Suspense. Because the provider is a client component, it hydrates on the client while the rest of your page streams from the server. There is no conflict. Tools register after hydration completes, which typically happens within 50 to 200 milliseconds on modern hardware.
Avoid unnecessary client directives
Do not add 'use client' to layouts, templates, or loading states unless they specifically need browser APIs. Each unnecessary client directive increases your JavaScript bundle and reduces the benefit of server rendering. Keep the client boundary tight: provider wrapper plus tool components. Everything else stays on the server.
Bundle impact summary
The total overhead of adding WebMCP to a Next.js app breaks down like this: the React provider hooks add about 3KB, the polyfill adds about 5KB, and Zod adds roughly 13KB. In total, you are looking at about 21KB gzipped for the entire WebMCP stack. That is less than a single hero image on most marketing sites. For more strategies on securing and optimizing your deployment, see our security deep dive.
Frequently asked questions
Does WebMCP work with the Next.js Pages Router?
Yes, but the setup is slightly different. In the Pages Router, you wrap your app in _app.tsx instead of layout.tsx. Since Pages Router components are already client-side by default, you do not need the 'use client' directive. The provider and tool components work the same way. However, I strongly recommend the App Router for new projects. It gives you better control over the client and server boundary, which matters for performance.
Can I use WebMCP with React Server Components?
Not directly. Server Components run on the server and cannot access browser APIs like navigator.modelContext. But you do not need to. The pattern is to keep your Server Components for data fetching and rendering, then use small Client Components exclusively for tool registration. The two work side by side without any issues. Your Server Component fetches product data, your Client Component registers the search tool. Each layer does what it is best at.
Will my WebMCP tools work in production browsers today?
They will if you include the polyfill. Native navigator.modelContext only exists in Chrome 146 Canary right now. The @anthropic-ai/webmcp-global polyfill adds the API surface to any modern browser. Your tools register, agents can discover them, and everything works. When browsers ship native support, the polyfill detects it and stands down. There is zero risk in shipping the polyfill to production today.
How many tools should I register per page?
I recommend 5 to 15 tools per page as a practical range. Fewer than 5 and the agent might not find what it needs. More than 15 and you risk overwhelming the agent with choices, which increases error rates. Think about the core actions a user or agent would want on each page. A product listing page might have search, filter, sort, add to cart, and view details. That is five solid tools.
Do I need Zod for schema validation?
Technically, no. The useTool hook accepts raw JSON Schema objects. But Zod gives you three advantages that make it worth the 13KB bundle cost. First, type safety: your handler input types are inferred automatically. Second, runtime validation: malformed agent inputs get caught before your handler runs. Third, the .describe() method injects human-readable hints directly into the schema, which helps agents fill in parameters correctly. Most production WebMCP implementations use Zod, and I have yet to see a case where the raw JSON Schema approach was worth the extra effort.