Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
72 changes: 34 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,54 +174,50 @@ using System.Text.Json;
McpServerOptions options = new()
{
ServerInfo = new Implementation { Name = "MyServer", Version = "1.0.0" },
Capabilities = new ServerCapabilities
Handlers = new McpServerHandlers()
{
Tools = new ToolsCapability
ListToolsHandler = (request, cancellationToken) =>
ValueTask.FromResult(new ListToolsResult
{
ListToolsHandler = (request, cancellationToken) =>
ValueTask.FromResult(new ListToolsResult
Tools =
[
new Tool
{
Tools =
[
new Tool
Name = "echo",
Description = "Echoes the input back to the client.",
InputSchema = JsonSerializer.Deserialize<JsonElement>("""
{
Name = "echo",
Description = "Echoes the input back to the client.",
InputSchema = JsonSerializer.Deserialize<JsonElement>("""
{
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "The input to echo back"
}
},
"required": ["message"]
}
"""),
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "The input to echo back"
}
},
"required": ["message"]
}
]
}),

CallToolHandler = (request, cancellationToken) =>
"""),
}
]
}),
CallToolHandler = (request, cancellationToken) =>
{
if (request.Params?.Name == "echo")
{
if (request.Params?.Name == "echo")
if (request.Params.Arguments?.TryGetValue("message", out var message) is not true)
{
if (request.Params.Arguments?.TryGetValue("message", out var message) is not true)
{
throw new McpException("Missing required argument 'message'");
}

return ValueTask.FromResult(new CallToolResult
{
Content = [new TextContentBlock { Text = $"Echo: {message}", Type = "text" }]
});
throw new McpException("Missing required argument 'message'");
}

throw new McpException($"Unknown tool: '{request.Params?.Name}'");
},
return ValueTask.FromResult(new CallToolResult
{
Content = [new TextContentBlock { Text = $"Echo: {message}", Type = "text" }]
});
}

throw new McpException($"Unknown tool: '{request.Params?.Name}'");
}
},
}
};

await using McpServer server = McpServer.Create(new StdioServerTransport("MyServer"), options);
Expand Down
7 changes: 2 additions & 5 deletions docs/concepts/elicitation/samples/client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,9 @@
Name = "ElicitationClient",
Version = "1.0.0"
},
Capabilities = new()
Handlers = new()
{
Elicitation = new()
{
ElicitationHandler = HandleElicitationAsync
}
ElicitationHandler = HandleElicitationAsync
}
};

Expand Down
2 changes: 1 addition & 1 deletion samples/AspNetCoreMcpPerSessionTools/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
{
mcpOptions.Capabilities = new();
mcpOptions.Capabilities.Tools = new();
var toolCollection = mcpOptions.Capabilities.Tools.ToolCollection = new();
var toolCollection = mcpOptions.ToolCollection = new();

foreach (var tool in tools)
{
Expand Down
5 changes: 4 additions & 1 deletion samples/ChatWithTools/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@
}),
clientOptions: new()
{
Capabilities = new() { Sampling = new() { SamplingHandler = samplingClient.CreateSamplingHandler() } },
Handlers = new()
{
SamplingHandler = samplingClient.CreateSamplingHandler()
}
},
loggerFactory: loggerFactory);

Expand Down
8 changes: 1 addition & 7 deletions samples/InMemoryTransport/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,7 @@
new StreamServerTransport(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream()),
new McpServerOptions()
{
Capabilities = new()
{
Tools = new()
{
ToolCollection = [McpServerTool.Create((string arg) => $"Echo: {arg}", new() { Name = "Echo" })]
}
}
ToolCollection = [McpServerTool.Create((string arg) => $"Echo: {arg}", new() { Name = "Echo" })]
});
_ = server.RunAsync();

Expand Down
4 changes: 2 additions & 2 deletions src/ModelContextProtocol.Core/Client/McpClient.Methods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -631,11 +631,11 @@ internal static CreateMessageResult ToCreateMessageResult(ChatResponse chatRespo
}

/// <summary>
/// Creates a sampling handler for use with <see cref="SamplingCapability.SamplingHandler"/> that will
/// Creates a sampling handler for use with <see cref="McpClientHandlers.SamplingHandler"/> that will
/// satisfy sampling requests using the specified <see cref="IChatClient"/>.
/// </summary>
/// <param name="chatClient">The <see cref="IChatClient"/> with which to satisfy sampling requests.</param>
/// <returns>The created handler delegate that can be assigned to <see cref="SamplingCapability.SamplingHandler"/>.</returns>
/// <returns>The created handler delegate that can be assigned to <see cref="McpClientHandlers.SamplingHandler"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="chatClient"/> is <see langword="null"/>.</exception>
public static Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>> CreateSamplingHandler(
IChatClient chatClient)
Expand Down
4 changes: 2 additions & 2 deletions src/ModelContextProtocol.Core/Client/McpClientExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ namespace ModelContextProtocol.Client;
public static class McpClientExtensions
{
/// <summary>
/// Creates a sampling handler for use with <see cref="SamplingCapability.SamplingHandler"/> that will
/// Creates a sampling handler for use with <see cref="McpClientHandlers.SamplingHandler"/> that will
/// satisfy sampling requests using the specified <see cref="IChatClient"/>.
/// </summary>
/// <param name="chatClient">The <see cref="IChatClient"/> with which to satisfy sampling requests.</param>
/// <returns>The created handler delegate that can be assigned to <see cref="SamplingCapability.SamplingHandler"/>.</returns>
/// <returns>The created handler delegate that can be assigned to <see cref="McpClientHandlers.SamplingHandler"/>.</returns>
/// <remarks>
/// <para>
/// This method creates a function that converts MCP message requests into chat client calls, enabling
Expand Down
89 changes: 89 additions & 0 deletions src/ModelContextProtocol.Core/Client/McpClientHandlers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using Microsoft.Extensions.AI;
using ModelContextProtocol.Protocol;

namespace ModelContextProtocol.Client;

/// <summary>
/// Provides a container for handlers used in the creation of an MCP client.
/// </summary>
/// <remarks>
/// <para>
/// This class provides a centralized collection of delegates that implement various capabilities of the Model Context Protocol.
/// </para>
/// <para>
/// Each handler in this class corresponds to a specific client endpoint in the Model Context Protocol and
/// is responsible for processing a particular type of message. The handlers are used to customize
/// the behavior of the MCP server by providing implementations for the various protocol operations.
/// </para>
/// <para>
/// When a server sends a message to the client, the appropriate handler is invoked to process it
/// according to the protocol specification. Which handler is selected
/// is done based on an ordinal, case-sensitive string comparison.
/// </para>
/// </remarks>
public class McpClientHandlers
{

/// <summary>Gets or sets notification handlers to register with the client.</summary>
/// <remarks>
/// <para>
/// When constructed, the client will enumerate these handlers once, which may contain multiple handlers per notification method key.
/// The client will not re-enumerate the sequence after initialization.
/// </para>
/// <para>
/// Notification handlers allow the client to respond to server-sent notifications for specific methods.
/// Each key in the collection is a notification method name, and each value is a callback that will be invoked
/// when a notification with that method is received.
/// </para>
/// <para>
/// Handlers provided via <see cref="NotificationHandlers"/> will be registered with the client for the lifetime of the client.
/// For transient handlers, <see cref="IMcpEndpoint.RegisterNotificationHandler"/> may be used to register a handler that can
/// then be unregistered by disposing of the <see cref="IAsyncDisposable"/> returned from the method.
/// </para>
/// </remarks>
public IEnumerable<KeyValuePair<string, Func<JsonRpcNotification, CancellationToken, ValueTask>>>? NotificationHandlers { get; set; }

/// <summary>
/// Gets or sets the handler for <see cref="RequestMethods.RootsList"/> requests.
/// </summary>
/// <remarks>
/// This handler is invoked when a client sends a <see cref="RequestMethods.RootsList"/> request to retrieve available roots.
/// The handler receives request parameters and should return a <see cref="ListRootsResult"/> containing the collection of available roots.
/// </remarks>
public Func<ListRootsRequestParams?, CancellationToken, ValueTask<ListRootsResult>>? RootsHandler { get; set; }

/// <summary>
/// Gets or sets the handler for processing <see cref="RequestMethods.ElicitationCreate"/> requests.
/// </summary>
/// <remarks>
/// <para>
/// This handler function is called when an MCP server requests the client to provide additional
/// information during interactions. The client must set this property for the elicitation capability to work.
/// </para>
/// <para>
/// The handler receives message parameters and a cancellation token.
/// It should return a <see cref="ElicitResult"/> containing the response to the elicitation request.
/// </para>
/// </remarks>
public Func<ElicitRequestParams?, CancellationToken, ValueTask<ElicitResult>>? ElicitationHandler { get; set; }

/// <summary>
/// Gets or sets the handler for processing <see cref="RequestMethods.SamplingCreateMessage"/> requests.
/// </summary>
/// <remarks>
/// <para>
/// This handler function is called when an MCP server requests the client to generate content
/// using an AI model. The client must set this property for the sampling capability to work.
/// </para>
/// <para>
/// The handler receives message parameters, a progress reporter for updates, and a
/// cancellation token. It should return a <see cref="CreateMessageResult"/> containing the
/// generated content.
/// </para>
/// <para>
/// You can create a handler using the <see cref="McpClientExtensions.CreateSamplingHandler"/> extension
/// method with any implementation of <see cref="IChatClient"/>.
/// </para>
/// </remarks>
public Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>>? SamplingHandler { get; set; }
}
41 changes: 16 additions & 25 deletions src/ModelContextProtocol.Core/Client/McpClientImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,28 +49,20 @@ internal McpClientImpl(ITransport transport, string endpointName, McpClientOptio
var notificationHandlers = new NotificationHandlers();
var requestHandlers = new RequestHandlers();

if (options.Capabilities is { } capabilities)
{
RegisterHandlers(capabilities, notificationHandlers, requestHandlers);
}
RegisterHandlers(options.Handlers, notificationHandlers, requestHandlers);

_sessionHandler = new McpSessionHandler(isServer: false, transport, endpointName, requestHandlers, notificationHandlers, _logger);
}

private void RegisterHandlers(ClientCapabilities capabilities, NotificationHandlers notificationHandlers, RequestHandlers requestHandlers)
private void RegisterHandlers(McpClientHandlers handlers, NotificationHandlers notificationHandlers, RequestHandlers requestHandlers)
{
if (capabilities.NotificationHandlers is { } notificationHandlersFromCapabilities)
if (handlers.NotificationHandlers is { } notificationHandlersFromOptions)
{
notificationHandlers.RegisterRange(notificationHandlersFromCapabilities);
notificationHandlers.RegisterRange(notificationHandlersFromOptions);
}

if (capabilities.Sampling is { } samplingCapability)
if (handlers.SamplingHandler is { } samplingHandler)
{
if (samplingCapability.SamplingHandler is not { } samplingHandler)
{
throw new InvalidOperationException("Sampling capability was set but it did not provide a handler.");
}

requestHandlers.Set(
RequestMethods.SamplingCreateMessage,
(request, _, cancellationToken) => samplingHandler(
Expand All @@ -79,34 +71,33 @@ private void RegisterHandlers(ClientCapabilities capabilities, NotificationHandl
cancellationToken),
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
McpJsonUtilities.JsonContext.Default.CreateMessageResult);

_options.Capabilities ??= new();
_options.Capabilities.Sampling ??= new();
}

if (capabilities.Roots is { } rootsCapability)
if (handlers.RootsHandler is { } rootsHandler)
{
if (rootsCapability.RootsHandler is not { } rootsHandler)
{
throw new InvalidOperationException("Roots capability was set but it did not provide a handler.");
}

requestHandlers.Set(
RequestMethods.RootsList,
(request, _, cancellationToken) => rootsHandler(request, cancellationToken),
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
McpJsonUtilities.JsonContext.Default.ListRootsResult);

_options.Capabilities ??= new();
_options.Capabilities.Roots ??= new();
}

if (capabilities.Elicitation is { } elicitationCapability)
if (handlers.ElicitationHandler is { } elicitationHandler)
{
if (elicitationCapability.ElicitationHandler is not { } elicitationHandler)
{
throw new InvalidOperationException("Elicitation capability was set but it did not provide a handler.");
}

requestHandlers.Set(
RequestMethods.ElicitationCreate,
(request, _, cancellationToken) => elicitationHandler(request, cancellationToken),
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
McpJsonUtilities.JsonContext.Default.ElicitResult);

_options.Capabilities ??= new();
_options.Capabilities.Elicitation ??= new();
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/ModelContextProtocol.Core/Client/McpClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ namespace ModelContextProtocol.Client;
/// </remarks>
public sealed class McpClientOptions
{
private McpClientHandlers? _handlers;

/// <summary>
/// Gets or sets information about this client implementation, including its name and version.
/// </summary>
Expand Down Expand Up @@ -63,4 +65,17 @@ public sealed class McpClientOptions
/// <para>The default value is 60 seconds.</para>
/// </remarks>
public TimeSpan InitializationTimeout { get; set; } = TimeSpan.FromSeconds(60);

/// <summary>
/// Gets or sets the container of handlers used by the client for processing protocol messages.
/// </summary>
public McpClientHandlers Handlers
{
get => _handlers ??= new();
set
{
Throw.IfNull(value);
_handlers = value;
}
}
}
12 changes: 9 additions & 3 deletions src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using ModelContextProtocol.Server;
using System.Text.Json.Serialization;
using ModelContextProtocol.Client;
using ModelContextProtocol.Server;

namespace ModelContextProtocol.Protocol;

Expand Down Expand Up @@ -45,7 +46,7 @@ public sealed class ClientCapabilities
/// </para>
/// <para>
/// The server can use <see cref="McpServer.RequestRootsAsync"/> to request the list of
/// available roots from the client, which will trigger the client's <see cref="RootsCapability.RootsHandler"/>.
/// available roots from the client, which will trigger the client's <see cref="ModelContextProtocol.Client.McpClientHandlers.RootsHandler"/>.
/// </para>
/// </remarks>
[JsonPropertyName("roots")]
Expand Down Expand Up @@ -83,5 +84,10 @@ public sealed class ClientCapabilities
/// </para>
/// </remarks>
[JsonIgnore]
public IEnumerable<KeyValuePair<string, Func<JsonRpcNotification, CancellationToken, ValueTask>>>? NotificationHandlers { get; set; }
[Obsolete($"Use {nameof(McpClientHandlers.NotificationHandlers)} instead.")]
public IEnumerable<KeyValuePair<string, Func<JsonRpcNotification, CancellationToken, ValueTask>>>? NotificationHandlers
{
get => throw new NotSupportedException($"Use {nameof(McpClientHandlers.NotificationHandlers)} instead.");
set => throw new NotSupportedException($"Use {nameof(McpClientHandlers.NotificationHandlers)} instead.");
}
}
Loading
Loading