diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py index 146828243b..8a9150757c 100644 --- a/src/fastmcp/prompts/prompt.py +++ b/src/fastmcp/prompts/prompt.py @@ -68,7 +68,6 @@ def __init__( self, content: Any, role: Literal["user", "assistant"] = "user", - **kwargs: Any, ): """Create Message with automatic serialization. @@ -88,7 +87,7 @@ def __init__( 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 @@ def __init__( messages: str | list[Message], description: str | None = None, meta: dict[str, Any] | None = None, - **kwargs: Any, ): """Create PromptResult. @@ -158,9 +156,7 @@ def __init__( 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 c511088577..1a90bdb70e 100644 --- a/src/fastmcp/resources/resource.py +++ b/src/fastmcp/resources/resource.py @@ -29,9 +29,7 @@ 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 @@ def __init__( content: Any, mime_type: str | None = None, meta: dict[str, Any] | None = None, - **kwargs: Any, ): """Create ResourceContent with automatic serialization. @@ -88,9 +85,7 @@ def __init__( 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 @@ def __init__( self, contents: str | bytes | list[ResourceContent], meta: dict[str, Any] | None = None, - **kwargs: Any, ): """Create ResourceResult. @@ -171,7 +165,7 @@ def __init__( 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 79f192000b..9304a931e5 100644 --- a/src/fastmcp/server/event_store.py +++ b/src/fastmcp/server/event_store.py @@ -16,14 +16,14 @@ 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 bfc42cef79..d4ac159295 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.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.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 @@ 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 @@ def unwrap(self) -> ResourceResult: ) -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 @@ def unwrap(self) -> ToolResult: ) -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 8bab07389f..3de4668889 100644 --- a/src/fastmcp/server/sampling/sampling_tool.py +++ b/src/fastmcp/server/sampling/sampling_tool.py @@ -7,12 +7,13 @@ 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 b4ea503c7d..ce897bc845 100644 --- a/src/fastmcp/tools/tool.py +++ b/src/fastmcp/tools/tool.py @@ -117,9 +117,7 @@ def __init__( ) 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 1a5d307eb1..cb4dd00a0e 100644 --- a/tests/cli/test_config.py +++ b/tests/cli/test_config.py @@ -432,3 +432,30 @@ def test_optional_sections(self): 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