Jeremiah Lowin commited on
Commit
090598b
·
1 Parent(s): e4725d7

Indicate that Image class is for returns

Browse files
README.md CHANGED
@@ -332,32 +332,30 @@ The `Context` object provides:
332
 
333
  ### Images
334
 
335
- Easily handle image input and output using the `fastmcp.Image` helper class.
 
 
 
 
336
 
337
  ```python
338
- from fastmcp import FastMCP, Image
339
- from PIL import Image as PILImage
340
- import io
 
 
341
 
342
- mcp = FastMCP("Image Demo")
343
 
344
- @mcp.tool()
345
- def create_thumbnail(image_data: Image) -> Image:
346
- """Creates a 100x100 thumbnail from the provided image."""
347
- img = PILImage.open(io.BytesIO(image_data.data)) # Assumes image_data received as Image with bytes
348
- img.thumbnail((100, 100))
349
- buffer = io.BytesIO()
350
- img.save(buffer, format="PNG")
351
- # Return a new Image object with the thumbnail data
352
- return Image(data=buffer.getvalue(), format="png")
353
 
354
  @mcp.tool()
355
- def load_image_from_disk(path: str) -> Image:
356
- """Loads an image from the specified path."""
357
- # Handles reading file and detecting format based on extension
358
- return Image(path=path)
 
359
  ```
360
- FastMCP handles the conversion to/from the base64-encoded format required by the MCP protocol.
361
 
362
 
363
  ### MCP Clients
 
332
 
333
  ### Images
334
 
335
+ Easily handle image outputs using the `fastmcp.Image` helper class.
336
+
337
+ <Tip>
338
+ The below code requires the `pillow` library to be installed.
339
+ </Tip>
340
 
341
  ```python
342
+ from mcp.server.fastmcp import FastMCP, Image
343
+ try:
344
+ from PIL import Image as PILImage
345
+ except ImportError:
346
+ raise ImportError("Please install the `pillow` library to run this example.")
347
 
348
+ mcp = FastMCP("My App")
349
 
 
 
 
 
 
 
 
 
 
350
 
351
  @mcp.tool()
352
+ def create_thumbnail(image_path: str) -> Image:
353
+ """Create a thumbnail from an image"""
354
+ img = PILImage.open(image_path)
355
+ img.thumbnail((100, 100))
356
+ return Image(data=img.tobytes(), format="png")
357
  ```
358
+ 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.
359
 
360
 
361
  ### MCP Clients
docs/servers/tools.mdx CHANGED
@@ -209,13 +209,18 @@ FastMCP automatically converts the value returned by your function into the appr
209
  - **`str`**: Sent as `TextContent`.
210
  - **`dict`, `list`, Pydantic `BaseModel`**: Serialized to a JSON string and sent as `TextContent`.
211
  - **`bytes`**: Base64 encoded and sent as `BlobResourceContents` (often within an `EmbeddedResource`).
212
- - **`fastmcp.utilities.types.Image`**: A helper class to easily return image data. Sent as `ImageContent`.
213
  - **`None`**: Results in an empty response (no content is sent back to the client).
214
 
215
  ```python
216
- from fastmcp.utilities.types import Image
217
- from PIL import Image as PILImage
218
  import io
 
 
 
 
 
 
219
 
220
  @mcp.tool()
221
  def generate_image(width: int, height: int, color: str) -> Image:
 
209
  - **`str`**: Sent as `TextContent`.
210
  - **`dict`, `list`, Pydantic `BaseModel`**: Serialized to a JSON string and sent as `TextContent`.
211
  - **`bytes`**: Base64 encoded and sent as `BlobResourceContents` (often within an `EmbeddedResource`).
212
+ - **`fastmcp.Image`**: A helper class for easily returning image data. Sent as `ImageContent`.
213
  - **`None`**: Results in an empty response (no content is sent back to the client).
214
 
215
  ```python
216
+ from fastmcp import FastMCP, Image
 
217
  import io
218
+ try:
219
+ from PIL import Image as PILImage
220
+ except ImportError:
221
+ raise ImportError("Please install the `pillow` library to run this example.")
222
+
223
+ mcp = FastMCP("Image Demo")
224
 
225
  @mcp.tool()
226
  def generate_image(width: int, height: int, color: str) -> Image:
tests/tools/test_tool_manager.py CHANGED
@@ -2,8 +2,10 @@ import json
2
  import logging
3
 
4
  import pytest
 
5
  from pydantic import BaseModel
6
 
 
7
  from fastmcp.exceptions import NotFoundError, ToolError
8
  from fastmcp.tools import ToolManager
9
  from fastmcp.tools.tool import Tool
@@ -68,6 +70,18 @@ class TestAddTools:
68
  assert "age" in tool.parameters["$defs"]["UserInput"]["properties"]
69
  assert "flag" in tool.parameters["properties"]
70
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  def test_add_invalid_tool(self):
72
  manager = ToolManager()
73
  with pytest.raises(AttributeError):
@@ -263,7 +277,6 @@ class TestCallTools:
263
  result = await manager.call_tool("double", {"n": 5})
264
  assert isinstance(result, list)
265
  assert len(result) == 1
266
- from mcp.types import TextContent
267
 
268
  assert isinstance(result[0], TextContent)
269
  assert result[0].text == "10"
@@ -279,7 +292,6 @@ class TestCallTools:
279
  result = await manager.call_tool("add", {"a": 1})
280
  assert isinstance(result, list)
281
  assert len(result) == 1
282
- from mcp.types import TextContent
283
 
284
  assert isinstance(result[0], TextContent)
285
  assert result[0].text == "2"
@@ -307,7 +319,6 @@ class TestCallTools:
307
  manager = ToolManager()
308
  manager.add_tool_from_fn(sum_vals)
309
  # Try both with plain list and with JSON list
310
- from mcp.types import TextContent
311
 
312
  result = await manager.call_tool("sum_vals", {"vals": "[1, 2, 3]"})
313
  assert isinstance(result, list)
@@ -329,7 +340,6 @@ class TestCallTools:
329
 
330
  manager = ToolManager()
331
  manager.add_tool_from_fn(concat_strs)
332
- from mcp.types import TextContent
333
 
334
  # Try both with plain python object and with JSON list
335
  result = await manager.call_tool("concat_strs", {"vals": ["a", "b", "c"]})
@@ -357,10 +367,6 @@ class TestCallTools:
357
  assert result[0].text == '"a"'
358
 
359
  async def test_call_tool_with_complex_model(self):
360
- from mcp.types import TextContent
361
-
362
- from fastmcp import Context
363
-
364
  class MyShrimpTank(BaseModel):
365
  class Shrimp(BaseModel):
366
  name: str
@@ -397,8 +403,6 @@ class TestCallTools:
397
 
398
  class TestToolSchema:
399
  async def test_context_arg_excluded_from_schema(self):
400
- from fastmcp import Context
401
-
402
  def something(a: int, ctx: Context) -> int:
403
  return a
404
 
@@ -415,7 +419,6 @@ class TestContextHandling:
415
  def test_context_parameter_detection(self):
416
  """Test that context parameters are properly detected in
417
  Tool.from_function()."""
418
- from fastmcp import Context
419
 
420
  def tool_with_context(x: int, ctx: Context) -> str:
421
  return str(x)
@@ -432,9 +435,6 @@ class TestContextHandling:
432
 
433
  async def test_context_injection(self):
434
  """Test that context is properly injected during tool execution."""
435
- from mcp.types import TextContent
436
-
437
- from fastmcp import Context, FastMCP
438
 
439
  def tool_with_context(x: int, ctx: Context) -> str:
440
  assert isinstance(ctx, Context)
@@ -453,9 +453,6 @@ class TestContextHandling:
453
 
454
  async def test_context_injection_async(self):
455
  """Test that context is properly injected in async tools."""
456
- from mcp.types import TextContent
457
-
458
- from fastmcp import Context, FastMCP
459
 
460
  async def async_tool(x: int, ctx: Context) -> str:
461
  assert isinstance(ctx, Context)
@@ -476,8 +473,6 @@ class TestContextHandling:
476
  """Test that context is optional when calling tools."""
477
  from mcp.types import TextContent
478
 
479
- from fastmcp import Context
480
-
481
  def tool_with_context(x: int, ctx: Context | None = None) -> str:
482
  return str(x)
483
 
@@ -492,7 +487,6 @@ class TestContextHandling:
492
 
493
  async def test_context_error_handling(self):
494
  """Test error handling when context injection fails."""
495
- from fastmcp import Context, FastMCP
496
 
497
  def tool_with_context(x: int, ctx: Context) -> str:
498
  raise ValueError("Test error")
 
2
  import logging
3
 
4
  import pytest
5
+ from mcp.types import ImageContent, TextContent
6
  from pydantic import BaseModel
7
 
8
+ from fastmcp import Context, FastMCP, Image
9
  from fastmcp.exceptions import NotFoundError, ToolError
10
  from fastmcp.tools import ToolManager
11
  from fastmcp.tools.tool import Tool
 
70
  assert "age" in tool.parameters["$defs"]["UserInput"]["properties"]
71
  assert "flag" in tool.parameters["properties"]
72
 
73
+ async def test_tool_with_image_return(self):
74
+ def image_tool(data: bytes) -> Image:
75
+ return Image(data=data)
76
+
77
+ manager = ToolManager()
78
+ manager.add_tool_from_fn(image_tool)
79
+
80
+ tool = manager.get_tool("image_tool")
81
+ result = await tool.run({"data": "test.png"})
82
+ assert tool.parameters["properties"]["data"]["type"] == "string"
83
+ assert isinstance(result[0], ImageContent)
84
+
85
  def test_add_invalid_tool(self):
86
  manager = ToolManager()
87
  with pytest.raises(AttributeError):
 
277
  result = await manager.call_tool("double", {"n": 5})
278
  assert isinstance(result, list)
279
  assert len(result) == 1
 
280
 
281
  assert isinstance(result[0], TextContent)
282
  assert result[0].text == "10"
 
292
  result = await manager.call_tool("add", {"a": 1})
293
  assert isinstance(result, list)
294
  assert len(result) == 1
 
295
 
296
  assert isinstance(result[0], TextContent)
297
  assert result[0].text == "2"
 
319
  manager = ToolManager()
320
  manager.add_tool_from_fn(sum_vals)
321
  # Try both with plain list and with JSON list
 
322
 
323
  result = await manager.call_tool("sum_vals", {"vals": "[1, 2, 3]"})
324
  assert isinstance(result, list)
 
340
 
341
  manager = ToolManager()
342
  manager.add_tool_from_fn(concat_strs)
 
343
 
344
  # Try both with plain python object and with JSON list
345
  result = await manager.call_tool("concat_strs", {"vals": ["a", "b", "c"]})
 
367
  assert result[0].text == '"a"'
368
 
369
  async def test_call_tool_with_complex_model(self):
 
 
 
 
370
  class MyShrimpTank(BaseModel):
371
  class Shrimp(BaseModel):
372
  name: str
 
403
 
404
  class TestToolSchema:
405
  async def test_context_arg_excluded_from_schema(self):
 
 
406
  def something(a: int, ctx: Context) -> int:
407
  return a
408
 
 
419
  def test_context_parameter_detection(self):
420
  """Test that context parameters are properly detected in
421
  Tool.from_function()."""
 
422
 
423
  def tool_with_context(x: int, ctx: Context) -> str:
424
  return str(x)
 
435
 
436
  async def test_context_injection(self):
437
  """Test that context is properly injected during tool execution."""
 
 
 
438
 
439
  def tool_with_context(x: int, ctx: Context) -> str:
440
  assert isinstance(ctx, Context)
 
453
 
454
  async def test_context_injection_async(self):
455
  """Test that context is properly injected in async tools."""
 
 
 
456
 
457
  async def async_tool(x: int, ctx: Context) -> str:
458
  assert isinstance(ctx, Context)
 
473
  """Test that context is optional when calling tools."""
474
  from mcp.types import TextContent
475
 
 
 
476
  def tool_with_context(x: int, ctx: Context | None = None) -> str:
477
  return str(x)
478
 
 
487
 
488
  async def test_context_error_handling(self):
489
  """Test error handling when context injection fails."""
 
490
 
491
  def tool_with_context(x: int, ctx: Context) -> str:
492
  raise ValueError("Test error")