Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/mcp/server/mcpserver/resources/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down
57 changes: 57 additions & 0 deletions tests/server/mcpserver/resources/test_file_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading