Add a runtime create_model variant to the schema_validators example#3148
Add a runtime create_model variant to the schema_validators example#3148Aaron-Oh wants to merge 1 commit into
Conversation
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.
There was a problem hiding this comment.
2 issues found across 4 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="examples/stories/schema_validators/README.md">
<violation number="1" location="examples/stories/schema_validators/README.md:6">
P3: The README now understates the typed variants with nested `who` schemas: `greet_dynamic` publishes the same schema as `greet_pydantic`, so readers should expect four rather than three. Update the nearby `client.py` bullet to include the runtime model.</violation>
</file>
<file name="examples/stories/schema_validators/server.py">
<violation number="1" location="examples/stories/schema_validators/server.py:48">
P2: External schemas that omit `required` cannot use this advertised pattern: evaluating the module raises `KeyError` before the server starts. Since `required` is optional in JSON Schema, consider defaulting it to an empty collection before testing membership so schemas with only optional properties build correctly.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| PersonDynamic = PersonModel | ||
| else: | ||
| _dynamic_fields: dict[str, Any] = { | ||
| field_name: (str, ... if field_name in PERSON_JSON_SCHEMA["required"] else field_schema.get("default")) |
There was a problem hiding this comment.
P2: External schemas that omit required cannot use this advertised pattern: evaluating the module raises KeyError before the server starts. Since required is optional in JSON Schema, consider defaulting it to an empty collection before testing membership so schemas with only optional properties build correctly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/stories/schema_validators/server.py, line 48:
<comment>External schemas that omit `required` cannot use this advertised pattern: evaluating the module raises `KeyError` before the server starts. Since `required` is optional in JSON Schema, consider defaulting it to an empty collection before testing membership so schemas with only optional properties build correctly.</comment>
<file context>
@@ -29,6 +29,28 @@ class PersonDC:
+ PersonDynamic = PersonModel
+else:
+ _dynamic_fields: dict[str, Any] = {
+ field_name: (str, ... if field_name in PERSON_JSON_SCHEMA["required"] else field_schema.get("default"))
+ for field_name, field_schema in PERSON_JSON_SCHEMA["properties"].items()
+ }
</file context>
| `BaseModel`, a `TypedDict`, a `@dataclass`, and a bare `dict[str, Any]`. The | ||
| client lists the tools, resolves each `who` schema, and round-trips a call. | ||
| `BaseModel`, a `TypedDict`, a `@dataclass`, a bare `dict[str, Any]`, and a | ||
| pydantic model built at runtime with `create_model`. The client lists the |
There was a problem hiding this comment.
P3: The README now understates the typed variants with nested who schemas: greet_dynamic publishes the same schema as greet_pydantic, so readers should expect four rather than three. Update the nearby client.py bullet to include the runtime model.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/stories/schema_validators/README.md, line 6:
<comment>The README now understates the typed variants with nested `who` schemas: `greet_dynamic` publishes the same schema as `greet_pydantic`, so readers should expect four rather than three. Update the nearby `client.py` bullet to include the runtime model.</comment>
<file context>
@@ -1,9 +1,10 @@
-`BaseModel`, a `TypedDict`, a `@dataclass`, and a bare `dict[str, Any]`. The
-client lists the tools, resolves each `who` schema, and round-trips a call.
+`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.
</file context>
What
The
schema_validatorsexample story shows four ways to type a toolparameter so
MCPServerderives and enforcesinputSchema: a pydanticBaseModel, aTypedDict, a@dataclass, and a baredict[str, Any].This adds a fifth variant: a pydantic model built at runtime with
create_modelfrom an external JSON Schema dict, then handed to@mcp.tool()exactly like a hand-writtenBaseModel.Why
Issues #323, #761, and #772 all asked the same thing: how to drive a
tool's
inputSchemafrom a JSON Schema you already hold (from OpenAPI, aconfig 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
create_model()result is opaque to static type checkers (its fieldsdon't exist until runtime, and a runtime variable can't appear in a type
annotation). A
TYPE_CHECKINGbranch aliases it to a same-shape declaredmodel 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.greet_pydanticvariant — thepoint is purely how the model is obtained, not a different wire shape.
server_lowlevel.py,client.py, andREADME.mdare updated to coverthe new variant.
Validation
uv run --frozen ruff format --check/ruff check— cleanuv run --frozen pyright— 0 errorsuv run --frozen pytest tests/examples -k schema— 14 passed(in-memory/http × modern/legacy × server/server_lowlevel, plus the
manifest and story-shape checks)