-
Notifications
You must be signed in to change notification settings - Fork 688
Expand file tree
/
Copy pathBlobResourceContents.cs
More file actions
130 lines (122 loc) · 4.87 KB
/
BlobResourceContents.cs
File metadata and controls
130 lines (122 loc) · 4.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
using System.Buffers;
using System.Buffers.Text;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol;
/// <summary>
/// Represents the binary contents of a resource in the Model Context Protocol.
/// </summary>
/// <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 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
/// <see cref="TextResourceContents"/> for text-based resources. When working with resources, the
/// appropriate type is chosen based on the nature of the content.
/// </para>
/// <para>
/// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for more details.
/// </para>
/// </remarks>
[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)
{
Debug.Assert(bytesWritten == buffer.Length, "Base64 encoding should always produce exact length for non-padded input");
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 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="DecodedData"/>.
/// </remarks>
[JsonPropertyName("blob")]
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> DecodedData
{
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 = _decodedData is null ? DebuggerDisplayHelper.GetBase64LengthDisplay(Blob) : $"{DecodedData.Length} bytes";
string mimeInfo = MimeType is not null ? $", MimeType = {MimeType}" : "";
return $"Uri = \"{Uri}\"{mimeInfo}, Length = {lengthDisplay}";
}
}
}