Skip to content

fix(mcp): generate truthful JSON Schemas from Python annotations - #1176

Open
GautamSharma99 wants to merge 1 commit into
anthropics:mainfrom
GautamSharma99:fix/1166-semantic-mcp-schemas
Open

fix(mcp): generate truthful JSON Schemas from Python annotations#1176
GautamSharma99 wants to merge 1 commit into
anthropics:mainfrom
GautamSharma99:fix/1166-semantic-mcp-schemas

Conversation

@GautamSharma99

Copy link
Copy Markdown

Summary

Make SDK MCP tool schemas faithfully represent the Python annotations supplied by tool authors.

  • preserve None in nullable unions
  • convert Literal and Enum constraints into JSON Schema enums
  • describe mapping value types with additionalProperties
  • support fixed-length and variadic tuples
  • represent Any and object as unconstrained JSON values
  • reject annotations that cannot be represented instead of advertising them as strings

Problem

The previous converter silently changed several handler contracts while building the schema exposed through MCP:

  • str | None became a non-nullable string
  • Literal["fast", "safe"] became an unconstrained string
  • dict[str, int] became an object with unconstrained values
  • unions discarded None
  • every unsupported custom class was presented as a string

Claude and the MCP validation layer consume this generated schema before the Python handler runs. A lossy schema can therefore reject valid inputs, permit values outside the annotation contract, or teach the model to construct the wrong arguments.

Implementation

Nullable unions

Union members are converted without removing NoneType; None now produces { "type": "null" }. Requiredness remains controlled independently by TypedDict Required and NotRequired metadata, so a required nullable key stays required while accepting null.

Literals and enums

Literal arguments and Python Enum member values become JSON Schema enum values. Homogeneous JSON scalar values also receive the matching type; mixed scalar enums remain valid heterogeneous enums without an incorrect single type.

Containers

  • dict[str, T] and other string-keyed Mapping annotations produce additionalProperties using the schema for T
  • non-string mapping keys fail because JSON object keys cannot represent that contract
  • tuple[T, ...] produces an array item schema
  • fixed tuples produce prefixItems plus exact minItems and maxItems
  • existing list behavior remains unchanged

Unsupported annotations

Custom annotations no longer silently fall back to { "type": "string" }. Server construction raises a descriptive TypeError directing the caller to provide an explicit JSON Schema dictionary. Any and object are intentionally supported as unconstrained schemas.

Compatibility

Schemas for existing primitives, bare lists and dictionaries, Annotated descriptions, and TypedDict required keys are unchanged. The unsupported-type fallback changes intentionally: creating a server with an annotation that cannot be represented now fails early instead of publishing a false schema.

Tests

Regression coverage includes:

  • PEP 604 nullable unions and multi-type unions containing None
  • required nullable and NotRequired nullable TypedDict keys
  • string Literals and Python Enums
  • nested typed mappings and rejection of non-string keys
  • fixed and variadic tuples
  • unconstrained Any
  • unsupported custom types through both the converter and public server-construction path
  • end-to-end tools/list preservation through MCP model validation

Validation

  • uv run --extra dev pytest -q — 1301 passed, 5 skipped
  • uv run --extra dev ruff check src tests — passed
  • uv run --extra dev ruff format --check src tests — passed
  • uv run --extra dev mypy src — passed

Fixes #1166

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.

MCP tool schema generation loses Optional, Literal, mapping, and unsupported-type semantics

1 participant