< Summary

Information
Class: UIBlazor.Services.Settings.McpSettingsProvider
Assembly: UIBlazor
File(s): /home/runner/work/InvAit/InvAit/UIBlazor/Services/Settings/McpSettingsProvider.cs
Tag: 14_22728831704
Line coverage
55%
Covered lines: 88
Uncovered lines: 70
Coverable lines: 158
Total lines: 302
Line coverage: 55.6%
Branch coverage
45%
Covered branches: 38
Total branches: 84
Branch coverage: 45.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
StopAllAsync()100%210%
ResetAsync()100%11100%
AfterInitAsync()100%210%
LoadMcpFileAsync()78.12%393281.25%
OpenSettingsFileAsync()100%210%
RefreshToolsAsync()11.11%5293627.53%
UpdateServerTools()75%4483.33%
ExtractRequiredArguments(...)50%231257.14%

File(s)

/home/runner/work/InvAit/InvAit/UIBlazor/Services/Settings/McpSettingsProvider.cs

#LineLine coverage
 1using System.Diagnostics;
 2using Shared.Contracts.Mcp;
 3
 4namespace UIBlazor.Services.Settings;
 5
 6public class McpSettingsProvider(
 7    ILocalStorageService storage,
 8    ILogger<McpSettingsProvider> logger,
 9    IVsBridge vsBridge,
 10    HttpClient httpClient)
 411    : BaseSettingsProvider<McpOptions>(storage, logger, "McpSettings"), IMcpSettingsProvider
 12{
 13    private static void Log(string message, string level = "INFO") =>
 14        Debug.WriteLine($"[MCP {level}] {message}");
 15
 16    public async Task StopAllAsync()
 17    {
 018        await vsBridge.ExecuteToolAsync(BasicEnum.McpStopAll);
 019    }
 20
 21    public override async Task ResetAsync()
 22    {
 123        Current.Enabled = true;
 124        Current.Servers = [];
 125        Current.ServerApprovalModes = [];
 126        Current.ServerErrors = [];
 127        Current.ServerEnabledStates = [];
 128        Current.ToolDisabledStates = [];
 129        await SaveAsync();
 130    }
 31
 32    protected override async Task AfterInitAsync()
 33    {
 034        await LoadMcpFileAsync();
 035    }
 36
 37    /// <summary>
 38    /// Load servers from %APPDATA%\Agent\mcp.json via VsBridge
 39    /// </summary>
 40    public async Task LoadMcpFileAsync()
 41    {
 42        try
 43        {
 244            Log("Loading MCP settings from mcp.json");
 245            Current.ServerErrors.Clear();
 246            var result = await vsBridge.ExecuteToolAsync(BasicEnum.ReadMcpSettingsFile);
 47
 48#if DEBUG
 49            result = HeadlessMocker.GetVsToolResult(result);
 50#endif
 51
 252            if (!result.Success || string.IsNullOrEmpty(result.Result))
 53            {
 154                Log(result.Success ? "mcp.json is empty" : $"Failed to read mcp.json: {result.ErrorMessage}", "WARN");
 155                return;
 56            }
 57
 158            var settingsFile = JsonUtils.Deserialize<McpSettingsFile>(result.Result);
 159            if (settingsFile?.McpServers == null)
 60            {
 061                Log("mcp.json has no servers defined", "WARN");
 062                return;
 63            }
 64
 165            var servers = new List<McpServerConfig>();
 466            foreach (var (name, entry) in settingsFile.McpServers)
 67            {
 168                Log($"Loading server: {name}");
 169                var isRemote = !string.IsNullOrEmpty(entry.Url);
 170                var server = new McpServerConfig
 171                {
 172                    Name = name,
 173                    Transport = isRemote ? "http" : "stdio",
 174                    Command = entry.Command ?? string.Empty,
 175                    Args = entry.Args ?? [],
 176                    Url = entry.Url ?? string.Empty,
 177                    Endpoint = entry.Url ?? string.Empty,
 178                    Env = entry.Env ?? [],
 179                    Enabled = true
 180                };
 81
 182                if (server.Tools.Count == 0)
 83                {
 184                    var toolsResult = await RefreshToolsAsync(server);
 185                    if (!toolsResult.StartsWith("Success"))
 86                    {
 087                        Log($"Failed to load tools for server {name}: {toolsResult}", "ERROR");
 088                        Current.ServerErrors[name] = toolsResult;
 089                        server.Enabled = false;
 90                    }
 91                    else
 92                    {
 193                        Log($"Loaded tools for server {name}: {toolsResult}");
 194                        Current.ServerErrors.Remove(name);
 95                    }
 96                }
 97
 98                // Restore tool enabled state from persisted settings
 199                if (server.Tools.Count > 0)
 100                {
 4101                    foreach (var tool in server.Tools)
 102                    {
 1103                        var toolKey = $"{name}:{tool.Name}";
 1104                        tool.Enabled = !Current.ToolDisabledStates.Contains(toolKey);
 105                    }
 106                }
 107
 1108                servers.Add(server);
 1109            }
 110
 1111            Current.Servers = servers;
 1112            Log($"MCP settings loaded: {servers.Count} servers");
 1113            await SaveAsync();
 1114        }
 0115        catch (Exception ex)
 116        {
 0117            Log($"Error loading MCP settings: {ex.Message}", "ERROR");
 0118            Current.ServerErrors["__global__"] = ex.Message;
 0119        }
 2120    }
 121
 122    /// <summary>
 123    /// Open mcp.json
 124    /// </summary>
 125    public async Task OpenSettingsFileAsync()
 126    {
 0127        await vsBridge.ExecuteToolAsync(BasicEnum.OpenMcpSettings);
 0128    }
 129
 130    public async Task<string> RefreshToolsAsync(McpServerConfig server)
 131    {
 132        try
 133        {
 1134            Log($"Refreshing tools for server: {server.Name} ({server.Transport})");
 135
 1136            if (server.Transport == "stdio")
 137            {
 1138                var argsString = string.Join(" ", server.Args);
 1139                var toolArgs = new Dictionary<string, object>
 1140                {
 1141                    { "serverId", server.Name },
 1142                    { "command", server.Command },
 1143                    { "args", argsString },
 1144                    { "env", server.Env }
 1145                };
 146
 1147                Log($"Starting stdio server: {server.Command} {argsString}");
 1148                var result = await vsBridge.ExecuteToolAsync(BasicEnum.McpGetTools, toolArgs);
 149#if DEBUG
 150                result = HeadlessMocker.GetVsToolResult(result);
 151#endif
 1152                if (!result.Success)
 153                {
 0154                    Log($"Failed to get tools from {server.Name}: {result.ErrorMessage}", "ERROR");
 0155                    return $"Error: {result.ErrorMessage}";
 156                }
 157
 1158                var mcpData = JsonUtils.Deserialize<McpResponse>(result.Result);
 1159                if (mcpData?.Result is JsonElement jsonElement)
 160                {
 1161                    var updateResult = await UpdateServerTools(server, jsonElement);
 1162                    Log($"Refresh result for {server.Name}: {updateResult}");
 1163                    return updateResult;
 164                }
 165
 0166                Log($"Could not parse tools list from {server.Name}", "ERROR");
 0167                return "Error: Could not parse tools list";
 168            }
 169            else // http
 170            {
 0171                Log($"Connecting to HTTP MCP server: {server.Url}");
 172                // MCP SSE handshake
 0173                using var request = new HttpRequestMessage(HttpMethod.Get, server.Url);
 0174                using var handshakeResponse = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRe
 0175                if (!handshakeResponse.IsSuccessStatusCode)
 176                {
 0177                    Log($"HTTP server {server.Name} returned status: {handshakeResponse.StatusCode}", "ERROR");
 0178                    return $"Error: HTTP {handshakeResponse.StatusCode}";
 179                }
 0180                var stream = await handshakeResponse.Content.ReadAsStreamAsync();
 0181                using var reader = new StreamReader(stream);
 182
 0183                var postUrl = string.Empty;
 184
 185                while (true)
 186                {
 0187                    var line = await reader.ReadLineAsync();
 0188                    if (line == null) break;
 189
 0190                    if (line.StartsWith("data: ") && !line.Contains("{"))
 191                    {
 0192                        var path = line.Substring(6).Trim();
 0193                        var baseUri = new Uri(server.Url);
 0194                        postUrl = new Uri(baseUri, path).ToString();
 0195                        Log($"HTTP MCP endpoint: {postUrl}");
 196
 0197                        var nextLine = await reader.ReadLineAsync();
 0198                        if (nextLine != null && nextLine.StartsWith("event: endpoint"))
 199                        {
 200                            break;
 201                        }
 202                    }
 203                }
 204
 0205                var mcpRequest = new McpRequest
 0206                {
 0207                    Method = "tools/list",
 0208                    Params = new { },
 0209                    Id = "list_req_" + Guid.NewGuid().ToString("N")
 0210                };
 211
 0212                using var requestMessage = new HttpRequestMessage(HttpMethod.Post, postUrl)
 0213                {
 0214                    Content = new StringContent(JsonUtils.Serialize(mcpRequest), Encoding.UTF8, "application/json")
 0215                };
 216
 0217                requestMessage.Content.Headers.ContentType!.CharSet = null;
 0218                Log($"Requesting tools list from {postUrl}");
 0219                var postResponse = await httpClient.SendAsync(requestMessage);
 220
 0221                if (postResponse.IsSuccessStatusCode)
 222                {
 223                    while (true)
 224                    {
 0225                        var line = await reader.ReadLineAsync();
 0226                        if (line == null) break;
 227
 0228                        if (line.StartsWith("data: "))
 229                        {
 0230                            var jsonData = line.Substring(6).Trim();
 0231                            var mcpData = JsonUtils.Deserialize<McpResponse>(jsonData);
 0232                            if (mcpData?.Id == mcpRequest.Id && mcpData.Result is JsonElement jsonElement)
 233                            {
 0234                                var updateResult = await UpdateServerTools(server, jsonElement);
 0235                                Log($"HTTP refresh result for {server.Name}: {updateResult}");
 0236                                return updateResult;
 237                            }
 238                        }
 239                    }
 240                }
 241
 0242                Log($"Could not parse tools list from HTTP server {server.Name}", "ERROR");
 0243                return "Error: Could not parse tools list";
 244            }
 245        }
 0246        catch (Exception ex)
 247        {
 0248            Log($"Error refreshing tools for {server.Name}: {ex.Message}", "ERROR");
 0249            return $"Error: {ex.Message}";
 250        }
 1251    }
 252
 253    private async Task<string> UpdateServerTools(McpServerConfig server, JsonElement resultElement)
 254    {
 1255        var listResult = resultElement.GetObject<McpListToolsResult>();
 1256        if (listResult == null)
 257        {
 0258            Log($"No tools found in response for {server.Name}", "ERROR");
 0259            return $"Error: Not found tools";
 260        }
 261
 2262        var newTools = listResult.Tools.Select(t => new McpToolConfig
 2263        {
 2264            Name = t.Name,
 2265            Description = t.Description,
 2266            InputSchema = t.InputSchema as JsonElement?,
 2267            RequiredArguments = ExtractRequiredArguments(t.InputSchema)
 2268        }).ToList();
 269
 1270        Log($"Updating {server.Name} with {newTools.Count} tools");
 271
 272        // Merge with existing tools (preserve enabled state)
 4273        foreach (var tool in newTools)
 274        {
 1275            var toolKey = $"{server.Name}:{tool.Name}";
 1276            tool.Enabled = !Current.ToolDisabledStates.Contains(toolKey);
 277        }
 278
 1279        server.Tools = newTools;
 1280        await SaveAsync();
 1281        return $"Success: Found {newTools.Count} tools";
 1282    }
 283
 284    private static List<string> ExtractRequiredArguments(object? rawSchema)
 285    {
 1286        var required = new List<string>();
 1287        if (rawSchema is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Object)
 288        {
 1289            if (jsonElement.TryGetProperty("required", out var requiredProp) && requiredProp.ValueKind == JsonValueKind.
 290            {
 0291                foreach (var item in requiredProp.EnumerateArray())
 292                {
 0293                    if (item.ValueKind == JsonValueKind.String)
 294                    {
 0295                        required.Add(item.GetString()!);
 296                    }
 297                }
 298            }
 299        }
 1300        return required;
 301    }
 302}