Integrate WebMCP with TanStack Start using client-only patterns for SSR safety.
WebMCP relies on browser APIs (navigator.modelContext) that don’t exist on the server. TanStack Start renders components on both server and client during SSR, so you must ensure WebMCP code only runs in the browser.
Unlike Next.js which has explicit 'use client' directives, TanStack Start components run during both SSR and client-side hydration by default. This means:
@mcp-b/global polyfill cannot be imported at module level
useWebMCP hooks will fail during SSR if not guarded
Wrap your WebMCP tools in a client-only component using dynamic imports:
Copy
import { createFileRoute } from '@tanstack/react-router';import { lazy, Suspense } from 'react';// Lazy load the tools component - only runs on clientconst HomeTools = lazy(() => import('../components/home-tools'));export const Route = createFileRoute('/')({ component: Home,});function Home() { 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.
If you prefer keeping tools in the same file, guard with an environment check:
Copy
import { createFileRoute } from '@tanstack/react-router';import { useEffect, useState } from 'react';export const Route = createFileRoute('/')({ component: Home,});function Home() { const [isClient, setIsClient] = useState(false); useEffect(() => { setIsClient(true); }, []); return ( <div> <h1>Home</h1> {isClient && <HomeTools />} </div> );}// Separate component that only mounts on clientfunction HomeTools() { // Dynamic import ensures this only loads on client const [ready, setReady] = useState(false); useEffect(() => { import('@mcp-b/global').then(() => { setReady(true); }); }, []); if (!ready) return null; return <ToolsInner />;}function ToolsInner() { const { useWebMCP } = require('@mcp-b/react-webmcp'); const { z } = require('zod'); useWebMCP({ name: 'greet', description: 'Greet a user', inputSchema: { name: z.string() }, handler: async ({ name }) => `Hello, ${name}!`, }); return null;}
The lazy import pattern (first example) is cleaner and recommended. The environment check pattern is useful when you can’t easily split into separate files.
'navigator is not defined' or 'navigator.modelContext is undefined'
Cause:@mcp-b/global is being imported during SSRSolution: Use lazy imports or dynamic import() 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 Suspense with 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 TanStack Start as well.