Skip to main content

Full Example

Production-ready React example with useWebMCP() hooks
React users should use @mcp-b/react-webmcp instead of @mcp-b/global directly—it handles lifecycle automatically and provides Zod validation. This guide covers SSR-specific challenges when using WebMCP with Next.js App Router.

The fundamental challenge

WebMCP relies on browser APIs (navigator.modelContext, postMessage, DOM access) that do not exist on the server. Next.js App Router defaults to Server Components, which means:
  • Tools cannot be registered in Server Components
  • The @mcp-b/global polyfill must run on the client
  • Any component using useWebMCP or useWebMCPPrompt must be a Client Component

Polyfill placement

The @mcp-b/global polyfill must be imported in a Client Component before any tools are registered. Import it at the highest layout that contains your tools—but not necessarily at the root.
Don’t make your root layout a Client Component unless you need to. This disables SSR for your entire application. Instead, import the polyfill in the feature layout(s) where tools are used.
'use client';

import '@mcp-b/global';  // Import FIRST, before other components
import { DashboardTools } from '@/components/dashboard-tools';

export default function DashboardLayout({ children }) {
  return (
    <>
      <DashboardTools />
      {children}
    </>
  );
}
If you have tools in multiple unrelated sections, import the polyfill in each feature layout:
src/app/layout.tsx              # Server Component - keeps SSR benefits
src/app/(dashboard)/layout.tsx  # 'use client' + @mcp-b/global
src/app/(settings)/layout.tsx   # 'use client' + @mcp-b/global
The polyfill is small and idempotent—importing it multiple times is fine.
If the polyfill isn’t working, you’ll see:
  • navigator.modelContext is undefined
  • Tools don’t appear in the MCP inspector
  • No errors, but tools simply don’t register

Client components are required

Every WebMCP hook needs 'use client':
// Server Component (default in App Router)
import { useWebMCP } from '@mcp-b/react-webmcp';

export function MyTools() {
  useWebMCP({ ... });  
  // Will fail - hooks can't run on server
  return null;
}
// Client Component
'use client';  

import { useWebMCP } from '@mcp-b/react-webmcp';

export function MyTools() {
  useWebMCP({ ... });  
  // Works - runs in browser
  return null;
}
Hooks that require Client Components:
  • useWebMCP() - tool registration
  • useWebMCPPrompt() - prompt registration
  • useWebMCPResource() - resource registration
  • useWebMCPContext() - context registration
  • useMcpClient() - MCP client access

Embedded agent setup

The embedded agent uses browser APIs and must be loaded client-side only using dynamic import:
'use client';

import dynamic from 'next/dynamic';

const EmbeddedAgent = dynamic(
  () => import('@mcp-b/embedded-agent/web-component').then(mod => mod.EmbeddedAgent),
  { ssr: false }  // Critical - prevents server-side rendering
);

export function MyEmbeddedAgent() {
  return (
    <EmbeddedAgent
      appId="your-app-id"
      devMode={{
        anthropicApiKey: process.env.NEXT_PUBLIC_ANTHROPIC_API_KEY || '',
      }}
    />
  );
}
ssr: false is required because the embedded agent accesses window, document, and other browser APIs. Server-side rendering will crash with “window is not defined”.

Avoiding duplicate components

Next.js App Router supports nested layouts. If you add the embedded agent to multiple layouts, it will render multiple times:
src/app/(calendar)/layout.tsx → Has <EmbeddedAgent />
src/app/(calendar)/week/layout.tsx → Also has <EmbeddedAgent />

Result: TWO embedded agents on the page!
Place the agent in ONE feature layout that wraps all pages needing AI access:
'use client';

import '@mcp-b/global';
import { CalendarProvider } from '@/contexts/calendar';
import { CalendarTools } from '@/components/calendar-tools';
import { MyEmbeddedAgent } from '@/components/embedded-agent';

export default function CalendarLayout({ children }) {
  return (
    <CalendarProvider>
      <CalendarTools />
      <MyEmbeddedAgent />
      {children}
    </CalendarProvider>
  );
}
This keeps your root layout as a Server Component while giving tools access to the context they need.

Tool registration with context

WebMCP tools frequently need access to application state. This creates a dependency chain:
Context Provider → Tools Component → Tools can access state
'use client';

import '@mcp-b/global';
import { FeatureProvider } from '@/contexts/feature';
import { FeatureTools } from '@/components/feature-tools';

export default function FeatureLayout({ children }) {
  return (
    <FeatureProvider>        {/* 1. Provider wraps everything */}
      <FeatureTools />       {/* 2. Tools inside provider */}
      {children}
    </FeatureProvider>
  );
}
'use client';

import { useWebMCP } from '@mcp-b/react-webmcp';
import { useFeature } from '@/contexts/feature';
import { z } from 'zod';

export function FeatureTools() {
  const { data, setData } = useFeature();

  useWebMCP({
    name: 'get_data',
    description: 'Get current feature data',
    handler: async () => data,
  });

  useWebMCP({
    name: 'update_data',
    description: 'Update feature data',
    inputSchema: { value: z.string() },
    handler: async (input) => {
      setData(input.value);
      return { success: true };
    },
  });

  return null;  // Tools component renders nothing
}
Tools placed outside the provider can’t access context:
// Tools can't access context here
export default function Layout({ children }) {
  return (
    <>
      <FeatureTools />      {/* Outside provider - useFeature() fails */}
      <FeatureProvider>
        {children}
      </FeatureProvider>
    </>
  );
}
Tools can navigate using Next.js router:
'use client';

import { useRouter, usePathname } from 'next/navigation';
import { useWebMCP } from '@mcp-b/react-webmcp';
import { z } from 'zod';

export function NavigationTools() {
  const router = useRouter();
  const pathname = usePathname();

  useWebMCP({
    name: 'navigate_to_page',
    description: 'Navigate to a specific page',
    inputSchema: {
      path: z.enum(['/dashboard', '/settings', '/profile']),
    },
    handler: async ({ path }) => {
      if (pathname !== path) {
        router.push(path);
      }
      return { navigatedTo: path };
    },
  });

  return null;
}
In Next.js App Router, the root layout persists across navigations. Tools registered in root layout stay registered, while tools in page components unmount/remount on navigation.

Common errors

Cause: Browser-only code running during SSRSolution:
// Use dynamic import with ssr: false
const Component = dynamic(() => import('./Component'), { ssr: false });

// Or check for browser environment
if (typeof window !== 'undefined') {
  // Browser-only code
}
Cause: Polyfill hasn’t initialized yetSolution:
// Ensure polyfill is imported before any tool registration
import '@mcp-b/global';  // FIRST
import { useWebMCP } from '@mcp-b/react-webmcp';  // AFTER
Possible Causes:
  1. Component using useWebMCP isn’t mounted
  2. Component is a Server Component (missing 'use client')
  3. Tool registration is conditional and condition is false
  4. Polyfill loaded after tools tried to register
Debug:
const { state } = useWebMCP({
  name: 'my_tool',
  // ...
});

console.log('Tool registered, state:', state);
Cause: Component mounted multiple times (e.g., in nested layouts)Solution: Only render WebMCP components in ONE layout. Use React DevTools to check component tree for duplicates.

Deployment checklist

Before deploying your WebMCP + Next.js app:
  • Root layout is a Server Component (no 'use client') to preserve SSR
  • Feature layout(s) have 'use client' and import @mcp-b/global
  • All components using WebMCP hooks have 'use client'
  • Embedded agent uses dynamic() with { ssr: false }
  • Embedded agent is rendered in only ONE layout
  • Tools that need context are inside their Provider
  • Environment variables use NEXT_PUBLIC_ prefix for client access
src/
├── app/
│   ├── layout.tsx          # Server Component - keeps SSR benefits
│   └── (feature)/
│       ├── layout.tsx      # 'use client' + @mcp-b/global + provider + tools + agent
│       └── page.tsx        # Can be Server Component for data fetching
├── components/
│   ├── embedded-agent.tsx  # Dynamic import, ssr: false
│   └── feature-tools.tsx   # 'use client', useWebMCP hooks
└── contexts/
    └── feature.tsx         # 'use client', React context
This structure ensures:
  1. Root layout stays a Server Component (SSR preserved)
  2. Polyfill loads in feature layouts where tools are used
  3. Tools have access to context (feature layout)
  4. Agent renders once (feature layout only)
  5. Pages can still use Server Components for data fetching

Development

Use Chrome DevTools MCP for AI-driven development - your AI can write, discover, and test tools in real-time.