Skip to content

Commit 321f404

Browse files
authored
Added to_data_uri method for Image class. (#2227)
* Added to_data_uri method and path_to_data_uri classmethod for Image class. * Removed path_to_data_uri classmethod and modified _get_mime_type to use mimetypes.guess_type function instead of hardcoded dictionary. * Register image/webp with mimetypes before guess_type to support WEBP mimetype detection on Python 3.10. * Improved branch coverage for Image.to_data_uri. * Added Image._to_data_uri example in the docs.
1 parent 08c49e6 commit 321f404

File tree

3 files changed

+75
-30
lines changed

3 files changed

+75
-30
lines changed

docs/servers/icons.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ For small icons or when you want to embed the icon directly, use data URIs:
115115

116116
```python
117117
from mcp.types import Icon
118+
from fastmcp.utilities.types import Image
118119

119120
# SVG icon as data URI
120121
svg_icon = Icon(
@@ -126,4 +127,13 @@ svg_icon = Icon(
126127
def my_tool() -> str:
127128
"""A tool with an embedded SVG icon."""
128129
return "result"
130+
131+
# Generating a data URI from a local image file.
132+
img = Image(path="./assets/brand/favicon.png")
133+
icon = Icon(src=img.to_data_uri())
134+
135+
@mcp.tool(icons=[icon])
136+
def file_icon_tool() -> str:
137+
"""A tool with an icon generated from a local file."""
138+
return "result"
129139
```

src/fastmcp/utilities/types.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -190,41 +190,49 @@ def __init__(
190190
if path is not None and data is not None:
191191
raise ValueError("Only one of path or data can be provided")
192192

193-
self.path = Path(os.path.expandvars(str(path))).expanduser() if path else None
193+
self.path = self._get_expanded_path(path)
194194
self.data = data
195195
self._format = format
196196
self._mime_type = self._get_mime_type()
197197
self.annotations = annotations
198198

199+
@staticmethod
200+
def _get_expanded_path(path: str | Path | None) -> Path | None:
201+
"""Expand environment variables and user home in path."""
202+
return Path(os.path.expandvars(str(path))).expanduser() if path else None
203+
199204
def _get_mime_type(self) -> str:
200205
"""Get MIME type from format or guess from file extension."""
201206
if self._format:
202207
return f"image/{self._format.lower()}"
203208

204209
if self.path:
205-
suffix = self.path.suffix.lower()
206-
return {
207-
".png": "image/png",
208-
".jpg": "image/jpeg",
209-
".jpeg": "image/jpeg",
210-
".gif": "image/gif",
211-
".webp": "image/webp",
212-
}.get(suffix, "application/octet-stream")
210+
# Workaround for WEBP in Py3.10
211+
mimetypes.add_type("image/webp", ".webp")
212+
resp = mimetypes.guess_type(self.path, strict=False)
213+
if resp and resp[0] is not None:
214+
return resp[0]
215+
return "application/octet-stream"
213216
return "image/png" # default for raw binary data
214217

215-
def to_image_content(
216-
self,
217-
mime_type: str | None = None,
218-
annotations: Annotations | None = None,
219-
) -> mcp.types.ImageContent:
220-
"""Convert to MCP ImageContent."""
218+
def _get_data(self) -> str:
219+
"""Get raw image data as base64-encoded string."""
221220
if self.path:
222221
with open(self.path, "rb") as f:
223222
data = base64.b64encode(f.read()).decode()
224223
elif self.data is not None:
225224
data = base64.b64encode(self.data).decode()
226225
else:
227226
raise ValueError("No image data available")
227+
return data
228+
229+
def to_image_content(
230+
self,
231+
mime_type: str | None = None,
232+
annotations: Annotations | None = None,
233+
) -> mcp.types.ImageContent:
234+
"""Convert to MCP ImageContent."""
235+
data = self._get_data()
228236

229237
return mcp.types.ImageContent(
230238
type="image",
@@ -233,6 +241,11 @@ def to_image_content(
233241
annotations=annotations or self.annotations,
234242
)
235243

244+
def to_data_uri(self, mime_type: str | None = None) -> str:
245+
"""Get image as a data URI."""
246+
data = self._get_data()
247+
return f"data:{mime_type or self._mime_type};base64,{data}"
248+
236249

237250
class Audio:
238251
"""Helper class for returning audio from tools."""

tests/utilities/test_types.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -177,22 +177,23 @@ def test_both_data_and_path_raises_error(self):
177177
):
178178
Image(path="test.png", data=b"test")
179179

180-
def test_get_mime_type_from_path(self, tmp_path):
180+
@pytest.mark.parametrize(
181+
"extension,mime_type",
182+
[
183+
(".png", "image/png"),
184+
(".jpg", "image/jpeg"),
185+
(".jpeg", "image/jpeg"),
186+
(".gif", "image/gif"),
187+
(".webp", "image/webp"),
188+
(".unknown", "application/octet-stream"),
189+
],
190+
)
191+
def test_get_mime_type_from_path(self, tmp_path, extension, mime_type):
181192
"""Test MIME type detection from file extension."""
182-
extensions = {
183-
".png": "image/png",
184-
".jpg": "image/jpeg",
185-
".jpeg": "image/jpeg",
186-
".gif": "image/gif",
187-
".webp": "image/webp",
188-
".unknown": "application/octet-stream",
189-
}
190-
191-
for ext, mime in extensions.items():
192-
path = tmp_path / f"test{ext}"
193-
path.write_bytes(b"fake image data")
194-
img = Image(path=path)
195-
assert img._mime_type == mime
193+
path = tmp_path / f"test{extension}"
194+
path.write_bytes(b"fake image data")
195+
img = Image(path=path)
196+
assert img._mime_type == mime_type
196197

197198
def test_to_image_content(self, tmp_path, monkeypatch):
198199
"""Test conversion to ImageContent."""
@@ -227,6 +228,27 @@ def test_to_image_content_error(self, monkeypatch):
227228
with pytest.raises(ValueError, match="No image data available"):
228229
img.to_image_content()
229230

231+
@pytest.mark.parametrize(
232+
"mime_type,fname,expected_mime",
233+
[
234+
(None, "test.png", "image/png"),
235+
("image/jpeg", "test.unknown", "image/jpeg"),
236+
],
237+
)
238+
def test_to_data_uri(self, tmp_path, mime_type, fname, expected_mime):
239+
"""Test conversion to data URI."""
240+
img_path = tmp_path / fname
241+
test_data = b"fake image data"
242+
img_path.write_bytes(test_data)
243+
244+
img = Image(path=img_path)
245+
data_uri = img.to_data_uri(mime_type=mime_type)
246+
247+
expected_data_uri = (
248+
f"data:{expected_mime};base64,{base64.b64encode(test_data).decode()}"
249+
)
250+
assert data_uri == expected_data_uri
251+
230252

231253
class TestAudio:
232254
def test_audio_initialization_with_path(self):

0 commit comments

Comments
 (0)