using System.Text; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using AIStudio.Tools.PluginSystem; namespace AIStudio.Tools.MCPClient; /// /// A thin wrapper around the MCP client SDK for talking to a locally configured /// MCP server that exposes an image-generation tool over Streamable HTTP. /// public static class MCPImageToolClient { // Local image generation can take a lot longer than typical API calls, so we use a // generous, dedicated timeout here instead of the app-wide HTTP client timeout. private static readonly TimeSpan TOOL_CALL_TIMEOUT = TimeSpan.FromMinutes(10); private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(MCPImageToolClient)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MCPImageToolClient).Namespace, nameof(MCPImageToolClient)); /// /// Connects to the given MCP server and lists the tools it offers. /// public static async Task>> ListToolsAsync(string serverUrl, string bearerToken, CancellationToken token = default) { try { await using var client = await CreateClientAsync(serverUrl, bearerToken, token); var tools = await client.ListToolsAsync(cancellationToken: token); return new() { Successful = true, Data = [..tools], }; } catch (TaskCanceledException e) { LOGGER.LogWarning(e, "Failed to connect to the MCP server '{ServerUrl}': the request was canceled or timed out.", serverUrl); return new() { Successful = false, Message = TB("Failed to connect to the MCP server: the request was canceled either by the user or due to a timeout."), }; } catch (Exception e) { LOGGER.LogError(e, "Failed to connect to the MCP server '{ServerUrl}'.", serverUrl); return new() { Successful = false, Message = string.Format(TB("Failed to connect to the MCP server due to an exception: {0}"), e.Message), }; } } /// /// Calls the given tool on the configured MCP server with a text prompt and extracts the returned image. /// public static async Task> CallImageToolAsync(string serverUrl, string bearerToken, string toolName, string prompt, CancellationToken token = default) { try { await using var client = await CreateClientAsync(serverUrl, bearerToken, token); var result = await client.CallToolAsync(toolName, new Dictionary { ["prompt"] = prompt, }, cancellationToken: token); if (result.IsError == true) { var errorText = result.Content.OfType().FirstOrDefault()?.Text; LOGGER.LogWarning("The MCP tool '{ToolName}' on server '{ServerUrl}' reported an error: {ErrorText}", toolName, serverUrl, errorText); return new() { Successful = false, Message = string.IsNullOrWhiteSpace(errorText) ? TB("The MCP tool reported an error without further details.") : errorText, }; } var imageBlock = result.Content.OfType().FirstOrDefault(); if (imageBlock is null) { LOGGER.LogWarning("The MCP tool '{ToolName}' on server '{ServerUrl}' did not return an image content block. Returned content types: {ContentTypes}", toolName, serverUrl, string.Join(", ", result.Content.Select(c => c.GetType().Name))); return new() { Successful = false, Message = TB("The MCP tool did not return an image."), }; } return new() { Successful = true, Data = new MCPGeneratedImage(Encoding.UTF8.GetString(imageBlock.Data.Span), imageBlock.MimeType), }; } catch (TaskCanceledException e) { LOGGER.LogWarning(e, "Failed to call the MCP tool '{ToolName}' on server '{ServerUrl}': the request was canceled or timed out.", toolName, serverUrl); return new() { Successful = false, Message = TB("Failed to generate the image: the request was canceled either by the user or due to a timeout."), }; } catch (Exception e) { LOGGER.LogError(e, "Failed to call the MCP tool '{ToolName}' on server '{ServerUrl}'.", toolName, serverUrl); return new() { Successful = false, Message = string.Format(TB("Failed to generate the image due to an exception: {0}"), e.Message), }; } } private static async Task CreateClientAsync(string serverUrl, string bearerToken, CancellationToken token) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(bearerToken)) headers["Authorization"] = $"Bearer {bearerToken}"; var httpClient = ExternalHttpClientTimeout.CreateHttpClient(ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED); httpClient.Timeout = TOOL_CALL_TIMEOUT; var transport = new HttpClientTransport(new HttpClientTransportOptions { Endpoint = new Uri(serverUrl), TransportMode = HttpTransportMode.StreamableHttp, AdditionalHeaders = headers, ConnectionTimeout = TOOL_CALL_TIMEOUT, }, httpClient, ownsHttpClient: true); return await McpClient.CreateAsync(transport, cancellationToken: token); } }