chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 12:10:27 +08:00
commit 49b9bb6724
992 changed files with 161690 additions and 0 deletions
@@ -0,0 +1,52 @@
# schema-validators
Four 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`, and a bare `dict[str, Any]`. The
client lists the tools, resolves each `who` schema, and round-trips a call.
## Run it
```bash
# 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_lowlevel
```
## What to look at
- `client.py` `main` — the body opens with `async with Client(target, mode=mode)
as client:`. `target` is anything `Client` accepts (an in-process server, a
transport, or an HTTP URL); the entry point picks it, the story constructs it.
- `server.py` — `who.name` vs `who["name"]`: pydantic and dataclass parameters
arrive as **instances** (attribute access); TypedDict and `dict[str, Any]`
arrive as plain dicts.
- `client.py` — the listed `inputSchema` for the three typed variants nests a
`$defs`/`$ref` object with a `name` property; `greet_dict` publishes 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 unpack
`params.arguments` yourself.
## Caveats
- Pydantic emits local `#/$defs/` references for nested models. The SDK does
not dereference network `$ref`s (SEP-2106 MUST NOT); only same-document refs
are resolved during validation.
- `PersonTD` is `total=True`, so its nested schema requires both `name` and
`title`; the `BaseModel` and `@dataclass` variants default `title="friend"`,
so only `name` is required there. Use `typing.NotRequired[...]` to mark
optional TypedDict fields.
## Spec
[Tools — input schema](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#input-schema)
## See also
`tools/` (output schema → `structuredContent`), `error_handling/` (what
happens when validation fails).
@@ -0,0 +1,38 @@
"""Asserts each variant publishes a `who` object schema and the call round-trips."""
from mcp_types import TextContent
from mcp.client import Client
from stories._harness import Target, run_client
async def main(target: Target, *, mode: str = "auto") -> None:
async with Client(target, mode=mode) as client:
listed = await client.list_tools()
by_name = {t.name: t for t in listed.tools}
assert set(by_name) == {"greet_pydantic", "greet_typeddict", "greet_dataclass", "greet_dict"}
for name in ("greet_pydantic", "greet_typeddict", "greet_dataclass"):
schema = by_name[name].input_schema
assert schema["required"] == ["who"], schema
# MCPServer emits a $defs/$ref pair; lowlevel inlines. Resolve either.
who = schema["properties"]["who"]
if "$ref" in who:
who = schema["$defs"][who["$ref"].rsplit("/", 1)[-1]]
assert "name" in who["properties"], who
result = await client.call_tool(name, {"who": {"name": "Ada", "title": "colleague"}})
assert not result.is_error, result
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "Hello Ada, my colleague"
# dict[str, Any] → free-form object schema, no nested `properties` required.
dict_who = by_name["greet_dict"].input_schema["properties"]["who"]
assert dict_who["type"] == "object" and "$ref" not in dict_who
result = await client.call_tool("greet_dict", {"who": {"name": "Ada"}})
assert isinstance(result.content[0], TextContent)
assert result.content[0].text == "Hello Ada, my friend"
if __name__ == "__main__":
run_client(main)
@@ -0,0 +1,59 @@
"""Four ways to type a tool parameter so MCPServer derives and enforces inputSchema."""
from dataclasses import dataclass
from typing import Any
from pydantic import BaseModel
# pydantic requires typing_extensions.TypedDict (not typing.TypedDict) on Python < 3.12
# when a TypedDict is used as a field/parameter type.
from typing_extensions import TypedDict
from mcp.server.mcpserver import MCPServer
from stories._hosting import run_server_from_args
class PersonModel(BaseModel):
name: str
title: str = "friend"
class PersonTD(TypedDict):
name: str
title: str
@dataclass
class PersonDC:
name: str
title: str = "friend"
def build_server() -> MCPServer:
mcp = MCPServer("schema-validators-example")
@mcp.tool()
def greet_pydantic(who: PersonModel) -> str:
"""`who` arrives as a validated PersonModel instance."""
return f"Hello {who.name}, my {who.title}"
@mcp.tool()
def greet_typeddict(who: PersonTD) -> str:
"""`who` arrives as a plain dict; TypedDict drives the schema and editor hints."""
return f"Hello {who['name']}, my {who['title']}"
@mcp.tool()
def greet_dataclass(who: PersonDC) -> str:
"""`who` arrives as a PersonDC instance (pydantic coerces the wire dict)."""
return f"Hello {who.name}, my {who.title}"
@mcp.tool()
def greet_dict(who: dict[str, Any]) -> str:
"""`who` is a free-form object — any dict passes; the handler must check it."""
return f"Hello {who['name']}, my {who.get('title', 'friend')}"
return mcp
if __name__ == "__main__":
run_server_from_args(build_server)
@@ -0,0 +1,55 @@
"""Same four tools via lowlevel.Server — inputSchema is hand-written JSON Schema."""
from typing import Any
import mcp_types as types
from mcp.server.context import ServerRequestContext
from mcp.server.lowlevel import Server
from stories._hosting import run_server_from_args
# With lowlevel.Server there is no reflection layer: you author the JSON Schema
# yourself and validate/unpack `params.arguments` in the handler.
PERSON_SCHEMA: dict[str, Any] = {
"type": "object",
"properties": {"name": {"type": "string"}, "title": {"type": "string"}},
"required": ["name"],
}
TOOLS = [
types.Tool(
name=f"greet_{variant}",
description=f"Greet ({variant} input shape)",
input_schema={"type": "object", "properties": {"who": PERSON_SCHEMA}, "required": ["who"]},
)
for variant in ("pydantic", "typeddict", "dataclass")
]
TOOLS.append(
types.Tool(
name="greet_dict",
description="Greet (free-form dict input)",
input_schema={
"type": "object",
"properties": {"who": {"type": "object", "additionalProperties": True}},
"required": ["who"],
},
)
)
def build_server() -> Server[Any]:
async def list_tools(
ctx: ServerRequestContext[Any], params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
return types.ListToolsResult(tools=TOOLS)
async def call_tool(ctx: ServerRequestContext[Any], params: types.CallToolRequestParams) -> types.CallToolResult:
assert params.arguments is not None
who = params.arguments["who"]
text = f"Hello {who['name']}, my {who.get('title', 'friend')}"
return types.CallToolResult(content=[types.TextContent(text=text)])
return Server("schema-validators-example", on_list_tools=list_tools, on_call_tool=call_tool)
if __name__ == "__main__":
run_server_from_args(build_server)