Description
Binary content blocks (ImageContentBlock, EmbeddedResourceBlock with BlobResourceContents) round-trip incorrectly through McpJsonUtilities.DefaultOptions in 2.2.0: the writer does not emit valid base64 for the binary payload, so deserializing the produced JSON throws
System.Text.Json.JsonException: The JSON value could not be converted to ModelContextProtocol.Protocol.ResourceContents.
---> System.FormatException: Cannot decode JSON text that is not encoded as valid Base64 to bytes.
at System.Text.Json.Utf8JsonReader.GetBytesFromBase64()
at ModelContextProtocol.Protocol.ResourceContents.Converter.Read(...)
In practice this means a tool that returns a PDF via EmbeddedResourceBlock/BlobResourceContents (or an image via ImageContentBlock) over the Streamable HTTP transport breaks the client-side deserialization of the whole CallToolResult — the client (McpClient.CallToolAsync) throws and the result is lost.
This looks like a regression of #1340 / #1064: ImageContentBlock.Data is serialized as the raw bytes reinterpreted as a UTF-16 string (mostly U+FFFD replacement characters and escaped control characters in the JSON), not as base64.
Repro (console app, ModelContextProtocol.Core 2.2.0, net10.0)
using System.Text.Json;
using ModelContextProtocol;
using ModelContextProtocol.Protocol;
var bytes = new byte[900_000];
Random.Shared.NextBytes(bytes);
var result = new CallToolResult
{
Content =
[
new EmbeddedResourceBlock
{
Resource = new BlobResourceContents
{
Uri = "demo://files/1/test.pdf",
MimeType = "application/pdf",
Blob = bytes,
},
},
],
};
var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions);
// throws FormatException("Cannot decode JSON text that is not encoded as valid Base64 to bytes."):
var back = JsonSerializer.Deserialize<CallToolResult>(json, McpJsonUtilities.DefaultOptions);
Same shape for images: serializing new ImageContentBlock { Data = bytes, MimeType = "image/png" } as ContentBlock produces a "data" property that is not base64 (raw bytes as string), and deserializing it back throws the same FormatException.
Interestingly, serializing the BlobResourceContents directly (not through the polymorphic CallToolResult/ContentBlock path) produces correct base64 — the asymmetry sits in the polymorphic converter path.
Expected behavior
blob/data are written as base64 strings (per spec) and round-trip through McpJsonUtilities.DefaultOptions.
Environment
- ModelContextProtocol / ModelContextProtocol.Core / ModelContextProtocol.AspNetCore 2.2.0
- .NET 10.0 (net10.0), macOS arm64
- Observed both in the minimal repro above and end-to-end over the Streamable HTTP transport (ASP.NET Core server to McpClient)
Description
Binary content blocks (
ImageContentBlock,EmbeddedResourceBlockwithBlobResourceContents) round-trip incorrectly throughMcpJsonUtilities.DefaultOptionsin 2.2.0: the writer does not emit valid base64 for the binary payload, so deserializing the produced JSON throwsIn practice this means a tool that returns a PDF via
EmbeddedResourceBlock/BlobResourceContents(or an image viaImageContentBlock) over the Streamable HTTP transport breaks the client-side deserialization of the wholeCallToolResult— the client (McpClient.CallToolAsync) throws and the result is lost.This looks like a regression of #1340 / #1064:
ImageContentBlock.Datais serialized as the raw bytes reinterpreted as a UTF-16 string (mostly U+FFFD replacement characters and escaped control characters in the JSON), not as base64.Repro (console app,
ModelContextProtocol.Core2.2.0, net10.0)Same shape for images: serializing
new ImageContentBlock { Data = bytes, MimeType = "image/png" }asContentBlockproduces a"data"property that is not base64 (raw bytes as string), and deserializing it back throws the sameFormatException.Interestingly, serializing the
BlobResourceContentsdirectly (not through the polymorphicCallToolResult/ContentBlockpath) produces correct base64 — the asymmetry sits in the polymorphic converter path.Expected behavior
blob/dataare written as base64 strings (per spec) and round-trip throughMcpJsonUtilities.DefaultOptions.Environment