Skip to content

Resources-enhancements #257

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

Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,68 @@ where t.GetCustomAttribute<McpServerPromptTypeAttribute>() is not null
}
#endregion

#region WithResources

private static IMcpServerBuilder AddResource(
this IMcpServerBuilder builder,
McpServerResource resource)
{
builder.Services.Configure<McpServerOptions>(s =>
{
var capabilities = s.Capabilities ??= new();
var resources = capabilities.Resources ??= new();
var collection = resources.ResourceCollection ??= [];
collection.Add(resource);
});

return builder;
}

private static IMcpServerBuilder AddResources(
this IMcpServerBuilder builder,
IEnumerable<McpServerResource> resources)
{
foreach (var resource in resources)
{
builder = builder.AddResource(resource);
}
return builder;
}

/// <summary>
/// Adds a resource to the server's capabilities.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="resource">The resource to add.</param>
/// <returns>The <see cref="IMcpServerBuilder"/> instance.</returns>
public static IMcpServerBuilder WithResource(
this IMcpServerBuilder builder,
McpServerResource resource)
{
Throw.IfNull(builder);
Throw.IfNull(resource);

return builder.AddResource(resource);
}

/// <summary>
/// Adds a collection of resources to the server's capabilities.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="resources">The collection of the resources.</param>
/// <returns>The <see cref="IMcpServerBuilder"/> instance.</returns>
public static IMcpServerBuilder WithResources(
this IMcpServerBuilder builder,
params IEnumerable<McpServerResource> resources)
{
Throw.IfNull(builder);
Throw.IfNull(resources);

return builder.AddResources(resources);
}

#endregion

#region Handlers
/// <summary>
/// Configures a handler for listing resource templates available from the Model Context Protocol server.
Expand Down
24 changes: 24 additions & 0 deletions src/ModelContextProtocol/Protocol/Types/IListCapability.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using ModelContextProtocol.Server;
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Protocol.Types;

/// <summary>
/// Represents the tools capability configuration.
/// </summary>
/// <typeparam name="TPrimitive">The type of the primitive.</typeparam>
internal interface IListCapability<TPrimitive>
where TPrimitive : IMcpServerPrimitive
{
/// <summary>
/// Gets or sets whether this server supports notifications for changes to the tool list.
/// </summary>
[JsonPropertyName("listChanged")]
public bool? ListChanged { get; set; }

/// <summary>
/// Gets or sets the handler for list tools requests.
/// </summary>
[JsonIgnore]
public McpServerPrimitiveCollection<TPrimitive>? Collection { get; set; }
}
8 changes: 7 additions & 1 deletion src/ModelContextProtocol/Protocol/Types/PromptsCapability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace ModelContextProtocol.Protocol.Types;
/// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for details.
/// </para>
/// </remarks>
public class PromptsCapability
public class PromptsCapability : IListCapability<McpServerPrompt>
{
/// <summary>
/// Gets or sets whether this server supports notifications for changes to the prompt list.
Expand Down Expand Up @@ -80,4 +80,10 @@ public class PromptsCapability
/// </remarks>
[JsonIgnore]
public McpServerPrimitiveCollection<McpServerPrompt>? PromptCollection { get; set; }

McpServerPrimitiveCollection<McpServerPrompt>? IListCapability<McpServerPrompt>.Collection
{
get => PromptCollection;
set => PromptCollection = value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ public class ReadResourceRequestParams : RequestParams
/// The URI of the resource to read. The URI can use any protocol; it is up to the server how to interpret it.
/// </summary>
[JsonPropertyName("uri")]
public string? Uri { get; init; }
public required string Uri { get; init; }
}
14 changes: 13 additions & 1 deletion src/ModelContextProtocol/Protocol/Types/ResourcesCapability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace ModelContextProtocol.Protocol.Types;
/// <remarks>
/// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for details.
/// </remarks>
public class ResourcesCapability
public class ResourcesCapability : IListCapability<McpServerResource>
{
/// <summary>
/// Gets or sets whether this server supports subscribing to resource updates.
Expand Down Expand Up @@ -87,4 +87,16 @@ public class ResourcesCapability
/// </remarks>
[JsonIgnore]
public Func<RequestContext<UnsubscribeRequestParams>, CancellationToken, ValueTask<EmptyResult>>? UnsubscribeFromResourcesHandler { get; set; }

/// <summary>
/// The list of resource templates that the server supports.
/// </summary>
[JsonIgnore]
public McpServerPrimitiveCollection<McpServerResource>? ResourceCollection { get; set; }

McpServerPrimitiveCollection<McpServerResource>? IListCapability<McpServerResource>.Collection
{
get => ResourceCollection;
set => ResourceCollection = value;
}
}
8 changes: 7 additions & 1 deletion src/ModelContextProtocol/Protocol/Types/ToolsCapability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace ModelContextProtocol.Protocol.Types;
/// Represents the tools capability configuration.
/// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for details.
/// </summary>
public class ToolsCapability
public class ToolsCapability : IListCapability<McpServerTool>
{
/// <summary>
/// Gets or sets whether this server supports notifications for changes to the tool list.
Expand Down Expand Up @@ -61,4 +61,10 @@ public class ToolsCapability
/// </remarks>
[JsonIgnore]
public McpServerPrimitiveCollection<McpServerTool>? ToolCollection { get; set; }

McpServerPrimitiveCollection<McpServerTool>? IListCapability<McpServerTool>.Collection
{
get => ToolCollection;
set => ToolCollection = value;
}
}
83 changes: 46 additions & 37 deletions src/ModelContextProtocol/Server/McpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
private readonly ITransport _sessionTransport;
private readonly bool _servicesScopePerRequest;

private readonly EventHandler? _toolsChangedDelegate;
private readonly EventHandler? _promptsChangedDelegate;

private string _endpointName;
private int _started;

Expand All @@ -35,6 +32,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
/// rather than a nullable to be able to manipulate it atomically.
/// </remarks>
private StrongBox<LoggingLevel>? _loggingLevel;
private readonly List<Action> _disposables = [];

/// <summary>
/// Creates a new instance of <see cref="McpServer"/>.
Expand Down Expand Up @@ -68,32 +66,17 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory?
SetCompletionHandler(options);
SetPingHandler();

var capabilities = options.Capabilities;

// Register any notification handlers that were provided.
if (options.Capabilities?.NotificationHandlers is { } notificationHandlers)
if (capabilities?.NotificationHandlers is { } notificationHandlers)
{
NotificationHandlers.RegisterRange(notificationHandlers);
}

// Now that everything has been configured, subscribe to any necessary notifications.
if (ServerOptions.Capabilities?.Tools?.ToolCollection is { } tools)
{
_toolsChangedDelegate = delegate
{
_ = SendMessageAsync(new JsonRpcNotification() { Method = NotificationMethods.ToolListChangedNotification });
};

tools.Changed += _toolsChangedDelegate;
}

if (ServerOptions.Capabilities?.Prompts?.PromptCollection is { } prompts)
{
_promptsChangedDelegate = delegate
{
_ = SendMessageAsync(new JsonRpcNotification() { Method = NotificationMethods.PromptListChangedNotification });
};

prompts.Changed += _promptsChangedDelegate;
}

RegisterListChange(capabilities?.Tools, NotificationMethods.ToolListChangedNotification);
RegisterListChange(capabilities?.Prompts, NotificationMethods.PromptListChangedNotification);
RegisterListChange(capabilities?.Resources, NotificationMethods.ResourceListChangedNotification);

// And initialize the session.
InitializeSession(transport);
Expand Down Expand Up @@ -140,18 +123,11 @@ public async Task RunAsync(CancellationToken cancellationToken = default)

public override async ValueTask DisposeUnsynchronizedAsync()
{
if (_toolsChangedDelegate is not null &&
ServerOptions.Capabilities?.Tools?.ToolCollection is { } tools)
{
tools.Changed -= _toolsChangedDelegate;
}

if (_promptsChangedDelegate is not null &&
ServerOptions.Capabilities?.Prompts?.PromptCollection is { } prompts)
foreach (var disposable in _disposables)
{
prompts.Changed -= _promptsChangedDelegate;
disposable();
}

_disposables.Clear();
await base.DisposeUnsynchronizedAsync().ConfigureAwait(false);
}

Expand Down Expand Up @@ -216,9 +192,26 @@ private void SetResourcesHandler(McpServerOptions options)

var listResourcesHandler = resourcesCapability.ListResourcesHandler;
var listResourceTemplatesHandler = resourcesCapability.ListResourceTemplatesHandler;
var readResourceHandler = resourcesCapability.ReadResourceHandler;
var resourceCollection = resourcesCapability.ResourceCollection;

if ((listResourcesHandler is not { } && listResourceTemplatesHandler is not { }) ||
resourcesCapability.ReadResourceHandler is not { } readResourceHandler)
var originalListResourcesHandler = listResourcesHandler;
listResourcesHandler = async (request, cancellationToken) =>
{
ListResourcesResult result = originalListResourcesHandler is not null ?
await originalListResourcesHandler(request, cancellationToken).ConfigureAwait(false) :
new();

if (request.Params?.Cursor is null && resourceCollection is not null)
{
result.Resources.AddRange(resourceCollection.Select(t => t.ProtocolResource));
}

return result;
};

var isMissingListResourceHandlers = originalListResourcesHandler is null && listResourceTemplatesHandler is null;
if (resourceCollection is not { IsEmpty: false } && (isMissingListResourceHandlers || readResourceHandler is not { }))
{
throw new InvalidOperationException(
$"{nameof(ServerCapabilities)}.{nameof(ServerCapabilities.Resources)} was enabled, " +
Expand All @@ -233,6 +226,7 @@ private void SetResourcesHandler(McpServerOptions options)
McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams,
McpJsonUtilities.JsonContext.Default.ListResourcesResult);

readResourceHandler ??= static async (_, _) => new();
SetHandler(
RequestMethods.ResourcesRead,
readResourceHandler,
Expand Down Expand Up @@ -551,6 +545,21 @@ private void SetHandler<TRequest, TResponse>(
requestTypeInfo, responseTypeInfo);
}

private void RegisterListChange<T>(IListCapability<T>? capability, string methodName)
where T : IMcpServerPrimitive
{
// https://modelcontextprotocol.io/specification/2024-11-05/server/tools#capabilities
// Look to spec for guidance on ListChanged over collection existance.
Comment on lines +551 to +552
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

if (capability?.Collection is { } collection)
//&& capability.ListChanged is true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

{
void ChangedDelegate(object? sender, EventArgs e)
=> _ = this.SendNotificationAsync(methodName);
collection.Changed += ChangedDelegate;
_disposables.Add(() => collection.Changed -= ChangedDelegate);
}
}

/// <summary>Maps a <see cref="LogLevel"/> to a <see cref="LoggingLevel"/>.</summary>
internal static LoggingLevel ToLoggingLevel(LogLevel level) =>
level switch
Expand Down
28 changes: 28 additions & 0 deletions src/ModelContextProtocol/Server/McpServerResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Microsoft.Extensions.FileProviders;
using ModelContextProtocol.Protocol.Types;

namespace ModelContextProtocol.Server;

/// <summary>
/// Represents a resource that the server supports.
/// </summary>
public abstract class McpServerResource : IMcpServerPrimitive
{
/// <summary>
/// The resource instance.
/// </summary>
public abstract required Resource ProtocolResource { get; init; }

/// <inheritdoc />
public string Name => ProtocolResource.Name;

/// <summary>
/// Gets the resource URI.
/// </summary>
/// <param name="request">The request context.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The file info of the resource.</returns>
public abstract Task<IFileInfo> GetFileInfoAsync(
RequestContext<ReadResourceRequestParams> request,
CancellationToken cancellationToken = default);
}
Loading
Loading