Implementing WebMCP: A Step-by-Step Developer Guide
Prerequisites
Before implementing WebMCP, ensure your environment meets these requirements:
- Your site is served over HTTPS (WebMCP is only available in secure contexts).
- Chrome 146+ Canary with the "WebMCP for testing" flag enabled, or the
@mcp-b/webmcp-polyfillpackage for broader browser support. - Familiarity with JSON Schema for defining tool input parameters.
Step 1: Feature Detection
Always check for WebMCP availability before attempting to register tools:
if ('modelContext' in navigator) {
// WebMCP is available
initializeTools();
} else {
console.log('WebMCP not supported in this browser');
}
Step 2: Your First Tool
Let us start with a simple product search tool for an e-commerce site:
async function initializeTools() {
await navigator.modelContext.registerTool({
name: "search_products",
description: "Search the product catalog by keyword, category, and price range",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search keywords"
},
category: {
type: "string",
enum: ["electronics", "clothing", "books", "home"],
description: "Product category filter"
},
maxPrice: {
type: "number",
description: "Maximum price in USD"
}
},
required: ["query"]
},
execute: async (params) => {
const response = await fetch(`/api/products?q=${encodeURIComponent(params.query)}&cat=${params.category || ''}&max=${params.maxPrice || ''}`);
const data = await response.json();
return {
count: data.products.length,
products: data.products.map(p => ({
name: p.name,
price: p.price,
rating: p.rating
}))
};
}
});
}
Step 3: Declarative Forms
For existing HTML forms, WebMCP offers a zero-JavaScript approach:
<form toolname="contact_support"
tooldescription="Submit a customer support request"
method="POST"
action="/api/support">
<label>
Subject
<input name="subject" type="text" required />
</label>
<label>
Priority
<select name="priority">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</label>
<label>
Description
<textarea name="description" required></textarea>
</label>
<button type="submit">Submit</button>
</form>
The browser automatically infers the tool schema from form fields. The SubmitEvent.agentInvoked flag lets your server-side code distinguish between human and agent submissions.
Step 4: Multi-Step Workflows
Complex workflows like booking a flight involve multiple sequential tools:
// Step 1: Search
await navigator.modelContext.registerTool({
name: "search_flights",
description: "Search available flights",
inputSchema: { /* ... */ },
execute: async (params) => {
const flights = await searchFlights(params);
return { flights, sessionId: crypto.randomUUID() };
}
});
// Step 2: Select and book
await navigator.modelContext.registerTool({
name: "book_flight",
description: "Book a selected flight. Requires a flight ID from search results.",
inputSchema: {
type: "object",
properties: {
flightId: { type: "string" },
passengers: {
type: "array",
items: {
type: "object",
properties: {
firstName: { type: "string" },
lastName: { type: "string" }
},
required: ["firstName", "lastName"]
}
}
},
required: ["flightId", "passengers"]
},
execute: async (params, client) => {
// Request user confirmation for sensitive action
const confirmed = await client.requestUserInput(
`Confirm booking flight ${params.flightId} for ${params.passengers.length} passenger(s)?`
);
if (!confirmed) return { status: "cancelled" };
const booking = await createBooking(params);
return { confirmation: booking.confirmationNumber };
}
});
Step 5: Error Handling
Robust error handling is essential for production WebMCP tools:
await navigator.modelContext.registerTool({
name: "update_cart",
description: "Add or remove items from the shopping cart",
inputSchema: { /* ... */ },
execute: async (params) => {
try {
const result = await updateCart(params);
return { success: true, cart: result };
} catch (error) {
if (error instanceof AuthError) {
return {
success: false,
error: "authentication_required",
message: "User must log in to modify cart"
};
}
return {
success: false,
error: "internal_error",
message: "Failed to update cart. Please try again."
};
}
}
});
Step 6: Cleanup and Lifecycle
Always clean up tools when they are no longer relevant:
// Remove a specific tool
navigator.modelContext.unregisterTool("search_flights");
// Clear all tools (e.g., on page transition in an SPA)
navigator.modelContext.clearContext();
// Re-register tools after SPA navigation
window.addEventListener('popstate', () => {
navigator.modelContext.clearContext();
registerToolsForCurrentPage();
});
Using the Polyfill
For production use before native browser support, use the MCP-B polyfill:
npm install @mcp-b/webmcp-polyfill
import '@mcp-b/webmcp-polyfill';
// The polyfill auto-detects native support.
// If the browser already has navigator.modelContext,
// the polyfill does nothing.
navigator.modelContext.registerTool({ /* ... */ });
Production Checklist
- Always validate inputs server-side, even though the schema validates client-side.
- Use
credentials: 'same-origin'on all fetch calls within tool handlers. - Only register tools for authenticated users where appropriate.
- Return structured error objects, not thrown exceptions.
- Log
SubmitEvent.agentInvokedfor analytics and audit trails. - Test with the Chrome Model Context Tool Inspector extension.