Skip to content

Commit 3c0679e

Browse files
aaronpowellstephentoubPederHP
authored
Overhaul of Resources (#125)
* Supporting resource templates with read resource callback Fixes #123 * Aligning the resource data structures with the 2024-11-05 schema - Introducing separate classes for Text and Blob ResourceContents - Custom JSON converter to allow those types to be returned, making it easier to follow the type system against the spec - Updated tests * Apply suggestions from code review Co-authored-by: Stephen Toub <stoub@microsoft.com> Co-authored-by: Peder Holdgaard Pedersen <127606677+PederHP@users.noreply.github.com> * Putting files back where they belong * Adding an internal constructor to prevent overloads * Addressing feedback * Apply suggestions from code review Co-authored-by: Stephen Toub <stoub@microsoft.com> * Update src/ModelContextProtocol/Protocol/Types/ResourceContents.cs * Addressing feedback --------- Co-authored-by: Stephen Toub <stoub@microsoft.com> Co-authored-by: Peder Holdgaard Pedersen <127606677+PederHP@users.noreply.github.com>
1 parent 2ab9db0 commit 3c0679e

File tree

13 files changed

+290
-49
lines changed

13 files changed

+290
-49
lines changed

src/ModelContextProtocol/AIContentExtensions.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,7 @@ public static AIContent ToAIContent(this Content content)
3939
}
4040
else if (content is { Type: "resource" } && content.Resource is { } resourceContents)
4141
{
42-
ac = resourceContents.Blob is not null && resourceContents.MimeType is not null ?
43-
new DataContent(Convert.FromBase64String(resourceContents.Blob), resourceContents.MimeType) :
44-
new TextContent(resourceContents.Text);
45-
46-
(ac.AdditionalProperties ??= [])["uri"] = resourceContents.Uri;
42+
ac = resourceContents.ToAIContent();
4743
}
4844
else
4945
{
@@ -62,9 +58,12 @@ public static AIContent ToAIContent(this ResourceContents content)
6258
{
6359
Throw.IfNull(content);
6460

65-
AIContent ac = content.Blob is not null && content.MimeType is not null ?
66-
new DataContent(Convert.FromBase64String(content.Blob), content.MimeType) :
67-
new TextContent(content.Text);
61+
AIContent ac = content switch
62+
{
63+
BlobResourceContents blobResource => new DataContent(Convert.FromBase64String(blobResource.Blob), blobResource.MimeType ?? "application/octet-stream"),
64+
TextResourceContents textResource => new TextContent(textResource.Text),
65+
_ => throw new NotSupportedException($"Resource type '{content.GetType().Name}' is not supported.")
66+
};
6867

6968
(ac.AdditionalProperties ??= [])["uri"] = content.Uri;
7069
ac.RawRepresentation = content;
@@ -79,7 +78,7 @@ public static IList<AIContent> ToAIContents(this IEnumerable<Content> contents)
7978
{
8079
Throw.IfNull(contents);
8180

82-
return contents.Select(ToAIContent).ToList();
81+
return [.. contents.Select(ToAIContent)];
8382
}
8483

8584
/// <summary>Creates a list of <see cref="AIContent"/> from a sequence of <see cref="ResourceContents"/>.</summary>
@@ -89,7 +88,7 @@ public static IList<AIContent> ToAIContents(this IEnumerable<ResourceContents> c
8988
{
9089
Throw.IfNull(contents);
9190

92-
return contents.Select(ToAIContent).ToList();
91+
return [.. contents.Select(ToAIContent)];
9392
}
9493

9594
/// <summary>Extracts the data from a <see cref="DataContent"/> as a Base64 string.</summary>

src/ModelContextProtocol/Client/McpClientExtensions.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -475,17 +475,7 @@ internal static (IList<ChatMessage> Messages, ChatOptions? Options) ToChatClient
475475
}
476476
else if (sm.Content is { Type: "resource", Resource: not null })
477477
{
478-
ResourceContents resource = sm.Content.Resource;
479-
480-
if (resource.Text is not null)
481-
{
482-
message.Contents.Add(new TextContent(resource.Text));
483-
}
484-
485-
if (resource.Blob is not null && resource.MimeType is not null)
486-
{
487-
message.Contents.Add(new DataContent(Convert.FromBase64String(resource.Blob), resource.MimeType));
488-
}
478+
message.Contents.Add(sm.Content.Resource.ToAIContent());
489479
}
490480

491481
messages.Add(message);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace ModelContextProtocol.Protocol.Types;
4+
5+
/// <summary>
6+
/// Binary contents of a resource.
7+
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">See the schema for details</see>
8+
/// </summary>
9+
public class BlobResourceContents : ResourceContents
10+
{
11+
/// <summary>
12+
/// The base64-encoded string representing the binary data of the item.
13+
/// </summary>
14+
[JsonPropertyName("blob")]
15+
public string Blob { get; set; } = string.Empty;
16+
}

src/ModelContextProtocol/Protocol/Types/Resource.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@ public record Resource
1616

1717
/// <summary>
1818
/// A human-readable name for this resource.
19+
///
20+
/// This can be used by clients to populate UI elements.
1921
/// </summary>
2022
[JsonPropertyName("name")]
2123
public required string Name { get; init; }
2224

2325
/// <summary>
2426
/// A description of what this resource represents.
27+
///
28+
/// This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a \"hint\" to the model.
2529
/// </summary>
2630
[JsonPropertyName("description")]
2731
public string? Description { get; init; }
@@ -32,6 +36,14 @@ public record Resource
3236
[JsonPropertyName("mimeType")]
3337
public string? MimeType { get; init; }
3438

39+
/// <summary>
40+
/// The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
41+
///
42+
/// This can be used by Hosts to display file sizes and estimate context window usage.
43+
/// </summary>
44+
[JsonPropertyName("size")]
45+
public long? Size { get; init; }
46+
3547
/// <summary>
3648
/// Optional annotations for the resource.
3749
/// </summary>
Lines changed: 107 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
using System.Text.Json.Serialization;
1+
using System.ComponentModel;
2+
using System.Diagnostics;
3+
using System.Text.Json;
4+
using System.Text.Json.Serialization;
25

36
namespace ModelContextProtocol.Protocol.Types;
47

58
/// <summary>
69
/// Represents the content of a resource.
710
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">See the schema for details</see>
811
/// </summary>
9-
public class ResourceContents
12+
[JsonConverter(typeof(Converter))]
13+
public abstract class ResourceContents
1014
{
15+
internal ResourceContents()
16+
{
17+
}
18+
1119
/// <summary>
1220
/// The URI of the resource.
1321
/// </summary>
@@ -20,16 +28,104 @@ public class ResourceContents
2028
[JsonPropertyName("mimeType")]
2129
public string? MimeType { get; set; }
2230

31+
2332
/// <summary>
24-
/// The text content of the resource.
33+
/// Converter for <see cref="ResourceContents"/>.
2534
/// </summary>
26-
[JsonPropertyName("text")]
27-
public string? Text { get; set; }
35+
[EditorBrowsable(EditorBrowsableState.Never)]
36+
public class Converter : JsonConverter<ResourceContents>
37+
{
38+
/// <inheritdoc/>
39+
public override ResourceContents? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
40+
{
41+
if (reader.TokenType == JsonTokenType.Null)
42+
{
43+
return null;
44+
}
2845

46+
if (reader.TokenType != JsonTokenType.StartObject)
47+
{
48+
throw new JsonException();
49+
}
2950

30-
/// <summary>
31-
/// The base64-encoded binary content of the resource.
32-
/// </summary>
33-
[JsonPropertyName("blob")]
34-
public string? Blob { get; set; }
35-
}
51+
string? uri = null;
52+
string? mimeType = null;
53+
string? blob = null;
54+
string? text = null;
55+
56+
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
57+
{
58+
if (reader.TokenType != JsonTokenType.PropertyName)
59+
{
60+
continue;
61+
}
62+
63+
string? propertyName = reader.GetString();
64+
65+
switch (propertyName)
66+
{
67+
case "uri":
68+
uri = reader.GetString();
69+
break;
70+
case "mimeType":
71+
mimeType = reader.GetString();
72+
break;
73+
case "blob":
74+
blob = reader.GetString();
75+
break;
76+
case "text":
77+
text = reader.GetString();
78+
break;
79+
default:
80+
break;
81+
}
82+
}
83+
84+
if (blob is not null)
85+
{
86+
return new BlobResourceContents
87+
{
88+
Uri = uri ?? string.Empty,
89+
MimeType = mimeType,
90+
Blob = blob
91+
};
92+
}
93+
94+
if (text is not null)
95+
{
96+
return new TextResourceContents
97+
{
98+
Uri = uri ?? string.Empty,
99+
MimeType = mimeType,
100+
Text = text
101+
};
102+
}
103+
104+
return null;
105+
}
106+
107+
/// <inheritdoc/>
108+
public override void Write(Utf8JsonWriter writer, ResourceContents value, JsonSerializerOptions options)
109+
{
110+
if (value is null)
111+
{
112+
writer.WriteNullValue();
113+
return;
114+
}
115+
116+
writer.WriteStartObject();
117+
writer.WriteString("uri", value.Uri);
118+
writer.WriteString("mimeType", value.MimeType);
119+
Debug.Assert(value is BlobResourceContents or TextResourceContents);
120+
if (value is BlobResourceContents blobResource)
121+
{
122+
writer.WriteString("blob", blobResource.Blob);
123+
}
124+
else if (value is TextResourceContents textResource)
125+
{
126+
writer.WriteString("text", textResource.Text);
127+
}
128+
writer.WriteEndObject();
129+
}
130+
}
131+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace ModelContextProtocol.Protocol.Types;
4+
5+
/// <summary>
6+
/// Text contents of a resource.
7+
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">See the schema for details</see>
8+
/// </summary>
9+
public class TextResourceContents : ResourceContents
10+
{
11+
/// <summary>
12+
/// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
13+
/// </summary>
14+
[JsonPropertyName("text")]
15+
public string Text { get; set; } = string.Empty;
16+
}

src/ModelContextProtocol/Server/McpServer.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -218,19 +218,21 @@ private void SetResourcesHandler(McpServerOptions options)
218218
return;
219219
}
220220

221-
if (resourcesCapability.ListResourcesHandler is not { } listResourcesHandler ||
221+
var listResourcesHandler = resourcesCapability.ListResourcesHandler;
222+
var listResourceTemplatesHandler = resourcesCapability.ListResourceTemplatesHandler;
223+
224+
if ((listResourcesHandler is not { } && listResourceTemplatesHandler is not { }) ||
222225
resourcesCapability.ReadResourceHandler is not { } readResourceHandler)
223226
{
224227
throw new McpServerException("Resources capability was enabled, but ListResources and/or ReadResource handlers were not specified.");
225228
}
226229

230+
listResourcesHandler ??= (static (_, _) => Task.FromResult(new ListResourcesResult()));
231+
227232
SetRequestHandler<ListResourcesRequestParams, ListResourcesResult>("resources/list", (request, ct) => listResourcesHandler(new(this, request), ct));
228233
SetRequestHandler<ReadResourceRequestParams, ReadResourceResult>("resources/read", (request, ct) => readResourceHandler(new(this, request), ct));
229234

230-
// Set the list resource templates handler, or use the default if not specified
231-
var listResourceTemplatesHandler = resourcesCapability.ListResourceTemplatesHandler
232-
?? (static (_, _) => Task.FromResult(new ListResourceTemplatesResult()));
233-
235+
listResourceTemplatesHandler ??= (static (_, _) => Task.FromResult(new ListResourceTemplatesResult()));
234236
SetRequestHandler<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>("resources/templates/list", (request, ct) => listResourceTemplatesHandler(new(this, request), ct));
235237

236238
if (resourcesCapability.Subscribe is not true)

tests/ModelContextProtocol.TestServer/Program.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ private static ResourcesCapability ConfigureResources()
327327
Name = $"Resource {i + 1}",
328328
MimeType = "text/plain"
329329
});
330-
resourceContents.Add(new ResourceContents()
330+
resourceContents.Add(new TextResourceContents()
331331
{
332332
Uri = uri,
333333
MimeType = "text/plain",
@@ -343,7 +343,7 @@ private static ResourcesCapability ConfigureResources()
343343
Name = $"Resource {i + 1}",
344344
MimeType = "application/octet-stream"
345345
});
346-
resourceContents.Add(new ResourceContents()
346+
resourceContents.Add(new BlobResourceContents()
347347
{
348348
Uri = uri,
349349
MimeType = "application/octet-stream",
@@ -417,7 +417,7 @@ private static ResourcesCapability ConfigureResources()
417417
return Task.FromResult(new ReadResourceResult()
418418
{
419419
Contents = [
420-
new ResourceContents()
420+
new TextResourceContents()
421421
{
422422
Uri = request.Params.Uri,
423423
MimeType = "text/plain",

tests/ModelContextProtocol.TestSseServer/Program.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
8181
Name = $"Resource {i + 1}",
8282
MimeType = "text/plain"
8383
});
84-
resourceContents.Add(new ResourceContents()
84+
resourceContents.Add(new TextResourceContents()
8585
{
8686
Uri = uri,
8787
MimeType = "text/plain",
@@ -97,7 +97,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
9797
Name = $"Resource {i + 1}",
9898
MimeType = "application/octet-stream"
9999
});
100-
resourceContents.Add(new ResourceContents()
100+
resourceContents.Add(new BlobResourceContents()
101101
{
102102
Uri = uri,
103103
MimeType = "application/octet-stream",
@@ -262,7 +262,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
262262
return Task.FromResult(new ReadResourceResult()
263263
{
264264
Contents = [
265-
new ResourceContents()
265+
new TextResourceContents()
266266
{
267267
Uri = request.Params.Uri,
268268
MimeType = "text/plain",

tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,9 @@ public async Task ReadResource_Stdio_TextResource(string clientId)
224224

225225
Assert.NotNull(result);
226226
Assert.Single(result.Contents);
227-
Assert.NotNull(result.Contents[0].Text);
227+
228+
TextResourceContents textResource = Assert.IsType<TextResourceContents>(result.Contents[0]);
229+
Assert.NotNull(textResource.Text);
228230
}
229231

230232
[Theory]
@@ -241,7 +243,9 @@ public async Task ReadResource_Stdio_BinaryResource(string clientId)
241243

242244
Assert.NotNull(result);
243245
Assert.Single(result.Contents);
244-
Assert.NotNull(result.Contents[0].Blob);
246+
247+
BlobResourceContents blobResource = Assert.IsType<BlobResourceContents>(result.Contents[0]);
248+
Assert.NotNull(blobResource.Blob);
245249
}
246250

247251
// Not supported by "everything" server version on npx

0 commit comments

Comments
 (0)