Model Context ProtocolMCP Python SDK 2.2 · LangChain 1.4 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
27 small wins to finish your pathNext lesson →

Tool arguments

Tool arguments are the typed parameters a model fills from a schema, and descriptions, limits and fixed choices make right arguments easier and wrong ones impossible.

Last updated: 29 Sep, 2026 · MCP 2.2

A tool so far took a plain order_id: str. A model writes arguments from the schema alone, so this lesson adds a search tool whose parameters carry descriptions, a range and a fixed set of choices.

Annotated, Field and Literal

python
from typing import Annotated, Literal
from pydantic import Field

# Annotated[type, Field(...)] attaches limits and a description to a type
# Literal[...] allows only the listed values
# = value makes an argument optional with a default

The help articles

Add the imports from the box above to the top of shop.py: from typing import Annotated, Literal and from pydantic import Field. The search tool looks in a small table of articles, each tagged with a topic.

python
ARTICLES = {
    "Where is my order?": "orders",
    "Changing an order": "orders",
    "How refunds work": "refunds",
    "Refunds for damaged items": "refunds",
    "Resetting your password": "account",
}

The search_help tool

Annotated[str, Field(description=...)] is still a str, with a description for the model. Field(ge=1, le=5) holds limit to 1 through 5. Literal[...] allows only those three topics, and | None = None makes the topic optional.

python
@mcp.tool()
def search_help(
    query: Annotated[str, Field(description="Words to look for in the help articles.")],
    topic: Literal["orders", "refunds", "account"] | None = None,
    limit: Annotated[int, Field(ge=1, le=5)] = 3,
) -> str:
    """Search the help centre and return matching article titles."""
    found = [title for title, t in ARTICLES.items() if query.lower() in title.lower() and topic in (None, t)]
    return "; ".join(found[:limit]) or "No articles found."
Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports this file. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep it in the same folder.
View the code here
shop.py
from typing import Annotated, Literal

from pydantic import Field

from mcp.server import MCPServer

mcp = MCPServer("Shop support")

ORDERS = {
    "A17": {"item": "blue mug", "status": "shipped", "total": 12.50},
    "B42": {"item": "desk lamp", "status": "processing", "total": 48.00},
}

ARTICLES = {
    "Where is my order?": "orders",
    "Changing an order": "orders",
    "How refunds work": "refunds",
    "Refunds for damaged items": "refunds",
    "Resetting your password": "account",
}


@mcp.tool()
def lookup_order(order_id: str) -> str:
    """Look up an order by its id and say where it is."""
    order = ORDERS[order_id]
    return f"Order {order_id}: {order['item']}, {order['status']}."


@mcp.tool()
def search_help(
    query: Annotated[str, Field(description="Words to look for in the help articles.")],
    topic: Literal["orders", "refunds", "account"] | None = None,
    limit: Annotated[int, Field(ge=1, le=5)] = 3,
) -> str:
    """Search the help centre and return matching article titles."""
    found = [title for title, t in ARTICLES.items() if query.lower() in title.lower() and topic in (None, t)]
    return "; ".join(found[:limit]) or "No articles found."

Reading the generated schema

Every limit you wrote lands in the schema the model receives.

Example
import asyncio

from mcp import Client
from shop import mcp


async def main():
    async with Client(mcp) as client:
        tools = await client.list_tools()
        schema = tools.tools[1].input_schema
        print(schema["properties"]["query"])
        print(schema["properties"]["topic"])
        print(schema["properties"]["limit"])
        print(schema["required"])


asyncio.run(main())

What landed in the schema

  • The description is attached to query for the model to read.
  • The topic became an enum of three values or null, with a default of null.
  • The limit carries minimum and maximum and a default of 3.
  • Only query is required; the two with defaults are optional.

A call that breaks the rules

A model calls the tool with limit=50, above the maximum you set.

Example
import asyncio

from mcp import Client
from shop import mcp


async def main():
    async with Client(mcp) as client:
        good = await client.call_tool("search_help", {"query": "refund", "topic": "refunds"})
        print(good.content[0].text)

        bad = await client.call_tool("search_help", {"query": "refund", "limit": 50})
        print(bad.is_error)
        print(bad.content[0].text)


asyncio.run(main())

limit=50 never reached your function. The SDK checked the arguments against the schema, and the result came back as an error whose text says what was wrong. A model reads that text as the tool's answer and can retry with a valid value, so a limit you write once also teaches the model.

A plain hint vs a schema limit

A hint in the docstringA Field limit
Where it livesProse the model may ignoreThe schema, enforced by the SDK
A bad valueReaches your functionRejected before your function runs
The model learnsOnly if it reads carefullyFrom the error text, every time

When to constrain an argument

  • Use Literal when only a fixed set of values makes sense, like a topic or a status.
  • Use Field(ge=, le=) for a number with a sensible range, so a huge value cannot slip through.
  • Use Field(description=) whenever the name alone does not tell the model what to send.
Watch out. A limit in the schema is checked by the SDK, not by a promise in the docstring. If you write the range in prose only, a bad value reaches your function and you have to check it yourself.
Try it yourself
  • Call search_help with "topic": "billing".
  • Remove the Field from query and compare its schema entry.
  • Call it with "limit": "two".

Little by little, you're building something great.