Build

Put the rules in the types

Put the rules in the types, not the docstring. The schema then tells Claude exactly what's allowed, and bad input is rejected before your code runs.

Rules hidden in prose

@mcp.tool()def search_issues(repo: str, state: str,                  labels: str, limit: int) -> str:    """Search issues.    repo: owner/name, e.g. octo/app    state: one of open, closed, all    labels: comma-separated label names    limit: max results, 1 to 50    Returns a JSON string of issues."""

The schema only says “string” and “int”. Claude has to read and obey the prose, and nothing checks it.

Rules in the types

from typing import Annotated, Literalfrom pydantic import BaseModel, Field class Issue(BaseModel):    number: int    title: str    url: str @mcp.tool()def search_issues(    repo: Annotated[str, Field(description="owner/name")],    state: Literal["open", "closed", "all"] = "open",    labels: list[str] = [],    limit: Annotated[int, Field(ge=1, le=50)] = 10,) -> list[Issue]:    """Search issues in a GitHub repository."""

Literal

Becomes an enum: only valid values.

Field(ge, le)

Becomes minimum and maximum, with descriptions.

Pydantic model

Validated input or output. A return type becomes an outputSchema.

TypedDict

Same schema, lighter: types only, no runtime validation.

21 / 28