using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Utils;
using ModelContextProtocol.Utils.Json;
using Microsoft.Extensions.AI;
using System.Runtime.CompilerServices;
using System.Text.Json;
#pragma warning disable CA1508 // Avoid dead conditional code
namespace ModelContextProtocol.Client;
///
/// Provides extensions for operating on MCP clients.
///
public static class McpClientExtensions
{
///
/// Sends a notification to the server with parameters.
///
/// The client.
/// The notification method name.
/// The parameters to send with the notification.
/// A token to cancel the operation.
public static Task SendNotificationAsync(this IMcpClient client, string method, object? parameters = null, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
return client.SendMessageAsync(
new JsonRpcNotification { Method = method, Params = parameters },
cancellationToken);
}
///
/// Sends a ping request to verify server connectivity.
///
/// The client.
/// A token to cancel the operation.
/// A task that completes when the ping is successful.
public static Task PingAsync(this IMcpClient client, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
return client.SendRequestAsync(
CreateRequest("ping", null),
cancellationToken);
}
///
/// Retrieves a sequence of available tools from the server.
///
/// The client.
/// A token to cancel the operation.
/// An asynchronous sequence of tool information.
public static async IAsyncEnumerable ListToolsAsync(
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
string? cursor = null;
do
{
var tools = await ListToolsAsync(client, cursor, cancellationToken).ConfigureAwait(false);
foreach (var tool in tools.Tools)
{
yield return tool;
}
cursor = tools.NextCursor;
}
while (cursor is not null);
}
///
/// Retrieves a sequence of available tools from the server.
///
/// The client.
/// A cursor to paginate the results.
/// A token to cancel the operation.
/// A task containing the server's response with tool information.
public static Task ListToolsAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
return client.SendRequestAsync(
CreateRequest("tools/list", CreateCursorDictionary(cursor)),
cancellationToken);
}
///
/// Retrieves a list of available prompts from the server.
///
/// The client.
/// A token to cancel the operation.
/// An asynchronous sequence of prompt information.
public static async IAsyncEnumerable ListPromptsAsync(
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
string? cursor = null;
do
{
var prompts = await ListPromptsAsync(client, cursor, cancellationToken).ConfigureAwait(false);
foreach (var prompt in prompts.Prompts)
{
yield return prompt;
}
cursor = prompts.NextCursor;
}
while (cursor is not null);
}
///
/// Retrieves a list of available prompts from the server.
///
/// The client.
/// A cursor to paginate the results.
/// A token to cancel the operation.
/// A task containing the server's response with prompt information.
public static Task ListPromptsAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
return client.SendRequestAsync(
CreateRequest("prompts/list", CreateCursorDictionary(cursor)),
cancellationToken);
}
///
/// Retrieves a specific prompt with optional arguments.
///
/// The client.
/// The name of the prompt to retrieve
/// Optional arguments for the prompt
/// A token to cancel the operation.
/// A task containing the prompt's content and messages.
public static Task GetPromptAsync(this IMcpClient client, string name, Dictionary? arguments = null, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
return client.SendRequestAsync(
CreateRequest("prompts/get", CreateParametersDictionary(name, arguments)),
cancellationToken);
}
///
/// Retrieves a sequence of available resources from the server.
///
/// The client.
/// A token to cancel the operation.
/// An asynchronous sequence of resource information.
public static async IAsyncEnumerable ListResourcesAsync(
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
string? cursor = null;
do
{
var resources = await ListResourcesAsync(client, cursor, cancellationToken).ConfigureAwait(false);
foreach (var resource in resources.Resources)
{
yield return resource;
}
cursor = resources.NextCursor;
}
while (cursor is not null);
}
///
/// Retrieves a list of available resources from the server.
///
/// The client.
/// A cursor to paginate the results.
/// A token to cancel the operation.
public static Task ListResourcesAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
return client.SendRequestAsync(
CreateRequest("resources/list", CreateCursorDictionary(cursor)),
cancellationToken);
}
///
/// Reads a resource from the server.
///
/// The client.
/// The uri of the resource.
/// A token to cancel the operation.
public static Task ReadResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
return client.SendRequestAsync(
CreateRequest("resources/read", new() { ["uri"] = uri }),
cancellationToken);
}
///
/// Gets the completion options for a resource or prompt reference and (named) argument.
///
/// The client.
/// A resource (uri) or prompt (name) reference
/// Name of argument. Must be non-null and non-empty.
/// Value of argument. Must be non-null.
/// A token to cancel the operation.
public static Task GetCompletionAsync(this IMcpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
Throw.IfNull(reference);
Throw.IfNullOrWhiteSpace(argumentName);
if (!reference.Validate(out string? validationMessage))
{
throw new ArgumentException($"Invalid reference: {validationMessage}", nameof(reference));
}
return client.SendRequestAsync(
CreateRequest("completion/complete", new()
{
["ref"] = reference,
["argument"] = new Argument { Name = argumentName, Value = argumentValue }
}),
cancellationToken);
}
///
/// Subscribes to a resource on the server.
///
/// The client.
/// The uri of the resource.
/// A token to cancel the operation.
public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
return client.SendRequestAsync(
CreateRequest("resources/subscribe", new() { ["uri"] = uri }),
cancellationToken);
}
///
/// Unsubscribes from a resource on the server.
///
/// The client.
/// The uri of the resource.
/// A token to cancel the operation.
public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
return client.SendRequestAsync(
CreateRequest("resources/unsubscribe", new() { ["uri"] = uri }),
cancellationToken);
}
///
/// Invokes a tool on the server with optional arguments.
///
/// The client.
/// The name of the tool to call.
/// Optional arguments for the tool.
/// A token to cancel the operation.
/// A task containing the tool's response.
public static Task CallToolAsync(this IMcpClient client, string toolName, Dictionary arguments, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
return client.SendRequestAsync(
CreateRequest("tools/call", CreateParametersDictionary(toolName, arguments)),
cancellationToken);
}
/// Gets instances for all of the tools available through the specified .
/// The client for which instances should be created.
/// A token to cancel the operation.
/// A task containing a list of the available functions.
public static async Task> GetAIFunctionsAsync(this IMcpClient client, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
List functions = [];
await foreach (var tool in client.ListToolsAsync(cancellationToken).ConfigureAwait(false))
{
functions.Add(AsAIFunction(client, tool));
}
return functions;
}
/// Gets an for invoking via this .
/// The client with which to perform the invocation.
/// The tool to be invoked.
/// An for performing the call.
///
/// This operation does not validate that is valid for the specified .
/// If the tool is not valid for the client, it will fail when invoked.
///
public static AIFunction AsAIFunction(this IMcpClient client, Tool tool)
{
Throw.IfNull(client);
Throw.IfNull(tool);
return new McpAIFunction(client, tool);
}
///
/// Converts the contents of a into a pair of
/// and instances to use
/// as inputs into a operation.
///
///
/// The created pair of messages and options.
internal static (IList Messages, ChatOptions? Options) ToChatClientArguments(
this 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 messages = [];
foreach (SamplingMessage sm in requestParams.Messages)
{
ChatMessage message = new()
{
Role = sm.Role == Role.User ? ChatRole.User : ChatRole.Assistant,
};
if (sm.Content is { Type: "text" })
{
message.Contents.Add(new TextContent(sm.Content.Text));
}
else if (sm.Content is { Type: "image", MimeType: not null, Data: not null })
{
message.Contents.Add(new DataContent(Convert.FromBase64String(sm.Content.Data), sm.Content.MimeType));
}
else if (sm.Content is { Type: "resource", Resource: not null })
{
ResourceContents resource = sm.Content.Resource;
if (resource.Text is not null)
{
message.Contents.Add(new TextContent(resource.Text));
}
if (resource.Blob is not null && resource.MimeType is not null)
{
message.Contents.Add(new DataContent(Convert.FromBase64String(resource.Blob), resource.MimeType));
}
}
messages.Add(message);
}
return (messages, options);
}
/// Converts the contents of a into a .
/// The whose contents should be extracted.
/// The created .
internal static CreateMessageResult ToCreateMessageResult(this 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();
Content? content = null;
if (lastMessage is not null)
{
foreach (var lmc in lastMessage.Contents)
{
if (lmc is DataContent dc && dc.HasTopLevelMediaType("image"))
{
content = new()
{
Type = "image",
MimeType = dc.MediaType,
Data = Convert.ToBase64String(dc.Data
#if NET
.Span),
#else
.ToArray()),
#endif
};
}
}
}
content ??= new()
{
Text = lastMessage?.Text ?? string.Empty,
Type = "text",
};
return new()
{
Content = content,
Model = chatResponse.ModelId ?? "unknown",
Role = lastMessage?.Role == ChatRole.User ? "user" : "assistant",
StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn",
};
}
///
/// Creates a sampling handler for use with that will
/// satisfy sampling requests using the specified .
///
/// The with which to satisfy sampling requests.
/// The created handler delegate.
public static Func> CreateSamplingHandler(this IChatClient chatClient)
{
Throw.IfNull(chatClient);
return async (requestParams, cancellationToken) =>
{
Throw.IfNull(requestParams);
var (messages, options) = requestParams.ToChatClientArguments();
var response = await chatClient.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false);
return response.ToCreateMessageResult();
};
}
private static JsonRpcRequest CreateRequest(string method, Dictionary? parameters) =>
new JsonRpcRequest
{
Method = method,
Params = parameters
};
private static Dictionary? CreateCursorDictionary(string? cursor) =>
cursor != null ? new() { ["cursor"] = cursor } : null;
private static Dictionary CreateParametersDictionary(string nameParameter, Dictionary? arguments)
{
Dictionary parameters = new()
{
["name"] = nameParameter
};
if (arguments != null)
{
parameters["arguments"] = arguments;
}
return parameters;
}
/// Provides an AI function that calls a tool through .
private sealed class McpAIFunction(IMcpClient client, Tool tool) : AIFunction
{
private JsonElement? _jsonSchema;
///
public override string Name => tool.Name;
///
public override string Description => tool.Description ?? string.Empty;
///
public override JsonElement JsonSchema => _jsonSchema ??=
JsonSerializer.SerializeToElement(new Dictionary
{
["type"] = "object",
["title"] = tool.Name,
["description"] = tool.Description ?? string.Empty,
["properties"] = tool.InputSchema?.Properties ?? [],
["required"] = tool.InputSchema?.Required ?? []
}, JsonSerializerOptionsExtensions.JsonContext.Default.DictionaryStringObject);
///
protected async override Task