Skip to content

[v1.x] Rebuild FastMCP Settings once FastMCP is defined - #3295

Closed
claude[bot] wants to merge 1 commit into
v1.xfrom
fix/v1-fastmcp-settings-model-rebuild
Closed

[v1.x] Rebuild FastMCP Settings once FastMCP is defined#3295
claude[bot] wants to merge 1 commit into
v1.xfrom
fix/v1-fastmcp-settings-model-rebuild

Conversation

@claude

@claude claude Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Requested by Felix Weinberger · Slack thread

Fixes #3294

Problem

Settings.lifespan forward-references FastMCP, which is defined later in mcp.server.fastmcp.server, so when the Settings class body executes the annotation is an unresolved ForwardRef and the model stays incomplete after import (Settings.__pydantic_complete__ is False).

Consequences:

  • pydantic-settings >= 2.15.0 emits IncompleteFieldDefinitionWarning on every FastMCP() construction (added in pydantic-settings#901). Consumers that promote warnings to errors — a common test-suite configuration — can no longer construct a server at all.
  • As the warning says, settings sources may fail to correctly resolve the incomplete field's value.

Fix

Rebuild the Settings model at module scope once FastMCP exists, mirroring the reporter's verified workaround but inside the SDK where it belongs. One line plus an explanatory comment; no behavior change for resolved fields.

Verification

  • Regression test test_settings_model_is_fully_defined_after_import asserts the model is complete and the lifespan annotation is no longer a ForwardRef. It fails on unpatched v1.x (Settings.__pydantic_complete__ is False) and passes with the fix.
  • Reproduced the reported failure against the published mcp==1.29.0 wheel with pydantic-settings==2.15.0: FastMCP("test") raises IncompleteFieldDefinitionWarning under warnings.simplefilter("error"). With this branch (editable install + pydantic-settings 2.15.0), the same script constructs the server cleanly.
  • tests/server/fastmcp/test_server.py: 85 passed. Ruff format/check and pyright clean on the changed files.

AI-assisted contribution disclosure

This PR was authored by an AI agent as part of a maintainer-commissioned triage workflow. The reproduction, fix, and tests were verified by running them as described above.


Generated by Claude Code

Settings.lifespan forward-references FastMCP, which is defined later in
the module, so the annotation was still an unresolved ForwardRef after
import and the model stayed incomplete. Settings sources could fail to
resolve the field, and pydantic-settings >= 2.15 emits
IncompleteFieldDefinitionWarning on every FastMCP() construction, which
breaks consumers that promote warnings to errors.

Rebuild the Settings model at module scope once FastMCP exists, and add
a regression test asserting the model is fully defined after import.

Github-Issue: #3294
Reported-by: igorkorsunsky

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LGTM — a one-line Settings.model_rebuild() at module scope with a regression test, fixing the incomplete-model warning from pydantic-settings >= 2.15.

What was reviewed

  • The rebuild's placement: it runs after FastMCP is defined in the same module, so the lifespan forward reference resolves from module globals; all other Settings field types are imported at the top of the file and resolvable at that point.
  • Checked that resolving the annotation doesn't change validation for existing users — tests/server/test_lifespan.py and tests/issues/test_355_type_error.py construct FastMCP with real lifespan callables and continue to exercise the field.
  • The regression test asserts __pydantic_complete__ and that the annotation is no longer a ForwardRef, which fails on the unpatched branch per the PR description.
Extended reasoning...

Overview

The PR touches two files: src/mcp/server/fastmcp/server.py gains a single Settings.model_rebuild() call at module scope (after the FastMCP class definition, with an explanatory comment), and tests/server/fastmcp/test_server.py gains a regression test asserting the model is complete after import. This resolves the Settings.lifespan forward reference to FastMCP, which is defined after Settings in the module, eliminating the IncompleteFieldDefinitionWarning emitted by pydantic-settings >= 2.15 on every FastMCP() construction.

Security risks

None. The change does not alter any runtime logic, auth, transport, or validation semantics — it only completes a type annotation that pydantic previously left unresolved. No user input paths are affected.

Level of scrutiny

Low-to-moderate. model_rebuild() is the canonical pydantic pattern for exactly this situation (a model forward-referencing a class defined later in the same module). The call site is correct: it executes after FastMCP exists, and every other field type on Settings (AuthSettings, TransportSecuritySettings, LifespanResultT, etc.) is imported at the top of the module, so the rebuild cannot fail at import time. The one behavioral question — whether resolving Callable[[FastMCP[...]], ...] | None could newly reject values previously accepted under the unresolved ForwardRef — is covered by existing tests that pass real lifespan callables through the constructor (tests/server/test_lifespan.py, tests/issues/test_355_type_error.py, tests/issues/test_1027_win_unreachable_cleanup.py), plus the 85-test file run reported in the PR description.

Other factors

The bug hunting system found no issues. The PR includes a targeted regression test that fails on the unpatched branch, and the description documents reproduction against the published mcp==1.29.0 wheel with pydantic-settings 2.15.0. The timeline has no outstanding reviewer comments. This is a small, self-contained, well-verified fix following an established pattern — suitable for approval without requiring human review.

@maxisbey maxisbey closed this Aug 12, 2026
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.

2 participants