Skip to content

Add a runtime create_model variant to the schema_validators example#3148

Open
Aaron-Oh wants to merge 2 commits into
modelcontextprotocol:mainfrom
Aaron-Oh:example-schema-validators-create-model
Open

Add a runtime create_model variant to the schema_validators example#3148
Aaron-Oh wants to merge 2 commits into
modelcontextprotocol:mainfrom
Aaron-Oh:example-schema-validators-create-model

Conversation

@Aaron-Oh

Copy link
Copy Markdown

What

The schema_validators example story shows four ways to type a tool
parameter so MCPServer derives and enforces inputSchema: a pydantic
BaseModel, a TypedDict, a @dataclass, and a bare dict[str, Any].

This adds a fifth variant: a pydantic model built at runtime with
create_model from an external JSON Schema dict, then handed to
@mcp.tool() exactly like a hand-written BaseModel.

Why

Issues #323, #761, and #772 all asked the same thing: how to drive a
tool's inputSchema from a JSON Schema you already hold (from OpenAPI, a
config file, a DB row) rather than a class written out in source. The
maintainer answer is "use a pydantic model as the parameter" — but the
example suite never showed how to get that model when it isn't declared
statically. This closes that documentation gap with a runnable variant.

Notes

  • A create_model() result is opaque to static type checkers (its fields
    don't exist until runtime, and a runtime variable can't appear in a type
    annotation). A TYPE_CHECKING branch aliases it to a same-shape declared
    model so type checkers can see the fields; at runtime the dynamic class is
    what @mcp.tool() reflects over. This is called out in the README.
  • The published schema is identical to the greet_pydantic variant — the
    point is purely how the model is obtained, not a different wire shape.
  • server_lowlevel.py, client.py, and README.md are updated to cover
    the new variant.

Validation

  • uv run --frozen ruff format --check / ruff check — clean
  • uv run --frozen pyright — 0 errors
  • uv run --frozen pytest tests/examples -k schema — 14 passed
    (in-memory/http × modern/legacy × server/server_lowlevel, plus the
    manifest and story-shape checks)

The story showed four ways to type a tool parameter (BaseModel, TypedDict,
dataclass, dict). Add a fifth: a pydantic model built at runtime with
create_model from an external JSON Schema dict, then handed to @mcp.tool()
like any BaseModel. Covers the doc gap behind issues modelcontextprotocol#323, modelcontextprotocol#761, modelcontextprotocol#772.

A create_model() result is opaque to static type checkers, so a
TYPE_CHECKING branch aliases it to a same-shape declared model while the
runtime uses the dynamic class. server_lowlevel.py, client.py and README.md
are updated to include the new variant.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread examples/stories/schema_validators/server.py Outdated
Comment thread examples/stories/schema_validators/README.md
- server.py: JSON Schema 'required' is optional, so default it to an
  empty list before membership testing. External schemas with only
  optional properties no longer raise KeyError at import time.
- README.md: greet_dynamic publishes the same schema as greet_pydantic,
  so the client bullet now says four typed variants, not three.
@Aaron-Oh

Copy link
Copy Markdown
Author

Thanks for the review — addressed both in 230f718:

  • P2 (server.py): required is optional in JSON Schema, so it's now defaulted to an empty list before the membership test. A schema with only optional properties (the OpenAPI/config/DB-row case this pattern advertises) builds instead of raising KeyError at import time. Verified an all-optional schema now builds while the original still behaves identically.
  • P3 (README.md): greet_dynamic publishes the same schema as greet_pydantic, so the client.py bullet now reads "four typed variants" (calling out the runtime create_model one) rather than three.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant