Five ways to type a tool parameter so MCPServer derives the JSON-Schema
inputSchema and validates arguments before your handler runs: a pydantic
BaseModel, a TypedDict, a @dataclass, a bare dict[str, Any], and a
pydantic model built at runtime with create_model. The client lists the
tools, resolves each who schema, and round-trips a call.
# stdio (default — the client spawns the server as a subprocess)
uv run python -m stories.schema_validators.client
# HTTP — the client self-hosts the server on a free port, runs, then tears it down
uv run python -m stories.schema_validators.client --http
# same, against the lowlevel-API server variant
uv run python -m stories.schema_validators.client --http --server server_lowlevelclient.pymain— the body opens withasync with Client(target, mode=mode) as client:.targetis anythingClientaccepts (an in-process server, a transport, or an HTTP URL); the entry point picks it, the story constructs it.server.py—who.namevswho["name"]: pydantic and dataclass parameters arrive as instances (attribute access); TypedDict anddict[str, Any]arrive as plain dicts.server.pygreet_dynamic— the parameter type is not written in the source at all:create_modelbuilds it at runtime from a JSON Schema dict (the shape you'd get from OpenAPI, a config file, or a DB row), then hands it to@mcp.tool()like anyBaseModel; the published schema is identical to thegreet_pydanticvariant. Acreate_modelresult is opaque to static type checkers, so aTYPE_CHECKINGbranch aliases it to a declared model of the same shape — the runtime still uses the dynamic class.client.py— the listedinputSchemafor the four typed variants (including the runtimecreate_modelone, whose schema matchesgreet_pydantic) nests a$defs/$refobject with anameproperty;greet_dictpublishes only{"type": "object", "additionalProperties": true}— no field validation.server_lowlevel.py— the same schemas written by hand. There is no reflection layer at this tier; you author JSON Schema and unpackparams.argumentsyourself.
- Pydantic emits local
#/$defs/references for nested models. The SDK does not dereference network$refs (SEP-2106 MUST NOT); only same-document refs are resolved during validation. PersonTDistotal=True, so its nested schema requires bothnameandtitle; theBaseModeland@dataclassvariants defaulttitle="friend", so onlynameis required there. Usetyping.NotRequired[...]to mark optional TypedDict fields.
tools/ (output schema → structuredContent), error_handling/ (what
happens when validation fails).