fix(mcpserver): keep Annotated field constraints on ordinary-class structured output (#3712)

Closed
PR
3d ago
+38-1

Fixes #3712

Motivation and Context

_create_output_model (in func_metadata.py) resolves an ordinary class's
field hints with get_type_hints(type_annotation), which strips Annotated
metadata. As a result a field declared Annotated[int, Field(gt=0)] collapses
to a bare int when the return annotation is turned into a structured-output
model:

  • the generated output schema loses exclusiveMinimum: 0 (only type: integer remains), and
  • convert_result(...) accepts an out-of-bound value such as 0 and returns structured_content={"value": 0} instead of raising ValidationError.

Pydantic itself rejects 0 for the exact same annotation, and the equivalent
BaseModel/TypedDict paths preserve the constraint, so the behaviour is
inconsistent and a declared bound is silently discarded.

The fix passes include_extras=True so the Annotated metadata survives into
the synthesized Pydantic model — the same way the TypedDict path already
resolves its hints (get_type_hints(td_type, include_extras=True)).

How Has This Been Tested?

Added test_structured_output_ordinary_class_keeps_annotated_field_constraints,
which asserts the constraint appears in the schema (exclusiveMinimum: 0),
that a valid value round-trips, and that an out-of-bound value raises
ValidationError. The test fails on main and passes with this change.

uv run pytest tests/server/mcpserver/test_func_metadata.py -q
# 66 passed
uv run pytest tests/server/mcpserver -q
# 651 passed

ruff check, ruff format --check, and pyright are clean on the touched files.

Breaking Changes

None. Output models for ordinary classes now enforce the constraints their
authors already declared; code that previously relied on invalid values
silently passing would have been emitting an out-of-spec schema.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I am assigned to the linked issue (or it is labeled help wanted, or I'm a maintainer)
  • I have disclosed any AI assistance and can explain the change in my own words
  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

I am not assigned to #3712 and it is not labeled help wanted, so I have left
that checkbox unchecked; I understand the linked-issue bot may close this PR
until a maintainer assigns the issue.

AI assistance: the fix and regression test were prepared with AI assistance
(Claude Code), with a human in the loop who reviewed the diff and the
reproduction.