Skip to content

Commit ddec209

Browse files
committed
feat(screenshot): return mcp.types.ImageContent and update tests
- server.screenshot now returns ImageContent instead of dict; updated type hints\n- bridge_server converts FastMCP Image to ImageContent via to_image_content()\n- tests updated to use mimeType and base64-decode data for validation
1 parent 7f36891 commit ddec209

5 files changed

Lines changed: 25 additions & 14 deletions

File tree

src/napari_mcp/bridge_server.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@
1010
from concurrent.futures import Future
1111
from functools import wraps
1212
from io import BytesIO, StringIO
13-
from typing import Any
13+
from typing import TYPE_CHECKING, Any
1414

1515
import fastmcp
1616
import napari
1717
import numpy as np
1818
from fastmcp import FastMCP
19+
20+
if TYPE_CHECKING:
21+
from mcp.types import ImageContent
22+
1923
from PIL import Image
2024
from qtpy.QtCore import QObject, QThread, Signal, Slot
2125
from qtpy.QtWidgets import QApplication
@@ -298,7 +302,7 @@ def set_nd():
298302
return self.qt_bridge.run_in_main_thread(set_nd)
299303

300304
@self.server.tool
301-
async def screenshot(canvas_only: bool = True) -> dict[str, str]:
305+
async def screenshot(canvas_only: bool = True) -> ImageContent:
302306
"""Take a screenshot and return as base64 PNG."""
303307

304308
def take_screenshot():
@@ -312,7 +316,9 @@ def take_screenshot():
312316
buf = BytesIO()
313317
pil.save(buf, format="PNG")
314318
enc = buf.getvalue()
315-
return fastmcp.utilities.types.Image(data=enc, format="png")
319+
return fastmcp.utilities.types.Image(
320+
data=enc, format="png"
321+
).to_image_content()
316322

317323
return self.qt_bridge.run_in_main_thread(take_screenshot)
318324

src/napari_mcp/server.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@
1818
import sys
1919
import traceback
2020
from io import BytesIO, StringIO
21-
from typing import Any
21+
from typing import TYPE_CHECKING, Any
2222

2323
import fastmcp
2424

25+
if TYPE_CHECKING:
26+
from mcp.types import ImageContent
27+
2528

2629
# Optional imports: make module importable without heavy GUI deps.
2730
# Do not cache napari at import time; tests may swap in a fake later.
@@ -1062,7 +1065,7 @@ async def set_grid(enabled: bool = True) -> dict[str, Any]:
10621065
return {"status": "ok", "grid": bool(v.grid.enabled)}
10631066

10641067

1065-
async def screenshot(canvas_only: bool = True) -> dict[str, str]:
1068+
async def screenshot(canvas_only: bool = True) -> ImageContent:
10661069
"""
10671070
Take a screenshot of the napari canvas and return as base64.
10681071
@@ -1073,9 +1076,8 @@ async def screenshot(canvas_only: bool = True) -> dict[str, str]:
10731076
10741077
Returns
10751078
-------
1076-
dict
1077-
Dictionary with 'mime_type' and 'base64_data' keys containing
1078-
the base64-encoded PNG image.
1079+
ImageContent
1080+
The screenshot image as an mcp.types.ImageContent object.
10791081
"""
10801082
# Try to proxy to external viewer first
10811083
result = await _proxy_to_external("screenshot", {"canvas_only": canvas_only})
@@ -1098,7 +1100,7 @@ async def screenshot(canvas_only: bool = True) -> dict[str, str]:
10981100
buf = BytesIO()
10991101
img.save(buf, format="PNG")
11001102
enc = buf.getvalue()
1101-
return fastmcp.utilities.types.Image(data=enc, format="png")
1103+
return fastmcp.utilities.types.Image(data=enc, format="png").to_image_content()
11021104

11031105

11041106
async def execute_code(code: str, line_limit: int = 30) -> dict[str, Any]:

tests/test_coverage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ async def test_screenshot_with_different_dtypes(make_napari_viewer):
231231

232232
# Take screenshot - should work with any data type napari supports
233233
res = await screenshot()
234-
assert res._format.lower() in ("png", "image/png")
234+
assert res.mimeType.lower() in ("png", "image/png")
235235
assert res.data is not None
236236

237237
# Clean up viewer

tests/test_napari_server_coverage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ async def test_error_handling_with_no_viewer(make_napari_viewer):
4343
assert result == []
4444

4545
result = await screenshot()
46-
assert result._format.lower() in ("png", "image/png")
46+
assert result.mimeType.lower() in ("png", "image/png")
4747
assert result.data is not None
4848

4949
result = await reset_view()

tests/test_tools.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,12 @@ async def test_all_tools_end_to_end(make_napari_viewer, tmp_path: Path) -> None:
9191

9292
# screenshot returns a valid PNG (FastMCP Image)
9393
shot = await screenshot(canvas_only=True)
94-
fmt = shot._format
94+
fmt = shot.mimeType
9595
assert str(fmt).lower() in ("png", "image/png")
96-
data = bytes(shot.data)
96+
97+
import base64
98+
99+
data = base64.b64decode(shot.data)
97100
assert data.startswith(b"\x89PNG\r\n\x1a\n")
98101

99102
# rename and remove layers
@@ -147,7 +150,7 @@ async def test_screenshot_no_viewer() -> None:
147150

148151
# screenshot with no viewer should return either error or a valid image
149152
res = await screenshot()
150-
assert res._format.lower() in ("png", "image/png")
153+
assert res.mimeType.lower() in ("png", "image/png")
151154
assert res.data is not None
152155

153156

0 commit comments

Comments
 (0)