MCPMCP Python SDK 2.2 · Python 3.10+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
24 small wins to finish your pathNext lesson

Tool arguments: descriptions, limits and choices

A model writes a tool's arguments from its schema alone. Descriptions, limits and fixed choices make right arguments easier and wrong ones impossible.

Exampleshop.py, new
ARTICLES = {
    "Where is my order?": "orders",
    "Changing an order": "orders",
    "How refunds work": "refunds",
    "Refunds for damaged items": "refunds",
    "Resetting your password": "account",
}
Exampleshop.py, new tool
@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."

The imports gain Annotated, Literal and Pydantic's Field. Annotated[str, Field(description=...)] is still a str, with a description attached for the model. Field(ge=1, le=5) limits limit to 1 to 5. Literal[...] allows only those three topics, and | None = None makes the topic optional.

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())

Every piece landed in the schema: the description, an enum of topics, minimum and maximum, and a default. Only query is required.

A call that breaks the rules

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 exactly what was wrong. A model reads that text as the tool's answer and can try again with a valid value, so a limit you write once also teaches the model.

Try it yourself
  • Call search_help with "topic": "billing".
  • Remove the Field from query and compare its schema entry.
  • Call it with "limit": "two".

This is what real progress feels like.