From b81d1a11ec8f901db02c5ccd07be4d608aab476c Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Thu, 24 Apr 2025 12:11:00 -0400 Subject: [PATCH] Indicate that Image class is for returns --- README.md | 36 +++++++++++++++----------------- docs/servers/tools.mdx | 11 +++++++--- tests/tools/test_tool_manager.py | 34 +++++++++++++----------------- 3 files changed, 39 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 03748d3f8..cad1d02e6 100644 --- a/README.md +++ b/README.md @@ -332,32 +332,30 @@ The `Context` object provides: ### Images -Easily handle image input and output using the `fastmcp.Image` helper class. +Easily handle image outputs using the `fastmcp.Image` helper class. + + +The below code requires the `pillow` library to be installed. + ```python -from fastmcp import FastMCP, Image -from PIL import Image as PILImage -import io +from mcp.server.fastmcp import FastMCP, Image +try: + from PIL import Image as PILImage +except ImportError: + raise ImportError("Please install the `pillow` library to run this example.") + +mcp = FastMCP("My App") -mcp = FastMCP("Image Demo") @mcp.tool() -def create_thumbnail(image_data: Image) -> Image: - """Creates a 100x100 thumbnail from the provided image.""" - img = PILImage.open(io.BytesIO(image_data.data)) # Assumes image_data received as Image with bytes +def create_thumbnail(image_path: str) -> Image: + """Create a thumbnail from an image""" + img = PILImage.open(image_path) img.thumbnail((100, 100)) - buffer = io.BytesIO() - img.save(buffer, format="PNG") - # Return a new Image object with the thumbnail data - return Image(data=buffer.getvalue(), format="png") - -@mcp.tool() -def load_image_from_disk(path: str) -> Image: - """Loads an image from the specified path.""" - # Handles reading file and detecting format based on extension - return Image(path=path) + return Image(data=img.tobytes(), format="png") ``` -FastMCP handles the conversion to/from the base64-encoded format required by the MCP protocol. +Return the `Image` helper class from your tool to send an image to the client. The `Image` helper class handles the conversion to/from the base64-encoded format required by the MCP protocol. It works with either a path to an image file, or a bytes object. ### MCP Clients diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx index df8e21bd6..7839efa89 100644 --- a/docs/servers/tools.mdx +++ b/docs/servers/tools.mdx @@ -209,13 +209,18 @@ FastMCP automatically converts the value returned by your function into the appr - **`str`**: Sent as `TextContent`. - **`dict`, `list`, Pydantic `BaseModel`**: Serialized to a JSON string and sent as `TextContent`. - **`bytes`**: Base64 encoded and sent as `BlobResourceContents` (often within an `EmbeddedResource`). -- **`fastmcp.utilities.types.Image`**: A helper class to easily return image data. Sent as `ImageContent`. +- **`fastmcp.Image`**: A helper class for easily returning image data. Sent as `ImageContent`. - **`None`**: Results in an empty response (no content is sent back to the client). ```python -from fastmcp.utilities.types import Image -from PIL import Image as PILImage +from fastmcp import FastMCP, Image import io +try: + from PIL import Image as PILImage +except ImportError: + raise ImportError("Please install the `pillow` library to run this example.") + +mcp = FastMCP("Image Demo") @mcp.tool() def generate_image(width: int, height: int, color: str) -> Image: diff --git a/tests/tools/test_tool_manager.py b/tests/tools/test_tool_manager.py index 8206fd645..c58406c6d 100644 --- a/tests/tools/test_tool_manager.py +++ b/tests/tools/test_tool_manager.py @@ -2,8 +2,10 @@ import json import logging import pytest +from mcp.types import ImageContent, TextContent from pydantic import BaseModel +from fastmcp import Context, FastMCP, Image from fastmcp.exceptions import NotFoundError, ToolError from fastmcp.tools import ToolManager from fastmcp.tools.tool import Tool @@ -68,6 +70,18 @@ class TestAddTools: assert "age" in tool.parameters["$defs"]["UserInput"]["properties"] assert "flag" in tool.parameters["properties"] + async def test_tool_with_image_return(self): + def image_tool(data: bytes) -> Image: + return Image(data=data) + + manager = ToolManager() + manager.add_tool_from_fn(image_tool) + + tool = manager.get_tool("image_tool") + result = await tool.run({"data": "test.png"}) + assert tool.parameters["properties"]["data"]["type"] == "string" + assert isinstance(result[0], ImageContent) + def test_add_invalid_tool(self): manager = ToolManager() with pytest.raises(AttributeError): @@ -263,7 +277,6 @@ class TestCallTools: result = await manager.call_tool("double", {"n": 5}) assert isinstance(result, list) assert len(result) == 1 - from mcp.types import TextContent assert isinstance(result[0], TextContent) assert result[0].text == "10" @@ -279,7 +292,6 @@ class TestCallTools: result = await manager.call_tool("add", {"a": 1}) assert isinstance(result, list) assert len(result) == 1 - from mcp.types import TextContent assert isinstance(result[0], TextContent) assert result[0].text == "2" @@ -307,7 +319,6 @@ class TestCallTools: manager = ToolManager() manager.add_tool_from_fn(sum_vals) # Try both with plain list and with JSON list - from mcp.types import TextContent result = await manager.call_tool("sum_vals", {"vals": "[1, 2, 3]"}) assert isinstance(result, list) @@ -329,7 +340,6 @@ class TestCallTools: manager = ToolManager() manager.add_tool_from_fn(concat_strs) - from mcp.types import TextContent # Try both with plain python object and with JSON list result = await manager.call_tool("concat_strs", {"vals": ["a", "b", "c"]}) @@ -357,10 +367,6 @@ class TestCallTools: assert result[0].text == '"a"' async def test_call_tool_with_complex_model(self): - from mcp.types import TextContent - - from fastmcp import Context - class MyShrimpTank(BaseModel): class Shrimp(BaseModel): name: str @@ -397,8 +403,6 @@ class TestCallTools: class TestToolSchema: async def test_context_arg_excluded_from_schema(self): - from fastmcp import Context - def something(a: int, ctx: Context) -> int: return a @@ -415,7 +419,6 @@ class TestContextHandling: def test_context_parameter_detection(self): """Test that context parameters are properly detected in Tool.from_function().""" - from fastmcp import Context def tool_with_context(x: int, ctx: Context) -> str: return str(x) @@ -432,9 +435,6 @@ class TestContextHandling: async def test_context_injection(self): """Test that context is properly injected during tool execution.""" - from mcp.types import TextContent - - from fastmcp import Context, FastMCP def tool_with_context(x: int, ctx: Context) -> str: assert isinstance(ctx, Context) @@ -453,9 +453,6 @@ class TestContextHandling: async def test_context_injection_async(self): """Test that context is properly injected in async tools.""" - from mcp.types import TextContent - - from fastmcp import Context, FastMCP async def async_tool(x: int, ctx: Context) -> str: assert isinstance(ctx, Context) @@ -476,8 +473,6 @@ class TestContextHandling: """Test that context is optional when calling tools.""" from mcp.types import TextContent - from fastmcp import Context - def tool_with_context(x: int, ctx: Context | None = None) -> str: return str(x) @@ -492,7 +487,6 @@ class TestContextHandling: async def test_context_error_handling(self): """Test error handling when context injection fails.""" - from fastmcp import Context, FastMCP def tool_with_context(x: int, ctx: Context) -> str: raise ValueError("Test error")