Add encoding parameter to FileResource (#3580)

* feat: Add encoding parameter to FileResource

- Add optional encoding field (str | None, default None) to FileResource.
- Pass encoding through to read_text() for cross-platform text file reading.
- Preserve backward compatibility by defaulting to system encoding.

* test: Add tests for FileResource encoding parameter

- Test UTF-8 reading with explicit encoding for non-ASCII content.
- Test backward compatibility when no encoding is specified.
- Test that encoding is ignored for binary file reads.
- Test Latin-1 reading with matching encoding.

* docs: Document FileResource encoding parameter

- Add encoding="utf-8" to FileResource example in resource classes guide.
- Update FileResource description to mention encoding support.

* feat: Change FileResource encoding default from None to utf-8

- Default to utf-8 instead of system encoding to prevent cross-platform footgun.
- Update field description to reflect new default.
- Update test to verify default encoding is utf-8 with non-ASCII content.
- Remove redundant encoding="utf-8" from docs example since it is now the default.
This commit is contained in:
Marcus Shu 2026-03-22 21:55:16 +08:00 committed by GitHub
commit 2d7bc4e73d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 70 additions and 2 deletions

View file

@ -349,7 +349,7 @@ if data_dir_path.is_dir():
- `TextResource`: For simple string content.
- `BinaryResource`: For raw `bytes` content.
- `FileResource`: Reads content from a local file path. Handles text/binary modes and lazy reading.
- `FileResource`: Reads content from a local file path. Handles text/binary modes, encoding, and lazy reading.
- `HttpResource`: Fetches content from an HTTP(S) URL (requires `httpx`).
- `DirectoryResource`: Lists files in a local directory (returns JSON).
- (`FunctionResource`: Internal class used by `@mcp.resource`).

View file

@ -65,6 +65,14 @@ class FileResource(Resource):
default="text/plain",
description="MIME type of the resource content",
)
encoding: str | None = Field(
default="utf-8",
description=(
"Encoding to use when reading text files. "
"Defaults to 'utf-8' for cross-platform compatibility. "
"Set to None to use the system default encoding."
),
)
@property
def _async_path(self) -> AsyncPath:
@ -94,7 +102,7 @@ class FileResource(Resource):
if self.is_binary:
content: str | bytes = await self._async_path.read_bytes()
else:
content = await self._async_path.read_text()
content = await self._async_path.read_text(encoding=self.encoding)
return ResourceResult(
contents=[ResourceContent(content=content, mime_type=self.mime_type)]
)

View file

@ -119,3 +119,63 @@ class TestFileResource:
await resource.read()
finally:
temp_file.chmod(0o644) # Restore permissions
async def test_read_utf8_with_encoding(self, tmp_path: Path):
"""FileResource should read UTF-8 files correctly when encoding is specified."""
content = (
"Smart quotes: \u201cleft\u201d and apostrophe\u2019s em-dash\u2014here"
)
file = tmp_path / "utf8_test.md"
file.write_text(content, encoding="utf-8")
resource = FileResource(
uri=FileUrl("file:///test/utf8"),
path=file,
mime_type="text/markdown",
encoding="utf-8",
)
result = await resource.read()
assert result.contents[0].content == content
async def test_default_encoding_is_utf8(self, tmp_path: Path):
"""FileResource defaults to UTF-8, reading non-ASCII without explicit encoding."""
content = "Smart quotes: \u201cleft\u201d and em-dash\u2014here"
file = tmp_path / "default_utf8_test.txt"
file.write_text(content, encoding="utf-8")
resource = FileResource(
uri=FileUrl("file:///test/default"),
path=file,
)
assert resource.encoding == "utf-8"
result = await resource.read()
assert result.contents[0].content == content
async def test_encoding_ignored_for_binary(self, tmp_path: Path):
"""Encoding field should be ignored when is_binary=True."""
data = b"\x00\x01\x02\xff"
file = tmp_path / "binary_test.bin"
file.write_bytes(data)
resource = FileResource(
uri=FileUrl("file:///test/binary"),
path=file,
mime_type="application/octet-stream",
encoding="utf-8",
)
result = await resource.read()
assert result.contents[0].content == data
async def test_read_latin1_with_encoding(self, tmp_path: Path):
"""FileResource should read non-UTF-8 files when correct encoding is specified."""
content = "na\u00efve"
file = tmp_path / "latin1_test.txt"
file.write_text(content, encoding="latin-1")
resource = FileResource(
uri=FileUrl("file:///test/latin1"),
path=file,
encoding="latin-1",
)
result = await resource.read()
assert result.contents[0].content == content