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 InvokeCoreAsync( IEnumerable> arguments, CancellationToken cancellationToken) { Throw.IfNull(arguments); Dictionary argDict = []; foreach (var arg in arguments) { if (arg.Value is not null) { argDict[arg.Key] = arg.Value; } } CallToolResponse result = await client.CallToolAsync(tool.Name, argDict, cancellationToken).ConfigureAwait(false); return JsonSerializer.SerializeToElement(result, JsonSerializerOptionsExtensions.JsonContext.Default.CallToolResponse); } } }