Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion samples/EverythingServer/Resources/SimpleResourceType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static ResourceContents TemplateResource(RequestContext<ReadResourceReque
} :
new BlobResourceContents
{
Blob = resource.Description!,
Blob = System.Text.Encoding.UTF8.GetBytes(resource.Description!),
MimeType = resource.MimeType,
Uri = resource.Uri,
};
Expand Down
2 changes: 1 addition & 1 deletion samples/EverythingServer/Tools/AnnotatedMessageTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public static IEnumerable<ContentBlock> AnnotatedMessage(MessageType messageType
{
contents.Add(new ImageContentBlock
{
Data = TinyImageTool.MCP_TINY_IMAGE.Split(",").Last(),
Data = System.Text.Encoding.UTF8.GetBytes(TinyImageTool.MCP_TINY_IMAGE.Split(",").Last()),
MimeType = "image/png",
Annotations = new() { Audience = [Role.User], Priority = 0.5f }
});
Expand Down
28 changes: 22 additions & 6 deletions src/ModelContextProtocol.Core/AIContentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,9 @@ public static IList<PromptMessage> ToPromptMessages(this ChatMessage chatMessage
{
TextContentBlock textContent => new TextContent(textContent.Text),

ImageContentBlock imageContent => new DataContent(Convert.FromBase64String(imageContent.Data), imageContent.MimeType),
ImageContentBlock imageContent => new DataContent(imageContent.DecodedData, imageContent.MimeType),

AudioContentBlock audioContent => new DataContent(Convert.FromBase64String(audioContent.Data), audioContent.MimeType),
AudioContentBlock audioContent => new DataContent(audioContent.DecodedData, audioContent.MimeType),

EmbeddedResourceBlock resourceContent => resourceContent.Resource.ToAIContent(),

Expand Down Expand Up @@ -324,7 +324,7 @@ public static AIContent ToAIContent(this ResourceContents content)

AIContent ac = content switch
{
BlobResourceContents blobResource => new DataContent(Convert.FromBase64String(blobResource.Blob), blobResource.MimeType ?? "application/octet-stream"),
BlobResourceContents blobResource => new DataContent(blobResource.Data, blobResource.MimeType ?? "application/octet-stream"),
TextResourceContents textResource => new TextContent(textResource.Text),
_ => throw new NotSupportedException($"Resource type '{content.GetType().Name}' is not supported.")
};
Expand Down Expand Up @@ -401,21 +401,21 @@ public static ContentBlock ToContentBlock(this AIContent content, JsonSerializer

DataContent dataContent when dataContent.HasTopLevelMediaType("image") => new ImageContentBlock
{
Data = dataContent.Base64Data.ToString(),
Data = GetUtf8Bytes(dataContent.Base64Data.Span),
MimeType = dataContent.MediaType,
},

DataContent dataContent when dataContent.HasTopLevelMediaType("audio") => new AudioContentBlock
{
Data = dataContent.Base64Data.ToString(),
Data = GetUtf8Bytes(dataContent.Base64Data.Span),
MimeType = dataContent.MediaType,
},

DataContent dataContent => new EmbeddedResourceBlock
{
Resource = new BlobResourceContents
{
Blob = dataContent.Base64Data.ToString(),
Blob = GetUtf8Bytes(dataContent.Base64Data.Span),
MimeType = dataContent.MediaType,
Uri = string.Empty,
}
Expand Down Expand Up @@ -448,6 +448,22 @@ public static ContentBlock ToContentBlock(this AIContent content, JsonSerializer
contentBlock.Meta = content.AdditionalProperties?.ToJsonObject(options);

return contentBlock;

unsafe byte[] GetUtf8Bytes(ReadOnlySpan<char> utf16)
{
// gets UTF-8 bytes from UTF-16 chars without intermediate string allocations
fixed (char* pChars = utf16)
{
var byteCount = System.Text.Encoding.UTF8.GetByteCount(pChars, utf16.Length);
var bytes = new byte[byteCount];

fixed (byte* pBytes = bytes)
{
System.Text.Encoding.UTF8.GetBytes(pChars, utf16.Length, pBytes, byteCount);
}
return bytes;
}
}
}

private sealed class ToolAIFunctionDeclaration(Tool tool) : AIFunctionDeclaration
Expand Down
96 changes: 91 additions & 5 deletions src/ModelContextProtocol.Core/Protocol/BlobResourceContents.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Buffers;
using System.Buffers.Text;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Protocol;
Expand All @@ -9,8 +12,8 @@ namespace ModelContextProtocol.Protocol;
/// <remarks>
/// <para>
/// <see cref="BlobResourceContents"/> is used when binary data needs to be exchanged through
/// the Model Context Protocol. The binary data is represented as a base64-encoded string
/// in the <see cref="Blob"/> property.
/// the Model Context Protocol. The binary data is represented as base64-encoded UTF-8 bytes
/// in the <see cref="Blob"/> property, providing a zero-copy representation of the wire payload.
/// </para>
/// <para>
/// This class inherits from <see cref="ResourceContents"/>, which also has a sibling implementation
Expand All @@ -24,18 +27,101 @@ namespace ModelContextProtocol.Protocol;
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class BlobResourceContents : ResourceContents
{
private ReadOnlyMemory<byte>? _decodedData;
private ReadOnlyMemory<byte> _blob;

/// <summary>
/// Creates an <see cref="BlobResourceContents"/> from raw data.
/// </summary>
/// <param name="data">The raw data.</param>
/// <param name="uri">The URI of the data.</param>
/// <param name="mimeType">The optional MIME type of the data.</param>
/// <returns>A new <see cref="BlobResourceContents"/> instance.</returns>
/// <exception cref="InvalidOperationException"></exception>
public static BlobResourceContents FromData(ReadOnlyMemory<byte> data, string uri, string? mimeType = null)
{
ReadOnlyMemory<byte> blob;

// Encode directly to UTF-8 base64 bytes without string intermediate
int maxLength = Base64.GetMaxEncodedToUtf8Length(data.Length);
byte[] buffer = new byte[maxLength];
if (Base64.EncodeToUtf8(data.Span, buffer, out _, out int bytesWritten) == OperationStatus.Done)
{
blob = buffer.AsMemory(0, bytesWritten);
}
else
{
throw new InvalidOperationException("Failed to encode binary data to base64");
}

return new()
{
_decodedData = data,
Blob = blob,
MimeType = mimeType,
Uri = uri
};
}

/// <summary>
/// Gets or sets the base64-encoded string representing the binary data of the item.
/// Gets or sets the base64-encoded UTF-8 bytes representing the binary data of the item.
/// </summary>
/// <remarks>
/// This is a zero-copy representation of the wire payload of this item. Setting this value will invalidate any cached value of <see cref="Data"/>.
/// </remarks>
[JsonPropertyName("blob")]
public required string Blob { get; set; }
public required ReadOnlyMemory<byte> Blob
{
get => _blob;
set
{
_blob = value;
_decodedData = null; // Invalidate cache
}
}

/// <summary>
/// Gets or sets the decoded data represented by <see cref="Blob"/>.
/// </summary>
/// <remarks>
/// <para>
/// When getting, this member will decode the value in <see cref="Blob"/> and cache the result.
/// Subsequent accesses return the cached value unless <see cref="Blob"/> is modified.
/// </para>
/// <para>
/// When setting, the binary data is stored without copying and <see cref="Blob"/> is updated
/// with the base64-encoded UTF-8 representation.
/// </para>
/// </remarks>
[JsonIgnore]
public ReadOnlyMemory<byte> Data
{
get
{
if (_decodedData is null)
{
// Decode directly from UTF-8 base64 bytes without string intermediate
int maxLength = Base64.GetMaxDecodedFromUtf8Length(Blob.Length);
byte[] buffer = new byte[maxLength];
if (Base64.DecodeFromUtf8(Blob.Span, buffer, out _, out int bytesWritten) == System.Buffers.OperationStatus.Done)
{
_decodedData = buffer.AsMemory(0, bytesWritten);
}
else
{
throw new FormatException("Invalid base64 data");
}
}
return _decodedData.Value;
}
}

[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay
{
get
{
string lengthDisplay = DebuggerDisplayHelper.GetBase64LengthDisplay(Blob);
string lengthDisplay = _decodedData is null ? DebuggerDisplayHelper.GetBase64LengthDisplay(Blob) : $"{Data.Length} bytes";
string mimeInfo = MimeType is not null ? $", MimeType = {MimeType}" : "";
return $"Uri = \"{Uri}\"{mimeInfo}, Length = {lengthDisplay}";
}
Expand Down
Loading
Loading