Skip to content

Commit 43bee4a

Browse files
fix(mcpserver): normalize Image and Audio format to registered MIME types
Closes #3585
1 parent f1b6589 commit 43bee4a

2 files changed

Lines changed: 114 additions & 19 deletions

File tree

‎src/mcp/server/mcpserver/utilities/types.py‎

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@
55

66
from mcp_types import AudioContent, ImageContent
77

8+
IMAGE_MIME_TYPES: dict[str, str] = {
9+
"png": "image/png",
10+
"jpg": "image/jpeg",
11+
"jpeg": "image/jpeg",
12+
"gif": "image/gif",
13+
"webp": "image/webp",
14+
}
15+
16+
AUDIO_MIME_TYPES: dict[str, str] = {
17+
"wav": "audio/wav",
18+
"mp3": "audio/mpeg",
19+
"ogg": "audio/ogg",
20+
"flac": "audio/flac",
21+
"aac": "audio/aac",
22+
"m4a": "audio/mp4",
23+
}
24+
825

926
class Image:
1027
"""Helper class for returning images from tools."""
@@ -28,17 +45,12 @@ def __init__(
2845
def _get_mime_type(self) -> str:
2946
"""Get MIME type from format or guess from file extension."""
3047
if self._format:
31-
return f"image/{self._format.lower()}"
48+
fmt = self._format.lower().lstrip(".")
49+
return IMAGE_MIME_TYPES.get(fmt, f"image/{fmt}")
3250

3351
if self.path:
34-
suffix = self.path.suffix.lower()
35-
return {
36-
".png": "image/png",
37-
".jpg": "image/jpeg",
38-
".jpeg": "image/jpeg",
39-
".gif": "image/gif",
40-
".webp": "image/webp",
41-
}.get(suffix, "application/octet-stream")
52+
suffix = self.path.suffix.lower().lstrip(".")
53+
return IMAGE_MIME_TYPES.get(suffix, "application/octet-stream")
4254
return "image/png" # default for raw binary data
4355

4456
def to_image_content(self) -> ImageContent:
@@ -74,18 +86,12 @@ def __init__(
7486
def _get_mime_type(self) -> str:
7587
"""Get MIME type from format or guess from file extension."""
7688
if self._format:
77-
return f"audio/{self._format.lower()}"
89+
fmt = self._format.lower().lstrip(".")
90+
return AUDIO_MIME_TYPES.get(fmt, f"audio/{fmt}")
7891

7992
if self.path:
80-
suffix = self.path.suffix.lower()
81-
return {
82-
".wav": "audio/wav",
83-
".mp3": "audio/mpeg",
84-
".ogg": "audio/ogg",
85-
".flac": "audio/flac",
86-
".aac": "audio/aac",
87-
".m4a": "audio/mp4",
88-
}.get(suffix, "application/octet-stream")
93+
suffix = self.path.suffix.lower().lstrip(".")
94+
return AUDIO_MIME_TYPES.get(suffix, "application/octet-stream")
8995
return "audio/wav" # default for raw binary data
9096

9197
def to_audio_content(self) -> AudioContent:
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
from mcp.server.mcpserver import Audio, Image
6+
7+
8+
@pytest.mark.parametrize(
9+
("format_val", "expected_mime"),
10+
[
11+
("jpg", "image/jpeg"),
12+
("jpeg", "image/jpeg"),
13+
("png", "image/png"),
14+
("gif", "image/gif"),
15+
("webp", "image/webp"),
16+
("bmp", "image/bmp"),
17+
(".jpg", "image/jpeg"),
18+
("JPG", "image/jpeg"),
19+
],
20+
)
21+
def test_image_format_mime_types(format_val: str, expected_mime: str) -> None:
22+
img = Image(data=b"\x00", format=format_val)
23+
assert img.to_image_content().mime_type == expected_mime
24+
25+
26+
@pytest.mark.parametrize(
27+
("filename", "expected_mime"),
28+
[
29+
("chart.jpg", "image/jpeg"),
30+
("chart.jpeg", "image/jpeg"),
31+
("chart.png", "image/png"),
32+
("chart.gif", "image/gif"),
33+
("chart.webp", "image/webp"),
34+
("chart.unknown", "application/octet-stream"),
35+
],
36+
)
37+
def test_image_path_mime_types(filename: str, expected_mime: str, tmp_path: Path) -> None:
38+
file_path = tmp_path / filename
39+
file_path.write_bytes(b"\x00")
40+
img = Image(path=file_path)
41+
assert img.to_image_content().mime_type == expected_mime
42+
43+
44+
def test_image_raw_data_default_mime_type() -> None:
45+
img = Image(data=b"\x00")
46+
assert img.to_image_content().mime_type == "image/png"
47+
48+
49+
@pytest.mark.parametrize(
50+
("format_val", "expected_mime"),
51+
[
52+
("mp3", "audio/mpeg"),
53+
("m4a", "audio/mp4"),
54+
("wav", "audio/wav"),
55+
("ogg", "audio/ogg"),
56+
("flac", "audio/flac"),
57+
("aac", "audio/aac"),
58+
("opus", "audio/opus"),
59+
(".mp3", "audio/mpeg"),
60+
("MP3", "audio/mpeg"),
61+
],
62+
)
63+
def test_audio_format_mime_types(format_val: str, expected_mime: str) -> None:
64+
audio = Audio(data=b"\x00", format=format_val)
65+
assert audio.to_audio_content().mime_type == expected_mime
66+
67+
68+
@pytest.mark.parametrize(
69+
("filename", "expected_mime"),
70+
[
71+
("chime.mp3", "audio/mpeg"),
72+
("chime.m4a", "audio/mp4"),
73+
("chime.wav", "audio/wav"),
74+
("chime.ogg", "audio/ogg"),
75+
("chime.flac", "audio/flac"),
76+
("chime.aac", "audio/aac"),
77+
("chime.unknown", "application/octet-stream"),
78+
],
79+
)
80+
def test_audio_path_mime_types(filename: str, expected_mime: str, tmp_path: Path) -> None:
81+
file_path = tmp_path / filename
82+
file_path.write_bytes(b"\x00")
83+
audio = Audio(path=file_path)
84+
assert audio.to_audio_content().mime_type == expected_mime
85+
86+
87+
def test_audio_raw_data_default_mime_type() -> None:
88+
audio = Audio(data=b"\x00")
89+
assert audio.to_audio_content().mime_type == "audio/wav"

0 commit comments

Comments
 (0)