Skip to content

Add a runtime create_model variant to the schema_validators example#3148

Open
Aaron-Oh wants to merge 1 commit 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 1 commit 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.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

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