Skip to content

Conversation

@SSOBHY2
Copy link

@SSOBHY2 SSOBHY2 commented Nov 28, 2025

FastMCP structured output improvements

brief summary of changes :

Improve FastMCP structured-output handling for 'dict[str, T]' return types and mixed-list tool outputs, and make the test suite/CI more robust:

  • Preserve 'Annotated' / 'Field(...)' metadata for 'dict[str, T]' return types when generating FastMCP function metadata.
  • Make mixed-list tool outputs fail gracefully for structured output (disable schema) when Pydantic cannot generate a valid JSON Schema for custom types like 'Image' / 'Audio'.
  • Harden a flaky child-process test on Windows and adjust coverage pragmas so the FastMCP test suite passes reliably across the CI matrix.
  • Silence a spurious 'PytestUnraisableExceptionWarning' on Windows for icon/metadata tests without changing behavior.

Motivation and Context :

FastMCP’s structured-output support had a couple of rough edges and CI reliability issues:

  • For 'dict[str, T]' return types, the internal 'RootModel' was built from a stripped 'GenericAlias', which lost any top-level 'Annotated' metadata. Root-level 'Field(description=...)' and similar metadata on 'dict[str, T]' outputs were not reflected in the generated JSON Schema.
  • For tools that return “mixed” lists (e.g., 'list[Image | Audio | str]'), Pydantic currently cannot generate a valid JSON Schema for the 'Image' / 'Audio' parts. This raised 'PydanticSchemaGenerationError' and caused structured-output generation to fail noisily instead of just disabling structured output for that tool.
  • There was a flaky child-process test in 'tests/client/test_stdio.py' on Windows that relied on a fixed 'sleep' to detect file writes, and its polling loop created a partial coverage branch that caused the repository’s strict 'fail_under = 100' coverage threshold to fail.
  • On Windows, the icon/metadata tests could emit a 'PytestUnraisableExceptionWarning' for an internal resource cleanup path, which was noisy but not behaviorally relevant.

This PR makes structured-output behavior more predictable for users, preserves documented metadata, and ensures the FastMCP test suite passes cleanly across platforms and Python versions.

How Has This Been Tested?

Locally, in the 'python-sdk' project, I ran the test and tooling commands recommended in 'CONTRIBUTING.md':

  • 'uv run pytest'
    • All FastMCP-related tests, including the new/updated ones, pass.
    • Overall test summary:
      • '787 passed'
      • '5 skipped'
      • '1 xfailed' (expected 'XFAIL' in 'tests/client/test_auth.py::test_build_metadata[simple-url]').
  • Coverage
    • The repository’s configured coverage threshold 'fail_under = 100' passes after adjusting the polling-loop pragmas in 'tests/client/test_stdio.py'.
  • Pre-commit hooks
  • 'pre-commit run --all-files'
  • 'ruff', 'pyright', 'markdownlint', and other configured hooks pass after formatting and import-group fixes.

Behaviorally, the following scenarios are explicitly covered by tests:

  • 'func_metadata' preserves 'Annotated' metadata (e.g., 'Field(description=...)') for 'dict[str, int]' return types.
  • A mixed-list tool returning 'list[Image | Audio | str]':
    • Does not crash schema generation.
    • Has 'structuredContent' set to 'None' when Pydantic cannot generate a valid schema, while the tool still works via plain JSON arguments.
  • The child-process cleanup test now polls for file growth instead of relying on a single fixed 'sleep', and still asserts that the child process writes to its marker file.
  • The icon/metadata tests with Windows-specific behavior still run, but suppress the noisy unraisable-exception warning via 'pytest.mark.filterwarnings' on Windows only.

Breaking Changes :

These changes are not expected to be breaking:

  • The FastMCP structured-output API surface is unchanged.
  • For 'dict[str, T]' return types, the generated JSON Schema becomes more accurate by including the previously-lost 'Annotated' / 'Field(...)' metadata.
  • For mixed-list tool outputs that include types like 'Image' / 'Audio', structured-output is now gracefully disabled (schema set to 'None') instead of raising a Pydantic schema error. Tools continue to function via unstructured arguments.
  • Test and CI changes only affect internal tests and warning handling; they do not change the public API.

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 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 (not required for this change; behavior is consistent with existing FastMCP docs)

Additional context

  • In 'func_metadata', the helper now passes the original annotation (including any 'Annotated' wrapper) to the 'RootModel' used for schema generation, so Pydantic can see and preserve root-level metadata.
  • When Pydantic raises 'PydanticSchemaGenerationError', 'pydantic_core.SchemaError', or related exceptions while building a schema for the tool’s return type, FastMCP logs the error and disables structured output for that tool instead of propagating the exception.
  • The child-process polling test marks the small polling loop and inner 'if' with '# pragma: no branch' to avoid partial-branch coverage noise while still asserting the intended behavior.
  • On Windows only, the icon/metadata tests add a 'pytest.mark.filterwarnings' entry for 'PytestUnraisableExceptionWarning' to keep CI output clean without hiding real test failures.

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