Conversation
A tool annotated `-> bytes` publishes output_schema
{"result": {"type": "string", "format": "binary"}}, but real binary
data crashed the conversion: pydantic decodes bytes as UTF-8 when
serializing to JSON, so the first non-UTF-8 byte raised
PydanticSerializationError and the client only saw a generic
"Error executing tool" with the payload lost. UTF-8-decodable bytes came
back as a raw string, and a bytes field inside an output model hit the
same crash.
Encode bytes as base64 — the encoding every other bytes path in this
package already uses (BlobResourceContents, Image/Audio content) — in
both channels: the structured dump and the unstructured content. A
payload without bytes takes the plain JSON dump, byte for byte.
Fixes modelcontextprotocol#3554
|
This PR has been closed automatically. This repo only keeps pull requests open when they come from a maintainer, or from a contributor a maintainer has assigned to the linked issue, and you aren't currently assigned to #3554. If a maintainer assigns you to #3554, this PR reopens on its own and there's nothing more you need to do here. Assignment is a maintainer call based on capacity; comments that only ask to be assigned don't factor in. What does help is engaging on the issue itself by confirming the repro, explaining why it matters for your use case, or describing the approach you'd take. You're welcome to keep pushing commits here (just avoid force-pushing, since GitHub can't reopen a rewritten branch), but that on its own won't get the PR reviewed or the issue assigned, and realistically most auto-closed PRs stay closed. There's no need to open a new PR either way. CONTRIBUTING.md has the full reasoning, but in short:
Maintainers: reopen, remove |
Fixes #3554
Motivation and Context
A tool annotated
-> bytespublishesoutput_schema {"result": {"type": "string", "format": "binary"}}, but real binary data crashed the conversion instead of reaching the client: pydantic decodes bytes as UTF-8 when serializing to JSON, so the first non-UTF-8 byte raisedPydanticSerializationErrorinside_convert_to_content(andUnicodeDecodeErrorfrommodel_dump(mode="json")), and the client only saw a genericError executing tool <name>with the payload lost. UTF-8-decodable bytes came back as a raw string rather than an encoding, and a bytes field inside an output model (e.g.class Thumb(BaseModel): data: bytes) hit the same crash.This encodes bytes leaves as base64 — the encoding every other bytes path in this package already uses (
BlobResourceContentsatserver.py:475, andImage/Audiocontent) — in both channels: the structured dump and the unstructured content. A payload without bytes takes the plain JSON dump, byte for byte.How Has This Been Tested?
uv run --frozen pytest tests/server/mcpserver/test_func_metadata.py tests/server/mcpserver/test_integration.py: 74 passed, including 6 new tests (binary and UTF-8-> bytesin both channels, a bytes field in an output model,list[bytes]/dict[str, bytes], and a no-bytes regression guard) plus an end-to-end in-memoryClient(server)call of a-> bytestool.uv run --frozen pytest): 5966 passed, 2 failed. Both failures reproduce on unpatched main on Windows (verified withgit stash):test_sse_client_closes_all_streams_on_connection_errorandtest_safe_join_rejects_symlink_escape(WinError 1314, symlink privilege) — unrelated to this change.ruff check/ruff format --check: clean.pyright(strict): no new errors (34 before, 34 after; all pre-existing Windows-platform diagnostics).Breaking Changes
None for payloads without bytes. Tools returning bytes now deliver base64 in
structuredContentand in their text content, where binary data previously failed the call and UTF-8 bytes previously arrived as a raw string.Types of changes
Checklist
help wanted, or I'm a maintainer)AI assistance disclosure: the diff was written with the ZCode coding agent; I verified the reproduction on Windows (both failure paths), ran the test suites and linters listed above, and reviewed the serializer behaviour against
server.py:475/ Image/Audio before submitting.