Restructure IMcpEndpoint, IMcpClient, and IMcpServer (#723)
* Remove `McpEndpoint` & `IMcpClient` * Revise naming, add abstract classes * Fix missing renames * Move extension methods to instance methods * Add back old API, but obsoleted * Add back obsoleted factory classes * Move deprecation attributes * `SseClientTransport` -> `HttpClientTransport` * Update src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs Co-authored-by: Stephen Halter <halter73@gmail.com> * PR feedback * PR feedback * Add back dispose lock * Fix docs projects * PR feedback * Undo `docs/*.md` changes --------- Co-authored-by: Stephen Halter <halter73@gmail.com>
This commit is contained in:
parent
fe2ce2bc60
commit
38b4a26998
17 changed files with 5169 additions and 3786 deletions
|
|
@ -12,14 +12,14 @@ namespace ModelContextProtocol.Client;
|
|||
/// </summary>
|
||||
internal sealed partial class AutoDetectingClientSessionTransport : ITransport
|
||||
{
|
||||
private readonly SseClientTransportOptions _options;
|
||||
private readonly HttpClientTransportOptions _options;
|
||||
private readonly McpHttpClient _httpClient;
|
||||
private readonly ILoggerFactory? _loggerFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly string _name;
|
||||
private readonly Channel<JsonRpcMessage> _messageChannel;
|
||||
|
||||
public AutoDetectingClientSessionTransport(string endpointName, SseClientTransportOptions transportOptions, McpHttpClient httpClient, ILoggerFactory? loggerFactory)
|
||||
public AutoDetectingClientSessionTransport(string endpointName, HttpClientTransportOptions transportOptions, McpHttpClient httpClient, ILoggerFactory? loggerFactory)
|
||||
{
|
||||
Throw.IfNull(transportOptions);
|
||||
Throw.IfNull(httpClient);
|
||||
|
|
|
|||
106
src/ModelContextProtocol.Core/Client/HttpClientTransport.cs
Normal file
106
src/ModelContextProtocol.Core/Client/HttpClientTransport.cs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Authentication;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an <see cref="IClientTransport"/> over HTTP using the Server-Sent Events (SSE) or Streamable HTTP protocol.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This transport connects to an MCP server over HTTP using SSE or Streamable HTTP,
|
||||
/// allowing for real-time server-to-client communication with a standard HTTP requests.
|
||||
/// Unlike the <see cref="StdioClientTransport"/>, this transport connects to an existing server
|
||||
/// rather than launching a new process.
|
||||
/// </remarks>
|
||||
public sealed class HttpClientTransport : IClientTransport, IAsyncDisposable
|
||||
{
|
||||
private readonly HttpClientTransportOptions _options;
|
||||
private readonly McpHttpClient _mcpHttpClient;
|
||||
private readonly ILoggerFactory? _loggerFactory;
|
||||
|
||||
private readonly HttpClient? _ownedHttpClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpClientTransport"/> class.
|
||||
/// </summary>
|
||||
/// <param name="transportOptions">Configuration options for the transport.</param>
|
||||
/// <param name="loggerFactory">Logger factory for creating loggers used for diagnostic output during transport operations.</param>
|
||||
public HttpClientTransport(HttpClientTransportOptions transportOptions, ILoggerFactory? loggerFactory = null)
|
||||
: this(transportOptions, new HttpClient(), loggerFactory, ownsHttpClient: true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpClientTransport"/> class with a provided HTTP client.
|
||||
/// </summary>
|
||||
/// <param name="transportOptions">Configuration options for the transport.</param>
|
||||
/// <param name="httpClient">The HTTP client instance used for requests.</param>
|
||||
/// <param name="loggerFactory">Logger factory for creating loggers used for diagnostic output during transport operations.</param>
|
||||
/// <param name="ownsHttpClient">
|
||||
/// <see langword="true"/> to dispose of <paramref name="httpClient"/> when the transport is disposed;
|
||||
/// <see langword="false"/> if the caller is retaining ownership of the <paramref name="httpClient"/>'s lifetime.
|
||||
/// </param>
|
||||
public HttpClientTransport(HttpClientTransportOptions transportOptions, HttpClient httpClient, ILoggerFactory? loggerFactory = null, bool ownsHttpClient = false)
|
||||
{
|
||||
Throw.IfNull(transportOptions);
|
||||
Throw.IfNull(httpClient);
|
||||
|
||||
_options = transportOptions;
|
||||
_loggerFactory = loggerFactory;
|
||||
Name = transportOptions.Name ?? transportOptions.Endpoint.ToString();
|
||||
|
||||
if (transportOptions.OAuth is { } clientOAuthOptions)
|
||||
{
|
||||
var oAuthProvider = new ClientOAuthProvider(_options.Endpoint, clientOAuthOptions, httpClient, loggerFactory);
|
||||
_mcpHttpClient = new AuthenticatingMcpHttpClient(httpClient, oAuthProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
_mcpHttpClient = new(httpClient);
|
||||
}
|
||||
|
||||
if (ownsHttpClient)
|
||||
{
|
||||
_ownedHttpClient = httpClient;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ITransport> ConnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _options.TransportMode switch
|
||||
{
|
||||
HttpTransportMode.AutoDetect => new AutoDetectingClientSessionTransport(Name, _options, _mcpHttpClient, _loggerFactory),
|
||||
HttpTransportMode.StreamableHttp => new StreamableHttpClientSessionTransport(Name, _options, _mcpHttpClient, messageChannel: null, _loggerFactory),
|
||||
HttpTransportMode.Sse => await ConnectSseTransportAsync(cancellationToken).ConfigureAwait(false),
|
||||
_ => throw new InvalidOperationException($"Unsupported transport mode: {_options.TransportMode}"),
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ITransport> ConnectSseTransportAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var sessionTransport = new SseClientSessionTransport(Name, _options, _mcpHttpClient, messageChannel: null, _loggerFactory);
|
||||
|
||||
try
|
||||
{
|
||||
await sessionTransport.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
||||
return sessionTransport;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await sessionTransport.DisposeAsync().ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
_ownedHttpClient?.Dispose();
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
using ModelContextProtocol.Authentication;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Provides options for configuring <see cref="HttpClientTransport"/> instances.
|
||||
/// </summary>
|
||||
public sealed class HttpClientTransportOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the base address of the server for SSE connections.
|
||||
/// </summary>
|
||||
public required Uri Endpoint
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value), "Endpoint cannot be null.");
|
||||
}
|
||||
if (!value.IsAbsoluteUri)
|
||||
{
|
||||
throw new ArgumentException("Endpoint must be an absolute URI.", nameof(value));
|
||||
}
|
||||
if (value.Scheme != Uri.UriSchemeHttp && value.Scheme != Uri.UriSchemeHttps)
|
||||
{
|
||||
throw new ArgumentException("Endpoint must use HTTP or HTTPS scheme.", nameof(value));
|
||||
}
|
||||
|
||||
field = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transport mode to use for the connection. Defaults to <see cref="HttpTransportMode.AutoDetect"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When set to <see cref="HttpTransportMode.AutoDetect"/> (the default), the client will first attempt to use
|
||||
/// Streamable HTTP transport and automatically fall back to SSE transport if the server doesn't support it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see href="https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http">Streamable HTTP transport specification</see>.
|
||||
/// <see href="https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse">HTTP with SSE transport specification</see>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public HttpTransportMode TransportMode { get; set; } = HttpTransportMode.AutoDetect;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a transport identifier used for logging purposes.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a timeout used to establish the initial connection to the SSE server. Defaults to 30 seconds.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This timeout controls how long the client waits for:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>The initial HTTP connection to be established with the SSE server</description></item>
|
||||
/// <item><description>The endpoint event to be received, which indicates the message endpoint URL</description></item>
|
||||
/// </list>
|
||||
/// If the timeout expires before the connection is established, a <see cref="TimeoutException"/> will be thrown.
|
||||
/// </remarks>
|
||||
public TimeSpan ConnectionTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// Gets custom HTTP headers to include in requests to the SSE server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use this property to specify custom HTTP headers that should be sent with each request to the server.
|
||||
/// </remarks>
|
||||
public IDictionary<string, string>? AdditionalHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets sor sets the authorization provider to use for authentication.
|
||||
/// </summary>
|
||||
public ClientOAuthOptions? OAuth { get; set; }
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ namespace ModelContextProtocol.Client;
|
|||
/// and servers, allowing different transport protocols to be used interchangeably.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When creating an <see cref="IMcpClient"/>, <see cref="McpClientFactory"/> is typically used, and is
|
||||
/// When creating an <see cref="McpClient"/>, <see cref="McpClient"/> is typically used, and is
|
||||
/// provided with the <see cref="IClientTransport"/> based on expected server configuration.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
|
|
@ -39,7 +39,7 @@ public interface IClientTransport
|
|||
/// the transport session as well.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This method is used by <see cref="McpClientFactory"/> to initialize the connection.
|
||||
/// This method is used by <see cref="McpClient"/> to initialize the connection.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="InvalidOperationException">The transport connection could not be established.</exception>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an instance of a Model Context Protocol (MCP) client that connects to and communicates with an MCP server.
|
||||
/// </summary>
|
||||
[Obsolete($"Use {nameof(McpClient)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public interface IMcpClient : IMcpEndpoint
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -44,4 +45,4 @@ public interface IMcpClient : IMcpEndpoint
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
string? ServerInstructions { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
713
src/ModelContextProtocol.Core/Client/McpClient.Methods.cs
Normal file
713
src/ModelContextProtocol.Core/Client/McpClient.Methods.cs
Normal file
|
|
@ -0,0 +1,713 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an instance of a Model Context Protocol (MCP) client session that connects to and communicates with an MCP server.
|
||||
/// </summary>
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public abstract partial class McpClient : McpSession, IMcpClient
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
/// <summary>Creates an <see cref="McpClient"/>, connecting it to the specified server.</summary>
|
||||
/// <param name="clientTransport">The transport instance used to communicate with the server.</param>
|
||||
/// <param name="clientOptions">
|
||||
/// A client configuration object which specifies client capabilities and protocol version.
|
||||
/// If <see langword="null"/>, details based on the current process will be employed.
|
||||
/// </param>
|
||||
/// <param name="loggerFactory">A logger factory for creating loggers for clients.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An <see cref="McpClient"/> that's connected to the specified server.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="clientTransport"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="clientOptions"/> is <see langword="null"/>.</exception>
|
||||
public static async Task<McpClient> CreateAsync(
|
||||
IClientTransport clientTransport,
|
||||
McpClientOptions? clientOptions = null,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(clientTransport);
|
||||
|
||||
var transport = await clientTransport.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
||||
var endpointName = clientTransport.Name;
|
||||
|
||||
var clientSession = new McpClientImpl(transport, endpointName, clientOptions, loggerFactory);
|
||||
try
|
||||
{
|
||||
await clientSession.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await clientSession.DisposeAsync().ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
return clientSession;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a ping request to verify server connectivity.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that completes when the ping is successful.</returns>
|
||||
/// <exception cref="McpException">Thrown when the server cannot be reached or returns an error response.</exception>
|
||||
public Task PingAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var opts = McpJsonUtilities.DefaultOptions;
|
||||
opts.MakeReadOnly();
|
||||
return SendRequestAsync<object?, object>(
|
||||
RequestMethods.Ping,
|
||||
parameters: null,
|
||||
serializerOptions: opts,
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of available tools from the server.
|
||||
/// </summary>
|
||||
/// <param name="serializerOptions">The serializer options governing tool parameter serialization. If null, the default options will be used.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A list of all available tools as <see cref="McpClientTool"/> instances.</returns>
|
||||
public async ValueTask<IList<McpClientTool>> ListToolsAsync(
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
|
||||
List<McpClientTool>? tools = null;
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var toolResults = await SendRequestAsync(
|
||||
RequestMethods.ToolsList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
tools ??= new List<McpClientTool>(toolResults.Tools.Count);
|
||||
foreach (var tool in toolResults.Tools)
|
||||
{
|
||||
tools.Add(new McpClientTool(this, tool, serializerOptions));
|
||||
}
|
||||
|
||||
cursor = toolResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an enumerable for asynchronously enumerating all available tools from the server.
|
||||
/// </summary>
|
||||
/// <param name="serializerOptions">The serializer options governing tool parameter serialization. If null, the default options will be used.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An asynchronous sequence of all available tools as <see cref="McpClientTool"/> instances.</returns>
|
||||
public async IAsyncEnumerable<McpClientTool> EnumerateToolsAsync(
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var toolResults = await SendRequestAsync(
|
||||
RequestMethods.ToolsList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var tool in toolResults.Tools)
|
||||
{
|
||||
yield return new McpClientTool(this, tool, serializerOptions);
|
||||
}
|
||||
|
||||
cursor = toolResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of available prompts from the server.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A list of all available prompts as <see cref="McpClientPrompt"/> instances.</returns>
|
||||
public async ValueTask<IList<McpClientPrompt>> ListPromptsAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<McpClientPrompt>? prompts = null;
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var promptResults = await SendRequestAsync(
|
||||
RequestMethods.PromptsList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
prompts ??= new List<McpClientPrompt>(promptResults.Prompts.Count);
|
||||
foreach (var prompt in promptResults.Prompts)
|
||||
{
|
||||
prompts.Add(new McpClientPrompt(this, prompt));
|
||||
}
|
||||
|
||||
cursor = promptResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
|
||||
return prompts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an enumerable for asynchronously enumerating all available prompts from the server.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An asynchronous sequence of all available prompts as <see cref="McpClientPrompt"/> instances.</returns>
|
||||
public async IAsyncEnumerable<McpClientPrompt> EnumeratePromptsAsync(
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var promptResults = await SendRequestAsync(
|
||||
RequestMethods.PromptsList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var prompt in promptResults.Prompts)
|
||||
{
|
||||
yield return new(this, prompt);
|
||||
}
|
||||
|
||||
cursor = promptResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a specific prompt from the MCP server.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the prompt to retrieve.</param>
|
||||
/// <param name="arguments">Optional arguments for the prompt. Keys are parameter names, and values are the argument values.</param>
|
||||
/// <param name="serializerOptions">The serialization options governing argument serialization.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task containing the prompt's result with content and messages.</returns>
|
||||
public ValueTask<GetPromptResult> GetPromptAsync(
|
||||
string name,
|
||||
IReadOnlyDictionary<string, object?>? arguments = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNullOrWhiteSpace(name);
|
||||
|
||||
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
|
||||
return SendRequestAsync(
|
||||
RequestMethods.PromptsGet,
|
||||
new() { Name = name, Arguments = ToArgumentsDictionary(arguments, serializerOptions) },
|
||||
McpJsonUtilities.JsonContext.Default.GetPromptRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.GetPromptResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of available resource templates from the server.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A list of all available resource templates as <see cref="ResourceTemplate"/> instances.</returns>
|
||||
public async ValueTask<IList<McpClientResourceTemplate>> ListResourceTemplatesAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<McpClientResourceTemplate>? resourceTemplates = null;
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var templateResults = await SendRequestAsync(
|
||||
RequestMethods.ResourcesTemplatesList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
resourceTemplates ??= new List<McpClientResourceTemplate>(templateResults.ResourceTemplates.Count);
|
||||
foreach (var template in templateResults.ResourceTemplates)
|
||||
{
|
||||
resourceTemplates.Add(new McpClientResourceTemplate(this, template));
|
||||
}
|
||||
|
||||
cursor = templateResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
|
||||
return resourceTemplates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an enumerable for asynchronously enumerating all available resource templates from the server.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An asynchronous sequence of all available resource templates as <see cref="ResourceTemplate"/> instances.</returns>
|
||||
public async IAsyncEnumerable<McpClientResourceTemplate> EnumerateResourceTemplatesAsync(
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var templateResults = await SendRequestAsync(
|
||||
RequestMethods.ResourcesTemplatesList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var templateResult in templateResults.ResourceTemplates)
|
||||
{
|
||||
yield return new McpClientResourceTemplate(this, templateResult);
|
||||
}
|
||||
|
||||
cursor = templateResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of available resources from the server.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A list of all available resources as <see cref="Resource"/> instances.</returns>
|
||||
public async ValueTask<IList<McpClientResource>> ListResourcesAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<McpClientResource>? resources = null;
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var resourceResults = await SendRequestAsync(
|
||||
RequestMethods.ResourcesList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
resources ??= new List<McpClientResource>(resourceResults.Resources.Count);
|
||||
foreach (var resource in resourceResults.Resources)
|
||||
{
|
||||
resources.Add(new McpClientResource(this, resource));
|
||||
}
|
||||
|
||||
cursor = resourceResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an enumerable for asynchronously enumerating all available resources from the server.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An asynchronous sequence of all available resources as <see cref="Resource"/> instances.</returns>
|
||||
public async IAsyncEnumerable<McpClientResource> EnumerateResourcesAsync(
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var resourceResults = await SendRequestAsync(
|
||||
RequestMethods.ResourcesList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var resource in resourceResults.Resources)
|
||||
{
|
||||
yield return new McpClientResource(this, resource);
|
||||
}
|
||||
|
||||
cursor = resourceResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a resource from the server.
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri of the resource.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
public ValueTask<ReadResourceResult> ReadResourceAsync(
|
||||
string uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNullOrWhiteSpace(uri);
|
||||
|
||||
return SendRequestAsync(
|
||||
RequestMethods.ResourcesRead,
|
||||
new() { Uri = uri },
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a resource from the server.
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri of the resource.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
public ValueTask<ReadResourceResult> ReadResourceAsync(
|
||||
Uri uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(uri);
|
||||
|
||||
return ReadResourceAsync(uri.ToString(), cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a resource from the server.
|
||||
/// </summary>
|
||||
/// <param name="uriTemplate">The uri template of the resource.</param>
|
||||
/// <param name="arguments">Arguments to use to format <paramref name="uriTemplate"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
public ValueTask<ReadResourceResult> ReadResourceAsync(
|
||||
string uriTemplate, IReadOnlyDictionary<string, object?> arguments, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNullOrWhiteSpace(uriTemplate);
|
||||
Throw.IfNull(arguments);
|
||||
|
||||
return SendRequestAsync(
|
||||
RequestMethods.ResourcesRead,
|
||||
new() { Uri = UriTemplate.FormatUri(uriTemplate, arguments) },
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests completion suggestions for a prompt argument or resource reference.
|
||||
/// </summary>
|
||||
/// <param name="reference">The reference object specifying the type and optional URI or name.</param>
|
||||
/// <param name="argumentName">The name of the argument for which completions are requested.</param>
|
||||
/// <param name="argumentValue">The current value of the argument, used to filter relevant completions.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="CompleteResult"/> containing completion suggestions.</returns>
|
||||
public ValueTask<CompleteResult> CompleteAsync(Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(reference);
|
||||
Throw.IfNullOrWhiteSpace(argumentName);
|
||||
|
||||
return SendRequestAsync(
|
||||
RequestMethods.CompletionComplete,
|
||||
new()
|
||||
{
|
||||
Ref = reference,
|
||||
Argument = new Argument { Name = argumentName, Value = argumentValue }
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.CompleteRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CompleteResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to a resource on the server to receive notifications when it changes.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the resource to which to subscribe.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task SubscribeToResourceAsync(string uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNullOrWhiteSpace(uri);
|
||||
|
||||
return SendRequestAsync(
|
||||
RequestMethods.ResourcesSubscribe,
|
||||
new() { Uri = uri },
|
||||
McpJsonUtilities.JsonContext.Default.SubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult,
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to a resource on the server to receive notifications when it changes.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the resource to which to subscribe.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task SubscribeToResourceAsync(Uri uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(uri);
|
||||
|
||||
return SubscribeToResourceAsync(uri.ToString(), cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from a resource on the server to stop receiving notifications about its changes.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the resource to unsubscribe from.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task UnsubscribeFromResourceAsync(string uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNullOrWhiteSpace(uri);
|
||||
|
||||
return SendRequestAsync(
|
||||
RequestMethods.ResourcesUnsubscribe,
|
||||
new() { Uri = uri },
|
||||
McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult,
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from a resource on the server to stop receiving notifications about its changes.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the resource to unsubscribe from.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task UnsubscribeFromResourceAsync(Uri uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(uri);
|
||||
|
||||
return UnsubscribeFromResourceAsync(uri.ToString(), cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a tool on the server.
|
||||
/// </summary>
|
||||
/// <param name="toolName">The name of the tool to call on the server..</param>
|
||||
/// <param name="arguments">An optional dictionary of arguments to pass to the tool.</param>
|
||||
/// <param name="progress">Optional progress reporter for server notifications.</param>
|
||||
/// <param name="serializerOptions">JSON serializer options.</param>
|
||||
/// <param name="cancellationToken">A cancellation token.</param>
|
||||
/// <returns>The <see cref="CallToolResult"/> from the tool execution.</returns>
|
||||
public ValueTask<CallToolResult> CallToolAsync(
|
||||
string toolName,
|
||||
IReadOnlyDictionary<string, object?>? arguments = null,
|
||||
IProgress<ProgressNotificationValue>? progress = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(toolName);
|
||||
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
|
||||
if (progress is not null)
|
||||
{
|
||||
return SendRequestWithProgressAsync(toolName, arguments, progress, serializerOptions, cancellationToken);
|
||||
}
|
||||
|
||||
return SendRequestAsync(
|
||||
RequestMethods.ToolsCall,
|
||||
new()
|
||||
{
|
||||
Name = toolName,
|
||||
Arguments = ToArgumentsDictionary(arguments, serializerOptions),
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CallToolResult,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
async ValueTask<CallToolResult> SendRequestWithProgressAsync(
|
||||
string toolName,
|
||||
IReadOnlyDictionary<string, object?>? arguments,
|
||||
IProgress<ProgressNotificationValue> progress,
|
||||
JsonSerializerOptions serializerOptions,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ProgressToken progressToken = new(Guid.NewGuid().ToString("N"));
|
||||
|
||||
await using var _ = RegisterNotificationHandler(NotificationMethods.ProgressNotification,
|
||||
(notification, cancellationToken) =>
|
||||
{
|
||||
if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.JsonContext.Default.ProgressNotificationParams) is { } pn &&
|
||||
pn.ProgressToken == progressToken)
|
||||
{
|
||||
progress.Report(pn.Progress);
|
||||
}
|
||||
|
||||
return default;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
return await SendRequestAsync(
|
||||
RequestMethods.ToolsCall,
|
||||
new()
|
||||
{
|
||||
Name = toolName,
|
||||
Arguments = ToArgumentsDictionary(arguments, serializerOptions),
|
||||
ProgressToken = progressToken,
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CallToolResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the contents of a <see cref="CreateMessageRequestParams"/> into a pair of
|
||||
/// <see cref="IEnumerable{ChatMessage}"/> and <see cref="ChatOptions"/> instances to use
|
||||
/// as inputs into a <see cref="IChatClient"/> operation.
|
||||
/// </summary>
|
||||
/// <param name="requestParams"></param>
|
||||
/// <returns>The created pair of messages and options.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
|
||||
internal static (IList<ChatMessage> Messages, ChatOptions? Options) ToChatClientArguments(
|
||||
CreateMessageRequestParams requestParams)
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
|
||||
ChatOptions? options = null;
|
||||
|
||||
if (requestParams.MaxTokens is int maxTokens)
|
||||
{
|
||||
(options ??= new()).MaxOutputTokens = maxTokens;
|
||||
}
|
||||
|
||||
if (requestParams.Temperature is float temperature)
|
||||
{
|
||||
(options ??= new()).Temperature = temperature;
|
||||
}
|
||||
|
||||
if (requestParams.StopSequences is { } stopSequences)
|
||||
{
|
||||
(options ??= new()).StopSequences = stopSequences.ToArray();
|
||||
}
|
||||
|
||||
List<ChatMessage> messages =
|
||||
(from sm in requestParams.Messages
|
||||
let aiContent = sm.Content.ToAIContent()
|
||||
where aiContent is not null
|
||||
select new ChatMessage(sm.Role == Role.Assistant ? ChatRole.Assistant : ChatRole.User, [aiContent]))
|
||||
.ToList();
|
||||
|
||||
return (messages, options);
|
||||
}
|
||||
|
||||
/// <summary>Converts the contents of a <see cref="ChatResponse"/> into a <see cref="CreateMessageResult"/>.</summary>
|
||||
/// <param name="chatResponse">The <see cref="ChatResponse"/> whose contents should be extracted.</param>
|
||||
/// <returns>The created <see cref="CreateMessageResult"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="chatResponse"/> is <see langword="null"/>.</exception>
|
||||
internal static CreateMessageResult ToCreateMessageResult(ChatResponse chatResponse)
|
||||
{
|
||||
Throw.IfNull(chatResponse);
|
||||
|
||||
// The ChatResponse can include multiple messages, of varying modalities, but CreateMessageResult supports
|
||||
// only either a single blob of text or a single image. Heuristically, we'll use an image if there is one
|
||||
// in any of the response messages, or we'll use all the text from them concatenated, otherwise.
|
||||
|
||||
ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault();
|
||||
|
||||
ContentBlock? content = null;
|
||||
if (lastMessage is not null)
|
||||
{
|
||||
foreach (var lmc in lastMessage.Contents)
|
||||
{
|
||||
if (lmc is DataContent dc && (dc.HasTopLevelMediaType("image") || dc.HasTopLevelMediaType("audio")))
|
||||
{
|
||||
content = dc.ToContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
Content = content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty },
|
||||
Model = chatResponse.ModelId ?? "unknown",
|
||||
Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant,
|
||||
StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a sampling handler for use with <see cref="SamplingCapability.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>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="chatClient"/> is <see langword="null"/>.</exception>
|
||||
public static Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>> CreateSamplingHandler(
|
||||
IChatClient chatClient)
|
||||
{
|
||||
Throw.IfNull(chatClient);
|
||||
|
||||
return async (requestParams, progress, cancellationToken) =>
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
|
||||
var (messages, options) = ToChatClientArguments(requestParams);
|
||||
var progressToken = requestParams.ProgressToken;
|
||||
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
updates.Add(update);
|
||||
|
||||
if (progressToken is not null)
|
||||
{
|
||||
progress.Report(new()
|
||||
{
|
||||
Progress = updates.Count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return ToCreateMessageResult(updates.ToChatResponse());
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the logging level for the server to control which log messages are sent to the client.
|
||||
/// </summary>
|
||||
/// <param name="level">The minimum severity level of log messages to receive from the server.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public Task SetLoggingLevel(LoggingLevel level, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return SendRequestAsync(
|
||||
RequestMethods.LoggingSetLevel,
|
||||
new() { Level = level },
|
||||
McpJsonUtilities.JsonContext.Default.SetLevelRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult,
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the logging level for the server to control which log messages are sent to the client.
|
||||
/// </summary>
|
||||
/// <param name="level">The minimum severity level of log messages to receive from the server.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public Task SetLoggingLevel(LogLevel level, CancellationToken cancellationToken = default) =>
|
||||
SetLoggingLevel(McpServerImpl.ToLoggingLevel(level), cancellationToken);
|
||||
|
||||
/// <summary>Convers a dictionary with <see cref="object"/> values to a dictionary with <see cref="JsonElement"/> values.</summary>
|
||||
private static Dictionary<string, JsonElement>? ToArgumentsDictionary(
|
||||
IReadOnlyDictionary<string, object?>? arguments, JsonSerializerOptions options)
|
||||
{
|
||||
var typeInfo = options.GetTypeInfo<object?>();
|
||||
|
||||
Dictionary<string, JsonElement>? result = null;
|
||||
if (arguments is not null)
|
||||
{
|
||||
result = new(arguments.Count);
|
||||
foreach (var kvp in arguments)
|
||||
{
|
||||
result.Add(kvp.Key, kvp.Value is JsonElement je ? je : JsonSerializer.SerializeToElement(kvp.Value, typeInfo));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,236 +1,49 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.Text.Json;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal sealed partial class McpClient : McpEndpoint, IMcpClient
|
||||
/// <summary>
|
||||
/// Represents an instance of a Model Context Protocol (MCP) client session that connects to and communicates with an MCP server.
|
||||
/// </summary>
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public abstract partial class McpClient : McpSession, IMcpClient
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private static Implementation DefaultImplementation { get; } = new()
|
||||
{
|
||||
Name = DefaultAssemblyName.Name ?? nameof(McpClient),
|
||||
Version = DefaultAssemblyName.Version?.ToString() ?? "1.0.0",
|
||||
};
|
||||
|
||||
private readonly IClientTransport _clientTransport;
|
||||
private readonly McpClientOptions _options;
|
||||
|
||||
private ITransport? _sessionTransport;
|
||||
private CancellationTokenSource? _connectCts;
|
||||
|
||||
private ServerCapabilities? _serverCapabilities;
|
||||
private Implementation? _serverInfo;
|
||||
private string? _serverInstructions;
|
||||
/// <summary>
|
||||
/// Gets the capabilities supported by the connected server.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">The client is not connected.</exception>
|
||||
public abstract ServerCapabilities ServerCapabilities { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpClient"/> class.
|
||||
/// Gets the implementation information of the connected server.
|
||||
/// </summary>
|
||||
/// <param name="clientTransport">The transport to use for communication with the server.</param>
|
||||
/// <param name="options">Options for the client, defining protocol version and capabilities.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public McpClient(IClientTransport clientTransport, McpClientOptions? options, ILoggerFactory? loggerFactory)
|
||||
: base(loggerFactory)
|
||||
{
|
||||
options ??= new();
|
||||
|
||||
_clientTransport = clientTransport;
|
||||
_options = options;
|
||||
|
||||
EndpointName = clientTransport.Name;
|
||||
|
||||
if (options.Capabilities is { } capabilities)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
RequestHandlers.Set(
|
||||
RequestMethods.RootsList,
|
||||
(request, _, cancellationToken) => rootsHandler(request, cancellationToken),
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsResult);
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
RequestHandlers.Set(
|
||||
RequestMethods.ElicitationCreate,
|
||||
(request, _, cancellationToken) => elicitationHandler(request, cancellationToken),
|
||||
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ElicitResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? SessionId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_sessionTransport is null)
|
||||
{
|
||||
throw new InvalidOperationException("Must have already initialized a session when invoking this property.");
|
||||
}
|
||||
|
||||
return _sessionTransport.SessionId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ServerCapabilities ServerCapabilities => _serverCapabilities ?? throw new InvalidOperationException("The client is not connected.");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Implementation ServerInfo => _serverInfo ?? throw new InvalidOperationException("The client is not connected.");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? ServerInstructions => _serverInstructions;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string EndpointName { get; }
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property provides identification details about the connected server, including its name and version.
|
||||
/// It is populated during the initialization handshake and is available after a successful connection.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This information can be useful for logging, debugging, compatibility checks, and displaying server
|
||||
/// information to users.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="InvalidOperationException">The client is not connected.</exception>
|
||||
public abstract Implementation ServerInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously connects to an MCP server, establishes the transport connection, and completes the initialization handshake.
|
||||
/// Gets any instructions describing how to use the connected server and its features.
|
||||
/// </summary>
|
||||
public async Task ConnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cancellationToken = _connectCts.Token;
|
||||
|
||||
try
|
||||
{
|
||||
// Connect transport
|
||||
_sessionTransport = await _clientTransport.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
||||
InitializeSession(_sessionTransport);
|
||||
// We don't want the ConnectAsync token to cancel the session after we've successfully connected.
|
||||
// The base class handles cleaning up the session in DisposeAsync without our help.
|
||||
StartSession(_sessionTransport, fullSessionCancellationToken: CancellationToken.None);
|
||||
|
||||
// Perform initialization sequence
|
||||
using var initializationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
initializationCts.CancelAfter(_options.InitializationTimeout);
|
||||
|
||||
try
|
||||
{
|
||||
// Send initialize request
|
||||
string requestProtocol = _options.ProtocolVersion ?? McpSession.LatestProtocolVersion;
|
||||
var initializeResponse = await this.SendRequestAsync(
|
||||
RequestMethods.Initialize,
|
||||
new InitializeRequestParams
|
||||
{
|
||||
ProtocolVersion = requestProtocol,
|
||||
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
|
||||
ClientInfo = _options.ClientInfo ?? DefaultImplementation,
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.InitializeResult,
|
||||
cancellationToken: initializationCts.Token).ConfigureAwait(false);
|
||||
|
||||
// Store server information
|
||||
if (_logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
LogServerCapabilitiesReceived(EndpointName,
|
||||
capabilities: JsonSerializer.Serialize(initializeResponse.Capabilities, McpJsonUtilities.JsonContext.Default.ServerCapabilities),
|
||||
serverInfo: JsonSerializer.Serialize(initializeResponse.ServerInfo, McpJsonUtilities.JsonContext.Default.Implementation));
|
||||
}
|
||||
|
||||
_serverCapabilities = initializeResponse.Capabilities;
|
||||
_serverInfo = initializeResponse.ServerInfo;
|
||||
_serverInstructions = initializeResponse.Instructions;
|
||||
|
||||
// Validate protocol version
|
||||
bool isResponseProtocolValid =
|
||||
_options.ProtocolVersion is { } optionsProtocol ? optionsProtocol == initializeResponse.ProtocolVersion :
|
||||
McpSession.SupportedProtocolVersions.Contains(initializeResponse.ProtocolVersion);
|
||||
if (!isResponseProtocolValid)
|
||||
{
|
||||
LogServerProtocolVersionMismatch(EndpointName, requestProtocol, initializeResponse.ProtocolVersion);
|
||||
throw new McpException($"Server protocol version mismatch. Expected {requestProtocol}, got {initializeResponse.ProtocolVersion}");
|
||||
}
|
||||
|
||||
// Send initialized notification
|
||||
await this.SendNotificationAsync(
|
||||
NotificationMethods.InitializedNotification,
|
||||
new InitializedNotificationParams(),
|
||||
McpJsonUtilities.JsonContext.Default.InitializedNotificationParams,
|
||||
cancellationToken: initializationCts.Token).ConfigureAwait(false);
|
||||
|
||||
}
|
||||
catch (OperationCanceledException oce) when (initializationCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
LogClientInitializationTimeout(EndpointName);
|
||||
throw new TimeoutException("Initialization timed out", oce);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogClientInitializationError(EndpointName, e);
|
||||
await DisposeAsync().ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask DisposeUnsynchronizedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_connectCts is not null)
|
||||
{
|
||||
await _connectCts.CancelAsync().ConfigureAwait(false);
|
||||
_connectCts.Dispose();
|
||||
}
|
||||
|
||||
await base.DisposeUnsynchronizedAsync().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_sessionTransport is not null)
|
||||
{
|
||||
await _sessionTransport.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} client received server '{ServerInfo}' capabilities: '{Capabilities}'.")]
|
||||
private partial void LogServerCapabilitiesReceived(string endpointName, string capabilities, string serverInfo);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} client initialization error.")]
|
||||
private partial void LogClientInitializationError(string endpointName, Exception exception);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} client initialization timed out.")]
|
||||
private partial void LogClientInitializationTimeout(string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} client protocol version mismatch with server. Expected '{Expected}', received '{Received}'.")]
|
||||
private partial void LogServerProtocolVersionMismatch(string endpointName, string expected, string received);
|
||||
}
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property contains instructions provided by the server during initialization that explain
|
||||
/// how to effectively use its capabilities. These instructions can include details about available
|
||||
/// tools, expected input formats, limitations, or any other helpful information.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This can be used by clients to improve an LLM's understanding of available tools, prompts, and resources.
|
||||
/// It can be thought of like a "hint" to the model and may be added to a system prompt.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract string? ServerInstructions { get; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for interacting with an <see cref="IMcpClient"/>.
|
||||
/// Provides extension methods for interacting with an <see cref="McpClient"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
|
|
@ -19,6 +18,53 @@ namespace ModelContextProtocol.Client;
|
|||
/// </remarks>
|
||||
public static class McpClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a sampling handler for use with <see cref="SamplingCapability.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>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method creates a function that converts MCP message requests into chat client calls, enabling
|
||||
/// an MCP client to generate text or other content using an actual AI model via the provided chat client.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The handler can process text messages, image messages, and resource messages as defined in the
|
||||
/// Model Context Protocol.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="chatClient"/> is <see langword="null"/>.</exception>
|
||||
public static Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>> CreateSamplingHandler(
|
||||
this IChatClient chatClient)
|
||||
{
|
||||
Throw.IfNull(chatClient);
|
||||
|
||||
return async (requestParams, progress, cancellationToken) =>
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
|
||||
var (messages, options) = requestParams.ToChatClientArguments();
|
||||
var progressToken = requestParams.ProgressToken;
|
||||
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
updates.Add(update);
|
||||
|
||||
if (progressToken is not null)
|
||||
{
|
||||
progress.Report(new()
|
||||
{
|
||||
Progress = updates.Count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return updates.ToChatResponse().ToCreateMessageResult();
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a ping request to verify server connectivity.
|
||||
/// </summary>
|
||||
|
|
@ -38,17 +84,9 @@ public static class McpClientExtensions
|
|||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="McpException">Thrown when the server cannot be reached or returns an error response.</exception>
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.PingAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static Task PingAsync(this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.Ping,
|
||||
parameters: null,
|
||||
McpJsonUtilities.JsonContext.Default.Object!,
|
||||
McpJsonUtilities.JsonContext.Default.Object,
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
=> AsClientOrThrow(client).PingAsync(cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of available tools from the server.
|
||||
|
|
@ -89,39 +127,12 @@ public static class McpClientExtensions
|
|||
/// </code>
|
||||
/// </example>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async ValueTask<IList<McpClientTool>> ListToolsAsync(
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListToolsAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static ValueTask<IList<McpClientTool>> ListToolsAsync(
|
||||
this IMcpClient client,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
|
||||
List<McpClientTool>? tools = null;
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var toolResults = await client.SendRequestAsync(
|
||||
RequestMethods.ToolsList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
tools ??= new List<McpClientTool>(toolResults.Tools.Count);
|
||||
foreach (var tool in toolResults.Tools)
|
||||
{
|
||||
tools.Add(new McpClientTool(client, tool, serializerOptions));
|
||||
}
|
||||
|
||||
cursor = toolResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
|
||||
return tools;
|
||||
}
|
||||
=> AsClientOrThrow(client).ListToolsAsync(serializerOptions, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an enumerable for asynchronously enumerating all available tools from the server.
|
||||
|
|
@ -155,35 +166,12 @@ public static class McpClientExtensions
|
|||
/// </code>
|
||||
/// </example>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async IAsyncEnumerable<McpClientTool> EnumerateToolsAsync(
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateToolsAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static IAsyncEnumerable<McpClientTool> EnumerateToolsAsync(
|
||||
this IMcpClient client,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var toolResults = await client.SendRequestAsync(
|
||||
RequestMethods.ToolsList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListToolsResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var tool in toolResults.Tools)
|
||||
{
|
||||
yield return new McpClientTool(client, tool, serializerOptions);
|
||||
}
|
||||
|
||||
cursor = toolResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
}
|
||||
CancellationToken cancellationToken = default)
|
||||
=> AsClientOrThrow(client).EnumerateToolsAsync(serializerOptions, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of available prompts from the server.
|
||||
|
|
@ -202,34 +190,10 @@ public static class McpClientExtensions
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async ValueTask<IList<McpClientPrompt>> ListPromptsAsync(
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListPromptsAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static ValueTask<IList<McpClientPrompt>> ListPromptsAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
List<McpClientPrompt>? prompts = null;
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var promptResults = await client.SendRequestAsync(
|
||||
RequestMethods.PromptsList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
prompts ??= new List<McpClientPrompt>(promptResults.Prompts.Count);
|
||||
foreach (var prompt in promptResults.Prompts)
|
||||
{
|
||||
prompts.Add(new McpClientPrompt(client, prompt));
|
||||
}
|
||||
|
||||
cursor = promptResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
|
||||
return prompts;
|
||||
}
|
||||
=> AsClientOrThrow(client).ListPromptsAsync(cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an enumerable for asynchronously enumerating all available prompts from the server.
|
||||
|
|
@ -258,30 +222,10 @@ public static class McpClientExtensions
|
|||
/// </code>
|
||||
/// </example>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async IAsyncEnumerable<McpClientPrompt> EnumeratePromptsAsync(
|
||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var promptResults = await client.SendRequestAsync(
|
||||
RequestMethods.PromptsList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListPromptsResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var prompt in promptResults.Prompts)
|
||||
{
|
||||
yield return new(client, prompt);
|
||||
}
|
||||
|
||||
cursor = promptResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
}
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumeratePromptsAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static IAsyncEnumerable<McpClientPrompt> EnumeratePromptsAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
=> AsClientOrThrow(client).EnumeratePromptsAsync(cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a specific prompt from the MCP server.
|
||||
|
|
@ -308,26 +252,14 @@ public static class McpClientExtensions
|
|||
/// </remarks>
|
||||
/// <exception cref="McpException">Thrown when the prompt does not exist, when required arguments are missing, or when the server encounters an error processing the prompt.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.GetPromptAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static ValueTask<GetPromptResult> GetPromptAsync(
|
||||
this IMcpClient client,
|
||||
string name,
|
||||
IReadOnlyDictionary<string, object?>? arguments = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(name);
|
||||
|
||||
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.PromptsGet,
|
||||
new() { Name = name, Arguments = ToArgumentsDictionary(arguments, serializerOptions) },
|
||||
McpJsonUtilities.JsonContext.Default.GetPromptRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.GetPromptResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
=> AsClientOrThrow(client).GetPromptAsync(name, arguments, serializerOptions, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of available resource templates from the server.
|
||||
|
|
@ -346,35 +278,10 @@ public static class McpClientExtensions
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async ValueTask<IList<McpClientResourceTemplate>> ListResourceTemplatesAsync(
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListResourceTemplatesAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static ValueTask<IList<McpClientResourceTemplate>> ListResourceTemplatesAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
List<McpClientResourceTemplate>? resourceTemplates = null;
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var templateResults = await client.SendRequestAsync(
|
||||
RequestMethods.ResourcesTemplatesList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
resourceTemplates ??= new List<McpClientResourceTemplate>(templateResults.ResourceTemplates.Count);
|
||||
foreach (var template in templateResults.ResourceTemplates)
|
||||
{
|
||||
resourceTemplates.Add(new McpClientResourceTemplate(client, template));
|
||||
}
|
||||
|
||||
cursor = templateResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
|
||||
return resourceTemplates;
|
||||
}
|
||||
=> AsClientOrThrow(client).ListResourceTemplatesAsync(cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an enumerable for asynchronously enumerating all available resource templates from the server.
|
||||
|
|
@ -403,30 +310,10 @@ public static class McpClientExtensions
|
|||
/// </code>
|
||||
/// </example>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async IAsyncEnumerable<McpClientResourceTemplate> EnumerateResourceTemplatesAsync(
|
||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var templateResults = await client.SendRequestAsync(
|
||||
RequestMethods.ResourcesTemplatesList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var templateResult in templateResults.ResourceTemplates)
|
||||
{
|
||||
yield return new McpClientResourceTemplate(client, templateResult);
|
||||
}
|
||||
|
||||
cursor = templateResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
}
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateResourceTemplatesAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static IAsyncEnumerable<McpClientResourceTemplate> EnumerateResourceTemplatesAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
=> AsClientOrThrow(client).EnumerateResourceTemplatesAsync(cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of available resources from the server.
|
||||
|
|
@ -457,35 +344,10 @@ public static class McpClientExtensions
|
|||
/// </code>
|
||||
/// </example>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async ValueTask<IList<McpClientResource>> ListResourcesAsync(
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListResourcesAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static ValueTask<IList<McpClientResource>> ListResourcesAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
List<McpClientResource>? resources = null;
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var resourceResults = await client.SendRequestAsync(
|
||||
RequestMethods.ResourcesList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
resources ??= new List<McpClientResource>(resourceResults.Resources.Count);
|
||||
foreach (var resource in resourceResults.Resources)
|
||||
{
|
||||
resources.Add(new McpClientResource(client, resource));
|
||||
}
|
||||
|
||||
cursor = resourceResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
|
||||
return resources;
|
||||
}
|
||||
=> AsClientOrThrow(client).ListResourcesAsync(cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an enumerable for asynchronously enumerating all available resources from the server.
|
||||
|
|
@ -514,30 +376,10 @@ public static class McpClientExtensions
|
|||
/// </code>
|
||||
/// </example>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static async IAsyncEnumerable<McpClientResource> EnumerateResourcesAsync(
|
||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var resourceResults = await client.SendRequestAsync(
|
||||
RequestMethods.ResourcesList,
|
||||
new() { Cursor = cursor },
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListResourcesResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var resource in resourceResults.Resources)
|
||||
{
|
||||
yield return new McpClientResource(client, resource);
|
||||
}
|
||||
|
||||
cursor = resourceResults.NextCursor;
|
||||
}
|
||||
while (cursor is not null);
|
||||
}
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateResourcesAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static IAsyncEnumerable<McpClientResource> EnumerateResourcesAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
=> AsClientOrThrow(client).EnumerateResourcesAsync(cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a resource from the server.
|
||||
|
|
@ -548,19 +390,10 @@ public static class McpClientExtensions
|
|||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="uri"/> is empty or composed entirely of whitespace.</exception>
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static ValueTask<ReadResourceResult> ReadResourceAsync(
|
||||
this IMcpClient client, string uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(uri);
|
||||
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.ResourcesRead,
|
||||
new() { Uri = uri },
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
=> AsClientOrThrow(client).ReadResourceAsync(uri, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a resource from the server.
|
||||
|
|
@ -570,14 +403,10 @@ public static class McpClientExtensions
|
|||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static ValueTask<ReadResourceResult> ReadResourceAsync(
|
||||
this IMcpClient client, Uri uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNull(uri);
|
||||
|
||||
return ReadResourceAsync(client, uri.ToString(), cancellationToken);
|
||||
}
|
||||
=> AsClientOrThrow(client).ReadResourceAsync(uri, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a resource from the server.
|
||||
|
|
@ -589,20 +418,10 @@ public static class McpClientExtensions
|
|||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="uriTemplate"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="uriTemplate"/> is empty or composed entirely of whitespace.</exception>
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static ValueTask<ReadResourceResult> ReadResourceAsync(
|
||||
this IMcpClient client, string uriTemplate, IReadOnlyDictionary<string, object?> arguments, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(uriTemplate);
|
||||
Throw.IfNull(arguments);
|
||||
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.ResourcesRead,
|
||||
new() { Uri = UriTemplate.FormatUri(uriTemplate, arguments) },
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ReadResourceResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
=> AsClientOrThrow(client).ReadResourceAsync(uriTemplate, arguments, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Requests completion suggestions for a prompt argument or resource reference.
|
||||
|
|
@ -633,23 +452,9 @@ public static class McpClientExtensions
|
|||
/// <exception cref="ArgumentNullException"><paramref name="argumentName"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="argumentName"/> is empty or composed entirely of whitespace.</exception>
|
||||
/// <exception cref="McpException">The server returned an error response.</exception>
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CompleteAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static ValueTask<CompleteResult> CompleteAsync(this IMcpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNull(reference);
|
||||
Throw.IfNullOrWhiteSpace(argumentName);
|
||||
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.CompletionComplete,
|
||||
new()
|
||||
{
|
||||
Ref = reference,
|
||||
Argument = new Argument { Name = argumentName, Value = argumentValue }
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.CompleteRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CompleteResult,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
=> AsClientOrThrow(client).CompleteAsync(reference, argumentName, argumentValue, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to a resource on the server to receive notifications when it changes.
|
||||
|
|
@ -676,18 +481,9 @@ public static class McpClientExtensions
|
|||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="uri"/> is empty or composed entirely of whitespace.</exception>
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(uri);
|
||||
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.ResourcesSubscribe,
|
||||
new() { Uri = uri },
|
||||
McpJsonUtilities.JsonContext.Default.SubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult,
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
=> AsClientOrThrow(client).SubscribeToResourceAsync(uri, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to a resource on the server to receive notifications when it changes.
|
||||
|
|
@ -713,13 +509,9 @@ public static class McpClientExtensions
|
|||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static Task SubscribeToResourceAsync(this IMcpClient client, Uri uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNull(uri);
|
||||
|
||||
return SubscribeToResourceAsync(client, uri.ToString(), cancellationToken);
|
||||
}
|
||||
=> AsClientOrThrow(client).SubscribeToResourceAsync(uri, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from a resource on the server to stop receiving notifications about its changes.
|
||||
|
|
@ -745,18 +537,9 @@ public static class McpClientExtensions
|
|||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="uri"/> is empty or composed entirely of whitespace.</exception>
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(uri);
|
||||
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.ResourcesUnsubscribe,
|
||||
new() { Uri = uri },
|
||||
McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult,
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
=> AsClientOrThrow(client).UnsubscribeFromResourceAsync(uri, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from a resource on the server to stop receiving notifications about its changes.
|
||||
|
|
@ -781,13 +564,9 @@ public static class McpClientExtensions
|
|||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNull(uri);
|
||||
|
||||
return UnsubscribeFromResourceAsync(client, uri.ToString(), cancellationToken);
|
||||
}
|
||||
=> AsClientOrThrow(client).UnsubscribeFromResourceAsync(uri, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a tool on the server.
|
||||
|
|
@ -824,6 +603,7 @@ public static class McpClientExtensions
|
|||
/// });
|
||||
/// </code>
|
||||
/// </example>
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CallToolAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static ValueTask<CallToolResult> CallToolAsync(
|
||||
this IMcpClient client,
|
||||
string toolName,
|
||||
|
|
@ -831,62 +611,28 @@ public static class McpClientExtensions
|
|||
IProgress<ProgressNotificationValue>? progress = null,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> AsClientOrThrow(client).CallToolAsync(toolName, arguments, progress, serializerOptions, cancellationToken);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
private static McpClient AsClientOrThrow(IMcpClient client, [CallerMemberName] string memberName = "")
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNull(toolName);
|
||||
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
||||
serializerOptions.MakeReadOnly();
|
||||
|
||||
if (progress is not null)
|
||||
if (client is not McpClient mcpClient)
|
||||
{
|
||||
return SendRequestWithProgressAsync(client, toolName, arguments, progress, serializerOptions, cancellationToken);
|
||||
ThrowInvalidEndpointType(memberName);
|
||||
}
|
||||
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.ToolsCall,
|
||||
new()
|
||||
{
|
||||
Name = toolName,
|
||||
Arguments = ToArgumentsDictionary(arguments, serializerOptions),
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CallToolResult,
|
||||
cancellationToken: cancellationToken);
|
||||
return mcpClient;
|
||||
|
||||
static async ValueTask<CallToolResult> SendRequestWithProgressAsync(
|
||||
IMcpClient client,
|
||||
string toolName,
|
||||
IReadOnlyDictionary<string, object?>? arguments,
|
||||
IProgress<ProgressNotificationValue> progress,
|
||||
JsonSerializerOptions serializerOptions,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ProgressToken progressToken = new(Guid.NewGuid().ToString("N"));
|
||||
|
||||
await using var _ = client.RegisterNotificationHandler(NotificationMethods.ProgressNotification,
|
||||
(notification, cancellationToken) =>
|
||||
{
|
||||
if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.JsonContext.Default.ProgressNotificationParams) is { } pn &&
|
||||
pn.ProgressToken == progressToken)
|
||||
{
|
||||
progress.Report(pn.Progress);
|
||||
}
|
||||
|
||||
return default;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
return await client.SendRequestAsync(
|
||||
RequestMethods.ToolsCall,
|
||||
new()
|
||||
{
|
||||
Name = toolName,
|
||||
Arguments = ToArgumentsDictionary(arguments, serializerOptions),
|
||||
ProgressToken = progressToken,
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CallToolResult,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowInvalidEndpointType(string memberName)
|
||||
=> throw new InvalidOperationException(
|
||||
$"Only arguments assignable to '{nameof(McpClient)}' are supported. " +
|
||||
$"Prefer using '{nameof(McpClient)}.{memberName}' instead, as " +
|
||||
$"'{nameof(McpClientExtensions)}.{memberName}' is obsolete and will be " +
|
||||
$"removed in the future.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -963,132 +709,4 @@ public static class McpClientExtensions
|
|||
StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a sampling handler for use with <see cref="SamplingCapability.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>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method creates a function that converts MCP message requests into chat client calls, enabling
|
||||
/// an MCP client to generate text or other content using an actual AI model via the provided chat client.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The handler can process text messages, image messages, and resource messages as defined in the
|
||||
/// Model Context Protocol.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="chatClient"/> is <see langword="null"/>.</exception>
|
||||
public static Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>> CreateSamplingHandler(
|
||||
this IChatClient chatClient)
|
||||
{
|
||||
Throw.IfNull(chatClient);
|
||||
|
||||
return async (requestParams, progress, cancellationToken) =>
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
|
||||
var (messages, options) = requestParams.ToChatClientArguments();
|
||||
var progressToken = requestParams.ProgressToken;
|
||||
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
updates.Add(update);
|
||||
|
||||
if (progressToken is not null)
|
||||
{
|
||||
progress.Report(new()
|
||||
{
|
||||
Progress = updates.Count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return updates.ToChatResponse().ToCreateMessageResult();
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the logging level for the server to control which log messages are sent to the client.
|
||||
/// </summary>
|
||||
/// <param name="client">The client instance used to communicate with the MCP server.</param>
|
||||
/// <param name="level">The minimum severity level of log messages to receive from the server.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// After this request is processed, the server will send log messages at or above the specified
|
||||
/// logging level as notifications to the client. For example, if <see cref="LoggingLevel.Warning"/> is set,
|
||||
/// the client will receive <see cref="LoggingLevel.Warning"/>, <see cref="LoggingLevel.Error"/>,
|
||||
/// <see cref="LoggingLevel.Critical"/>, <see cref="LoggingLevel.Alert"/>, and <see cref="LoggingLevel.Emergency"/>
|
||||
/// level messages.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To receive all log messages, set the level to <see cref="LoggingLevel.Debug"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Log messages are delivered as notifications to the client and can be captured by registering
|
||||
/// appropriate event handlers with the client implementation, such as with <see cref="IMcpEndpoint.RegisterNotificationHandler"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static Task SetLoggingLevel(this IMcpClient client, LoggingLevel level, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync(
|
||||
RequestMethods.LoggingSetLevel,
|
||||
new() { Level = level },
|
||||
McpJsonUtilities.JsonContext.Default.SetLevelRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.EmptyResult,
|
||||
cancellationToken: cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the logging level for the server to control which log messages are sent to the client.
|
||||
/// </summary>
|
||||
/// <param name="client">The client instance used to communicate with the MCP server.</param>
|
||||
/// <param name="level">The minimum severity level of log messages to receive from the server.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// After this request is processed, the server will send log messages at or above the specified
|
||||
/// logging level as notifications to the client. For example, if <see cref="LogLevel.Warning"/> is set,
|
||||
/// the client will receive <see cref="LogLevel.Warning"/>, <see cref="LogLevel.Error"/>,
|
||||
/// and <see cref="LogLevel.Critical"/> level messages.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To receive all log messages, set the level to <see cref="LogLevel.Trace"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Log messages are delivered as notifications to the client and can be captured by registering
|
||||
/// appropriate event handlers with the client implementation, such as with <see cref="IMcpEndpoint.RegisterNotificationHandler"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
|
||||
public static Task SetLoggingLevel(this IMcpClient client, LogLevel level, CancellationToken cancellationToken = default) =>
|
||||
SetLoggingLevel(client, McpServer.ToLoggingLevel(level), cancellationToken);
|
||||
|
||||
/// <summary>Convers a dictionary with <see cref="object"/> values to a dictionary with <see cref="JsonElement"/> values.</summary>
|
||||
private static Dictionary<string, JsonElement>? ToArgumentsDictionary(
|
||||
IReadOnlyDictionary<string, object?>? arguments, JsonSerializerOptions options)
|
||||
{
|
||||
var typeInfo = options.GetTypeInfo<object?>();
|
||||
|
||||
Dictionary<string, JsonElement>? result = null;
|
||||
if (arguments is not null)
|
||||
{
|
||||
result = new(arguments.Count);
|
||||
foreach (var kvp in arguments)
|
||||
{
|
||||
result.Add(kvp.Key, kvp.Value is JsonElement je ? je : JsonSerializer.SerializeToElement(kvp.Value, typeInfo));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
|
|
@ -10,6 +10,7 @@ namespace ModelContextProtocol.Client;
|
|||
/// that connect to MCP servers. It handles the creation and connection
|
||||
/// of appropriate implementations through the supplied transport.
|
||||
/// </remarks>
|
||||
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CreateAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
|
||||
public static partial class McpClientFactory
|
||||
{
|
||||
/// <summary>Creates an <see cref="IMcpClient"/>, connecting it to the specified server.</summary>
|
||||
|
|
@ -28,27 +29,5 @@ public static partial class McpClientFactory
|
|||
McpClientOptions? clientOptions = null,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(clientTransport);
|
||||
|
||||
McpClient client = new(clientTransport, clientOptions, loggerFactory);
|
||||
try
|
||||
{
|
||||
await client.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (loggerFactory?.CreateLogger(typeof(McpClientFactory)) is ILogger logger)
|
||||
{
|
||||
logger.LogClientCreated(client.EndpointName);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
await client.DisposeAsync().ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} client created and connected.")]
|
||||
private static partial void LogClientCreated(this ILogger logger, string endpointName);
|
||||
=> await McpClient.CreateAsync(clientTransport, clientOptions, loggerFactory, cancellationToken);
|
||||
}
|
||||
246
src/ModelContextProtocol.Core/Client/McpClientImpl.cs
Normal file
246
src/ModelContextProtocol.Core/Client/McpClientImpl.cs
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal sealed partial class McpClientImpl : McpClient
|
||||
{
|
||||
private static Implementation DefaultImplementation { get; } = new()
|
||||
{
|
||||
Name = AssemblyNameHelper.DefaultAssemblyName.Name ?? nameof(McpClient),
|
||||
Version = AssemblyNameHelper.DefaultAssemblyName.Version?.ToString() ?? "1.0.0",
|
||||
};
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly ITransport _transport;
|
||||
private readonly string _endpointName;
|
||||
private readonly McpClientOptions _options;
|
||||
private readonly McpSessionHandler _sessionHandler;
|
||||
private readonly SemaphoreSlim _disposeLock = new(1, 1);
|
||||
|
||||
private CancellationTokenSource? _connectCts;
|
||||
|
||||
private ServerCapabilities? _serverCapabilities;
|
||||
private Implementation? _serverInfo;
|
||||
private string? _serverInstructions;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpClientImpl"/> class.
|
||||
/// </summary>
|
||||
/// <param name="transport">The transport to use for communication with the server.</param>
|
||||
/// <param name="endpointName">The name of the endpoint for logging and debug purposes.</param>
|
||||
/// <param name="options">Options for the client, defining protocol version and capabilities.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
internal McpClientImpl(ITransport transport, string endpointName, McpClientOptions? options, ILoggerFactory? loggerFactory)
|
||||
{
|
||||
options ??= new();
|
||||
|
||||
_transport = transport;
|
||||
_endpointName = $"Client ({options.ClientInfo?.Name ?? DefaultImplementation.Name} {options.ClientInfo?.Version ?? DefaultImplementation.Version})";
|
||||
_options = options;
|
||||
_logger = loggerFactory?.CreateLogger<McpClient>() ?? NullLogger<McpClient>.Instance;
|
||||
|
||||
var notificationHandlers = new NotificationHandlers();
|
||||
var requestHandlers = new RequestHandlers();
|
||||
|
||||
if (options.Capabilities is { } capabilities)
|
||||
{
|
||||
RegisterHandlers(capabilities, notificationHandlers, requestHandlers);
|
||||
}
|
||||
|
||||
_sessionHandler = new McpSessionHandler(isServer: false, transport, endpointName, requestHandlers, notificationHandlers, _logger);
|
||||
}
|
||||
|
||||
private void RegisterHandlers(ClientCapabilities capabilities, NotificationHandlers notificationHandlers, RequestHandlers requestHandlers)
|
||||
{
|
||||
if (capabilities.NotificationHandlers is { } notificationHandlersFromCapabilities)
|
||||
{
|
||||
notificationHandlers.RegisterRange(notificationHandlersFromCapabilities);
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
requestHandlers.Set(
|
||||
RequestMethods.RootsList,
|
||||
(request, _, cancellationToken) => rootsHandler(request, cancellationToken),
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsResult);
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
requestHandlers.Set(
|
||||
RequestMethods.ElicitationCreate,
|
||||
(request, _, cancellationToken) => elicitationHandler(request, cancellationToken),
|
||||
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ElicitResult);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? SessionId => _transport.SessionId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ServerCapabilities ServerCapabilities => _serverCapabilities ?? throw new InvalidOperationException("The client is not connected.");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Implementation ServerInfo => _serverInfo ?? throw new InvalidOperationException("The client is not connected.");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string? ServerInstructions => _serverInstructions;
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously connects to an MCP server, establishes the transport connection, and completes the initialization handshake.
|
||||
/// </summary>
|
||||
public async Task ConnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cancellationToken = _connectCts.Token;
|
||||
|
||||
try
|
||||
{
|
||||
// We don't want the ConnectAsync token to cancel the message processing loop after we've successfully connected.
|
||||
// The session handler handles cancelling the loop upon its disposal.
|
||||
_ = _sessionHandler.ProcessMessagesAsync(CancellationToken.None);
|
||||
|
||||
// Perform initialization sequence
|
||||
using var initializationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
initializationCts.CancelAfter(_options.InitializationTimeout);
|
||||
|
||||
try
|
||||
{
|
||||
// Send initialize request
|
||||
string requestProtocol = _options.ProtocolVersion ?? McpSessionHandler.LatestProtocolVersion;
|
||||
var initializeResponse = await this.SendRequestAsync(
|
||||
RequestMethods.Initialize,
|
||||
new InitializeRequestParams
|
||||
{
|
||||
ProtocolVersion = requestProtocol,
|
||||
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
|
||||
ClientInfo = _options.ClientInfo ?? DefaultImplementation,
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.InitializeResult,
|
||||
cancellationToken: initializationCts.Token).ConfigureAwait(false);
|
||||
|
||||
// Store server information
|
||||
if (_logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
LogServerCapabilitiesReceived(_endpointName,
|
||||
capabilities: JsonSerializer.Serialize(initializeResponse.Capabilities, McpJsonUtilities.JsonContext.Default.ServerCapabilities),
|
||||
serverInfo: JsonSerializer.Serialize(initializeResponse.ServerInfo, McpJsonUtilities.JsonContext.Default.Implementation));
|
||||
}
|
||||
|
||||
_serverCapabilities = initializeResponse.Capabilities;
|
||||
_serverInfo = initializeResponse.ServerInfo;
|
||||
_serverInstructions = initializeResponse.Instructions;
|
||||
|
||||
// Validate protocol version
|
||||
bool isResponseProtocolValid =
|
||||
_options.ProtocolVersion is { } optionsProtocol ? optionsProtocol == initializeResponse.ProtocolVersion :
|
||||
McpSessionHandler.SupportedProtocolVersions.Contains(initializeResponse.ProtocolVersion);
|
||||
if (!isResponseProtocolValid)
|
||||
{
|
||||
LogServerProtocolVersionMismatch(_endpointName, requestProtocol, initializeResponse.ProtocolVersion);
|
||||
throw new McpException($"Server protocol version mismatch. Expected {requestProtocol}, got {initializeResponse.ProtocolVersion}");
|
||||
}
|
||||
|
||||
// Send initialized notification
|
||||
await this.SendNotificationAsync(
|
||||
NotificationMethods.InitializedNotification,
|
||||
new InitializedNotificationParams(),
|
||||
McpJsonUtilities.JsonContext.Default.InitializedNotificationParams,
|
||||
cancellationToken: initializationCts.Token).ConfigureAwait(false);
|
||||
|
||||
}
|
||||
catch (OperationCanceledException oce) when (initializationCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
LogClientInitializationTimeout(_endpointName);
|
||||
throw new TimeoutException("Initialization timed out", oce);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogClientInitializationError(_endpointName, e);
|
||||
await DisposeAsync().ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
LogClientConnected(_endpointName);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default)
|
||||
=> _sessionHandler.SendRequestAsync(request, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
=> _sessionHandler.SendMessageAsync(message, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IAsyncDisposable RegisterNotificationHandler(string method, Func<JsonRpcNotification, CancellationToken, ValueTask> handler)
|
||||
=> _sessionHandler.RegisterNotificationHandler(method, handler);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
using var _ = await _disposeLock.LockAsync().ConfigureAwait(false);
|
||||
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
await _sessionHandler.DisposeAsync().ConfigureAwait(false);
|
||||
await _transport.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} client received server '{ServerInfo}' capabilities: '{Capabilities}'.")]
|
||||
private partial void LogServerCapabilitiesReceived(string endpointName, string capabilities, string serverInfo);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} client initialization error.")]
|
||||
private partial void LogClientInitializationError(string endpointName, Exception exception);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} client initialization timed out.")]
|
||||
private partial void LogClientInitializationTimeout(string endpointName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} client protocol version mismatch with server. Expected '{Expected}', received '{Received}'.")]
|
||||
private partial void LogServerProtocolVersionMismatch(string endpointName, string expected, string received);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} client created and connected.")]
|
||||
private partial void LogClientConnected(string endpointName);
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@ using ModelContextProtocol.Protocol;
|
|||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Provides configuration options for creating <see cref="IMcpClient"/> instances.
|
||||
/// Provides configuration options for creating <see cref="McpClient"/> instances.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These options are typically passed to <see cref="McpClientFactory.CreateAsync"/> when creating a client.
|
||||
/// These options are typically passed to <see cref="McpClient.CreateAsync"/> when creating a client.
|
||||
/// They define client capabilities, protocol version, and other client-specific settings.
|
||||
/// </remarks>
|
||||
public sealed class McpClientOptions
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ namespace ModelContextProtocol.Client;
|
|||
/// <para>
|
||||
/// This class provides a client-side wrapper around a prompt defined on an MCP server. It allows
|
||||
/// retrieving the prompt's content by sending a request to the server with optional arguments.
|
||||
/// Instances of this class are typically obtained by calling <see cref="McpClientExtensions.ListPromptsAsync"/>
|
||||
/// or <see cref="McpClientExtensions.EnumeratePromptsAsync"/>.
|
||||
/// Instances of this class are typically obtained by calling <see cref="McpClient.ListPromptsAsync"/>
|
||||
/// or <see cref="McpClient.EnumeratePromptsAsync"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Each prompt has a name and optionally a description, and it can be invoked with arguments
|
||||
|
|
@ -20,9 +20,9 @@ namespace ModelContextProtocol.Client;
|
|||
/// </remarks>
|
||||
public sealed class McpClientPrompt
|
||||
{
|
||||
private readonly IMcpClient _client;
|
||||
private readonly McpClient _client;
|
||||
|
||||
internal McpClientPrompt(IMcpClient client, Prompt prompt)
|
||||
internal McpClientPrompt(McpClient client, Prompt prompt)
|
||||
{
|
||||
_client = client;
|
||||
ProtocolPrompt = prompt;
|
||||
|
|
@ -63,7 +63,7 @@ public sealed class McpClientPrompt
|
|||
/// The server will process the request and return a result containing messages or other content.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This is a convenience method that internally calls <see cref="McpClientExtensions.GetPromptAsync"/>
|
||||
/// This is a convenience method that internally calls <see cref="McpClient.GetPromptAsync"/>
|
||||
/// with this prompt's name and arguments.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@ namespace ModelContextProtocol.Client;
|
|||
/// <para>
|
||||
/// This class provides a client-side wrapper around a resource defined on an MCP server. It allows
|
||||
/// retrieving the resource's content by sending a request to the server with the resource's URI.
|
||||
/// Instances of this class are typically obtained by calling <see cref="McpClientExtensions.ListResourcesAsync"/>
|
||||
/// or <see cref="McpClientExtensions.EnumerateResourcesAsync"/>.
|
||||
/// Instances of this class are typically obtained by calling <see cref="McpClient.ListResourcesAsync"/>
|
||||
/// or <see cref="McpClient.EnumerateResourcesAsync"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class McpClientResource
|
||||
{
|
||||
private readonly IMcpClient _client;
|
||||
private readonly McpClient _client;
|
||||
|
||||
internal McpClientResource(IMcpClient client, Resource resource)
|
||||
internal McpClientResource(McpClient client, Resource resource)
|
||||
{
|
||||
_client = client;
|
||||
ProtocolResource = resource;
|
||||
|
|
@ -58,7 +58,7 @@ public sealed class McpClientResource
|
|||
/// <returns>A <see cref="ValueTask{ReadResourceResult}"/> containing the resource's result with content and messages.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is a convenience method that internally calls <see cref="McpClientExtensions.ReadResourceAsync(IMcpClient, string, CancellationToken)"/>.
|
||||
/// This is a convenience method that internally calls <see cref="McpClient.ReadResourceAsync(string, CancellationToken)"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ValueTask<ReadResourceResult> ReadAsync(
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@ namespace ModelContextProtocol.Client;
|
|||
/// <para>
|
||||
/// This class provides a client-side wrapper around a resource template defined on an MCP server. It allows
|
||||
/// retrieving the resource template's content by sending a request to the server with the resource's URI.
|
||||
/// Instances of this class are typically obtained by calling <see cref="McpClientExtensions.ListResourceTemplatesAsync"/>
|
||||
/// or <see cref="McpClientExtensions.EnumerateResourceTemplatesAsync"/>.
|
||||
/// Instances of this class are typically obtained by calling <see cref="McpClient.ListResourceTemplatesAsync"/>
|
||||
/// or <see cref="McpClient.EnumerateResourceTemplatesAsync"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class McpClientResourceTemplate
|
||||
{
|
||||
private readonly IMcpClient _client;
|
||||
private readonly McpClient _client;
|
||||
|
||||
internal McpClientResourceTemplate(IMcpClient client, ResourceTemplate resourceTemplate)
|
||||
internal McpClientResourceTemplate(McpClient client, ResourceTemplate resourceTemplate)
|
||||
{
|
||||
_client = client;
|
||||
ProtocolResourceTemplate = resourceTemplate;
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ using System.Text.Json;
|
|||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an <see cref="AIFunction"/> that calls a tool via an <see cref="IMcpClient"/>.
|
||||
/// Provides an <see cref="AIFunction"/> that calls a tool via an <see cref="McpClient"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The <see cref="McpClientTool"/> class encapsulates an <see cref="IMcpClient"/> along with a description of
|
||||
/// The <see cref="McpClientTool"/> class encapsulates an <see cref="McpClient"/> along with a description of
|
||||
/// a tool available via that client, allowing it to be invoked as an <see cref="AIFunction"/>. This enables integration
|
||||
/// with AI models that support function calling capabilities.
|
||||
/// </para>
|
||||
|
|
@ -19,8 +19,8 @@ namespace ModelContextProtocol.Client;
|
|||
/// <see cref="WithName"/> and <see cref="WithDescription"/> without changing the underlying tool functionality.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Typically, you would get instances of this class by calling the <see cref="McpClientExtensions.ListToolsAsync"/>
|
||||
/// or <see cref="McpClientExtensions.EnumerateToolsAsync"/> extension methods on an <see cref="IMcpClient"/> instance.
|
||||
/// Typically, you would get instances of this class by calling the <see cref="McpClient.ListToolsAsync"/>
|
||||
/// or <see cref="McpClient.EnumerateToolsAsync"/> extension methods on an <see cref="McpClient"/> instance.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class McpClientTool : AIFunction
|
||||
|
|
@ -32,13 +32,13 @@ public sealed class McpClientTool : AIFunction
|
|||
["Strict"] = false, // some MCP schemas may not meet "strict" requirements
|
||||
});
|
||||
|
||||
private readonly IMcpClient _client;
|
||||
private readonly McpClient _client;
|
||||
private readonly string _name;
|
||||
private readonly string _description;
|
||||
private readonly IProgress<ProgressNotificationValue>? _progress;
|
||||
|
||||
internal McpClientTool(
|
||||
IMcpClient client,
|
||||
McpClient client,
|
||||
Tool tool,
|
||||
JsonSerializerOptions serializerOptions,
|
||||
string? name = null,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace ModelContextProtocol.Client;
|
|||
internal sealed partial class SseClientSessionTransport : TransportBase
|
||||
{
|
||||
private readonly McpHttpClient _httpClient;
|
||||
private readonly SseClientTransportOptions _options;
|
||||
private readonly HttpClientTransportOptions _options;
|
||||
private readonly Uri _sseEndpoint;
|
||||
private Uri? _messageEndpoint;
|
||||
private readonly CancellationTokenSource _connectionCts;
|
||||
|
|
@ -29,7 +29,7 @@ internal sealed partial class SseClientSessionTransport : TransportBase
|
|||
/// </summary>
|
||||
public SseClientSessionTransport(
|
||||
string endpointName,
|
||||
SseClientTransportOptions transportOptions,
|
||||
HttpClientTransportOptions transportOptions,
|
||||
McpHttpClient httpClient,
|
||||
Channel<JsonRpcMessage>? messageChannel,
|
||||
ILoggerFactory? loggerFactory)
|
||||
|
|
@ -42,7 +42,7 @@ internal sealed partial class SseClientSessionTransport : TransportBase
|
|||
_sseEndpoint = transportOptions.Endpoint;
|
||||
_httpClient = httpClient;
|
||||
_connectionCts = new CancellationTokenSource();
|
||||
_logger = (ILogger?)loggerFactory?.CreateLogger<SseClientTransport>() ?? NullLogger.Instance;
|
||||
_logger = (ILogger?)loggerFactory?.CreateLogger<HttpClientTransport>() ?? NullLogger.Instance;
|
||||
_connectionEstablished = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
private static readonly MediaTypeWithQualityHeaderValue s_textEventStreamMediaType = new("text/event-stream");
|
||||
|
||||
private readonly McpHttpClient _httpClient;
|
||||
private readonly SseClientTransportOptions _options;
|
||||
private readonly HttpClientTransportOptions _options;
|
||||
private readonly CancellationTokenSource _connectionCts;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
|
||||
public StreamableHttpClientSessionTransport(
|
||||
string endpointName,
|
||||
SseClientTransportOptions transportOptions,
|
||||
HttpClientTransportOptions transportOptions,
|
||||
McpHttpClient httpClient,
|
||||
Channel<JsonRpcMessage>? messageChannel,
|
||||
ILoggerFactory? loggerFactory)
|
||||
|
|
@ -41,10 +41,10 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
_options = transportOptions;
|
||||
_httpClient = httpClient;
|
||||
_connectionCts = new CancellationTokenSource();
|
||||
_logger = (ILogger?)loggerFactory?.CreateLogger<SseClientTransport>() ?? NullLogger.Instance;
|
||||
_logger = (ILogger?)loggerFactory?.CreateLogger<HttpClientTransport>() ?? NullLogger.Instance;
|
||||
|
||||
// We connect with the initialization request with the MCP transport. This means that any errors won't be observed
|
||||
// until the first call to SendMessageAsync. Fortunately, that happens internally in McpClientFactory.ConnectAsync
|
||||
// until the first call to SendMessageAsync. Fortunately, that happens internally in McpClient.ConnectAsync
|
||||
// so we still throw any connection-related Exceptions from there and never expose a pre-connected client to the user.
|
||||
SetConnected();
|
||||
}
|
||||
|
|
@ -291,7 +291,7 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
|
|||
{
|
||||
if (!headers.TryAddWithoutValidation(header.Key, header.Value))
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to add header '{header.Key}' with value '{header.Value}' from {nameof(SseClientTransportOptions.AdditionalHeaders)}.");
|
||||
throw new InvalidOperationException($"Failed to add header '{header.Key}' with value '{header.Value}' from {nameof(HttpClientTransportOptions.AdditionalHeaders)}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue