Skip to main content
Schemas are the contract between your tools and the AI agents that call them. They specify what inputs are required, what types they are, and provide examples that help agents understand what values to send.

Input Schema (JSON Schema)

WebMCP uses JSON Schema for input validation:
navigator.modelContext.registerTool({
  name: "search_products",
  description: "Search for products by various criteria",
  inputSchema: {
    type: "object",
    properties: {
      query: {
        type: "string",
        description: "Search query text",
        minLength: 1,
        maxLength: 100
      },
      category: {
        type: "string",
        enum: ["electronics", "clothing", "books", "home"],
        description: "Product category to filter by"
      },
      minPrice: {
        type: "number",
        minimum: 0,
        description: "Minimum price in dollars"
      },
      maxPrice: {
        type: "number",
        minimum: 0,
        description: "Maximum price in dollars"
      },
      limit: {
        type: "number",
        minimum: 1,
        maximum: 100,
        default: 10,
        description: "Maximum number of results to return"
      }
    },
    required: ["query"]
  },
  async execute({ query, category, minPrice, maxPrice, limit = 10 }) {
    // Implementation
  }
});

Common JSON Schema Types

{
  type: "string",
  minLength: 1,
  maxLength: 100,
  pattern: "^[a-zA-Z0-9]+$",  // Regex validation
  format: "email",             // Built-in formats
  enum: ["option1", "option2"], // Allowed values
  description: "User-friendly description"
}
Built-in formats:
  • email - Email address
  • uri - URI/URL
  • date - ISO 8601 date
  • date-time - ISO 8601 date-time
  • uuid - UUID string
{
  type: "number",        // or "integer" for whole numbers
  minimum: 0,
  maximum: 100,
  exclusiveMinimum: 0,   // Greater than (not equal to)
  multipleOf: 0.01,      // Must be divisible by
  default: 10,
  description: "Quantity between 0 and 100"
}
{
  type: "boolean",
  default: false,
  description: "Include archived items"
}
{
  type: "array",
  items: {
    type: "string"       // Each item must be a string
  },
  minItems: 1,
  maxItems: 10,
  uniqueItems: true,
  description: "List of tags"
}
{
  type: "object",
  properties: {
    address: {
      type: "object",
      properties: {
        street: { type: "string" },
        city: { type: "string" },
        zip: { type: "string", pattern: "^\\d{5}$" }
      },
      required: ["street", "city"]
    }
  }
}

Zod Schema (React)

When using React and TypeScript, Zod provides type-safe schema validation:
import { useWebMCP } from '@mcp-b/react-webmcp';
import { z } from 'zod';

function ProductSearch() {
  useWebMCP({
    name: "search_products",
    description: "Search for products by various criteria",
    inputSchema: {
      query: z.string().min(1).max(100).describe("Search query text"),
      category: z.enum(["electronics", "clothing", "books", "home"])
        .optional()
        .describe("Product category to filter by"),
      minPrice: z.number().min(0).optional().describe("Minimum price in dollars"),
      maxPrice: z.number().min(0).optional().describe("Maximum price in dollars"),
      limit: z.number().int().min(1).max(100).default(10)
        .describe("Maximum number of results")
    },
    handler: async ({ query, category, minPrice, maxPrice, limit }) => {
      // TypeScript infers types from Zod schema!
      // query: string
      // category: "electronics" | "clothing" | "books" | "home" | undefined
      // limit: number (defaults to 10)

      // Implementation
    }
  });
}

Zod Schema Patterns

z.string()
  .min(1, "Required")
  .max(100, "Too long")
  .email("Invalid email")
  .url("Invalid URL")
  .uuid("Invalid UUID")
  .regex(/^[a-zA-Z0-9]+$/, "Alphanumeric only")
  .startsWith("prefix_")
  .endsWith(".com")
  .includes("keyword")
  .trim()  // Automatically trim whitespace
  .toLowerCase()  // Convert to lowercase
  .describe("User-friendly description")
z.number()
  .int("Must be integer")
  .positive("Must be positive")
  .min(0, "Minimum is 0")
  .max(100, "Maximum is 100")
  .multipleOf(0.01, "Max 2 decimal places")
  .finite("Must be finite")
  .safe("Must be safe integer")
  .default(10)
  .describe("Quantity between 0 and 100")
// Enum (one of several values)
z.enum(["small", "medium", "large"])

// Literal (exact value)
z.literal("exact_value")

// Union (one of multiple types)
z.union([z.string(), z.number()])

// Discriminated union
z.discriminatedUnion("type", [
  z.object({ type: z.literal("email"), address: z.string().email() }),
  z.object({ type: z.literal("phone"), number: z.string() })
])
// Array
z.array(z.string())
  .min(1, "At least one item required")
  .max(10, "Maximum 10 items")
  .nonempty("Required")

// Object
z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().int().positive().optional()
})

// Nested object
z.object({
  user: z.object({
    name: z.string(),
    address: z.object({
      street: z.string(),
      city: z.string()
    })
  })
})
// Optional (field may be undefined)
z.string().optional()

// Nullable (field may be null)
z.string().nullable()

// Both
z.string().optional().nullable()

// With default
z.string().optional().default("default value")

Schema Best Practices

Descriptions help AI agents understand how to use your tools:
// ✅ Good
{
  userId: {
    type: "string",
    pattern: "^[a-zA-Z0-9-]+$",
    description: "Unique identifier for the user (alphanumeric and hyphens only)"
  }
}

// ❌ Bad
{
  userId: {
    type: "string",
    pattern: "^[a-zA-Z0-9-]+$"
    // No description!
  }
}
Use schema validation instead of manual checks:
// ✅ Good - validation in schema
inputSchema: {
  quantity: {
    type: "number",
    minimum: 1,
    maximum: 1000
  }
}

// ❌ Bad - manual validation
inputSchema: {
  quantity: { type: "number" }
},
async execute({ quantity }) {
  if (quantity < 1 || quantity > 1000) {
    throw new Error("Invalid quantity");
  }
}
Apply relevant constraints to prevent invalid inputs:
{
  email: {
    type: "string",
    format: "email",        // Validate email format
    maxLength: 255          // Prevent extremely long inputs
  },
  age: {
    type: "integer",
    minimum: 0,
    maximum: 150            // Reasonable bounds
  },
  tags: {
    type: "array",
    items: { type: "string" },
    maxItems: 20,           // Prevent excessive arrays
    uniqueItems: true       // No duplicates
  }
}
Use default values for optional parameters:
{
  limit: {
    type: "number",
    minimum: 1,
    maximum: 100,
    default: 10            // Sensible default
  },
  sortOrder: {
    type: "string",
    enum: ["asc", "desc"],
    default: "asc"
  }
}
Don’t make schemas overly complex. Split into multiple tools if needed:
// ✅ Good - focused tools
registerTool({ name: "search_products", ... });
registerTool({ name: "filter_products", ... });

// ❌ Bad - one tool doing too much
registerTool({
  name: "manage_products",
  inputSchema: {
    action: { enum: ["search", "filter", "sort", "export", ...] },
    // Too many conditional fields
  }
});

Validation Error Handling

When validation fails, the error is caught before your handler executes:
// Your handler won't be called if validation fails
async execute({ userId, quantity }) {
  // If we get here, inputs are valid per schema
  // No need to revalidate
  return await processOrder(userId, quantity);
}
The AI agent receives a clear error message indicating what validation failed.