Home
Contact Us

WebMCP Input Schema Design: Writing JSON Schemas AI Agents Actually Understand

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

TypeUse for
stringText, names, IDs, dates as strings
numberIntegers and floats
booleanFlags, toggles
arrayLists of values
objectNested 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}&region=${region}&currency=${currency}`);
    const data = await res.json();
    return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
  }
});

Pro tip: Include the valid options in your description too — some agents read both the enum array 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:

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.


Further Reading

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *