Level: Intermediate
Prerequisites: Basic JavaScript, familiarity with JSON Schema draft-07
Chrome Version Required: 146+ (Canary / Beta with flag enabled)
Introduction
When you implement WebMCP, the single most important thing you write isn’t your execute() function — it’s your inputSchema. A poorly designed schema forces the AI agent to guess, retry, and occasionally fail entirely. A well-designed schema means the agent calls your tool correctly every single time.
This guide covers everything you need to know about designing WebMCP input schemas: typed inputs, enums, required fields, descriptions, and the subtle patterns that separate good schemas from great ones.
1. The Anatomy of a WebMCP Tool Registration
Before diving into schema design, here’s the full structure of a tool registered via the Imperative API:
javascript
if ("modelContext" in navigator) {
navigator.modelContext.registerTool({
name: "searchProducts", // camelCase string (required)
description: "...", // agent-readable description (required)
inputSchema: { ... }, // JSON Schema object (required)
readOnly: true, // optional: true = no confirmation needed
async execute(params) { // required: async function
return {
content: [{ type: "text", text: "result" }]
};
}
});
}
The inputSchema follows JSON Schema draft-07 format. Every field you define here is a direct instruction to the AI agent about what data to provide and in what shape.
2. Always Start With Feature Detection
Never call navigator.modelContext without checking if it exists. Browsers that don’t support WebMCP won’t have it, and your page will throw a runtime error.
javascript
// ✅ Correct
if ("modelContext" in navigator) {
navigator.modelContext.registerTool({ ... });
}
// ❌ Wrong — will throw in non-WebMCP browsers
navigator.modelContext.registerTool({ ... });
3. Typed Inputs: Be Explicit, Always
The cardinal rule of WebMCP schema design: declare a specific type for every parameter. Using vague or untyped fields forces the model to infer type from context — which adds cognitive load and increases the chance of a wrong call.
Supported Primitive Types
| Type | Use for |
|---|---|
string | Text, names, IDs, dates as strings |
number | Integers and floats |
boolean | Flags, toggles |
array | Lists of values |
object | Nested structured data |
Example: Weak vs. Strong Schema
❌ Weak — no types, no constraints:
json
{
"type": "object",
"properties": {
"date": {},
"quantity": {},
"express": {}
}
}
✅ Strong — typed, constrained, described:
json
{
"type": "object",
"properties": {
"date": {
"type": "string",
"description": "Delivery date in YYYY-MM-DD format, e.g. 2026-06-15"
},
"quantity": {
"type": "number",
"minimum": 1,
"maximum": 100,
"description": "Number of units to order"
},
"express": {
"type": "boolean",
"description": "Set to true to enable express shipping (2-day delivery)"
}
},
"required": ["date", "quantity"]
}
The agent now knows exactly what type to pass, what range is valid, and what the field means.
4. Enums: Constrain Choices to Valid Options
Whenever a field has a fixed set of valid values, use an enum. This prevents the agent from inventing an invalid value (like passing "FAST" when you only accept "express" or "standard").
javascript
navigator.modelContext.registerTool({
name: "getServicePricing",
description: "Returns pricing for a specific service tier and region.",
readOnly: true,
inputSchema: {
type: "object",
properties: {
serviceType: {
type: "string",
enum: ["basic", "pro", "enterprise"],
description: "The subscription tier to fetch pricing for"
},
region: {
type: "string",
enum: ["us-east", "us-west", "eu", "apac"],
description: "Geographic region for localized pricing"
},
currency: {
type: "string",
enum: ["USD", "EUR", "GBP", "INR"],
description: "Currency to return prices in. Defaults to USD."
}
},
required: ["serviceType", "region"]
},
async execute({ serviceType, region, currency = "USD" }) {
const res = await fetch(`/api/pricing?tier=${serviceType}®ion=${region}¤cy=${currency}`);
const data = await res.json();
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
});
Pro tip: Include the valid options in your
descriptiontoo — some agents read both theenumarray and the description when deciding what to pass.
5. The required Array: Separate Mandatory from Optional
Always explicitly declare which fields are mandatory using the required array. Fields omitted from required should have sensible defaults in your execute() function.
javascript
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query string"
},
category: {
type: "string",
enum: ["electronics", "clothing", "books", "home"],
description: "Product category to filter results (optional)"
},
maxResults: {
type: "number",
minimum: 1,
maximum: 50,
description: "Maximum number of results to return. Defaults to 10."
},
sortBy: {
type: "string",
enum: ["relevance", "price_asc", "price_desc", "newest"],
description: "Sort order. Defaults to relevance."
}
},
required: ["query"] // only query is mandatory
}
And in execute():
javascript
async execute({ query, category, maxResults = 10, sortBy = "relevance" }) {
// safe to use all params with defaults applied
}
6. Descriptions: The Most Underrated Schema Field
Every property should have a description. This is the text the agent reads to understand why a field exists and how to fill it correctly. Think of it as documentation written directly for the AI.
The Chrome Team’s Rule
From the official WebMCP best practices:
“Explain why you’ve made certain choices. What choice you’ve made should be self-explanatory. The why helps agents make better choices.”
For example, if you use a natural language shipping label instead of an internal ID:
json
"shippingMethod": {
"type": "string",
"enum": ["Standard", "Express", "Overnight"],
"description": "Shipping speed. Use 'Express' for 2-day delivery, 'Overnight' for next-day. Human-readable labels are used instead of internal IDs for agent clarity."
}
This is far better than:
json
"shipping_id": {
"type": "number",
"description": "Shipping method ID"
}
The agent doesn’t know your internal ID system. Don’t make it guess.
7. Accept Raw User Input — Don’t Ask Agents to Do Math
A critical best practice from the Chrome team: accept raw user input strings and transform them yourself in execute(). Don’t ask the model to perform calculations or transformations as part of schema input.
❌ Bad: asking the model to calculate
json
"durationMinutes": {
"type": "number",
"description": "Calculate the duration in minutes between the start and end time"
}
✅ Good: accept raw strings, compute internally
json
"startTime": {
"type": "string",
"description": "Start time as entered by the user, e.g. '11:00' or '11am'"
},
"endTime": {
"type": "string",
"description": "End time as entered by the user, e.g. '15:00' or '3pm'"
}
Then parse and compute in your execute() function. Keep the schema simple; let code do the work.
8. Nested Objects and Arrays
For structured inputs, use nested object or array types. Always define items for arrays.
javascript
inputSchema: {
type: "object",
properties: {
passenger: {
type: "object",
description: "Passenger details for the booking",
properties: {
firstName: { type: "string", description: "First name" },
lastName: { type: "string", description: "Last name" },
email: { type: "string", description: "Contact email address" }
},
required: ["firstName", "lastName", "email"]
},
selectedSeats: {
type: "array",
description: "List of seat numbers to reserve, e.g. ['12A', '12B']",
items: {
type: "string"
},
minItems: 1,
maxItems: 6
}
},
required: ["passenger"]
}
9. Full Real-World Example: Flight Search Tool
Here’s a complete, production-quality example combining everything above:
if ("modelContext" in navigator) { navigator.modelContext.registerTool({ name: "searchFlights", description: "Search for available flights between two airports on a given date. Returns a list of options with prices and duration.", readOnly: true, inputSchema: { type: "object", properties: { origin: { type: "string", description: "Departure airport IATA code, e.g. 'DEL' for Delhi, 'BOM' for Mumbai" }, destination: { type: "string", description: "Arrival airport IATA code, e.g. 'SIN' for Singapore" }, departureDate: { type: "string", description: "Departure date in YYYY-MM-DD format, e.g. '2026-07-15'" }, cabinClass: { type: "string", enum: ["Economy", "PremiumEconomy", "Business", "First"], description: "Cabin class preference. Defaults to Economy." }, passengers: { type: "number", minimum: 1, maximum: 9, description: "Number of adult passengers. Defaults to 1." }, directOnly: { type: "boolean", description: "Set to true to return only non-stop flights. Defaults to false." } }, required: ["origin", "destination", "departureDate"] }, async execute({ origin, destination, departureDate, cabinClass = "Economy", passengers = 1, directOnly = false }) { try { const params = new URLSearchParams({ origin, destination, departureDate, cabin: cabinClass, pax: passengers, direct: directOnly }); const res = await fetch(`/api/flights/search?${params}`); if (!res.ok) throw new Error(`API error: ${res.status}`); const flights = await res.json(); return { content: [{ type: "text", text: JSON.stringify(flights, null, 2) }] }; } catch (err) { return { content: [{ type: "text", text: `Error searching flights: ${err.message}. Please try again or search manually.` }] }; } } }); }
10. Schema Design Checklist
Before shipping any WebMCP tool, run through this checklist:
| ✅ | Check |
|---|---|
| ☐ | Every property has an explicit type |
| ☐ | Fixed-value fields use enum |
| ☐ | Every property has a meaningful description |
| ☐ | required array lists only truly mandatory fields |
| ☐ | Optional fields have defaults in execute() |
| ☐ | Number fields use minimum/maximum where applicable |
| ☐ | Arrays define items type |
| ☐ | No math or transforms asked of the agent |
| ☐ | Human-readable values used instead of internal IDs |
| ☐ | execute() has a try/catch with a meaningful error message |
| ☐ | Feature-detected with "modelContext" in navigator |
Summary
A great WebMCP input schema is:
- Typed — every field has an explicit
type - Constrained — enums, min/max, required arrays narrow the solution space
- Descriptive —
descriptiontells the agent not just what but why - Forgiving — optional fields default gracefully in
execute() - Raw-input friendly — no math or complex transforms expected from the agent
Get the schema right and your agent-ready tools will be called accurately, reliably, and without retries — which is exactly what great agentic UX looks like.
Leave a Reply