modelcontextprotocol / modelcontextprotocol/csharp-sdk
2.2.0: binary content blocks (ImageContentBlock, BlobResourceContents) serialize invalid base64 - client-side CallToolResult deserialization throws (regression of #1340)
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 4.5k
- Forks
- 814
- Avg merge
- 9d 19h
- Merged PRs (30d)
- 4
Description
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)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the provided console reproduction using McpJsonUtilities.DefaultOptions, then trace the polymorphic ContentBlock and ResourceContents serialization path and compare it with direct BlobResourceContents serialization. The fix is complete when ImageContentBlock data and BlobResourceContents blobs are emitted as valid base64 and CallToolResult round-trips without a JsonException.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100