Skip to content

Commit 975c7f7

Browse files
committed
fix(server): classify structured file MIME types as text
1 parent 161834d commit 975c7f7

2 files changed

Lines changed: 79 additions & 1 deletion

File tree

src/mcp/server/mcpserver/resources/types.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,27 @@
1818
from mcp.shared._callable_inspection import is_async_callable
1919
from mcp.types import Annotations, Icon
2020

21+
_TEXT_LIKE_MIME_TYPES = {
22+
"application/ecmascript",
23+
"application/javascript",
24+
"application/json",
25+
"application/xml",
26+
"application/x-yaml",
27+
"application/yaml",
28+
"image/svg+xml",
29+
}
30+
31+
_TEXT_LIKE_MIME_SUFFIXES = ("+json", "+xml", "+yaml")
32+
33+
34+
def _is_text_like_mime_type(mime_type: str) -> bool:
35+
media_type = mime_type.split(";", 1)[0].strip().lower()
36+
return (
37+
media_type.startswith("text/")
38+
or media_type in _TEXT_LIKE_MIME_TYPES
39+
or media_type.endswith(_TEXT_LIKE_MIME_SUFFIXES)
40+
)
41+
2142

2243
class TextResource(Resource):
2344
"""A resource that reads from a string."""
@@ -139,7 +160,7 @@ def set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> boo
139160
if is_binary:
140161
return True
141162
mime_type = info.data.get("mime_type", "text/plain")
142-
return not mime_type.startswith("text/")
163+
return not _is_text_like_mime_type(mime_type)
143164

144165
async def read(self) -> str | bytes:
145166
"""Read the file content."""

tests/server/mcpserver/resources/test_file_resources.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,60 @@ async def test_permission_error(self, temp_file: Path): # pragma: lax no cover
114114
await resource.read()
115115
finally:
116116
temp_file.chmod(0o644) # Restore permissions
117+
118+
119+
@pytest.mark.anyio
120+
@pytest.mark.parametrize(
121+
"mime_type",
122+
[
123+
"application/json",
124+
"application/ld+json; charset=utf-8",
125+
"application/xml",
126+
"application/atom+xml",
127+
"application/x-yaml",
128+
"image/svg+xml",
129+
],
130+
)
131+
async def test_file_resource_reads_text_like_mime_types_as_text(temp_file: Path, mime_type: str):
132+
resource = FileResource(
133+
uri=temp_file.as_uri(),
134+
name="test",
135+
path=temp_file,
136+
mime_type=mime_type,
137+
)
138+
139+
content = await resource.read()
140+
141+
assert resource.is_binary is False
142+
assert content == "test content"
143+
144+
145+
@pytest.mark.anyio
146+
async def test_file_resource_reads_octet_stream_as_binary(temp_file: Path):
147+
resource = FileResource(
148+
uri=temp_file.as_uri(),
149+
name="test",
150+
path=temp_file,
151+
mime_type="application/octet-stream",
152+
)
153+
154+
content = await resource.read()
155+
156+
assert resource.is_binary is True
157+
assert content == b"test content"
158+
159+
160+
@pytest.mark.anyio
161+
async def test_file_resource_explicit_binary_overrides_text_like_mime_type(temp_file: Path):
162+
resource = FileResource(
163+
uri=temp_file.as_uri(),
164+
name="test",
165+
path=temp_file,
166+
mime_type="application/json",
167+
is_binary=True,
168+
)
169+
170+
content = await resource.read()
171+
172+
assert resource.is_binary is True
173+
assert content == b"test content"

0 commit comments

Comments
 (0)