diff --git a/src/ModelContextProtocol/AIContentExtensions.cs b/src/ModelContextProtocol/AIContentExtensions.cs
index be5eabe..0fe00fb 100644
--- a/src/ModelContextProtocol/AIContentExtensions.cs
+++ b/src/ModelContextProtocol/AIContentExtensions.cs
@@ -39,11 +39,7 @@ public static class AIContentExtensions
}
else if (content is { Type: "resource" } && content.Resource is { } resourceContents)
{
- ac = resourceContents.Blob is not null && resourceContents.MimeType is not null ?
- new DataContent(Convert.FromBase64String(resourceContents.Blob), resourceContents.MimeType) :
- new TextContent(resourceContents.Text);
-
- (ac.AdditionalProperties ??= [])["uri"] = resourceContents.Uri;
+ ac = resourceContents.ToAIContent();
}
else
{
@@ -62,9 +58,12 @@ public static class AIContentExtensions
{
Throw.IfNull(content);
- AIContent ac = content.Blob is not null && content.MimeType is not null ?
- new DataContent(Convert.FromBase64String(content.Blob), content.MimeType) :
- new TextContent(content.Text);
+ AIContent ac = content switch
+ {
+ BlobResourceContents blobResource => new DataContent(Convert.FromBase64String(blobResource.Blob), blobResource.MimeType ?? "application/octet-stream"),
+ TextResourceContents textResource => new TextContent(textResource.Text),
+ _ => throw new NotSupportedException($"Resource type '{content.GetType().Name}' is not supported.")
+ };
(ac.AdditionalProperties ??= [])["uri"] = content.Uri;
ac.RawRepresentation = content;
@@ -79,7 +78,7 @@ public static class AIContentExtensions
{
Throw.IfNull(contents);
- return contents.Select(ToAIContent).ToList();
+ return [.. contents.Select(ToAIContent)];
}
/// Creates a list of from a sequence of .
@@ -89,7 +88,7 @@ public static class AIContentExtensions
{
Throw.IfNull(contents);
- return contents.Select(ToAIContent).ToList();
+ return [.. contents.Select(ToAIContent)];
}
/// Extracts the data from a as a Base64 string.
diff --git a/src/ModelContextProtocol/Client/McpClientExtensions.cs b/src/ModelContextProtocol/Client/McpClientExtensions.cs
index ced728e..436adb2 100644
--- a/src/ModelContextProtocol/Client/McpClientExtensions.cs
+++ b/src/ModelContextProtocol/Client/McpClientExtensions.cs
@@ -475,17 +475,7 @@ public static class McpClientExtensions
}
else if (sm.Content is { Type: "resource", Resource: not null })
{
- ResourceContents resource = sm.Content.Resource;
-
- if (resource.Text is not null)
- {
- message.Contents.Add(new TextContent(resource.Text));
- }
-
- if (resource.Blob is not null && resource.MimeType is not null)
- {
- message.Contents.Add(new DataContent(Convert.FromBase64String(resource.Blob), resource.MimeType));
- }
+ message.Contents.Add(sm.Content.Resource.ToAIContent());
}
messages.Add(message);
diff --git a/src/ModelContextProtocol/Protocol/Types/BlobResourceContents.cs b/src/ModelContextProtocol/Protocol/Types/BlobResourceContents.cs
new file mode 100644
index 0000000..f0a16ea
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Types/BlobResourceContents.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Protocol.Types;
+
+///
+/// Binary contents of a resource.
+/// See the schema for details
+///
+public class BlobResourceContents : ResourceContents
+{
+ ///
+ /// The base64-encoded string representing the binary data of the item.
+ ///
+ [JsonPropertyName("blob")]
+ public string Blob { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Protocol/Types/Resource.cs b/src/ModelContextProtocol/Protocol/Types/Resource.cs
index 249c584..b5fc15f 100644
--- a/src/ModelContextProtocol/Protocol/Types/Resource.cs
+++ b/src/ModelContextProtocol/Protocol/Types/Resource.cs
@@ -16,12 +16,16 @@ public record Resource
///
/// A human-readable name for this resource.
+ ///
+ /// This can be used by clients to populate UI elements.
///
[JsonPropertyName("name")]
public required string Name { get; init; }
///
/// A description of what this resource represents.
+ ///
+ /// 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.
///
[JsonPropertyName("description")]
public string? Description { get; init; }
@@ -32,6 +36,14 @@ public record Resource
[JsonPropertyName("mimeType")]
public string? MimeType { get; init; }
+ ///
+ /// The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
+ ///
+ /// This can be used by Hosts to display file sizes and estimate context window usage.
+ ///
+ [JsonPropertyName("size")]
+ public long? Size { get; init; }
+
///
/// Optional annotations for the resource.
///
diff --git a/src/ModelContextProtocol/Protocol/Types/ResourceContents.cs b/src/ModelContextProtocol/Protocol/Types/ResourceContents.cs
index 4dc524a..1daece1 100644
--- a/src/ModelContextProtocol/Protocol/Types/ResourceContents.cs
+++ b/src/ModelContextProtocol/Protocol/Types/ResourceContents.cs
@@ -1,4 +1,7 @@
-using System.Text.Json.Serialization;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Text.Json;
+using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol.Types;
@@ -6,8 +9,13 @@ namespace ModelContextProtocol.Protocol.Types;
/// Represents the content of a resource.
/// See the schema for details
///
-public class ResourceContents
+[JsonConverter(typeof(Converter))]
+public abstract class ResourceContents
{
+ internal ResourceContents()
+ {
+ }
+
///
/// The URI of the resource.
///
@@ -20,16 +28,104 @@ public class ResourceContents
[JsonPropertyName("mimeType")]
public string? MimeType { get; set; }
- ///
- /// The text content of the resource.
- ///
- [JsonPropertyName("text")]
- public string? Text { get; set; }
-
///
- /// The base64-encoded binary content of the resource.
+ /// Converter for .
///
- [JsonPropertyName("blob")]
- public string? Blob { get; set; }
-}
\ No newline at end of file
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public class Converter : JsonConverter
+ {
+ ///
+ public override ResourceContents? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ return null;
+ }
+
+ if (reader.TokenType != JsonTokenType.StartObject)
+ {
+ throw new JsonException();
+ }
+
+ string? uri = null;
+ string? mimeType = null;
+ string? blob = null;
+ string? text = null;
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ string? propertyName = reader.GetString();
+
+ switch (propertyName)
+ {
+ case "uri":
+ uri = reader.GetString();
+ break;
+ case "mimeType":
+ mimeType = reader.GetString();
+ break;
+ case "blob":
+ blob = reader.GetString();
+ break;
+ case "text":
+ text = reader.GetString();
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (blob is not null)
+ {
+ return new BlobResourceContents
+ {
+ Uri = uri ?? string.Empty,
+ MimeType = mimeType,
+ Blob = blob
+ };
+ }
+
+ if (text is not null)
+ {
+ return new TextResourceContents
+ {
+ Uri = uri ?? string.Empty,
+ MimeType = mimeType,
+ Text = text
+ };
+ }
+
+ return null;
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, ResourceContents value, JsonSerializerOptions options)
+ {
+ if (value is null)
+ {
+ writer.WriteNullValue();
+ return;
+ }
+
+ writer.WriteStartObject();
+ writer.WriteString("uri", value.Uri);
+ writer.WriteString("mimeType", value.MimeType);
+ Debug.Assert(value is BlobResourceContents or TextResourceContents);
+ if (value is BlobResourceContents blobResource)
+ {
+ writer.WriteString("blob", blobResource.Blob);
+ }
+ else if (value is TextResourceContents textResource)
+ {
+ writer.WriteString("text", textResource.Text);
+ }
+ writer.WriteEndObject();
+ }
+ }
+}
diff --git a/src/ModelContextProtocol/Protocol/Types/TextResourceContents.cs b/src/ModelContextProtocol/Protocol/Types/TextResourceContents.cs
new file mode 100644
index 0000000..dd14a6c
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Types/TextResourceContents.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Protocol.Types;
+
+///
+/// Text contents of a resource.
+/// See the schema for details
+///
+public class TextResourceContents : ResourceContents
+{
+ ///
+ /// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
+ ///
+ [JsonPropertyName("text")]
+ public string Text { get; set; } = string.Empty;
+}
diff --git a/src/ModelContextProtocol/Server/McpServer.cs b/src/ModelContextProtocol/Server/McpServer.cs
index af2c664..f2ed6e3 100644
--- a/src/ModelContextProtocol/Server/McpServer.cs
+++ b/src/ModelContextProtocol/Server/McpServer.cs
@@ -218,19 +218,21 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
return;
}
- if (resourcesCapability.ListResourcesHandler is not { } listResourcesHandler ||
+ var listResourcesHandler = resourcesCapability.ListResourcesHandler;
+ var listResourceTemplatesHandler = resourcesCapability.ListResourceTemplatesHandler;
+
+ if ((listResourcesHandler is not { } && listResourceTemplatesHandler is not { }) ||
resourcesCapability.ReadResourceHandler is not { } readResourceHandler)
{
throw new McpServerException("Resources capability was enabled, but ListResources and/or ReadResource handlers were not specified.");
}
+ listResourcesHandler ??= (static (_, _) => Task.FromResult(new ListResourcesResult()));
+
SetRequestHandler("resources/list", (request, ct) => listResourcesHandler(new(this, request), ct));
SetRequestHandler("resources/read", (request, ct) => readResourceHandler(new(this, request), ct));
- // Set the list resource templates handler, or use the default if not specified
- var listResourceTemplatesHandler = resourcesCapability.ListResourceTemplatesHandler
- ?? (static (_, _) => Task.FromResult(new ListResourceTemplatesResult()));
-
+ listResourceTemplatesHandler ??= (static (_, _) => Task.FromResult(new ListResourceTemplatesResult()));
SetRequestHandler("resources/templates/list", (request, ct) => listResourceTemplatesHandler(new(this, request), ct));
if (resourcesCapability.Subscribe is not true)
diff --git a/tests/ModelContextProtocol.TestServer/Program.cs b/tests/ModelContextProtocol.TestServer/Program.cs
index 6cb30a1..312013f 100644
--- a/tests/ModelContextProtocol.TestServer/Program.cs
+++ b/tests/ModelContextProtocol.TestServer/Program.cs
@@ -327,7 +327,7 @@ internal static class Program
Name = $"Resource {i + 1}",
MimeType = "text/plain"
});
- resourceContents.Add(new ResourceContents()
+ resourceContents.Add(new TextResourceContents()
{
Uri = uri,
MimeType = "text/plain",
@@ -343,7 +343,7 @@ internal static class Program
Name = $"Resource {i + 1}",
MimeType = "application/octet-stream"
});
- resourceContents.Add(new ResourceContents()
+ resourceContents.Add(new BlobResourceContents()
{
Uri = uri,
MimeType = "application/octet-stream",
@@ -417,7 +417,7 @@ internal static class Program
return Task.FromResult(new ReadResourceResult()
{
Contents = [
- new ResourceContents()
+ new TextResourceContents()
{
Uri = request.Params.Uri,
MimeType = "text/plain",
diff --git a/tests/ModelContextProtocol.TestSseServer/Program.cs b/tests/ModelContextProtocol.TestSseServer/Program.cs
index df722a3..b875112 100644
--- a/tests/ModelContextProtocol.TestSseServer/Program.cs
+++ b/tests/ModelContextProtocol.TestSseServer/Program.cs
@@ -81,7 +81,7 @@ public class Program
Name = $"Resource {i + 1}",
MimeType = "text/plain"
});
- resourceContents.Add(new ResourceContents()
+ resourceContents.Add(new TextResourceContents()
{
Uri = uri,
MimeType = "text/plain",
@@ -97,7 +97,7 @@ public class Program
Name = $"Resource {i + 1}",
MimeType = "application/octet-stream"
});
- resourceContents.Add(new ResourceContents()
+ resourceContents.Add(new BlobResourceContents()
{
Uri = uri,
MimeType = "application/octet-stream",
@@ -262,7 +262,7 @@ public class Program
return Task.FromResult(new ReadResourceResult()
{
Contents = [
- new ResourceContents()
+ new TextResourceContents()
{
Uri = request.Params.Uri,
MimeType = "text/plain",
diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs
index 8b8be80..76d91f6 100644
--- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs
+++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs
@@ -224,7 +224,9 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture(result.Contents[0]);
+ Assert.NotNull(textResource.Text);
}
[Theory]
@@ -241,7 +243,9 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture(result.Contents[0]);
+ Assert.NotNull(blobResource.Blob);
}
// Not supported by "everything" server version on npx
diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs
new file mode 100644
index 0000000..ff94633
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs
@@ -0,0 +1,100 @@
+using Microsoft.Extensions.DependencyInjection;
+using ModelContextProtocol.Protocol.Types;
+using ModelContextProtocol.Server;
+
+namespace ModelContextProtocol.Tests.Server;
+
+public class McpServerResourceTests
+{
+ [Fact]
+ public void CanCreateServerWithResourceTemplates()
+ {
+ var services = new ServiceCollection();
+
+ services.AddMcpServer()
+ .WithStdioServerTransport()
+ .WithListResourceTemplatesHandler((ctx, ct) =>
+ {
+ return Task.FromResult(new ListResourceTemplatesResult
+ {
+ ResourceTemplates =
+ [
+ new ResourceTemplate { Name = "Static Resource", Description = "A static resource with a numeric ID", UriTemplate = "test://static/resource/{id}" }
+ ]
+ });
+ })
+ .WithReadResourceHandler((ctx, ct) =>
+ {
+ return Task.FromResult(new ReadResourceResult
+ {
+ Contents = [new TextResourceContents
+ {
+ Uri = ctx.Params!.Uri!,
+ Text = "Static Resource",
+ MimeType = "text/plain",
+ }]
+ });
+ });
+
+ var provider = services.BuildServiceProvider();
+
+ provider.GetRequiredService();
+ }
+
+ [Fact]
+ public void CanCreateServerWithResources()
+ {
+ var services = new ServiceCollection();
+
+ services.AddMcpServer()
+ .WithStdioServerTransport()
+ .WithListResourcesHandler((ctx, ct) =>
+ {
+ return Task.FromResult(new ListResourcesResult
+ {
+ Resources =
+ [
+ new Resource { Name = "Static Resource", Description = "A static resource with a numeric ID", Uri = "test://static/resource/foo.txt" }
+ ]
+ });
+ })
+ .WithReadResourceHandler((ctx, ct) =>
+ {
+ return Task.FromResult(new ReadResourceResult
+ {
+ Contents = [new TextResourceContents
+ {
+ Uri = ctx.Params!.Uri!,
+ Text = "Static Resource",
+ MimeType = "text/plain",
+ }]
+ });
+ });
+
+ var provider = services.BuildServiceProvider();
+
+ provider.GetRequiredService();
+ }
+
+ [Fact]
+ public void CreatingReadHandlerWithNoListHandlerFails()
+ {
+ var services = new ServiceCollection();
+ services.AddMcpServer()
+ .WithStdioServerTransport()
+ .WithReadResourceHandler((ctx, ct) =>
+ {
+ return Task.FromResult(new ReadResourceResult
+ {
+ Contents = [new TextResourceContents
+ {
+ Uri = ctx.Params!.Uri!,
+ Text = "Static Resource",
+ MimeType = "text/plain",
+ }]
+ });
+ });
+ var sp = services.BuildServiceProvider();
+ Assert.Throws(() => sp.GetRequiredService());
+ }
+}
diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs
index 12b8f52..f745529 100644
--- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs
+++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs
@@ -349,7 +349,7 @@ public class McpServerTests : LoggedTest
{
return Task.FromResult(new ReadResourceResult
{
- Contents = [new() { Text = "test" }]
+ Contents = [new TextResourceContents() { Text = "test" }]
});
},
ListResourcesHandler = (request, ct) => throw new NotImplementedException(),
@@ -364,7 +364,9 @@ public class McpServerTests : LoggedTest
var result = (ReadResourceResult)response;
Assert.NotNull(result.Contents);
Assert.NotEmpty(result.Contents);
- Assert.Equal("test", result.Contents[0].Text);
+
+ TextResourceContents textResource = Assert.IsType(result.Contents[0]);
+ Assert.Equal("test", textResource.Text);
});
}
diff --git a/tests/ModelContextProtocol.Tests/SseServerIntegrationTests.cs b/tests/ModelContextProtocol.Tests/SseServerIntegrationTests.cs
index 82b1e63..0f6d90d 100644
--- a/tests/ModelContextProtocol.Tests/SseServerIntegrationTests.cs
+++ b/tests/ModelContextProtocol.Tests/SseServerIntegrationTests.cs
@@ -120,7 +120,9 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture(result.Contents[0]);
+ Assert.NotNull(textContent.Text);
}
[Fact]
@@ -137,7 +139,9 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture(result.Contents[0]);
+ Assert.NotNull(blobContent.Blob);
}
[Fact]