Skip to content
Open
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
81 changes: 40 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,52 +176,51 @@ McpServerOptions options = new()
ServerInfo = new Implementation { Name = "MyServer", Version = "1.0.0" },
Capabilities = new ServerCapabilities
{
Tools = new ToolsCapability
{
ListToolsHandler = (request, cancellationToken) =>
ValueTask.FromResult(new ListToolsResult
{
Tools =
[
new Tool
{
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"]
}
"""),
}
]
}),

CallToolHandler = (request, cancellationToken) =>
Tools = new ToolsCapability(),
},
};

options.Handlers.ListToolsHandler = (request, cancellationToken) =>
Copy link
Member

Choose a reason for hiding this comment

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

Not being able to set a handler within the object expression seems to me like a regression from a usability perspective. If decoupling handlers from the capability types is essential, consider using a structure that doesn't force this pattern (e.g. putting all handler properties on the ServerCapabilities type itself).

Copy link
Author

Choose a reason for hiding this comment

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

That could be enabled by adding a setter to McpServerOptions.Handlers.
I thought of having Handlers initialized by default to avoid the null-terminating operator in cases like Handlers?.ListToolsHandler. We could still achieve both if we make that the setter disallows null.

ValueTask.FromResult(new ListToolsResult
{
Tools =
[
new Tool
{
if (request.Params?.Name == "echo")
{
if (request.Params.Arguments?.TryGetValue("message", out var message) is not true)
Name = "echo",
Description = "Echoes the input back to the client.",
InputSchema = JsonSerializer.Deserialize<JsonElement>("""
{
throw new McpException("Missing required argument 'message'");
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "The input to echo back"
}
},
"required": ["message"]
}
"""),
}
]
});

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

throw new McpException($"Unknown tool: '{request.Params?.Name}'");
},
options.Handlers.CallToolHandler = (request, cancellationToken) =>
{
if (request.Params?.Name == "echo")
{
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($"Unknown tool: '{request.Params?.Name}'");
};

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

await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport, options);
// </snippet_McpInitialize>
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
8 changes: 4 additions & 4 deletions samples/ChatWithTools/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@
.UseOpenTelemetry(loggerFactory: loggerFactory, configure: o => o.EnableSensitiveData = true)
.Build();

var clientOptions = new McpClientOptions();
clientOptions.Handlers.SamplingHandler = samplingClient.CreateSamplingHandler();

var mcpClient = await McpClientFactory.CreateAsync(
new StdioClientTransport(new()
{
Command = "npx",
Arguments = ["-y", "--verbose", "@modelcontextprotocol/server-everything"],
Name = "Everything",
}),
clientOptions: new()
{
Capabilities = new() { Sampling = new() { SamplingHandler = samplingClient.CreateSamplingHandler() } },
},
clientOptions: clientOptions,
loggerFactory: loggerFactory);

// Get all available tools
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
85 changes: 38 additions & 47 deletions src/ModelContextProtocol.Core/Client/McpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,57 +39,48 @@ public McpClient(IClientTransport clientTransport, McpClientOptions? options, IL

EndpointName = clientTransport.Name;

if (options.Capabilities is { } capabilities)
if (options.Handlers.NotificationHandlers is { } notificationHandlers)
{
if (capabilities.NotificationHandlers is { } notificationHandlers)
{
NotificationHandlers.RegisterRange(notificationHandlers);
}

if (capabilities.Sampling is { } samplingCapability)
{
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(
request,
request?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
cancellationToken),
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
McpJsonUtilities.JsonContext.Default.CreateMessageResult);
}

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

RequestHandlers.Set(
RequestMethods.RootsList,
(request, _, cancellationToken) => rootsHandler(request, cancellationToken),
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
McpJsonUtilities.JsonContext.Default.ListRootsResult);
}
if (options.Handlers.SamplingHandler is { } samplingHandler)
{
RequestHandlers.Set(
RequestMethods.SamplingCreateMessage,
(request, _, cancellationToken) => samplingHandler(
request,
request?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
cancellationToken),
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
McpJsonUtilities.JsonContext.Default.CreateMessageResult);

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

if (capabilities.Elicitation is { } elicitationCapability)
{
if (elicitationCapability.ElicitationHandler is not { } elicitationHandler)
{
throw new InvalidOperationException("Elicitation capability was set but it did not provide a handler.");
}
if (options.Handlers.RootsHandler is { } rootsHandler)
{
RequestHandlers.Set(
RequestMethods.RootsList,
(request, _, cancellationToken) => rootsHandler(request, cancellationToken),
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
McpJsonUtilities.JsonContext.Default.ListRootsResult);

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

RequestHandlers.Set(
RequestMethods.ElicitationCreate,
(request, _, cancellationToken) => elicitationHandler(request, cancellationToken),
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
McpJsonUtilities.JsonContext.Default.ElicitResult);
}
if (options.Handlers.ElicitationHandler is { } elicitationHandler)
{
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
4 changes: 2 additions & 2 deletions src/ModelContextProtocol.Core/Client/McpClientExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -965,11 +965,11 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat
}

/// <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; }
}
5 changes: 5 additions & 0 deletions src/ModelContextProtocol.Core/Client/McpClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,9 @@ 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; } = new();
}
Loading
Loading