< Summary

Information
Class: UIBlazor.Services.Settings.McpSettingsProvider
Assembly: UIBlazor
File(s): /home/runner/work/InvAit/InvAit/UIBlazor/Services/Settings/McpSettingsProvider.cs
Tag: 71_26091983037
Line coverage
57%
Covered lines: 93
Uncovered lines: 70
Coverable lines: 163
Total lines: 300
Line coverage: 57%
Branch coverage
45%
Covered branches: 36
Total branches: 80
Branch coverage: 45%
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()75%262484.21%
InitToolsAsync()75%5466.66%
OpenSettingsFileAsync()100%210%
RefreshToolsAsync()16.66%4283632.87%
UpdateServerToolsAsync()75%4483.33%
ExtractRequiredArguments(...)50%251255.55%

File(s)

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

#LineLine coverage
 1using Shared.Contracts.Mcp;
 2
 3namespace UIBlazor.Services.Settings;
 4
 5public class McpSettingsProvider(
 6    ILocalStorageService storage,
 7    ILogger<McpSettingsProvider> logger,
 8    IVsBridge vsBridge,
 9    HttpClient httpClient)
 410    : BaseSettingsProvider<McpOptions>(storage, logger, "McpSettings"), IMcpSettingsProvider
 11{
 12    public async Task StopAllAsync()
 13    {
 014        await vsBridge.ExecuteToolAsync(BasicEnum.McpStopAll);
 015    }
 16
 17    public override async Task ResetAsync()
 18    {
 119        Current.Enabled = true;
 120        Current.Servers = [];
 121        Current.ServerApprovalModes = [];
 122        Current.ServerErrors = [];
 123        Current.ServerEnabledStates = [];
 124        Current.ToolDisabledStates = [];
 125        await SaveAsync();
 126    }
 27
 28    protected override async Task AfterInitAsync()
 29    {
 030        _ = LoadMcpFileAsync();
 031    }
 32
 33    /// <summary>
 34    /// Load servers from %APPDATA%\Agent\mcp.json via VsBridge
 35    /// </summary>
 36    public async Task LoadMcpFileAsync()
 37    {
 38        try
 39        {
 240            logger.LogInformation("Loading MCP settings from mcp.json");
 241            Current.ServerErrors.Clear();
 242            var result = await vsBridge.ExecuteToolAsync(BasicEnum.ReadMcpSettingsFile);
 43
 44#if DEBUG
 45            result = HeadlessMocker.GetVsToolResult(result);
 46#endif
 47
 248            if (!result.Success || string.IsNullOrEmpty(result.Result))
 49            {
 150                logger.LogWarning(result.Success ? "mcp.json is empty" : $"Failed to read mcp.json: {result.ErrorMessage
 151                return;
 52            }
 53
 154            var settingsFile = JsonUtils.Deserialize<McpSettingsFile>(result.Result);
 155            if (settingsFile?.McpServers == null)
 56            {
 057                logger.LogWarning("mcp.json has no servers defined");
 058                return;
 59            }
 60
 161            var servers = new List<McpServerConfig>();
 162            var initServerTasks = new List<Task>();
 463            foreach (var (name, entry) in settingsFile.McpServers)
 64            {
 165                logger.LogInformation($"Loading server: {name}");
 166                var isRemote = !string.IsNullOrEmpty(entry.Url);
 167                var server = new McpServerConfig
 168                {
 169                    Name = name,
 170                    Transport = isRemote ? "http" : "stdio",
 171                    Command = entry.Command ?? string.Empty,
 172                    Args = entry.Args ?? [],
 173                    Url = entry.Url ?? string.Empty,
 174                    Endpoint = entry.Url ?? string.Empty,
 175                    Env = entry.Env ?? [],
 176                    Enabled = true
 177                };
 78
 179                servers.Add(server);
 180                initServerTasks.Add(InitToolsAsync(server));
 81            }
 82
 183            await Task.WhenAll(initServerTasks);
 184            Current.Servers = servers;
 185            logger.LogInformation($"MCP settings loaded: {servers.Count} servers");
 186            await SaveAsync();
 187        }
 088        catch (Exception ex)
 89        {
 090            logger.LogError($"Error loading MCP settings: {ex.Message}");
 091            Current.ServerErrors["__global__"] = ex.Message;
 092        }
 293    }
 94
 95    private async Task InitToolsAsync(McpServerConfig server)
 96    {
 197        if (server.Tools.Count == 0)
 98        {
 199            var toolsResult = await RefreshToolsAsync(server);
 1100            if (!toolsResult.StartsWith("Success"))
 101            {
 0102                logger.LogError($"Failed to load tools for server {server.Name}: {toolsResult}");
 0103                Current.ServerErrors[server.Name] = toolsResult;
 0104                server.Enabled = false;
 105            }
 106            else
 107            {
 1108                logger.LogInformation($"Loaded tools for server {server.Name}: {toolsResult}");
 1109                Current.ServerErrors.Remove(server.Name);
 110            }
 111        }
 1112    }
 113
 114    /// <summary>
 115    /// Открыть mcp.json в редакторе VS
 116    /// </summary>
 117    public async Task OpenSettingsFileAsync()
 118    {
 0119        await vsBridge.ExecuteToolAsync(BasicEnum.OpenMcpSettings);
 0120    }
 121
 122    public async Task<string> RefreshToolsAsync(McpServerConfig server)
 123    {
 1124        var updateResult = $"Error refreshing tools for {server.Name}.";
 125        try
 126        {
 1127            logger.LogInformation($"Refreshing tools for server: {server.Name} ({server.Transport})");
 128
 1129            if (server.Transport == "stdio")
 130            {
 1131                var argsString = string.Join(" ", server.Args);
 1132                var toolArgs = new Dictionary<string, object>
 1133                {
 1134                    { "serverId", server.Name },
 1135                    { "command", server.Command },
 1136                    { "args", argsString },
 1137                    { "env", server.Env }
 1138                };
 139
 1140                logger.LogInformation($"Starting stdio server: {server.Command} {argsString}");
 1141                var result = await vsBridge.ExecuteToolAsync(BasicEnum.McpGetTools, toolArgs);
 142#if DEBUG
 143                result = HeadlessMocker.GetVsToolResult(result);
 144#endif
 1145                if (!result.Success)
 146                {
 0147                    logger.LogError($"Failed to get tools from {server.Name}: {result.ErrorMessage}");
 0148                    return $"Error: {result.ErrorMessage}";
 149                }
 150
 1151                var mcpData = JsonUtils.Deserialize<JsonElement>(result.Result);
 1152                updateResult = await UpdateServerToolsAsync(server, mcpData);
 1153                logger.LogInformation($"Refresh result for {server.Name}: {updateResult}");
 154            }
 155            else // http sse
 156            {
 0157                logger.LogInformation($"Connecting to HTTP MCP server: {server.Url}");
 158                // MCP SSE handshake
 0159                using var request = new HttpRequestMessage(HttpMethod.Get, server.Url);
 0160                using var handshakeResponse = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRe
 0161                if (!handshakeResponse.IsSuccessStatusCode)
 162                {
 0163                    logger.LogError($"HTTP server {server.Name} returned status: {handshakeResponse.StatusCode}");
 0164                    return $"Error: HTTP {handshakeResponse.StatusCode}";
 165                }
 0166                var stream = await handshakeResponse.Content.ReadAsStreamAsync();
 0167                using var reader = new StreamReader(stream);
 168
 0169                var postUrl = string.Empty;
 170
 171                while (true)
 172                {
 0173                    var line = await reader.ReadLineAsync();
 0174                    if (line == null) break;
 175
 0176                    if (line.StartsWith("data: ") && !line.Contains('{'))
 177                    {
 0178                        var path = line[6..].Trim();
 0179                        var baseUri = new Uri(server.Url);
 0180                        postUrl = new Uri(baseUri, path).ToString();
 0181                        logger.LogInformation($"HTTP MCP endpoint: {postUrl}");
 182
 0183                        var nextLine = await reader.ReadLineAsync();
 0184                        if (nextLine != null && nextLine.StartsWith("event: endpoint"))
 185                        {
 186                            break;
 187                        }
 188                    }
 189                }
 190
 0191                var mcpRequest = new McpRequest
 0192                {
 0193                    Method = "tools/list",
 0194                    Params = new { },
 0195                    Id = "list_req_" + Guid.NewGuid().ToString("N")
 0196                };
 197
 0198                using var requestMessage = new HttpRequestMessage(HttpMethod.Post, postUrl)
 0199                {
 0200                    Content = new StringContent(JsonUtils.Serialize(mcpRequest), Encoding.UTF8, "application/json")
 0201                };
 202
 0203                requestMessage.Content.Headers.ContentType!.CharSet = null;
 0204                logger.LogInformation($"Requesting tools list from {postUrl}");
 0205                var postResponse = await httpClient.SendAsync(requestMessage);
 206
 0207                if (!postResponse.IsSuccessStatusCode)
 208                {
 0209                    logger.LogError($"HTTP server {server.Name} returned code: {postResponse.StatusCode}");
 0210                    return $"{updateResult} {postResponse.StatusCode}";
 211                }
 212
 213                while (true)
 214                {
 0215                    var line = await reader.ReadLineAsync();
 0216                    if (line == null)
 217                        break;
 218
 0219                    if (line.StartsWith("data: "))
 220                    {
 0221                        var jsonData = line[6..].Trim();
 0222                        var mcpData = JsonUtils.Deserialize<McpResponse>(jsonData);
 0223                        if (mcpData?.Id == mcpRequest.Id && mcpData.Result is JsonElement jsonElement)
 224                        {
 0225                            updateResult = await UpdateServerToolsAsync(server, jsonElement);
 0226                            logger.LogInformation($"HTTP refresh result for {server.Name}: {updateResult}");
 0227                            break;
 228                        }
 229                    }
 230                }
 0231            }
 1232        }
 0233        catch (Exception ex)
 234        {
 0235            logger.LogError($"Error refreshing tools for {server.Name}: {ex.Message}");
 0236            return $"Error: {ex.Message}";
 237        }
 238
 239        // Restore tool enabled state from persisted settings
 1240        if (server.Tools.Count > 0)
 241        {
 4242            foreach (var tool in server.Tools)
 243            {
 1244                var toolKey = $"{server.Name}:{tool.Name}";
 1245                tool.Enabled = !Current.ToolDisabledStates.Contains(toolKey);
 246            }
 247        }
 248
 1249        return updateResult;
 1250    }
 251
 252    private async Task<string> UpdateServerToolsAsync(McpServerConfig server, JsonElement resultElement)
 253    {
 1254        var listResult = resultElement.GetObject<McpListToolsResult>();
 1255        if (listResult == null)
 256        {
 0257            logger.LogError($"No tools found in response for {server.Name}");
 0258            return "Error: Not found tools";
 259        }
 260
 2261        var newTools = listResult.Tools.Select(t => new McpToolConfig
 2262        {
 2263            Name = t.Name,
 2264            Description = t.Description,
 2265            InputSchema = t.InputSchema as JsonElement?,
 2266            RequiredArguments = ExtractRequiredArguments(t.InputSchema)
 2267        }).ToList();
 268
 1269        logger.LogInformation($"Updating {server.Name} with {newTools.Count} tools");
 270
 271        // Merge with existing tools (preserve enabled state)
 4272        foreach (var tool in newTools)
 273        {
 1274            var toolKey = $"{server.Name}:{tool.Name}";
 1275            tool.Enabled = !Current.ToolDisabledStates.Contains(toolKey);
 276        }
 277
 1278        server.Tools = newTools;
 1279        await SaveAsync();
 1280        return $"Success: Found {newTools.Count} tools";
 1281    }
 282
 283    private static List<string> ExtractRequiredArguments(object? rawSchema)
 284    {
 1285        var required = new List<string>();
 1286        if (rawSchema is not JsonElement { ValueKind: JsonValueKind.Object } jsonElement ||
 1287            !jsonElement.TryGetProperty("required", out var requiredProp) ||
 1288            requiredProp.ValueKind != JsonValueKind.Array)
 1289            return required;
 290
 0291        foreach (var item in requiredProp.EnumerateArray())
 292        {
 0293            if (item.ValueKind == JsonValueKind.String)
 294            {
 0295                required.Add(item.GetString()!);
 296            }
 297        }
 0298        return required;
 299    }
 300}