diff --git a/src/mcp/server/fastmcp/prompts/base.py b/src/mcp/server/fastmcp/prompts/base.py index aa3d1eac9..cdd320994 100644 --- a/src/mcp/server/fastmcp/prompts/base.py +++ b/src/mcp/server/fastmcp/prompts/base.py @@ -7,9 +7,9 @@ import pydantic_core from pydantic import BaseModel, Field, TypeAdapter, validate_call -from mcp.types import EmbeddedResource, ImageContent, TextContent +from mcp.types import EmbeddedResource, ImageContent, TextContent, FileContent -CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource +CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource | FileContent class Message(BaseModel): diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 3282baae6..a28f30069 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -57,6 +57,7 @@ ImageContent, TextContent, ToolAnnotations, + FileContent, ) from mcp.types import Prompt as MCPPrompt from mcp.types import PromptArgument as MCPPromptArgument @@ -273,7 +274,7 @@ def get_context(self) -> Context[ServerSession, object]: async def call_tool( self, name: str, arguments: dict[str, Any] - ) -> Sequence[TextContent | ImageContent | EmbeddedResource]: + ) -> Sequence[TextContent | ImageContent | EmbeddedResource | FileContent]: """Call a tool by name with arguments.""" context = self.get_context() result = await self._tool_manager.call_tool(name, arguments, context=context) @@ -873,12 +874,12 @@ async def get_prompt( def _convert_to_content( result: Any, -) -> Sequence[TextContent | ImageContent | EmbeddedResource]: +) -> Sequence[TextContent | ImageContent | EmbeddedResource | FileContent]: """Convert a result to a sequence of content objects.""" if result is None: return [] - if isinstance(result, TextContent | ImageContent | EmbeddedResource): + if isinstance(result, TextContent | ImageContent | EmbeddedResource | FileContent): return [result] if isinstance(result, Image): diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index 876aef817..4eaea8da7 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -400,7 +400,10 @@ def decorator( ..., Awaitable[ Iterable[ - types.TextContent | types.ImageContent | types.EmbeddedResource + types.TextContent + | types.ImageContent + | types.EmbeddedResource + | types.FileContent ] ], ], diff --git a/src/mcp/types.py b/src/mcp/types.py index 465fc6ee6..833909b56 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -653,6 +653,21 @@ class ImageContent(BaseModel): model_config = ConfigDict(extra="allow") +class FileContent(BaseModel): + """File content for a message.""" + + type: Literal["file"] + filename: str + data: str + """The base64-encoded file data.""" + mimeType: str + """ + The file mimetype + """ + annotations: Annotations | None = None + model_config = ConfigDict(extra="allow") + + class SamplingMessage(BaseModel): """Describes a message issued to or received from an LLM API.""" @@ -797,7 +812,7 @@ class CallToolRequest(Request[CallToolRequestParams, Literal["tools/call"]]): class CallToolResult(Result): """The server's response to a tool call.""" - content: list[TextContent | ImageContent | EmbeddedResource] + content: list[TextContent | ImageContent | EmbeddedResource | FileContent] isError: bool = False diff --git a/tests/server/fastmcp/test_server.py b/tests/server/fastmcp/test_server.py index b817761ea..cdaf888ee 100644 --- a/tests/server/fastmcp/test_server.py +++ b/tests/server/fastmcp/test_server.py @@ -18,6 +18,7 @@ from mcp.types import ( BlobResourceContents, ImageContent, + FileContent, TextContent, TextResourceContents, ) @@ -59,7 +60,7 @@ async def test_sse_app_with_mount_path(self): # Test with default mount path mcp = FastMCP() with patch.object( - mcp, "_normalize_path", return_value="/messages/" + mcp, "_normalize_path", return_value="/messages/" ) as mock_normalize: mcp.sse_app() # Verify _normalize_path was called with correct args @@ -69,7 +70,7 @@ async def test_sse_app_with_mount_path(self): mcp = FastMCP() mcp.settings.mount_path = "/custom" with patch.object( - mcp, "_normalize_path", return_value="/custom/messages/" + mcp, "_normalize_path", return_value="/custom/messages/" ) as mock_normalize: mcp.sse_app() # Verify _normalize_path was called with correct args @@ -78,7 +79,7 @@ async def test_sse_app_with_mount_path(self): # Test with mount_path parameter mcp = FastMCP() with patch.object( - mcp, "_normalize_path", return_value="/param/messages/" + mcp, "_normalize_path", return_value="/param/messages/" ) as mock_normalize: mcp.sse_app(mount_path="/param") # Verify _normalize_path was called with correct args @@ -103,7 +104,7 @@ async def test_starlette_routes_with_mount_path(self): # Verify path values assert sse_routes[0].path == "/sse", "SSE route path should be /sse" assert ( - mount_routes[0].path == "/messages" + mount_routes[0].path == "/messages" ), "Mount route path should be /messages" # Test with mount path as parameter @@ -121,7 +122,7 @@ async def test_starlette_routes_with_mount_path(self): # Verify path values assert sse_routes[0].path == "/sse", "SSE route path should be /sse" assert ( - mount_routes[0].path == "/messages" + mount_routes[0].path == "/messages" ), "Mount route path should be /messages" @pytest.mark.anyio @@ -131,7 +132,7 @@ async def test_non_ascii_description(self): @mcp.tool( description=( - "🌟 This tool uses emojis and UTF-8 characters: á é í ó ú ñ 漢字 🎉" + "🌟 This tool uses emojis and UTF-8 characters: á é í ó ú ñ 漢字 🎉" ) ) def hello_world(name: str = "世界") -> str: @@ -167,7 +168,6 @@ async def test_add_tool_decorator_incorrect_usage(self): mcp = FastMCP() with pytest.raises(TypeError, match="The @tool decorator was used incorrectly"): - @mcp.tool # Missing parentheses #type: ignore def add(x: int, y: int) -> int: return x + y @@ -187,9 +187,8 @@ async def test_add_resource_decorator_incorrect_usage(self): mcp = FastMCP() with pytest.raises( - TypeError, match="The @resource decorator was used incorrectly" + TypeError, match="The @resource decorator was used incorrectly" ): - @mcp.resource # Missing parentheses #type: ignore def get_data(x: str) -> str: return f"Data: {x}" @@ -207,10 +206,11 @@ def image_tool_fn(path: str) -> Image: return Image(path) -def mixed_content_tool_fn() -> list[TextContent | ImageContent]: +def mixed_content_tool_fn() -> list[TextContent | ImageContent | FileContent]: return [ TextContent(type="text", text="Hello"), ImageContent(type="image", data="abc", mimeType="image/png"), + FileContent(type="file", data="abc", filename="test.pdf", mimeType="application/pdf"), ] @@ -312,14 +312,18 @@ async def test_tool_mixed_content(self): mcp.add_tool(mixed_content_tool_fn) async with client_session(mcp._mcp_server) as client: result = await client.call_tool("mixed_content_tool_fn", {}) - assert len(result.content) == 2 + assert len(result.content) == 3 content1 = result.content[0] content2 = result.content[1] + content3 = result.content[2] assert isinstance(content1, TextContent) assert content1.text == "Hello" assert isinstance(content2, ImageContent) assert content2.mimeType == "image/png" assert content2.data == "abc" + assert isinstance(content3, FileContent) + assert content3.mimeType == "application/pdf" + assert content3.data == "abc" @pytest.mark.anyio async def test_tool_mixed_list_with_image(self, tmp_path: Path): @@ -437,8 +441,8 @@ async def test_file_resource_binary(self, tmp_path: Path): result = await client.read_resource(AnyUrl("file://test.bin")) assert isinstance(result.contents[0], BlobResourceContents) assert ( - result.contents[0].blob - == base64.b64encode(b"Binary file data").decode() + result.contents[0].blob + == base64.b64encode(b"Binary file data").decode() ) @pytest.mark.anyio @@ -468,7 +472,6 @@ async def test_resource_with_params(self): mcp = FastMCP() with pytest.raises(ValueError, match="Mismatch between URI parameters"): - @mcp.resource("resource://data") def get_data_fn(param: str) -> str: return f"Data: {param}" @@ -479,7 +482,6 @@ async def test_resource_with_uri_params(self): mcp = FastMCP() with pytest.raises(ValueError, match="Mismatch between URI parameters"): - @mcp.resource("resource://{param}") def get_data() -> str: return "Data" @@ -513,7 +515,6 @@ async def test_resource_mismatched_params(self): mcp = FastMCP() with pytest.raises(ValueError, match="Mismatch between URI parameters"): - @mcp.resource("resource://{name}/data") def get_data(user: str) -> str: return f"Data for {user}" @@ -540,7 +541,6 @@ async def test_resource_multiple_mismatched_params(self): mcp = FastMCP() with pytest.raises(ValueError, match="Mismatch between URI parameters"): - @mcp.resource("resource://{org}/{repo}/data") def get_data_mismatched(org: str, repo_2: str) -> str: return f"Data for {org}" @@ -774,7 +774,6 @@ def test_prompt_decorator_error(self): """Test error when decorator is used incorrectly.""" mcp = FastMCP() with pytest.raises(TypeError, match="decorator was used incorrectly"): - @mcp.prompt # type: ignore def fn() -> str: return "Hello, world!"