Skip to content

Commit 5860b78

Browse files
committed
Address feedback
1 parent d90737f commit 5860b78

9 files changed

Lines changed: 83 additions & 85 deletions

File tree

src/Common/EncodingUtilities.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System.Buffers;
2+
using System.Buffers.Text;
3+
using System.Diagnostics;
4+
using System.Text;
5+
6+
namespace ModelContextProtocol;
7+
8+
/// <summary>Provides helper methods for encoding operations.</summary>
9+
internal static class EncodingUtilities
10+
{
11+
/// <summary>
12+
/// Converts UTF-16 characters to UTF-8 bytes without intermediate string allocations.
13+
/// </summary>
14+
/// <param name="utf16">The UTF-16 character span to convert.</param>
15+
/// <returns>A byte array containing the UTF-8 encoded bytes.</returns>
16+
public static byte[] GetUtf8Bytes(ReadOnlySpan<char> utf16)
17+
{
18+
byte[] bytes = new byte[Encoding.UTF8.GetByteCount(utf16)];
19+
Encoding.UTF8.GetBytes(utf16, bytes);
20+
return bytes;
21+
}
22+
23+
/// <summary>
24+
/// Encodes binary data to base64-encoded UTF-8 bytes.
25+
/// </summary>
26+
/// <param name="data">The binary data to encode.</param>
27+
/// <returns>A ReadOnlyMemory containing the base64-encoded UTF-8 bytes.</returns>
28+
public static ReadOnlyMemory<byte> EncodeToBase64Utf8(ReadOnlyMemory<byte> data)
29+
{
30+
int maxLength = Base64.GetMaxEncodedToUtf8Length(data.Length);
31+
byte[] buffer = new byte[maxLength];
32+
OperationStatus status = Base64.EncodeToUtf8(data.Span, buffer, out _, out int bytesWritten);
33+
Debug.Assert(status == OperationStatus.Done, "Base64 encoding should succeed for valid input data");
34+
Debug.Assert(bytesWritten == buffer.Length, "Base64 encoding should always produce the same length as the max length");
35+
return buffer.AsMemory(0, bytesWritten);
36+
}
37+
38+
/// <summary>
39+
/// Decodes base64-encoded UTF-8 bytes to binary data.
40+
/// </summary>
41+
/// <param name="base64Data">The base64-encoded UTF-8 bytes to decode.</param>
42+
/// <returns>A ReadOnlyMemory containing the decoded binary data.</returns>
43+
/// <exception cref="FormatException">The input is not valid base64 data.</exception>
44+
public static ReadOnlyMemory<byte> DecodeFromBase64Utf8(ReadOnlyMemory<byte> base64Data)
45+
{
46+
int maxLength = Base64.GetMaxDecodedFromUtf8Length(base64Data.Length);
47+
byte[] buffer = new byte[maxLength];
48+
if (Base64.DecodeFromUtf8(base64Data.Span, buffer, out _, out int bytesWritten) == OperationStatus.Done)
49+
{
50+
// Base64 decoding may produce fewer bytes than the max length, due to whitespace anywhere in the string or padding.
51+
Debug.Assert(bytesWritten <= buffer.Length, "Base64 decoding should never produce more bytes than the max length");
52+
return buffer.AsMemory(0, bytesWritten);
53+
}
54+
else
55+
{
56+
throw new FormatException("Invalid base64 data");
57+
}
58+
}
59+
}

src/ModelContextProtocol.Core/AIContentExtensions.cs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -399,21 +399,21 @@ public static ContentBlock ToContentBlock(this AIContent content, JsonSerializer
399399

400400
DataContent dataContent when dataContent.HasTopLevelMediaType("image") => new ImageContentBlock
401401
{
402-
Data = GetUtf8Bytes(dataContent.Base64Data.Span),
402+
Data = EncodingUtilities.GetUtf8Bytes(dataContent.Base64Data.Span),
403403
MimeType = dataContent.MediaType,
404404
},
405405

406406
DataContent dataContent when dataContent.HasTopLevelMediaType("audio") => new AudioContentBlock
407407
{
408-
Data = GetUtf8Bytes(dataContent.Base64Data.Span),
408+
Data = EncodingUtilities.GetUtf8Bytes(dataContent.Base64Data.Span),
409409
MimeType = dataContent.MediaType,
410410
},
411411

412412
DataContent dataContent => new EmbeddedResourceBlock
413413
{
414414
Resource = new BlobResourceContents
415415
{
416-
Blob = GetUtf8Bytes(dataContent.Base64Data.Span),
416+
Blob = EncodingUtilities.GetUtf8Bytes(dataContent.Base64Data.Span),
417417
MimeType = dataContent.MediaType,
418418
Uri = string.Empty,
419419
}
@@ -446,14 +446,6 @@ public static ContentBlock ToContentBlock(this AIContent content, JsonSerializer
446446
contentBlock.Meta = content.AdditionalProperties?.ToJsonObject(options);
447447

448448
return contentBlock;
449-
450-
static byte[] GetUtf8Bytes(ReadOnlySpan<char> utf16)
451-
{
452-
// Get UTF-8 bytes from UTF-16 chars without intermediate string allocations
453-
byte[] bytes = new byte[Encoding.UTF8.GetByteCount(utf16)];
454-
Encoding.UTF8.GetBytes(utf16, bytes);
455-
return bytes;
456-
}
457449
}
458450

459451
private sealed class ToolAIFunctionDeclaration(Tool tool) : AIFunctionDeclaration

src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<Compile Include="..\Common\Throw.cs" Link="Throw.cs" />
2626
<Compile Include="..\Common\Obsoletions.cs" Link="Obsoletions.cs" />
2727
<Compile Include="..\Common\Experimentals.cs" Link="Experimentals.cs" />
28+
<Compile Include="..\Common\EncodingUtilities.cs" Link="EncodingUtilities.cs" />
2829
<Compile Include="..\Common\HttpResponseMessageExtensions.cs" Link="HttpResponseMessageExtensions.cs" />
2930
<Compile Include="..\Common\ServerSentEvents\**\*.cs" Link="ServerSentEvents\%(RecursiveDir)%(FileName)%(Extension)" />
3031
</ItemGroup>

src/ModelContextProtocol.Core/Protocol/BlobResourceContents.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,18 @@ public sealed class BlobResourceContents : ResourceContents
3333
/// <summary>
3434
/// Creates an <see cref="BlobResourceContents"/> from raw data.
3535
/// </summary>
36-
/// <param name="data">The raw data.</param>
36+
/// <param name="bytes">The raw unencoded data.</param>
3737
/// <param name="uri">The URI of the data.</param>
3838
/// <param name="mimeType">The optional MIME type of the data.</param>
3939
/// <returns>A new <see cref="BlobResourceContents"/> instance.</returns>
4040
/// <exception cref="InvalidOperationException"></exception>
41-
public static BlobResourceContents FromData(ReadOnlyMemory<byte> data, string uri, string? mimeType = null)
41+
public static BlobResourceContents FromBytes(ReadOnlyMemory<byte> bytes, string uri, string? mimeType = null)
4242
{
43-
ReadOnlyMemory<byte> blob = Base64Helpers.EncodeToBase64Utf8(data);
43+
ReadOnlyMemory<byte> blob = EncodingUtilities.EncodeToBase64Utf8(bytes);
4444

4545
return new()
4646
{
47-
_decodedData = data,
47+
_decodedData = bytes,
4848
Blob = blob,
4949
MimeType = mimeType,
5050
Uri = uri
@@ -88,7 +88,7 @@ public ReadOnlyMemory<byte> DecodedData
8888
{
8989
if (_decodedData is null)
9090
{
91-
_decodedData = Base64Helpers.DecodeFromBase64Utf8(Blob);
91+
_decodedData = EncodingUtilities.DecodeFromBase64Utf8(Blob);
9292
}
9393

9494
return _decodedData.Value;

src/ModelContextProtocol.Core/Protocol/ContentBlock.cs

Lines changed: 10 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -385,20 +385,20 @@ public sealed class ImageContentBlock : ContentBlock
385385
/// <summary>
386386
/// Creates an <see cref="ImageContentBlock"/> from decoded image bytes.
387387
/// </summary>
388-
/// <param name="imageData">The decoded image bytes.</param>
388+
/// <param name="bytes">The unencoded image bytes.</param>
389389
/// <param name="mimeType">The MIME type of the image.</param>
390390
/// <returns>A new <see cref="ImageContentBlock"/> instance.</returns>
391391
/// <remarks>
392392
/// This method stores the provided bytes as <see cref="DecodedData"/> and encodes them to base64 UTF-8 bytes for <see cref="Data"/>.
393393
/// </remarks>
394394
/// <exception cref="InvalidOperationException"></exception>
395-
public static ImageContentBlock FromBytes(ReadOnlyMemory<byte> imageData, string mimeType)
395+
public static ImageContentBlock FromBytes(ReadOnlyMemory<byte> bytes, string mimeType)
396396
{
397-
ReadOnlyMemory<byte> data = Base64Helpers.EncodeToBase64Utf8(imageData);
397+
ReadOnlyMemory<byte> data = EncodingUtilities.EncodeToBase64Utf8(bytes);
398398

399399
return new()
400400
{
401-
_decodedData = imageData,
401+
_decodedData = bytes,
402402
Data = data,
403403
MimeType = mimeType
404404
};
@@ -444,7 +444,7 @@ public ReadOnlyMemory<byte> DecodedData
444444
{
445445
if (_decodedData is null)
446446
{
447-
_decodedData = Base64Helpers.DecodeFromBase64Utf8(Data);
447+
_decodedData = EncodingUtilities.DecodeFromBase64Utf8(Data);
448448
}
449449

450450
return _decodedData.Value;
@@ -474,20 +474,20 @@ public sealed class AudioContentBlock : ContentBlock
474474
/// <summary>
475475
/// Creates an <see cref="AudioContentBlock"/> from decoded audio bytes.
476476
/// </summary>
477-
/// <param name="audioData">The decoded audio bytes.</param>
477+
/// <param name="bytes">The unencoded audio bytes.</param>
478478
/// <param name="mimeType">The MIME type of the audio.</param>
479479
/// <returns>A new <see cref="AudioContentBlock"/> instance.</returns>
480480
/// <remarks>
481481
/// This method stores the provided bytes as <see cref="DecodedData"/> and encodes them to base64 UTF-8 bytes for <see cref="Data"/>.
482482
/// </remarks>
483483
/// <exception cref="InvalidOperationException"></exception>
484-
public static AudioContentBlock FromBytes(ReadOnlyMemory<byte> audioData, string mimeType)
484+
public static AudioContentBlock FromBytes(ReadOnlyMemory<byte> bytes, string mimeType)
485485
{
486-
ReadOnlyMemory<byte> data = Base64Helpers.EncodeToBase64Utf8(audioData);
486+
ReadOnlyMemory<byte> data = EncodingUtilities.EncodeToBase64Utf8(bytes);
487487

488488
return new()
489489
{
490-
_decodedData = audioData,
490+
_decodedData = bytes,
491491
Data = data,
492492
MimeType = mimeType
493493
};
@@ -533,7 +533,7 @@ public ReadOnlyMemory<byte> DecodedData
533533
{
534534
if (_decodedData is null)
535535
{
536-
_decodedData = Base64Helpers.DecodeFromBase64Utf8(Data);
536+
_decodedData = EncodingUtilities.DecodeFromBase64Utf8(Data);
537537
}
538538

539539
return _decodedData.Value;
@@ -757,46 +757,3 @@ private string DebuggerDisplay
757757
}
758758
}
759759
}
760-
761-
/// <summary>
762-
/// Helper methods for base64 encoding and decoding operations.
763-
/// </summary>
764-
internal static class Base64Helpers
765-
{
766-
/// <summary>
767-
/// Encodes binary data to base64-encoded UTF-8 bytes.
768-
/// </summary>
769-
internal static ReadOnlyMemory<byte> EncodeToBase64Utf8(ReadOnlyMemory<byte> data)
770-
{
771-
int maxLength = Base64.GetMaxEncodedToUtf8Length(data.Length);
772-
byte[] buffer = new byte[maxLength];
773-
if (Base64.EncodeToUtf8(data.Span, buffer, out _, out int bytesWritten) == OperationStatus.Done)
774-
{
775-
Debug.Assert(bytesWritten == buffer.Length, "Base64 encoding should always produce the same length as the max length");
776-
return buffer.AsMemory(0, bytesWritten);
777-
}
778-
else
779-
{
780-
throw new InvalidOperationException("Failed to encode binary data to base64");
781-
}
782-
}
783-
784-
/// <summary>
785-
/// Decodes base64-encoded UTF-8 bytes to binary data.
786-
/// </summary>
787-
internal static ReadOnlyMemory<byte> DecodeFromBase64Utf8(ReadOnlyMemory<byte> base64Data)
788-
{
789-
int maxLength = Base64.GetMaxDecodedFromUtf8Length(base64Data.Length);
790-
byte[] buffer = new byte[maxLength];
791-
if (Base64.DecodeFromUtf8(base64Data.Span, buffer, out _, out int bytesWritten) == OperationStatus.Done)
792-
{
793-
// Base64 decoding may produce fewer bytes than the max length, due to whitespace anywhere in the string or padding.
794-
Debug.Assert(bytesWritten <= buffer.Length, "Base64 decoding should never produce more bytes than the max length");
795-
return buffer.AsMemory(0, bytesWritten);
796-
}
797-
else
798-
{
799-
throw new FormatException("Invalid base64 data");
800-
}
801-
}
802-
}

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ public override async ValueTask<ReadResourceResult> ReadAsync(
396396
{
397397
Uri = request.Params!.Uri,
398398
MimeType = dc.MediaType,
399-
Blob = GetBase64Utf8Bytes(dc)
399+
Blob = EncodingUtilities.GetUtf8Bytes(dc.Base64Data.Span)
400400
}],
401401
},
402402

@@ -426,7 +426,7 @@ public override async ValueTask<ReadResourceResult> ReadAsync(
426426
{
427427
Uri = request.Params!.Uri,
428428
MimeType = dc.MediaType,
429-
Blob = GetBase64Utf8Bytes(dc)
429+
Blob = EncodingUtilities.GetUtf8Bytes(dc.Base64Data.Span)
430430
},
431431

432432
_ => throw new InvalidOperationException($"Unsupported AIContent type '{ac.GetType()}' returned from resource function."),
@@ -448,15 +448,4 @@ public override async ValueTask<ReadResourceResult> ReadAsync(
448448
_ => throw new InvalidOperationException($"Unsupported result type '{result.GetType()}' returned from resource function."),
449449
};
450450
}
451-
452-
/// <summary>
453-
/// Helper method to get UTF-8 bytes from DataContent.Base64Data efficiently.
454-
/// </summary>
455-
private static ReadOnlyMemory<byte> GetBase64Utf8Bytes(DataContent dataContent)
456-
{
457-
var utf16 = dataContent.Base64Data.Span;
458-
byte[] bytes = new byte[Encoding.UTF8.GetByteCount(utf16)];
459-
Encoding.UTF8.GetBytes(utf16, bytes);
460-
return bytes;
461-
}
462451
}

tests/ModelContextProtocol.TestServer/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ private static void ConfigureResources(McpServerOptions options)
384384
Name = $"Resource {i + 1}",
385385
MimeType = "application/octet-stream"
386386
});
387-
resourceContents.Add(BlobResourceContents.FromData(buffer, uri, "application/octet-stream"));
387+
resourceContents.Add(BlobResourceContents.FromBytes(buffer, uri, "application/octet-stream"));
388388
}
389389
}
390390

tests/ModelContextProtocol.TestSseServer/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
8585
Name = $"Resource {i + 1}",
8686
MimeType = "application/octet-stream"
8787
});
88-
resourceContents.Add(BlobResourceContents.FromData(buffer, uri, "application/octet-stream"));
88+
resourceContents.Add(BlobResourceContents.FromBytes(buffer, uri, "application/octet-stream"));
8989
}
9090
}
9191

tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ public async Task CanReturnCollectionOfResourceContents()
575575
return (IList<ResourceContents>)
576576
[
577577
new TextResourceContents { Text = "hello", Uri = "" },
578-
BlobResourceContents.FromData((byte[])[1, 2, 3], ""),
578+
BlobResourceContents.FromBytes((byte[])[1, 2, 3], ""),
579579
];
580580
}, new() { Name = "Test" });
581581
var result = await resource.ReadAsync(

0 commit comments

Comments
 (0)