Integrate WebMCP with Remix using client-only patterns for SSR safety.
WebMCP relies on browser APIs (navigator.modelContext) that don’t exist on the server. Remix renders components on both server and client during SSR, so you must ensure WebMCP code only runs in the browser.
import { lazy, Suspense } from 'react';// Lazy load the tools component - only runs on clientconst HomeTools = lazy(() => import('~/components/home-tools'));export default function Index() { return ( <div> <h1>Home</h1> <Suspense fallback={null}> <HomeTools /> </Suspense> </div> );}
Copy
import '@mcp-b/global'; // Safe - only imported when this module loads on clientimport { useWebMCP } from '@mcp-b/react-webmcp';import { z } from 'zod';export default function HomeTools() { useWebMCP({ name: 'greet', description: 'Greet a user', inputSchema: { name: z.string() }, handler: async ({ name }) => `Hello, ${name}!`, }); return null; // Tools component renders nothing}
Don’t import @mcp-b/global in route files directly. Route files are bundled for SSR and will fail when the polyfill tries to access navigator. Always import it in lazily-loaded client components or inside ClientOnly.
'navigator is not defined' or 'navigator.modelContext is undefined'
Cause:@mcp-b/global is being imported during SSRSolution: Use ClientOnly from remix-utils or lazy imports to ensure the polyfill only loads on the client.
Tools not appearing after navigation
Cause: Tools are registered in page components that unmount on navigationSolution: Move persistent tools to root.tsx using the client-only pattern above.
Hydration mismatch errors
Cause: Server HTML doesn’t match client HTML because tools render differentlySolution: Ensure tool components return null and use ClientOnly fallback={null} for consistent server/client output.
For complex patterns (nested layouts, context providers, embedded agents), see the Next.js guide which covers React SSR patterns in depth—the concepts apply to Remix as well.