From a39ab0734e1808c8b837644d30b04033a22d5f21 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 26 Dec 2025 08:33:23 -0500 Subject: [PATCH] Update classes to inherit from FastMCPBaseModel instead of BaseModel (#2739) --- src/fastmcp/prompts/prompt.py | 8 ++---- src/fastmcp/resources/resource.py | 12 +++------ src/fastmcp/server/event_store.py | 6 ++--- src/fastmcp/server/middleware/caching.py | 15 ++++++----- src/fastmcp/server/sampling/sampling_tool.py | 5 ++-- src/fastmcp/tools/tool.py | 4 +-- tests/cli/test_config.py | 27 ++++++++++++++++++++ 7 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py index 146828243..8a9150757 100644 --- a/src/fastmcp/prompts/prompt.py +++ b/src/fastmcp/prompts/prompt.py @@ -68,7 +68,6 @@ class Message(pydantic.BaseModel): self, content: Any, role: Literal["user", "assistant"] = "user", - **kwargs: Any, ): """Create Message with automatic serialization. @@ -88,7 +87,7 @@ class Message(pydantic.BaseModel): serialized = pydantic_core.to_json(content, fallback=str).decode() normalized_content = TextContent(type="text", text=serialized) - super().__init__(role=role, content=normalized_content, **kwargs) + super().__init__(role=role, content=normalized_content) def to_mcp_prompt_message(self) -> PromptMessage: """Convert to MCP PromptMessage.""" @@ -148,7 +147,6 @@ class PromptResult(pydantic.BaseModel): messages: str | list[Message], description: str | None = None, meta: dict[str, Any] | None = None, - **kwargs: Any, ): """Create PromptResult. @@ -158,9 +156,7 @@ class PromptResult(pydantic.BaseModel): meta: Optional metadata about the prompt result. """ normalized = self._normalize_messages(messages) - super().__init__( - messages=normalized, description=description, meta=meta, **kwargs - ) + super().__init__(messages=normalized, description=description, meta=meta) @staticmethod def _normalize_messages( diff --git a/src/fastmcp/resources/resource.py b/src/fastmcp/resources/resource.py index c51108857..1a90bdb70 100644 --- a/src/fastmcp/resources/resource.py +++ b/src/fastmcp/resources/resource.py @@ -29,9 +29,7 @@ from typing_extensions import Self from fastmcp.server.dependencies import without_injected_parameters from fastmcp.server.tasks.config import TaskConfig from fastmcp.utilities.components import FastMCPComponent -from fastmcp.utilities.types import ( - get_fn_name, -) +from fastmcp.utilities.types import get_fn_name class ResourceContent(pydantic.BaseModel): @@ -65,7 +63,6 @@ class ResourceContent(pydantic.BaseModel): content: Any, mime_type: str | None = None, meta: dict[str, Any] | None = None, - **kwargs: Any, ): """Create ResourceContent with automatic serialization. @@ -88,9 +85,7 @@ class ResourceContent(pydantic.BaseModel): normalized_content = pydantic_core.to_json(content, fallback=str).decode() mime_type = mime_type or "application/json" - super().__init__( - content=normalized_content, mime_type=mime_type, meta=meta, **kwargs - ) + super().__init__(content=normalized_content, mime_type=mime_type, meta=meta) def to_mcp_resource_contents( self, uri: AnyUrl | str @@ -162,7 +157,6 @@ class ResourceResult(pydantic.BaseModel): self, contents: str | bytes | list[ResourceContent], meta: dict[str, Any] | None = None, - **kwargs: Any, ): """Create ResourceResult. @@ -171,7 +165,7 @@ class ResourceResult(pydantic.BaseModel): meta: Optional metadata about the resource result. """ normalized = self._normalize_contents(contents) - super().__init__(contents=normalized, meta=meta, **kwargs) + super().__init__(contents=normalized, meta=meta) @staticmethod def _normalize_contents( diff --git a/src/fastmcp/server/event_store.py b/src/fastmcp/server/event_store.py index 79f192000..9304a931e 100644 --- a/src/fastmcp/server/event_store.py +++ b/src/fastmcp/server/event_store.py @@ -16,14 +16,14 @@ from key_value.aio.stores.memory import MemoryStore from mcp.server.streamable_http import EventCallback, EventId, EventMessage, StreamId from mcp.server.streamable_http import EventStore as SDKEventStore from mcp.types import JSONRPCMessage -from pydantic import BaseModel from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.types import FastMCPBaseModel logger = get_logger(__name__) -class EventEntry(BaseModel): +class EventEntry(FastMCPBaseModel): """Stored event entry.""" event_id: str @@ -31,7 +31,7 @@ class EventEntry(BaseModel): message: dict | None # JSONRPCMessage serialized to dict -class StreamEventList(BaseModel): +class StreamEventList(FastMCPBaseModel): """List of event IDs for a stream.""" event_ids: list[str] diff --git a/src/fastmcp/server/middleware/caching.py b/src/fastmcp/server/middleware/caching.py index bfc42cef7..d4ac15929 100644 --- a/src/fastmcp/server/middleware/caching.py +++ b/src/fastmcp/server/middleware/caching.py @@ -14,7 +14,7 @@ from key_value.aio.wrappers.statistics import StatisticsWrapper from key_value.aio.wrappers.statistics.wrapper import ( KVStoreCollectionStatistics, ) -from pydantic import BaseModel, Field +from pydantic import Field from typing_extensions import NotRequired, Self, override from fastmcp.prompts.prompt import Message, Prompt, PromptResult @@ -22,6 +22,7 @@ from fastmcp.resources.resource import Resource, ResourceContent, ResourceResult from fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext from fastmcp.tools.tool import Tool, ToolResult from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.types import FastMCPBaseModel logger: Logger = get_logger(name=__name__) @@ -34,7 +35,7 @@ ONE_MB_IN_BYTES = 1024 * 1024 GLOBAL_KEY = "__global__" -class CachableResourceContent(BaseModel): +class CachableResourceContent(FastMCPBaseModel): """A wrapper for ResourceContent that can be cached.""" content: str | bytes @@ -42,7 +43,7 @@ class CachableResourceContent(BaseModel): meta: dict[str, Any] | None = None -class CachableResourceResult(BaseModel): +class CachableResourceResult(FastMCPBaseModel): """A wrapper for ResourceResult that can be cached.""" contents: list[CachableResourceContent] @@ -75,7 +76,7 @@ class CachableResourceResult(BaseModel): ) -class CachableToolResult(BaseModel): +class CachableToolResult(FastMCPBaseModel): content: list[mcp.types.ContentBlock] structured_content: dict[str, Any] | None meta: dict[str, Any] | None @@ -96,14 +97,14 @@ class CachableToolResult(BaseModel): ) -class CachableMessage(BaseModel): +class CachableMessage(FastMCPBaseModel): """A wrapper for Message that can be cached.""" role: str content: mcp.types.TextContent | mcp.types.EmbeddedResource -class CachablePromptResult(BaseModel): +class CachablePromptResult(FastMCPBaseModel): """A wrapper for PromptResult that can be cached.""" messages: list[CachableMessage] @@ -168,7 +169,7 @@ class GetPromptSettings(SharedMethodSettings): """Configuration options for Prompt-related caching.""" -class ResponseCachingStatistics(BaseModel): +class ResponseCachingStatistics(FastMCPBaseModel): list_tools: KVStoreCollectionStatistics | None = Field(default=None) list_resources: KVStoreCollectionStatistics | None = Field(default=None) list_prompts: KVStoreCollectionStatistics | None = Field(default=None) diff --git a/src/fastmcp/server/sampling/sampling_tool.py b/src/fastmcp/server/sampling/sampling_tool.py index 8bab07389..3de466888 100644 --- a/src/fastmcp/server/sampling/sampling_tool.py +++ b/src/fastmcp/server/sampling/sampling_tool.py @@ -7,12 +7,13 @@ from collections.abc import Callable from typing import Any from mcp.types import Tool as SDKTool -from pydantic import BaseModel, ConfigDict +from pydantic import ConfigDict from fastmcp.tools.tool import ParsedFunction +from fastmcp.utilities.types import FastMCPBaseModel -class SamplingTool(BaseModel): +class SamplingTool(FastMCPBaseModel): """A tool that can be used during LLM sampling. SamplingTools bundle a tool's schema (name, description, parameters) with diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py index b4ea503c7..ce897bc84 100644 --- a/src/fastmcp/tools/tool.py +++ b/src/fastmcp/tools/tool.py @@ -117,9 +117,7 @@ class ToolResult(BaseModel): ) super().__init__( - content=converted_content, - structured_content=structured_content, - meta=meta, + content=converted_content, structured_content=structured_content, meta=meta ) def to_mcp_result( diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py index 1a5d307eb..cb4dd00a0 100644 --- a/tests/cli/test_config.py +++ b/tests/cli/test_config.py @@ -432,3 +432,30 @@ class TestMCPServerConfig: if field != "type" ) assert config.deployment.transport == "http" + + +class TestMCPServerConfigRoundtrip: + """Test that MCPServerConfig survives model_dump() -> reconstruct pattern. + + This is used by the CLI to apply overrides immutably. + """ + + def test_roundtrip_preserves_schema(self): + """Ensure schema_ field survives dump/reconstruct cycle.""" + config = MCPServerConfig(source=FileSystemSource(path="server.py")) + config_dict = config.model_dump() + reconstructed = MCPServerConfig(**config_dict) + assert reconstructed.schema_ == config.schema_ + + def test_roundtrip_with_all_fields(self): + """Full config survives dump/reconstruct.""" + config = MCPServerConfig( + source=FileSystemSource(path="server.py", entrypoint="app"), + environment=UVEnvironment(python="3.11"), + deployment=Deployment(transport="http", port=8080), + ) + config_dict = config.model_dump() + reconstructed = MCPServerConfig(**config_dict) + assert reconstructed.source.path == "server.py" + assert reconstructed.environment.python == "3.11" + assert reconstructed.deployment.port == 8080