Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion src/ModelContextProtocol/ModelContextProtocol.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI.Abstractions"/>
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
Expand Down
35 changes: 0 additions & 35 deletions src/ModelContextProtocol/Protocol/Types/ResourceContents.cs

This file was deleted.

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/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public class BlobResourceContents : ResourceContents
{
/// <summary>
/// The binary content of the resource.
/// </summary>
[JsonPropertyName("blob")]
public string Blob { get; set; } = default!;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace ModelContextProtocol.Protocol.Types;

/// <summary>
/// Represents a known resource that the server is capable of reading.
/// A known resource that the server is capable of reading.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public record Resource : Annotated
Expand All @@ -16,12 +16,16 @@ public record Resource : Annotated

/// <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 @@ -31,4 +35,12 @@ public record Resource : Annotated
/// </summary>
[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; }
}
120 changes: 120 additions & 0 deletions src/ModelContextProtocol/Protocol/Types/Resources/ResourceContents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Protocol.Types;

/// <summary>
/// The contents of a specific resource or sub-resource.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
[JsonConverter(typeof(ResourceContentsConverter))]
public abstract class ResourceContents
{
/// <summary>
/// The URI of the resource.
/// </summary>
[JsonPropertyName("uri")]
public string Uri { get; set; } = string.Empty;

/// <summary>
/// The type of content.
/// </summary>
[JsonPropertyName("mimeType")]
public string? MimeType { get; set; }
}

internal class ResourceContentsConverter : JsonConverter<ResourceContents>
{
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())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}

string? propertyName = reader.GetString();

if (propertyName == null)
{
continue;
}

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 == null)
{
writer.WriteNullValue();
return;
}

writer.WriteStartObject();
writer.WriteString("uri", value.Uri);
writer.WriteString("mimeType", value.MimeType);
if (value is BlobResourceContents blobResource)
{
writer.WriteString("blob", blobResource.Blob);
}
else if (value is TextResourceContents textResource)
{
writer.WriteString("text", textResource.Text);
}
writer.WriteEndObject();
}
}
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/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public class TextResourceContents : ResourceContents
{
/// <summary>
/// The text content of the resource.
/// </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 @@ -170,19 +170,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 @@ -330,7 +330,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 @@ -346,7 +346,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 @@ -420,7 +420,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 @@ -83,7 +83,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 @@ -99,7 +99,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 @@ -264,7 +264,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
Loading