Skip to content

Commit 0c7c333

Browse files
committed
Reject non-text codecs and clarify the migration edge cases
Validate encoding by decoding a probe byte instead of codecs.lookup, so registered-but-not-text codecs (base64_codec, rot13, hex) are rejected at construction alongside unknown names, rather than passing validation and failing on read. Scope the migration guide's "flips the other way" example to a newly textual type (application/xml) - text/* files were never blobs in v1 - and call out separately that text/* files previously decoded with the platform locale rather than UTF-8.
1 parent 769fd84 commit 0c7c333

3 files changed

Lines changed: 21 additions & 6 deletions

File tree

docs/migration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -838,7 +838,7 @@ The field is now `encoding: str | None`. A string means "decode with this encodi
838838

839839
Passing the removed `is_binary=` argument now raises a `ValidationError` at construction (see the section above) rather than being silently ignored. A misspelled `encoding` also fails at construction rather than on the first read.
840840

841-
One case flips the other way: a non-UTF-8 file with a textual mime type (say UTF-16 XML) previously shipped byte-exact as a blob and now fails to decode. Set `encoding` to the file's real encoding, or `encoding=None` to keep serving it as a blob.
841+
Two edge cases to check. A non-UTF-8 file with a *newly* textual mime type (say a UTF-16 `application/xml`) previously shipped byte-exact as a blob and now fails to decode. And an existing `text/*` file that was only readable through your platform's locale encoding (v1 decoded these with the locale, not UTF-8) now fails too. In both cases set `encoding` to the file's real encoding, or `encoding=None` to serve the bytes as a blob.
842842

843843
**Before (v1):**
844844

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from __future__ import annotations
44

5-
import codecs
65
import json
76
from collections.abc import Callable
87
from functools import partial
@@ -169,13 +168,17 @@ def validate_absolute_path(cls, path: Path) -> Path:
169168

170169
@pydantic.field_validator("encoding")
171170
@classmethod
172-
def validate_known_encoding(cls, encoding: str | None) -> str | None:
173-
"""Ensure the encoding names a known codec, so a typo fails at construction not at read."""
171+
def validate_text_encoding(cls, encoding: str | None) -> str | None:
172+
"""Ensure the encoding names a usable text codec, so a mistake fails at construction not at read."""
174173
if encoding is not None:
174+
# Decoding a probe byte rejects both unknown names and codecs that
175+
# aren't text encodings (base64_codec, rot13, ...) via LookupError.
175176
try:
176-
codecs.lookup(encoding)
177+
b"x".decode(encoding)
177178
except LookupError as e:
178-
raise ValueError(f"unknown encoding: {encoding}") from e
179+
raise ValueError(str(e)) from e
180+
except UnicodeError:
181+
pass # a real text encoding; the probe byte just doesn't decode in it
179182
return encoding
180183

181184
async def read(self) -> str | bytes:

tests/server/mcpserver/resources/test_file_resources.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,18 @@ def test_unknown_declared_charset_is_rejected(temp_file: Path):
124124
FileResource(uri=temp_file.as_uri(), path=temp_file, mime_type="text/plain; charset=not-a-codec")
125125

126126

127+
def test_multibyte_encoding_is_accepted(temp_file: Path):
128+
"""UTF-16 can't decode a lone probe byte but is still a valid text encoding."""
129+
resource = FileResource(uri=temp_file.as_uri(), path=temp_file, encoding="utf-16")
130+
assert resource.encoding == "utf-16"
131+
132+
133+
def test_non_text_codec_is_rejected(temp_file: Path):
134+
"""A registered codec that isn't a text encoding (bytes-to-bytes) is not a usable encoding."""
135+
with pytest.raises(ValidationError, match="not a text encoding"):
136+
FileResource(uri=temp_file.as_uri(), path=temp_file, encoding="base64_codec")
137+
138+
127139
@pytest.mark.anyio
128140
async def test_json_file_is_served_as_text_by_default(temp_file: Path):
129141
"""The mime type that motivated the encoding field: JSON must not become a base64 blob."""

0 commit comments

Comments
 (0)