Skip to content

Commit 29fa71b

Browse files
committed
fix blob and content block data handling / transcoding
1 parent c3377d7 commit 29fa71b

File tree

3 files changed

+147
-44
lines changed

3 files changed

+147
-44
lines changed

src/ModelContextProtocol.Core/Protocol/BlobResourceContents.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public required string Blob
5555
set
5656
{
5757
_blob = value;
58-
_blobUtf8 = System.Text.Encoding.UTF8.GetBytes(value);
58+
_blobUtf8 = Encoding.UTF8.GetBytes(value);
5959
_decodedData = default; // Invalidate cache
6060
}
6161
}
@@ -73,7 +73,7 @@ public ReadOnlyMemory<byte> BlobUtf8
7373
? _blob is null
7474
? _decodedData.IsEmpty
7575
? ReadOnlyMemory<byte>.Empty
76-
: EncodeToUtf8( _decodedData )
76+
: EncodeToUtf8(_decodedData)
7777
: Encoding.UTF8.GetBytes(_blob)
7878
: _blobUtf8;
7979
set
@@ -98,6 +98,11 @@ private ReadOnlyMemory<byte> EncodeToUtf8(ReadOnlyMemory<byte> decodedData)
9898
}
9999
}
100100

101+
[JsonIgnore]
102+
internal bool HasBlobUtf8 => !_blobUtf8.IsEmpty;
103+
104+
internal ReadOnlySpan<byte> GetBlobUtf8Span() => _blobUtf8.Span;
105+
101106
/// <summary>
102107
/// Gets the decoded data represented by <see cref="BlobUtf8"/>.
103108
/// </summary>
@@ -118,12 +123,8 @@ public ReadOnlyMemory<byte> DecodedData
118123
_decodedData = Convert.FromBase64String(_blob);
119124
return _decodedData;
120125
}
121-
#if NET
122126
// Decode directly from UTF-8 base64 bytes without string intermediate
123127
int maxLength = Base64.GetMaxDecodedFromUtf8Length(BlobUtf8.Length);
124-
#else
125-
int maxLength = ((BlobUtf8.Length + 3) / 4) * 3;
126-
#endif
127128
byte[] buffer = new byte[maxLength];
128129
if (Base64.DecodeFromUtf8(BlobUtf8.Span, buffer, out _, out int bytesWritten) == System.Buffers.OperationStatus.Done)
129130
{

src/ModelContextProtocol.Core/Protocol/ContentBlock.cs

Lines changed: 128 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Buffers;
2+
using System.Buffers.Text;
23
using System.ComponentModel;
34
using System.Diagnostics;
45
using System.Diagnostics.CodeAnalysis;
@@ -98,7 +99,7 @@ internal Converter(bool materializeUtf8TextContentBlocks) =>
9899
string? type = null;
99100
ReadOnlyMemory<byte>? utf8Text = null;
100101
string? name = null;
101-
string? data = null;
102+
ReadOnlyMemory<byte>? dataUtf8 = null;
102103
string? mimeType = null;
103104
string? uri = null;
104105
string? description = null;
@@ -141,7 +142,7 @@ internal Converter(bool materializeUtf8TextContentBlocks) =>
141142
break;
142143

143144
case "data":
144-
data = reader.GetString();
145+
dataUtf8 = ReadUtf8StringValueAsBytes(ref reader);
145146
break;
146147

147148
case "mimeType":
@@ -229,13 +230,13 @@ internal Converter(bool materializeUtf8TextContentBlocks) =>
229230

230231
"image" => new ImageContentBlock
231232
{
232-
Data = data ?? throw new JsonException("Image data must be provided for 'image' type."),
233+
DataUtf8 = dataUtf8 ?? throw new JsonException("Image data must be provided for 'image' type."),
233234
MimeType = mimeType ?? throw new JsonException("MIME type must be provided for 'image' type."),
234235
},
235236

236237
"audio" => new AudioContentBlock
237238
{
238-
Data = data ?? throw new JsonException("Audio data must be provided for 'audio' type."),
239+
DataUtf8 = dataUtf8 ?? throw new JsonException("Audio data must be provided for 'audio' type."),
239240
MimeType = mimeType ?? throw new JsonException("MIME type must be provided for 'audio' type."),
240241
},
241242

@@ -277,7 +278,7 @@ internal Converter(bool materializeUtf8TextContentBlocks) =>
277278
return block;
278279
}
279280

280-
private static ReadOnlyMemory<byte> ReadUtf8StringValueAsBytes(ref Utf8JsonReader reader)
281+
internal static ReadOnlyMemory<byte> ReadUtf8StringValueAsBytes(ref Utf8JsonReader reader)
281282
{
282283
if (reader.TokenType != JsonTokenType.String)
283284
{
@@ -488,12 +489,26 @@ public override void Write(Utf8JsonWriter writer, ContentBlock value, JsonSerial
488489
break;
489490

490491
case ImageContentBlock imageContent:
491-
writer.WriteString("data", imageContent.Data);
492+
if (imageContent.HasDataUtf8)
493+
{
494+
writer.WriteString("data", imageContent.GetDataUtf8Span());
495+
}
496+
else
497+
{
498+
writer.WriteString("data", imageContent.Data);
499+
}
492500
writer.WriteString("mimeType", imageContent.MimeType);
493501
break;
494502

495503
case AudioContentBlock audioContent:
496-
writer.WriteString("data", audioContent.Data);
504+
if (audioContent.HasDataUtf8)
505+
{
506+
writer.WriteString("data", audioContent.GetDataUtf8Span());
507+
}
508+
else
509+
{
510+
writer.WriteString("data", audioContent.Data);
511+
}
497512
writer.WriteString("mimeType", audioContent.MimeType);
498513
break;
499514

@@ -675,9 +690,9 @@ public static implicit operator Utf8TextContentBlock(TextContentBlock text)
675690
[DebuggerDisplay("{DebuggerDisplay,nq}")]
676691
public sealed class ImageContentBlock : ContentBlock
677692
{
678-
private byte[]? _dataUtf8;
679-
private byte[]? _decodedData;
680-
private string _data = string.Empty;
693+
private ReadOnlyMemory<byte> _dataUtf8;
694+
private ReadOnlyMemory<byte> _decodedData;
695+
private string? _data;
681696

682697
/// <inheritdoc/>
683698
public override string Type => "image";
@@ -686,14 +701,16 @@ public sealed class ImageContentBlock : ContentBlock
686701
/// Gets or sets the base64-encoded image data.
687702
/// </summary>
688703
[JsonPropertyName("data")]
689-
public required string Data
704+
public string Data
690705
{
691-
get => _data;
706+
get => _data ??= !_dataUtf8.IsEmpty
707+
? Core.McpTextUtilities.GetStringFromUtf8(_dataUtf8.Span)
708+
: string.Empty;
692709
set
693710
{
694711
_data = value;
695-
_dataUtf8 = null;
696-
_decodedData = null;
712+
_dataUtf8 = System.Text.Encoding.UTF8.GetBytes(value);
713+
_decodedData = default; // Invalidate cache
697714
}
698715
}
699716

@@ -703,20 +720,54 @@ public required string Data
703720
[JsonIgnore]
704721
public ReadOnlyMemory<byte> DataUtf8
705722
{
706-
get => _dataUtf8 ??= System.Text.Encoding.UTF8.GetBytes(Data);
723+
get => _dataUtf8.IsEmpty
724+
? _data is null
725+
? ReadOnlyMemory<byte>.Empty
726+
: System.Text.Encoding.UTF8.GetBytes(_data)
727+
: _dataUtf8;
707728
set
708729
{
709-
_dataUtf8 = value.Span.ToArray();
710-
_data = System.Text.Encoding.UTF8.GetString(_dataUtf8);
711-
_decodedData = null;
730+
_data = null;
731+
_dataUtf8 = value;
732+
_decodedData = default; // Invalidate cache
712733
}
713734
}
714735

715736
/// <summary>
716-
/// Gets the decoded image data represented by <see cref="Data"/>.
737+
/// Gets the decoded image data represented by <see cref="DataUtf8"/>.
717738
/// </summary>
739+
/// <remarks>
740+
/// Accessing this member will decode the value in <see cref="DataUtf8"/> and cache the result.
741+
/// Subsequent accesses return the cached value unless <see cref="Data"/> or <see cref="DataUtf8"/> is modified.
742+
/// </remarks>
718743
[JsonIgnore]
719-
public ReadOnlyMemory<byte> DecodedData => _decodedData ??= Convert.FromBase64String(Data);
744+
public ReadOnlyMemory<byte> DecodedData
745+
{
746+
get
747+
{
748+
if (_decodedData.IsEmpty)
749+
{
750+
if (_data is not null)
751+
{
752+
_decodedData = Convert.FromBase64String(_data);
753+
return _decodedData;
754+
}
755+
756+
int maxLength = Base64.GetMaxDecodedFromUtf8Length(DataUtf8.Length);
757+
byte[] buffer = new byte[maxLength];
758+
if (Base64.DecodeFromUtf8(DataUtf8.Span, buffer, out _, out int bytesWritten) == OperationStatus.Done)
759+
{
760+
_decodedData = bytesWritten == maxLength ? buffer : buffer.AsMemory(0, bytesWritten).ToArray();
761+
}
762+
else
763+
{
764+
throw new FormatException("Invalid base64 data");
765+
}
766+
}
767+
768+
return _decodedData;
769+
}
770+
}
720771

721772
/// <summary>
722773
/// Gets or sets the MIME type (or "media type") of the content, specifying the format of the data.
@@ -727,6 +778,10 @@ public ReadOnlyMemory<byte> DataUtf8
727778
[JsonPropertyName("mimeType")]
728779
public required string MimeType { get; set; }
729780

781+
internal bool HasDataUtf8 => !_dataUtf8.IsEmpty;
782+
783+
internal ReadOnlySpan<byte> GetDataUtf8Span() => _dataUtf8.Span;
784+
730785
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
731786
private string DebuggerDisplay => $"MimeType = {MimeType}, Length = {DebuggerDisplayHelper.GetBase64LengthDisplay(Data)}";
732787
}
@@ -735,9 +790,9 @@ public ReadOnlyMemory<byte> DataUtf8
735790
[DebuggerDisplay("{DebuggerDisplay,nq}")]
736791
public sealed class AudioContentBlock : ContentBlock
737792
{
738-
private byte[]? _dataUtf8;
739-
private byte[]? _decodedData;
740-
private string _data = string.Empty;
793+
private ReadOnlyMemory<byte> _dataUtf8;
794+
private ReadOnlyMemory<byte> _decodedData;
795+
private string? _data;
741796

742797
/// <inheritdoc/>
743798
public override string Type => "audio";
@@ -746,14 +801,16 @@ public sealed class AudioContentBlock : ContentBlock
746801
/// Gets or sets the base64-encoded audio data.
747802
/// </summary>
748803
[JsonPropertyName("data")]
749-
public required string Data
804+
public string Data
750805
{
751-
get => _data;
806+
get => _data ??= !_dataUtf8.IsEmpty
807+
? Core.McpTextUtilities.GetStringFromUtf8(_dataUtf8.Span)
808+
: string.Empty;
752809
set
753810
{
754811
_data = value;
755-
_dataUtf8 = null;
756-
_decodedData = null;
812+
_dataUtf8 = System.Text.Encoding.UTF8.GetBytes(value);
813+
_decodedData = default; // Invalidate cache
757814
}
758815
}
759816

@@ -763,20 +820,54 @@ public required string Data
763820
[JsonIgnore]
764821
public ReadOnlyMemory<byte> DataUtf8
765822
{
766-
get => _dataUtf8 ??= System.Text.Encoding.UTF8.GetBytes(Data);
823+
get => _dataUtf8.IsEmpty
824+
? _data is null
825+
? ReadOnlyMemory<byte>.Empty
826+
: System.Text.Encoding.UTF8.GetBytes(_data)
827+
: _dataUtf8;
767828
set
768829
{
769-
_dataUtf8 = value.Span.ToArray();
770-
_data = System.Text.Encoding.UTF8.GetString(_dataUtf8);
771-
_decodedData = null;
830+
_data = null;
831+
_dataUtf8 = value;
832+
_decodedData = default; // Invalidate cache
772833
}
773834
}
774835

775836
/// <summary>
776-
/// Gets the decoded audio data represented by <see cref="Data"/>.
837+
/// Gets the decoded audio data represented by <see cref="DataUtf8"/>.
777838
/// </summary>
839+
/// <remarks>
840+
/// Accessing this member will decode the value in <see cref="DataUtf8"/> and cache the result.
841+
/// Subsequent accesses return the cached value unless <see cref="Data"/> or <see cref="DataUtf8"/> is modified.
842+
/// </remarks>
778843
[JsonIgnore]
779-
public ReadOnlyMemory<byte> DecodedData => _decodedData ??= Convert.FromBase64String(Data);
844+
public ReadOnlyMemory<byte> DecodedData
845+
{
846+
get
847+
{
848+
if (_decodedData.IsEmpty)
849+
{
850+
if (_data is not null)
851+
{
852+
_decodedData = Convert.FromBase64String(_data);
853+
return _decodedData;
854+
}
855+
856+
int maxLength = Base64.GetMaxDecodedFromUtf8Length(DataUtf8.Length);
857+
byte[] buffer = new byte[maxLength];
858+
if (Base64.DecodeFromUtf8(DataUtf8.Span, buffer, out _, out int bytesWritten) == OperationStatus.Done)
859+
{
860+
_decodedData = bytesWritten == maxLength ? buffer : buffer.AsMemory(0, bytesWritten).ToArray();
861+
}
862+
else
863+
{
864+
throw new FormatException("Invalid base64 data");
865+
}
866+
}
867+
868+
return _decodedData;
869+
}
870+
}
780871

781872
/// <summary>
782873
/// Gets or sets the MIME type (or "media type") of the content, specifying the format of the data.
@@ -787,6 +878,10 @@ public ReadOnlyMemory<byte> DataUtf8
787878
[JsonPropertyName("mimeType")]
788879
public required string MimeType { get; set; }
789880

881+
internal bool HasDataUtf8 => !_dataUtf8.IsEmpty;
882+
883+
internal ReadOnlySpan<byte> GetDataUtf8Span() => _dataUtf8.Span;
884+
790885
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
791886
private string DebuggerDisplay => $"MimeType = {MimeType}, Length = {DebuggerDisplayHelper.GetBase64LengthDisplay(Data)}";
792887
}

src/ModelContextProtocol.Core/Protocol/ResourceContents.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public class Converter : JsonConverter<ResourceContents>
7878

7979
string? uri = null;
8080
string? mimeType = null;
81-
string? blob = null;
81+
ReadOnlyMemory<byte>? blobUtf8 = null;
8282
string? text = null;
8383
JsonObject? meta = null;
8484

@@ -104,7 +104,7 @@ public class Converter : JsonConverter<ResourceContents>
104104
break;
105105

106106
case "blob":
107-
blob = reader.GetString();
107+
blobUtf8 = ContentBlock.Converter.ReadUtf8StringValueAsBytes(ref reader);
108108
break;
109109

110110
case "text":
@@ -121,13 +121,13 @@ public class Converter : JsonConverter<ResourceContents>
121121
}
122122
}
123123

124-
if (blob is not null)
124+
if (blobUtf8 is not null)
125125
{
126126
return new BlobResourceContents
127127
{
128128
Uri = uri ?? string.Empty,
129129
MimeType = mimeType,
130-
Blob = blob,
130+
BlobUtf8 = blobUtf8.Value,
131131
Meta = meta,
132132
};
133133
}
@@ -162,7 +162,14 @@ public override void Write(Utf8JsonWriter writer, ResourceContents value, JsonSe
162162
Debug.Assert(value is BlobResourceContents or TextResourceContents);
163163
if (value is BlobResourceContents blobResource)
164164
{
165-
writer.WriteString("blob", blobResource.Blob);
165+
if (blobResource.HasBlobUtf8)
166+
{
167+
writer.WriteString("blob", blobResource.GetBlobUtf8Span());
168+
}
169+
else
170+
{
171+
writer.WriteString("blob", blobResource.Blob);
172+
}
166173
}
167174
else if (value is TextResourceContents textResource)
168175
{

0 commit comments

Comments
 (0)