From 02b2ec1adfbc6126a492bdfaf7f22dc2e38c9037 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 20 Jun 2025 09:56:21 -0400 Subject: [PATCH] Update types and tests for upcoming SDK release --- src/fastmcp/client/client.py | 4 ++-- src/fastmcp/prompts/prompt.py | 5 ++--- src/fastmcp/server/context.py | 4 ++-- src/fastmcp/server/openapi.py | 5 ++--- src/fastmcp/server/proxy.py | 8 +++++--- src/fastmcp/server/server.py | 10 ++++++---- src/fastmcp/tools/tool.py | 13 ++++++------- src/fastmcp/tools/tool_manager.py | 7 ++++--- src/fastmcp/tools/tool_transform.py | 6 +++--- src/fastmcp/utilities/types.py | 7 ++----- tests/server/openapi/test_openapi.py | 4 ++++ tests/tools/test_tool.py | 12 +++++++----- 12 files changed, 45 insertions(+), 40 deletions(-) diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py index 26b14586b..54baa80be 100644 --- a/src/fastmcp/client/client.py +++ b/src/fastmcp/client/client.py @@ -9,6 +9,7 @@ import httpx import mcp.types from exceptiongroup import catch from mcp import ClientSession +from mcp.types import ContentBlock from pydantic import AnyUrl import fastmcp @@ -29,7 +30,6 @@ from fastmcp.exceptions import ToolError from fastmcp.server import FastMCP from fastmcp.utilities.exceptions import get_catch_handlers from fastmcp.utilities.mcp_config import MCPConfig -from fastmcp.utilities.types import MCPContent from .transports import ( ClientTransportT, @@ -659,7 +659,7 @@ class Client(Generic[ClientTransportT]): arguments: dict[str, Any] | None = None, timeout: datetime.timedelta | float | int | None = None, progress_handler: ProgressHandler | None = None, - ) -> list[MCPContent]: + ) -> list[ContentBlock]: """Call a tool on the server. Unlike call_tool_mcp, this method raises a ToolError if the tool call results in an error. diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py index 48343e2bb..28bc977f2 100644 --- a/src/fastmcp/prompts/prompt.py +++ b/src/fastmcp/prompts/prompt.py @@ -8,9 +8,9 @@ from collections.abc import Awaitable, Callable, Sequence from typing import TYPE_CHECKING, Any import pydantic_core +from mcp.types import ContentBlock, PromptMessage, Role, TextContent from mcp.types import Prompt as MCPPrompt from mcp.types import PromptArgument as MCPPromptArgument -from mcp.types import PromptMessage, Role, TextContent from pydantic import Field, TypeAdapter, validate_call from fastmcp.exceptions import PromptError @@ -20,7 +20,6 @@ from fastmcp.utilities.json_schema import compress_schema from fastmcp.utilities.logging import get_logger from fastmcp.utilities.types import ( FastMCPBaseModel, - MCPContent, find_kwarg_by_type, get_cached_typeadapter, ) @@ -33,7 +32,7 @@ logger = get_logger(__name__) def Message( - content: str | MCPContent, role: Role | None = None, **kwargs: Any + content: str | ContentBlock, role: Role | None = None, **kwargs: Any ) -> PromptMessage: """A user-friendly constructor for PromptMessage.""" if isinstance(content, str): diff --git a/src/fastmcp/server/context.py b/src/fastmcp/server/context.py index 994052724..0a9c9e157 100644 --- a/src/fastmcp/server/context.py +++ b/src/fastmcp/server/context.py @@ -11,6 +11,7 @@ from mcp.server.lowlevel.helper_types import ReadResourceContents from mcp.server.lowlevel.server import request_ctx from mcp.shared.context import RequestContext from mcp.types import ( + ContentBlock, CreateMessageResult, ModelHint, ModelPreferences, @@ -25,7 +26,6 @@ import fastmcp.server.dependencies from fastmcp import settings from fastmcp.server.server import FastMCP from fastmcp.utilities.logging import get_logger -from fastmcp.utilities.types import MCPContent logger = get_logger(__name__) @@ -243,7 +243,7 @@ class Context: temperature: float | None = None, max_tokens: int | None = None, model_preferences: ModelPreferences | str | list[str] | None = None, - ) -> MCPContent: + ) -> ContentBlock: """ Send a sampling request to the client and await the response. diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py index 8d5128280..02a68de5d 100644 --- a/src/fastmcp/server/openapi.py +++ b/src/fastmcp/server/openapi.py @@ -13,7 +13,7 @@ from re import Pattern from typing import TYPE_CHECKING, Any, Literal import httpx -from mcp.types import ToolAnnotations +from mcp.types import ContentBlock, ToolAnnotations from pydantic.networks import AnyUrl import fastmcp @@ -29,7 +29,6 @@ from fastmcp.utilities.openapi import ( _combine_schemas, format_description_with_responses, ) -from fastmcp.utilities.types import MCPContent if TYPE_CHECKING: from fastmcp.server import Context @@ -255,7 +254,7 @@ class OpenAPITool(Tool): """Custom representation to prevent recursion errors when printing.""" return f"OpenAPITool(name={self.name!r}, method={self._route.method}, path={self._route.path})" - async def run(self, arguments: dict[str, Any]) -> list[MCPContent]: + async def run(self, arguments: dict[str, Any]) -> list[ContentBlock]: """Execute the HTTP request based on the route configuration.""" # Prepare URL diff --git a/src/fastmcp/server/proxy.py b/src/fastmcp/server/proxy.py index aeabf499f..f80c9c38e 100644 --- a/src/fastmcp/server/proxy.py +++ b/src/fastmcp/server/proxy.py @@ -8,6 +8,7 @@ from mcp.shared.exceptions import McpError from mcp.types import ( METHOD_NOT_FOUND, BlobResourceContents, + ContentBlock, GetPromptResult, TextResourceContents, ) @@ -25,7 +26,6 @@ from fastmcp.server.server import FastMCP from fastmcp.tools.tool import Tool from fastmcp.tools.tool_manager import ToolManager from fastmcp.utilities.logging import get_logger -from fastmcp.utilities.types import MCPContent if TYPE_CHECKING: from fastmcp.server import Context @@ -67,7 +67,9 @@ class ProxyToolManager(ToolManager): tools_dict = await self.get_tools() return list(tools_dict.values()) - async def call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]: + async def call_tool( + self, key: str, arguments: dict[str, Any] + ) -> list[ContentBlock]: """Calls a tool, trying local/mounted first, then proxy if not found.""" try: # First try local and mounted tools @@ -230,7 +232,7 @@ class ProxyTool(Tool): self, arguments: dict[str, Any], context: Context | None = None, - ) -> list[MCPContent]: + ) -> list[ContentBlock]: """Executes the tool by making a call through the client.""" # This is where the remote execution logic lives. async with self._client: diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index d87aacd41..6f52a7c8a 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -27,6 +27,7 @@ from mcp.server.lowlevel.server import Server as MCPServer from mcp.server.stdio import stdio_server from mcp.types import ( AnyFunction, + ContentBlock, GetPromptResult, ToolAnnotations, ) @@ -62,7 +63,6 @@ from fastmcp.utilities.cache import TimedCache from fastmcp.utilities.components import FastMCPComponent from fastmcp.utilities.logging import get_logger from fastmcp.utilities.mcp_config import MCPConfig -from fastmcp.utilities.types import MCPContent if TYPE_CHECKING: from fastmcp.client import Client @@ -586,7 +586,7 @@ class FastMCP(Generic[LifespanResultT]): async def _mcp_call_tool( self, key: str, arguments: dict[str, Any] - ) -> list[MCPContent]: + ) -> list[ContentBlock]: """ Handle MCP 'callTool' requests. @@ -609,14 +609,16 @@ class FastMCP(Generic[LifespanResultT]): except NotFoundError: raise NotFoundError(f"Unknown tool: {key}") - async def _call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]: + async def _call_tool( + self, key: str, arguments: dict[str, Any] + ) -> list[ContentBlock]: """ Applies this server's middleware and delegates the filtered call to the manager. """ async def _handler( context: MiddlewareContext[mcp.types.CallToolRequestParams], - ) -> list[MCPContent]: + ) -> list[ContentBlock]: tool = await self._tool_manager.get_tool(context.message.name) if not self._should_enable_component(tool): raise NotFoundError(f"Unknown tool: {context.message.name!r}") diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py index fd40f0d2a..08518db97 100644 --- a/src/fastmcp/tools/tool.py +++ b/src/fastmcp/tools/tool.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any import pydantic_core -from mcp.types import TextContent, ToolAnnotations +from mcp.types import ContentBlock, TextContent, ToolAnnotations from mcp.types import Tool as MCPTool from pydantic import Field @@ -20,7 +20,6 @@ from fastmcp.utilities.types import ( Audio, File, Image, - MCPContent, find_kwarg_by_type, get_cached_typeadapter, ) @@ -78,7 +77,7 @@ class Tool(FastMCPComponent): enabled=enabled, ) - async def run(self, arguments: dict[str, Any]) -> list[MCPContent]: + async def run(self, arguments: dict[str, Any]) -> list[ContentBlock]: """Run the tool with arguments.""" raise NotImplementedError("Subclasses must implement run()") @@ -143,7 +142,7 @@ class FunctionTool(Tool): enabled=enabled if enabled is not None else True, ) - async def run(self, arguments: dict[str, Any]) -> list[MCPContent]: + async def run(self, arguments: dict[str, Any]) -> list[ContentBlock]: """Run the tool with arguments.""" from fastmcp.server.context import Context @@ -264,12 +263,12 @@ def _convert_to_content( result: Any, serializer: Callable[[Any], str] | None = None, _process_as_single_item: bool = False, -) -> list[MCPContent]: +) -> list[ContentBlock]: """Convert a result to a sequence of content objects.""" if result is None: return [] - if isinstance(result, MCPContent): + if isinstance(result, ContentBlock): return [result] if isinstance(result, Image): @@ -292,7 +291,7 @@ def _convert_to_content( other_content = [] for item in result: - if isinstance(item, MCPContent | Image | Audio | File): + if isinstance(item, ContentBlock | Image | Audio | File): mcp_types.append(_convert_to_content(item)[0]) else: other_content.append(item) diff --git a/src/fastmcp/tools/tool_manager.py b/src/fastmcp/tools/tool_manager.py index 0d51ba32e..facf5fbba 100644 --- a/src/fastmcp/tools/tool_manager.py +++ b/src/fastmcp/tools/tool_manager.py @@ -4,14 +4,13 @@ import warnings from collections.abc import Callable from typing import TYPE_CHECKING, Any -from mcp.types import ToolAnnotations +from mcp.types import ContentBlock, ToolAnnotations from fastmcp import settings from fastmcp.exceptions import NotFoundError, ToolError from fastmcp.settings import DuplicateBehavior from fastmcp.tools.tool import Tool from fastmcp.utilities.logging import get_logger -from fastmcp.utilities.types import MCPContent if TYPE_CHECKING: from fastmcp.server.server import MountedServer @@ -170,7 +169,9 @@ class ToolManager: else: raise NotFoundError(f"Tool {key!r} not found") - async def call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]: + async def call_tool( + self, key: str, arguments: dict[str, Any] + ) -> list[ContentBlock]: """ Internal API for servers: Finds and calls a tool, respecting the filtered protocol path. diff --git a/src/fastmcp/tools/tool_transform.py b/src/fastmcp/tools/tool_transform.py index 149469a4c..43fa369d7 100644 --- a/src/fastmcp/tools/tool_transform.py +++ b/src/fastmcp/tools/tool_transform.py @@ -7,12 +7,12 @@ from dataclasses import dataclass from types import EllipsisType from typing import Any, Literal -from mcp.types import ToolAnnotations +from mcp.types import ContentBlock, ToolAnnotations from pydantic import ConfigDict from fastmcp.tools.tool import ParsedFunction, Tool from fastmcp.utilities.logging import get_logger -from fastmcp.utilities.types import MCPContent, get_cached_typeadapter +from fastmcp.utilities.types import get_cached_typeadapter logger = get_logger(__name__) @@ -202,7 +202,7 @@ class TransformedTool(Tool): forwarding_fn: Callable[..., Any] # Always present, handles arg transformation transform_args: dict[str, ArgTransform] - async def run(self, arguments: dict[str, Any]) -> list[MCPContent]: + async def run(self, arguments: dict[str, Any]) -> list[ContentBlock]: """Run the tool with context set for forward() functions. This method executes the tool's function while setting up the context diff --git a/src/fastmcp/utilities/types.py b/src/fastmcp/utilities/types.py index 2397f73bf..8c65bd82c 100644 --- a/src/fastmcp/utilities/types.py +++ b/src/fastmcp/utilities/types.py @@ -7,7 +7,7 @@ from collections.abc import Callable from functools import lru_cache from pathlib import Path from types import UnionType -from typing import Annotated, TypeAlias, TypeVar, Union, get_args, get_origin +from typing import Annotated, TypeVar, Union, get_args, get_origin from mcp.types import ( Annotations, @@ -15,15 +15,12 @@ from mcp.types import ( BlobResourceContents, EmbeddedResource, ImageContent, - TextContent, - TextResourceContents, # Added import + TextResourceContents, ) from pydantic import AnyUrl, BaseModel, ConfigDict, TypeAdapter, UrlConstraints T = TypeVar("T") -MCPContent: TypeAlias = TextContent | ImageContent | AudioContent | EmbeddedResource - class FastMCPBaseModel(BaseModel): """Base model for FastMCP models.""" diff --git a/tests/server/openapi/test_openapi.py b/tests/server/openapi/test_openapi.py index 9afb1d656..6422bd298 100644 --- a/tests/server/openapi/test_openapi.py +++ b/tests/server/openapi/test_openapi.py @@ -223,6 +223,8 @@ class TestTools: assert tools[0].model_dump() == dict( name="create_user_users_post", + meta=None, + title=None, annotations=None, description=IsStr(regex=r"^Create a new user\..*$", regex_flags=re.DOTALL), inputSchema={ @@ -236,6 +238,8 @@ class TestTools: ) assert tools[1].model_dump() == dict( name="update_user_name_users", + meta=None, + title=None, annotations=None, description=IsStr( regex=r"^Update a user's name\..*$", regex_flags=re.DOTALL diff --git a/tests/tools/test_tool.py b/tests/tools/test_tool.py index 54ac2061e..89a72a482 100644 --- a/tests/tools/test_tool.py +++ b/tests/tools/test_tool.py @@ -1,3 +1,5 @@ +import json + import pytest from mcp.types import ( AudioContent, @@ -696,7 +698,7 @@ class TestConvertResultToContent: assert len(result) == 1 assert isinstance(result[0], TextContent) # Should fall back to default serializer (pydantic_core.to_json) - assert result[0].text == '{\n "a": 1\n}' + assert json.loads(result[0].text) == {"a": 1} assert "Error serializing tool result" in caplog.text def test_process_as_single_item_flag(self): @@ -714,7 +716,7 @@ class TestConvertResultToContent: assert len(result) == 1 assert isinstance(result[0], TextContent) - assert ( - result[0].text - == '[\n 1,\n {\n "type": "text",\n "text": "hello",\n "annotations": null\n }\n]' - ) + assert json.loads(result[0].text) == [ + 1, + {"type": "text", "text": "hello", "annotations": None, "_meta": None}, + ]