Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
114 changes: 114 additions & 0 deletions src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ModelContextProtocol.Hosting;
Expand Down Expand Up @@ -242,6 +243,119 @@ where t.GetCustomAttribute<McpServerPromptTypeAttribute>() is not null
}
#endregion

#region WithResources

private static McpServerResource ToResource(
this IFileInfo fileInfo)
{
Throw.IfNullOrWhiteSpace(fileInfo.PhysicalPath);

return new()
{
ProtocolResource = new()
{
Uri = fileInfo.PhysicalPath,
Name = fileInfo.Name,
},
};
}

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 resource to the server's capabilities.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="fileInfo">The file info of the resource.</param>
/// <returns>The <see cref="IMcpServerBuilder"/> instance.</returns>
public static IMcpServerBuilder WithResource(
this IMcpServerBuilder builder,
IFileInfo fileInfo)
{
Throw.IfNull(builder);
Throw.IfNull(fileInfo);

var resource = fileInfo.ToResource();
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);
}

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

return builder.AddResources(
from fileInfo in fileInfos
where fileInfo is not null
select fileInfo.ToResource());
}

#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, Task<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;
}
}
7 changes: 6 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,9 @@ public class ToolsCapability
/// </remarks>
[JsonIgnore]
public McpServerPrimitiveCollection<McpServerTool>? ToolCollection { get; set; }
McpServerPrimitiveCollection<McpServerTool>? IListCapability<McpServerTool>.Collection
{
get => ToolCollection;
set => ToolCollection = value;
}
}
86 changes: 46 additions & 40 deletions src/ModelContextProtocol/Server/McpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ internal sealed class McpServer : McpEndpoint, IMcpServer

private readonly ITransport _sessionTransport;

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

private string _endpointName;
private int _started;

Expand All @@ -32,6 +29,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<Disposable> _disposables = [];

/// <summary>
/// Creates a new instance of <see cref="McpServer"/>.
Expand Down Expand Up @@ -64,32 +62,16 @@ 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 @@ -136,18 +118,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.Dispose();
}

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

Expand Down Expand Up @@ -210,21 +185,37 @@ 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) =>
{
throw new McpException("Resources capability was enabled, but ListResources and/or ReadResource handlers were not specified.");
}
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));
}

listResourcesHandler ??= (static (_, _) => Task.FromResult(new ListResourcesResult()));
return result;
};

var isMissingListResourceHandlers = originalListResourcesHandler is not { } && listResourceTemplatesHandler is not { };
if (resourceCollection is not { IsEmpty: false } && (isMissingListResourceHandlers || readResourceHandler is not { }))
{
throw new McpException("Resources capability was enabled, but ListResources, ListResourceTemplates, and/or ReadResource handlers were not specified.");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand this logic. Why is "is missing" true if both originalListResourcesHandler and listResourceTemplatesHandler are non-null?


RequestHandlers.Set(
RequestMethods.ResourcesList,
(request, cancellationToken) => listResourcesHandler(new(this, request), cancellationToken),
McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams,
McpJsonUtilities.JsonContext.Default.ListResourcesResult);

readResourceHandler ??= static (_, _) => Task.FromResult(new ReadResourceResult());
RequestHandlers.Set(
RequestMethods.ResourcesRead,
(request, cancellationToken) => readResourceHandler(new(this, request), cancellationToken),
Expand Down Expand Up @@ -483,6 +474,21 @@ private void SetSetLoggingLevelHandler(McpServerOptions options)
McpJsonUtilities.JsonContext.Default.EmptyResult);
}

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(new(() => 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
17 changes: 17 additions & 0 deletions src/ModelContextProtocol/Server/McpServerResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using ModelContextProtocol.Protocol.Types;

namespace ModelContextProtocol.Server;

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

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