diff --git a/src/mcp/server/mcpserver/resources/types.py b/src/mcp/server/mcpserver/resources/types.py index d9e472e36..51747d255 100644 --- a/src/mcp/server/mcpserver/resources/types.py +++ b/src/mcp/server/mcpserver/resources/types.py @@ -18,6 +18,27 @@ from mcp.shared._callable_inspection import is_async_callable from mcp.types import Annotations, Icon +_TEXT_LIKE_MIME_TYPES = { + "application/ecmascript", + "application/javascript", + "application/json", + "application/xml", + "application/x-yaml", + "application/yaml", + "image/svg+xml", +} + +_TEXT_LIKE_MIME_SUFFIXES = ("+json", "+xml", "+yaml") + + +def _is_text_like_mime_type(mime_type: str) -> bool: + media_type = mime_type.split(";", 1)[0].strip().lower() + return ( + media_type.startswith("text/") + or media_type in _TEXT_LIKE_MIME_TYPES + or media_type.endswith(_TEXT_LIKE_MIME_SUFFIXES) + ) + class TextResource(Resource): """A resource that reads from a string.""" @@ -139,7 +160,7 @@ def set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> boo if is_binary: return True mime_type = info.data.get("mime_type", "text/plain") - return not mime_type.startswith("text/") + return not _is_text_like_mime_type(mime_type) async def read(self) -> str | bytes: """Read the file content.""" diff --git a/tests/server/mcpserver/resources/test_file_resources.py b/tests/server/mcpserver/resources/test_file_resources.py index 94885113a..f3e0957aa 100644 --- a/tests/server/mcpserver/resources/test_file_resources.py +++ b/tests/server/mcpserver/resources/test_file_resources.py @@ -114,3 +114,60 @@ async def test_permission_error(self, temp_file: Path): # pragma: lax no cover await resource.read() finally: temp_file.chmod(0o644) # Restore permissions + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "mime_type", + [ + "application/json", + "application/ld+json; charset=utf-8", + "application/xml", + "application/atom+xml", + "application/x-yaml", + "image/svg+xml", + ], +) +async def test_file_resource_reads_text_like_mime_types_as_text(temp_file: Path, mime_type: str): + resource = FileResource( + uri=temp_file.as_uri(), + name="test", + path=temp_file, + mime_type=mime_type, + ) + + content = await resource.read() + + assert resource.is_binary is False + assert content == "test content" + + +@pytest.mark.anyio +async def test_file_resource_reads_octet_stream_as_binary(temp_file: Path): + resource = FileResource( + uri=temp_file.as_uri(), + name="test", + path=temp_file, + mime_type="application/octet-stream", + ) + + content = await resource.read() + + assert resource.is_binary is True + assert content == b"test content" + + +@pytest.mark.anyio +async def test_file_resource_explicit_binary_overrides_text_like_mime_type(temp_file: Path): + resource = FileResource( + uri=temp_file.as_uri(), + name="test", + path=temp_file, + mime_type="application/json", + is_binary=True, + ) + + content = await resource.read() + + assert resource.is_binary is True + assert content == b"test content"