Skip to content

Overhaul of Resources #125

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 31, 2025
Merged
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
19 changes: 9 additions & 10 deletions src/ModelContextProtocol/AIContentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,7 @@ public static AIContent ToAIContent(this Content content)
}
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
{
Expand All @@ -62,9 +58,12 @@ public static AIContent ToAIContent(this ResourceContents content)
{
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;
Expand All @@ -79,7 +78,7 @@ public static IList<AIContent> ToAIContents(this IEnumerable<Content> contents)
{
Throw.IfNull(contents);

return contents.Select(ToAIContent).ToList();
return [.. contents.Select(ToAIContent)];
}

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

return contents.Select(ToAIContent).ToList();
return [.. contents.Select(ToAIContent)];
}

/// <summary>Extracts the data from a <see cref="DataContent"/> as a Base64 string.</summary>
Expand Down
12 changes: 1 addition & 11 deletions src/ModelContextProtocol/Client/McpClientExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -475,17 +475,7 @@ internal static (IList<ChatMessage> Messages, ChatOptions? Options) ToChatClient
}
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);
Expand Down
16 changes: 16 additions & 0 deletions src/ModelContextProtocol/Protocol/Types/BlobResourceContents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Protocol.Types;

/// <summary>
/// Binary contents of a resource.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">See the schema for details</see>
/// </summary>
public class BlobResourceContents : ResourceContents
{
/// <summary>
/// The base64-encoded string representing the binary data of the item.
/// </summary>
[JsonPropertyName("blob")]
public string Blob { get; set; } = string.Empty;
}
12 changes: 12 additions & 0 deletions src/ModelContextProtocol/Protocol/Types/Resource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ public record Resource

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

/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("description")]
public string? Description { get; init; }
Expand All @@ -32,6 +36,14 @@ public record Resource
[JsonPropertyName("mimeType")]
public string? MimeType { get; init; }

/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("size")]
public long? Size { get; init; }

/// <summary>
/// Optional annotations for the resource.
/// </summary>
Expand Down
118 changes: 107 additions & 11 deletions src/ModelContextProtocol/Protocol/Types/ResourceContents.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
using System.Text.Json.Serialization;
using System.ComponentModel;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Protocol.Types;

/// <summary>
/// Represents the content of a resource.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">See the schema for details</see>
/// </summary>
public class ResourceContents
[JsonConverter(typeof(Converter))]
public abstract class ResourceContents
{
internal ResourceContents()
{
}

/// <summary>
/// The URI of the resource.
/// </summary>
Expand All @@ -20,16 +28,104 @@ public class ResourceContents
[JsonPropertyName("mimeType")]
public string? MimeType { get; set; }


/// <summary>
/// The text content of the resource.
/// Converter for <see cref="ResourceContents"/>.
/// </summary>
[JsonPropertyName("text")]
public string? Text { get; set; }
[EditorBrowsable(EditorBrowsableState.Never)]
public class Converter : JsonConverter<ResourceContents>
{
/// <inheritdoc/>
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();
}

/// <summary>
/// The base64-encoded binary content of the resource.
/// </summary>
[JsonPropertyName("blob")]
public string? Blob { get; set; }
}
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;
}

/// <inheritdoc/>
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();
}
}
}
16 changes: 16 additions & 0 deletions src/ModelContextProtocol/Protocol/Types/TextResourceContents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Protocol.Types;

/// <summary>
/// Text contents of a resource.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">See the schema for details</see>
/// </summary>
public class TextResourceContents : ResourceContents
{
/// <summary>
/// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
/// </summary>
[JsonPropertyName("text")]
public string Text { get; set; } = string.Empty;
}
12 changes: 7 additions & 5 deletions src/ModelContextProtocol/Server/McpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,19 +218,21 @@ private void SetResourcesHandler(McpServerOptions options)
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<ListResourcesRequestParams, ListResourcesResult>("resources/list", (request, ct) => listResourcesHandler(new(this, request), ct));
SetRequestHandler<ReadResourceRequestParams, ReadResourceResult>("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<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>("resources/templates/list", (request, ct) => listResourceTemplatesHandler(new(this, request), ct));

if (resourcesCapability.Subscribe is not true)
Expand Down
6 changes: 3 additions & 3 deletions tests/ModelContextProtocol.TestServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ private static ResourcesCapability ConfigureResources()
Name = $"Resource {i + 1}",
MimeType = "text/plain"
});
resourceContents.Add(new ResourceContents()
resourceContents.Add(new TextResourceContents()
{
Uri = uri,
MimeType = "text/plain",
Expand All @@ -343,7 +343,7 @@ private static ResourcesCapability ConfigureResources()
Name = $"Resource {i + 1}",
MimeType = "application/octet-stream"
});
resourceContents.Add(new ResourceContents()
resourceContents.Add(new BlobResourceContents()
{
Uri = uri,
MimeType = "application/octet-stream",
Expand Down Expand Up @@ -417,7 +417,7 @@ private static ResourcesCapability ConfigureResources()
return Task.FromResult(new ReadResourceResult()
{
Contents = [
new ResourceContents()
new TextResourceContents()
{
Uri = request.Params.Uri,
MimeType = "text/plain",
Expand Down
6 changes: 3 additions & 3 deletions tests/ModelContextProtocol.TestSseServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
Name = $"Resource {i + 1}",
MimeType = "text/plain"
});
resourceContents.Add(new ResourceContents()
resourceContents.Add(new TextResourceContents()
{
Uri = uri,
MimeType = "text/plain",
Expand All @@ -97,7 +97,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
Name = $"Resource {i + 1}",
MimeType = "application/octet-stream"
});
resourceContents.Add(new ResourceContents()
resourceContents.Add(new BlobResourceContents()
{
Uri = uri,
MimeType = "application/octet-stream",
Expand Down Expand Up @@ -262,7 +262,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
return Task.FromResult(new ReadResourceResult()
{
Contents = [
new ResourceContents()
new TextResourceContents()
{
Uri = request.Params.Uri,
MimeType = "text/plain",
Expand Down
8 changes: 6 additions & 2 deletions tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,9 @@ public async Task ReadResource_Stdio_TextResource(string clientId)

Assert.NotNull(result);
Assert.Single(result.Contents);
Assert.NotNull(result.Contents[0].Text);

TextResourceContents textResource = Assert.IsType<TextResourceContents>(result.Contents[0]);
Assert.NotNull(textResource.Text);
}

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

Assert.NotNull(result);
Assert.Single(result.Contents);
Assert.NotNull(result.Contents[0].Blob);

BlobResourceContents blobResource = Assert.IsType<BlobResourceContents>(result.Contents[0]);
Assert.NotNull(blobResource.Blob);
}

// Not supported by "everything" server version on npx
Expand Down
Loading