1. Open Chrome 146 (Beta channel or later)
2. Navigate to chrome://flags/#model-context-protocol
3. Enable the flag and restart Chrome
Google's WebMCP protocol shipped in Chrome 146. This SDK gives you React hooks, App Router integration, and the only testing suite that proves AI agents use your tools correctly.
MIT licensed · Free SDK · Paid plans add testing & optimization
1 'use client'; 2 import { useTool } from '@webmcp/next'; 3 4 export function ProductSearch() { 5 const { isRegistered } = useTool({ 6 name: 'search_products', 7 description: 'Search catalog by keyword', 8 inputSchema: { 9 type: 'object', 10 properties: { 11 query: { type: 'string' }, 12 }, 13 }, 14 execute: async (params) => { 15 const res = await fetch(`/api/search?q=${params.query}`); 16 return { content: [{ type: 'text', text: await res.text() }] }; 17 } 18 }); 19 }
Search catalog by keyword
A browser protocol by Google and Microsoft. Websites publish structured tools AI agents discover and invoke directly — typed parameters, validated inputs, structured responses. One API under the hood:
navigator.modelContext.registerTool({
name: 'search_flights',
description: 'Search available flights between two airports by date',
inputSchema: {
type: 'object',
properties: {
origin: { type: 'string', description: 'Departure IATA airport code' },
destination: { type: 'string', description: 'Arrival IATA airport code' },
date: { type: 'string', description: 'Departure date (YYYY-MM-DD)' }
},
required: ['origin', 'destination', 'date']
},
execute: async ({ origin, destination, date }) => {
const flights = await fetch(`/api/flights?from=${origin}&to=${destination}&date=${date}`);
const data = await flights.json();
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
}
});This SDK wraps the raw API with React lifecycle management, TypeScript inference, SSR safety, and testing tools that prove your implementation works.
Full spec →10 minutes to your first working WebMCP tool.
WebMCP is available today in Chrome 146+ behind a flag.
1. Open Chrome 146 (Beta channel or later)
2. Navigate to chrome://flags/#model-context-protocol
3. Enable the flag and restart Chrome
$ npm install @webmcp/nextimport { WebMCPProvider } from '@webmcp/next';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<WebMCPProvider
siteId="your-site-id" // Optional: enables analytics
fallback="graceful" // Degrades silently
>
{children}
</WebMCPProvider>
</body>
</html>
);
}'use client' — Tools MUST register in Client Components (browser context required).
'use client';
import { useTool } from '@webmcp/next';
export function ProductSearch() {
const { isRegistered } = useTool({
name: 'search_products',
description: 'Search the product catalog by keyword, category, or price range',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search keywords' },
category: { type: 'string', description: 'Product category',
enum: ['electronics', 'clothing', 'home', 'sports'] },
maxPrice: { type: 'number', description: 'Maximum price in USD' }
},
required: ['query']
},
execute: async ({ query, category, maxPrice }) => {
const res = await fetch(
`/api/products/search?q=${query}&cat=${category || ''}&max=${maxPrice || ''}`
);
const products = await res.json();
return { content: [{ type: 'text', text: JSON.stringify(products) }] };
}
});
return (
<form action="/search">
<input name="q" placeholder="Search products..." />
<button type="submit">Search</button>
</form>
);
}The WebMCP + Next.js architecture. Tools register in the browser. Heavy lifting happens on the server. WebMCP makes it feel like writing normal React.
navigator.modelContext only exists in the browser. This is how the protocol works — and it's actually GOOD architecture: your tool registration is declarative (React components), your tool execution can call any server-side resource, and your UI gracefully degrades when WebMCP isn't available.import { ProductSearch } from './ProductSearch';
export default async function ProductsPage() {
const categories = await db.categories.findMany();
const featured = await db.products.findMany({
where: { featured: true }, take: 10
});
return (
<div>
<h1>Products</h1>
<ProductSearch
categories={categories}
featured={featured}
/>
</div>
);
}'use client';
import { useTool } from '@webmcp/next';
export function ProductSearch({ categories, featured }) {
useTool({
name: 'search_products',
description: 'Search products...',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string' },
category: {
type: 'string',
enum: categories.map(c => c.slug)
}
}
},
execute: async (params) => {
const res = await fetch('/api/products/search', {
method: 'POST',
body: JSON.stringify(params)
});
return { content: [{ type: 'text',
text: await res.text() }] };
}
});
return <form>{/* Search UI */}</form>;
}Server Component fetches categories from DB → passes to Client Component → Client Component uses them as enum values in the tool schema. Best of both worlds.
WebMCP has a declarative API that turns standard HTML forms into agent-callable tools — no JavaScript required. Just add a few attributes.
export default function ContactPage() {return (<form action="/api/contact" method="POST"><label htmlFor="name">Full Name</label><input id="name" name="name" required /><label htmlFor="email">Email Address</label><input id="email" name="email" type="email" required /><label htmlFor="message">Message</label><textarea id="message" name="message" required /><button type="submit">Send</button></form>);}
export default function ContactPage() {return (<form action="/api/contact" method="POST"toolname="send_contact_message"tooldescription="Send a message to support"><label htmlFor="name">Full Name</label><input id="name" name="name" requiredtoolparamdescription="Full name of sender"/><label htmlFor="email">Email Address</label><input id="email" name="email" type="email" requiredtoolparamdescription="Email for reply"/><label htmlFor="message">Message</label><textarea id="message" name="message" requiredtoolparamdescription="The message content"/><button type="submit">Send</button></form>);}
<input type='email'> becomes { type: 'string', format: 'email' }. A <select> becomes an enum. You get structured tool definitions for free.Same result with TypeScript prop validation and analytics integration:
'use client';
import { ToolForm, ToolInput, ToolTextarea } from '@webmcp/next';
export function ContactForm() {
return (
<ToolForm
name="send_contact_message"
description="Send a message to support"
action="/api/contact"
>
<ToolInput name="sender_name" label="Full Name" required
paramDescription="Full name of the sender" />
<ToolInput name="sender_email" label="Email" type="email" required
paramDescription="Email for reply" />
<ToolTextarea name="message_body" label="Message" required
paramDescription="The message content" />
<button type="submit">Send Message</button>
</ToolForm>
);
}Define your tool's input schema once as a TypeScript interface. WebMCP generates the JSON Schema for WebMCP automatically and infers the execute function's parameter types.
import { defineTool } from '@webmcp/next';
// Define your input type
interface FlightSearchParams {
origin: string; // Departure IATA code
destination: string; // Arrival IATA code
date: string; // YYYY-MM-DD format
passengers?: number; // Default: 1
class?: 'economy' | 'business' | 'first';
}
// defineTool infers execute params from the interface
export const flightSearchTool = defineTool<FlightSearchParams>({
name: 'search_flights',
description: 'Search available flights between two airports',
// inputSchema auto-generated from FlightSearchParams
execute: async (params) => {
// params is fully typed as FlightSearchParams
// params.origin → string (autocomplete works)
// params.class → 'economy' | 'business' | 'first' | undefined
const flights = await searchFlightsAPI(params);
return { content: [{ type: 'text', text: JSON.stringify(flights) }] };
},
annotations: {
readOnlyHint: true, // Doesn't modify data
openWorldHint: true, // External API results
}
});'use client';
import { useTool } from '@webmcp/next';
import { flightSearchTool } from '@/lib/tools/flight-search';
export function FlightSearch() {
const { isRegistered } = useTool(flightSearchTool);
return <div>{/* Flight search UI */}</div>;
}{
name: "search_flights",
description: "Search available flights between two airports",
inputSchema: {
type: "object",
properties: {
origin: { type: "string", description: "Departure IATA code" },
destination: { type: "string", description: "Arrival IATA code" },
date: { type: "string", description: "YYYY-MM-DD format" },
passengers: { type: "number", description: "Default: 1" },
class: { type: "string",
enum: ["economy", "business", "first"] }
},
required: ["origin", "destination", "date"]
}
}Transparency builds trust. You can always see exactly what the browser receives.
WebMCP tools register in the browser. But the heavy lifting — database writes, payments, emails — happens on the server. Next.js Server Actions are the perfect bridge.
'use server';
import { revalidatePath } from 'next/cache';
export async function createBooking(data: {
date: string; time: string;
service: string; name: string;
email: string;
}) {
// Full server access
const booking = await db.bookings
.create({ data });
await sendEmail({
to: data.email,
subject: `Booking confirmed`,
});
revalidatePath('/appointments');
return {
id: booking.id,
confirmed: true,
message: `Booking confirmed`
};
}'use client';
import { useTool } from '@webmcp/next';
import { createBooking } from
'../actions/booking';
export function BookingTool({
availableServices,
availableTimes
}) {
useTool({
name: 'book_appointment',
description: 'Book an appointment',
inputSchema: {
type: 'object',
properties: {
service: {
type: 'string',
enum: availableServices
},
date: { type: 'string' },
time: { type: 'string',
enum: availableTimes },
name: { type: 'string' },
email: { type: 'string' }
},
required: ['service',
'date', 'time', 'name', 'email']
},
annotations: {
destructiveHint: true
},
execute: async (params) => {
const result = await
createBooking(params);
return { content: [{
type: 'text',
text: JSON.stringify(result)
}] };
}
});
return <div>{/* Form UI */}</div>;
}Each hook maps 1:1 to a WebMCP browser API. Click to expand.
Register a WebMCP tool tied to a component's lifecycle. Registers on mount, unregisters on unmount.
useTool({ name, description, inputSchema, execute, annotations? })Register a declarative form tool with React ergonomics. Maps to HTML toolname/tooldescription attributes.
useToolForm({ name, description, autoSubmit?, onAgentSubmit? })Provide contextual information to agents about the current page state. Update as UI state changes.
setContext('Found 23 flights from JFK to LAX')Listen to WebMCP lifecycle events for visual feedback when agents interact with your tools.
useAgentEvent('toolactivated', (e) => showLoading(e.tool.name))Detect WebMCP availability for conditional rendering. Your app works perfectly without it.
if (!isSupported) return null;You've registered your tools. But will Gemini actually invoke search_products when a user says “find me running shoes under $100”? You can't answer that without testing across real models.
No LLM calls, no API key — runs in under 1 second.
Test across Gemini, GPT-4, and Claude simultaneously.
name: WebMCP CI
on: [push, pull_request]
jobs:
agent-readiness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npm run build
- run: |
npx webmcp-cli ci \
--url http://localhost:3000 \
--min-score 75 \
--coverage-threshold 85 \
--models gemini
env:
WEBMCP_API_KEY: ${{ secrets.WEBMCP_API_KEY }}Three production-tested patterns for common Next.js architectures. Click to expand.
ONE search_products tool (not one per category). add_to_cart takes a product_id. checkout uses requestUserInteraction for financial actions.
'use client';
import { useTool } from '@webmcp/next';
export function CheckoutTool({ cartItems, total }) {
useTool({
name: 'complete_checkout',
description: 'Complete purchase of cart items',
annotations: { destructiveHint: true },
execute: async (params) => {
// Human confirmation for purchases
await navigator.modelContext
.requestUserInteraction();
const result = await fetch(
'/api/checkout', {
method: 'POST',
body: JSON.stringify({
...params, items: cartItems
})
});
return { content: [{ type: 'text',
text: `Order confirmed. Total: ${total}`
}] };
}
});
}Available tools change based on which page the user is viewing. Uses provideContext() to tell agents what's currently available.
'use client';
import { useAgentContext } from '@webmcp/next';
import { usePathname } from 'next/navigation';
export default function DashboardLayout({ children }) {
const { setContext } = useAgentContext();
const pathname = usePathname();
useEffect(() => {
const section = pathname.split('/')[2] || 'overview';
setContext(
`User is viewing ${section} section`
);
}, [pathname, setContext]);
return <div>{children}</div>;
}For blogs and docs — the declarative API is often all you need. Two agent-ready tools. Zero JavaScript. Pure HTML attributes.
export default function BlogPage() {
return (
<div>
<form
action="/api/blog/search"
toolname="search_articles"
tooldescription="Search blog articles"
>
<input
name="q" type="search"
toolparamdescription="Keywords"
/>
<button type="submit">Search</button>
</form>
<form
action="/api/newsletter"
method="POST"
toolname="subscribe_newsletter"
tooldescription="Subscribe to newsletter"
toolautosubmit
>
<input name="email" type="email" required
toolparamdescription="Email address" />
<button type="submit">Subscribe</button>
</form>
</div>
);
}WebMCP tools run in the browser with full page access. WebMCP ensures your tools are safe before they reach a single agent.
Tool descriptions and parameter names are checked for prompt injection patterns. The linter catches hidden instructions, Unicode exploits, and adversarial framing before they reach an agent.
Tools with 8+ parameters confuse agents and increase error rates. The schema linter flags over-parameterized tools and suggests breaking them into focused, composable tools.
Financial transactions, account deletions, data exports — anything destructive requires explicit human confirmation. WebMCP's linter flags tools missing this annotation.
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const RATE_LIMIT = 100; // per minute per IP
const counts = new Map<string, number[]>();
export function middleware(req: NextRequest) {
const ip = req.headers.get('x-forwarded-for') || 'unknown';
const now = Date.now();
const window = (counts.get(ip) || []).filter(
(t) => now - t < 60000
);
if (window.length >= RATE_LIMIT) {
return NextResponse.json(
{ error: 'Rate limited' },
{ status: 429 }
);
}
window.push(now);
counts.set(ip, window);
return NextResponse.next();
}'use client';
import { WebMCPProvider } from '@webmcp/next';
export function Providers({ children }) {
return (
<WebMCPProvider
fallback="graceful"
firewall={{
maxToolCallsPerMinute: 20,
blockedPatterns: [
'/admin/*',
'/api/internal/*'
],
onViolation: (event) => {
analytics.track('firewall_violation', event);
}
}}
>
{children}
</WebMCPProvider>
);
}Production-ready Next.js starters with WebMCP tools pre-configured. Clone, customize, and ship your agent-ready site in minutes.
All templates include full test suites, CI/CD configs, and documentation. View all templates on GitHub →
Every hook, every form attribute, every type — free and open source. Pay only for AI-powered testing.
SDK runs in your browser — zero API calls, zero cost. Paid features use LLM APIs at bulk pricing. Enterprise pricing →
The first sites that implement WebMCP correctly will own the AI agent channel. The SDK is ready. Will you be first?
Not ready? WebMCP Weekly — protocol updates, SDK releases, tutorials.