Tool arguments and input schemas

Model Context Protocol (MCP) · Essentials · 12 min

The schema is the contract the model follows, and good constraints make agents correct their own mistakes.

Every tool advertises an input schema: a JSON Schema document (a standard JSON format for describing what fields an object has and their types). The model reads it to know which arguments to send. In the Python SDK you never write it by hand; it is generated from your type hints.

  • query: str, limit: int makes two required fields, a string and an integer.
  • A default value (limit: int = 10) makes the field optional.
  • Literal["open", "closed"] limits the value to a fixed list.
  • Annotated[int, Field(ge=1, le=50, description="...")] adds a per-argument description and bounds. Field comes from Pydantic, the validation library the SDK is built on.
  • A Pydantic model as a parameter becomes a nested object, and your function receives a real, validated instance.

These are not just documentation. If the model sends limit=999, the SDK rejects the call before your function runs and returns the validation message as an error result. The model reads it and usually retries with a valid value.

You can also set a display title and annotations, hints for the host such as read_only_hint=True or destructive_hint=True. Hosts may use them to decide whether to ask the user before running a tool. They are hints from the server, not guarantees.

Write descriptions for the model, not for humans skimming code: say what the tool does, when to use it, and what each argument means.

Example

from typing import Annotated, Literal
from pydantic import Field
from mcp.server import MCPServer
from mcp.types import ToolAnnotations

mcp = MCPServer("Tickets")


@mcp.tool(title="Search tickets", annotations=ToolAnnotations(read_only_hint=True))
def search_tickets(
    query: Annotated[str, Field(description="Words to match in title or body")],
    status: Literal["open", "closed", "any"] = "open",
    limit: Annotated[int, Field(ge=1, le=50)] = 10,
) -> list[str]:
    """Search support tickets. Use before creating a ticket to avoid duplicates."""
    return [f"#101 {query} ({status})"][:limit]

# Generated input schema (shortened):
# {"type": "object",
#  "properties": {"query": {"type": "string", "description": ...},
#                 "status": {"enum": ["open", "closed", "any"], "default": "open"},
#                 "limit": {"type": "integer", "minimum": 1, "maximum": 50, "default": 10}},
#  "required": ["query"]}

Remember

  • Type hints are the schema; defaults make arguments optional
  • Use Literal for choices and Field for descriptions and bounds
  • Invalid arguments are rejected before your code runs and returned to the model
  • Annotations like read_only_hint are hints, not security controls

Check yourself

  1. The model calls search_tickets with limit=200. What happens?
  2. How do you make a tool argument optional?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: py.sdk.modelcontextprotocol.io/servers/tools/