| | | 1 | | using System.Diagnostics; |
| | | 2 | | using Shared.Contracts.Mcp; |
| | | 3 | | |
| | | 4 | | namespace UIBlazor.Services.Settings; |
| | | 5 | | |
| | | 6 | | public class McpSettingsProvider( |
| | | 7 | | ILocalStorageService storage, |
| | | 8 | | ILogger<McpSettingsProvider> logger, |
| | | 9 | | IVsBridge vsBridge, |
| | | 10 | | HttpClient httpClient) |
| | 4 | 11 | | : 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 | | { |
| | 0 | 18 | | await vsBridge.ExecuteToolAsync(BasicEnum.McpStopAll); |
| | 0 | 19 | | } |
| | | 20 | | |
| | | 21 | | public override async Task ResetAsync() |
| | | 22 | | { |
| | 1 | 23 | | Current.Enabled = true; |
| | 1 | 24 | | Current.Servers = []; |
| | 1 | 25 | | Current.ServerApprovalModes = []; |
| | 1 | 26 | | Current.ServerErrors = []; |
| | 1 | 27 | | Current.ServerEnabledStates = []; |
| | 1 | 28 | | Current.ToolDisabledStates = []; |
| | 1 | 29 | | await SaveAsync(); |
| | 1 | 30 | | } |
| | | 31 | | |
| | | 32 | | protected override async Task AfterInitAsync() |
| | | 33 | | { |
| | 0 | 34 | | await LoadMcpFileAsync(); |
| | 0 | 35 | | } |
| | | 36 | | |
| | | 37 | | /// <summary> |
| | | 38 | | /// Load servers from %APPDATA%\Agent\mcp.json via VsBridge |
| | | 39 | | /// </summary> |
| | | 40 | | public async Task LoadMcpFileAsync() |
| | | 41 | | { |
| | | 42 | | try |
| | | 43 | | { |
| | 2 | 44 | | Log("Loading MCP settings from mcp.json"); |
| | 2 | 45 | | Current.ServerErrors.Clear(); |
| | 2 | 46 | | var result = await vsBridge.ExecuteToolAsync(BasicEnum.ReadMcpSettingsFile); |
| | | 47 | | |
| | | 48 | | #if DEBUG |
| | | 49 | | result = HeadlessMocker.GetVsToolResult(result); |
| | | 50 | | #endif |
| | | 51 | | |
| | 2 | 52 | | if (!result.Success || string.IsNullOrEmpty(result.Result)) |
| | | 53 | | { |
| | 1 | 54 | | Log(result.Success ? "mcp.json is empty" : $"Failed to read mcp.json: {result.ErrorMessage}", "WARN"); |
| | 1 | 55 | | return; |
| | | 56 | | } |
| | | 57 | | |
| | 1 | 58 | | var settingsFile = JsonUtils.Deserialize<McpSettingsFile>(result.Result); |
| | 1 | 59 | | if (settingsFile?.McpServers == null) |
| | | 60 | | { |
| | 0 | 61 | | Log("mcp.json has no servers defined", "WARN"); |
| | 0 | 62 | | return; |
| | | 63 | | } |
| | | 64 | | |
| | 1 | 65 | | var servers = new List<McpServerConfig>(); |
| | 4 | 66 | | foreach (var (name, entry) in settingsFile.McpServers) |
| | | 67 | | { |
| | 1 | 68 | | Log($"Loading server: {name}"); |
| | 1 | 69 | | var isRemote = !string.IsNullOrEmpty(entry.Url); |
| | 1 | 70 | | var server = new McpServerConfig |
| | 1 | 71 | | { |
| | 1 | 72 | | Name = name, |
| | 1 | 73 | | Transport = isRemote ? "http" : "stdio", |
| | 1 | 74 | | Command = entry.Command ?? string.Empty, |
| | 1 | 75 | | Args = entry.Args ?? [], |
| | 1 | 76 | | Url = entry.Url ?? string.Empty, |
| | 1 | 77 | | Endpoint = entry.Url ?? string.Empty, |
| | 1 | 78 | | Env = entry.Env ?? [], |
| | 1 | 79 | | Enabled = true |
| | 1 | 80 | | }; |
| | | 81 | | |
| | 1 | 82 | | if (server.Tools.Count == 0) |
| | | 83 | | { |
| | 1 | 84 | | var toolsResult = await RefreshToolsAsync(server); |
| | 1 | 85 | | if (!toolsResult.StartsWith("Success")) |
| | | 86 | | { |
| | 0 | 87 | | Log($"Failed to load tools for server {name}: {toolsResult}", "ERROR"); |
| | 0 | 88 | | Current.ServerErrors[name] = toolsResult; |
| | 0 | 89 | | server.Enabled = false; |
| | | 90 | | } |
| | | 91 | | else |
| | | 92 | | { |
| | 1 | 93 | | Log($"Loaded tools for server {name}: {toolsResult}"); |
| | 1 | 94 | | Current.ServerErrors.Remove(name); |
| | | 95 | | } |
| | | 96 | | } |
| | | 97 | | |
| | | 98 | | // Restore tool enabled state from persisted settings |
| | 1 | 99 | | if (server.Tools.Count > 0) |
| | | 100 | | { |
| | 4 | 101 | | foreach (var tool in server.Tools) |
| | | 102 | | { |
| | 1 | 103 | | var toolKey = $"{name}:{tool.Name}"; |
| | 1 | 104 | | tool.Enabled = !Current.ToolDisabledStates.Contains(toolKey); |
| | | 105 | | } |
| | | 106 | | } |
| | | 107 | | |
| | 1 | 108 | | servers.Add(server); |
| | 1 | 109 | | } |
| | | 110 | | |
| | 1 | 111 | | Current.Servers = servers; |
| | 1 | 112 | | Log($"MCP settings loaded: {servers.Count} servers"); |
| | 1 | 113 | | await SaveAsync(); |
| | 1 | 114 | | } |
| | 0 | 115 | | catch (Exception ex) |
| | | 116 | | { |
| | 0 | 117 | | Log($"Error loading MCP settings: {ex.Message}", "ERROR"); |
| | 0 | 118 | | Current.ServerErrors["__global__"] = ex.Message; |
| | 0 | 119 | | } |
| | 2 | 120 | | } |
| | | 121 | | |
| | | 122 | | /// <summary> |
| | | 123 | | /// Open mcp.json |
| | | 124 | | /// </summary> |
| | | 125 | | public async Task OpenSettingsFileAsync() |
| | | 126 | | { |
| | 0 | 127 | | await vsBridge.ExecuteToolAsync(BasicEnum.OpenMcpSettings); |
| | 0 | 128 | | } |
| | | 129 | | |
| | | 130 | | public async Task<string> RefreshToolsAsync(McpServerConfig server) |
| | | 131 | | { |
| | | 132 | | try |
| | | 133 | | { |
| | 1 | 134 | | Log($"Refreshing tools for server: {server.Name} ({server.Transport})"); |
| | | 135 | | |
| | 1 | 136 | | if (server.Transport == "stdio") |
| | | 137 | | { |
| | 1 | 138 | | var argsString = string.Join(" ", server.Args); |
| | 1 | 139 | | var toolArgs = new Dictionary<string, object> |
| | 1 | 140 | | { |
| | 1 | 141 | | { "serverId", server.Name }, |
| | 1 | 142 | | { "command", server.Command }, |
| | 1 | 143 | | { "args", argsString }, |
| | 1 | 144 | | { "env", server.Env } |
| | 1 | 145 | | }; |
| | | 146 | | |
| | 1 | 147 | | Log($"Starting stdio server: {server.Command} {argsString}"); |
| | 1 | 148 | | var result = await vsBridge.ExecuteToolAsync(BasicEnum.McpGetTools, toolArgs); |
| | | 149 | | #if DEBUG |
| | | 150 | | result = HeadlessMocker.GetVsToolResult(result); |
| | | 151 | | #endif |
| | 1 | 152 | | if (!result.Success) |
| | | 153 | | { |
| | 0 | 154 | | Log($"Failed to get tools from {server.Name}: {result.ErrorMessage}", "ERROR"); |
| | 0 | 155 | | return $"Error: {result.ErrorMessage}"; |
| | | 156 | | } |
| | | 157 | | |
| | 1 | 158 | | var mcpData = JsonUtils.Deserialize<McpResponse>(result.Result); |
| | 1 | 159 | | if (mcpData?.Result is JsonElement jsonElement) |
| | | 160 | | { |
| | 1 | 161 | | var updateResult = await UpdateServerTools(server, jsonElement); |
| | 1 | 162 | | Log($"Refresh result for {server.Name}: {updateResult}"); |
| | 1 | 163 | | return updateResult; |
| | | 164 | | } |
| | | 165 | | |
| | 0 | 166 | | Log($"Could not parse tools list from {server.Name}", "ERROR"); |
| | 0 | 167 | | return "Error: Could not parse tools list"; |
| | | 168 | | } |
| | | 169 | | else // http |
| | | 170 | | { |
| | 0 | 171 | | Log($"Connecting to HTTP MCP server: {server.Url}"); |
| | | 172 | | // MCP SSE handshake |
| | 0 | 173 | | using var request = new HttpRequestMessage(HttpMethod.Get, server.Url); |
| | 0 | 174 | | using var handshakeResponse = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRe |
| | 0 | 175 | | if (!handshakeResponse.IsSuccessStatusCode) |
| | | 176 | | { |
| | 0 | 177 | | Log($"HTTP server {server.Name} returned status: {handshakeResponse.StatusCode}", "ERROR"); |
| | 0 | 178 | | return $"Error: HTTP {handshakeResponse.StatusCode}"; |
| | | 179 | | } |
| | 0 | 180 | | var stream = await handshakeResponse.Content.ReadAsStreamAsync(); |
| | 0 | 181 | | using var reader = new StreamReader(stream); |
| | | 182 | | |
| | 0 | 183 | | var postUrl = string.Empty; |
| | | 184 | | |
| | | 185 | | while (true) |
| | | 186 | | { |
| | 0 | 187 | | var line = await reader.ReadLineAsync(); |
| | 0 | 188 | | if (line == null) break; |
| | | 189 | | |
| | 0 | 190 | | if (line.StartsWith("data: ") && !line.Contains("{")) |
| | | 191 | | { |
| | 0 | 192 | | var path = line.Substring(6).Trim(); |
| | 0 | 193 | | var baseUri = new Uri(server.Url); |
| | 0 | 194 | | postUrl = new Uri(baseUri, path).ToString(); |
| | 0 | 195 | | Log($"HTTP MCP endpoint: {postUrl}"); |
| | | 196 | | |
| | 0 | 197 | | var nextLine = await reader.ReadLineAsync(); |
| | 0 | 198 | | if (nextLine != null && nextLine.StartsWith("event: endpoint")) |
| | | 199 | | { |
| | | 200 | | break; |
| | | 201 | | } |
| | | 202 | | } |
| | | 203 | | } |
| | | 204 | | |
| | 0 | 205 | | var mcpRequest = new McpRequest |
| | 0 | 206 | | { |
| | 0 | 207 | | Method = "tools/list", |
| | 0 | 208 | | Params = new { }, |
| | 0 | 209 | | Id = "list_req_" + Guid.NewGuid().ToString("N") |
| | 0 | 210 | | }; |
| | | 211 | | |
| | 0 | 212 | | using var requestMessage = new HttpRequestMessage(HttpMethod.Post, postUrl) |
| | 0 | 213 | | { |
| | 0 | 214 | | Content = new StringContent(JsonUtils.Serialize(mcpRequest), Encoding.UTF8, "application/json") |
| | 0 | 215 | | }; |
| | | 216 | | |
| | 0 | 217 | | requestMessage.Content.Headers.ContentType!.CharSet = null; |
| | 0 | 218 | | Log($"Requesting tools list from {postUrl}"); |
| | 0 | 219 | | var postResponse = await httpClient.SendAsync(requestMessage); |
| | | 220 | | |
| | 0 | 221 | | if (postResponse.IsSuccessStatusCode) |
| | | 222 | | { |
| | | 223 | | while (true) |
| | | 224 | | { |
| | 0 | 225 | | var line = await reader.ReadLineAsync(); |
| | 0 | 226 | | if (line == null) break; |
| | | 227 | | |
| | 0 | 228 | | if (line.StartsWith("data: ")) |
| | | 229 | | { |
| | 0 | 230 | | var jsonData = line.Substring(6).Trim(); |
| | 0 | 231 | | var mcpData = JsonUtils.Deserialize<McpResponse>(jsonData); |
| | 0 | 232 | | if (mcpData?.Id == mcpRequest.Id && mcpData.Result is JsonElement jsonElement) |
| | | 233 | | { |
| | 0 | 234 | | var updateResult = await UpdateServerTools(server, jsonElement); |
| | 0 | 235 | | Log($"HTTP refresh result for {server.Name}: {updateResult}"); |
| | 0 | 236 | | return updateResult; |
| | | 237 | | } |
| | | 238 | | } |
| | | 239 | | } |
| | | 240 | | } |
| | | 241 | | |
| | 0 | 242 | | Log($"Could not parse tools list from HTTP server {server.Name}", "ERROR"); |
| | 0 | 243 | | return "Error: Could not parse tools list"; |
| | | 244 | | } |
| | | 245 | | } |
| | 0 | 246 | | catch (Exception ex) |
| | | 247 | | { |
| | 0 | 248 | | Log($"Error refreshing tools for {server.Name}: {ex.Message}", "ERROR"); |
| | 0 | 249 | | return $"Error: {ex.Message}"; |
| | | 250 | | } |
| | 1 | 251 | | } |
| | | 252 | | |
| | | 253 | | private async Task<string> UpdateServerTools(McpServerConfig server, JsonElement resultElement) |
| | | 254 | | { |
| | 1 | 255 | | var listResult = resultElement.GetObject<McpListToolsResult>(); |
| | 1 | 256 | | if (listResult == null) |
| | | 257 | | { |
| | 0 | 258 | | Log($"No tools found in response for {server.Name}", "ERROR"); |
| | 0 | 259 | | return $"Error: Not found tools"; |
| | | 260 | | } |
| | | 261 | | |
| | 2 | 262 | | var newTools = listResult.Tools.Select(t => new McpToolConfig |
| | 2 | 263 | | { |
| | 2 | 264 | | Name = t.Name, |
| | 2 | 265 | | Description = t.Description, |
| | 2 | 266 | | InputSchema = t.InputSchema as JsonElement?, |
| | 2 | 267 | | RequiredArguments = ExtractRequiredArguments(t.InputSchema) |
| | 2 | 268 | | }).ToList(); |
| | | 269 | | |
| | 1 | 270 | | Log($"Updating {server.Name} with {newTools.Count} tools"); |
| | | 271 | | |
| | | 272 | | // Merge with existing tools (preserve enabled state) |
| | 4 | 273 | | foreach (var tool in newTools) |
| | | 274 | | { |
| | 1 | 275 | | var toolKey = $"{server.Name}:{tool.Name}"; |
| | 1 | 276 | | tool.Enabled = !Current.ToolDisabledStates.Contains(toolKey); |
| | | 277 | | } |
| | | 278 | | |
| | 1 | 279 | | server.Tools = newTools; |
| | 1 | 280 | | await SaveAsync(); |
| | 1 | 281 | | return $"Success: Found {newTools.Count} tools"; |
| | 1 | 282 | | } |
| | | 283 | | |
| | | 284 | | private static List<string> ExtractRequiredArguments(object? rawSchema) |
| | | 285 | | { |
| | 1 | 286 | | var required = new List<string>(); |
| | 1 | 287 | | if (rawSchema is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Object) |
| | | 288 | | { |
| | 1 | 289 | | if (jsonElement.TryGetProperty("required", out var requiredProp) && requiredProp.ValueKind == JsonValueKind. |
| | | 290 | | { |
| | 0 | 291 | | foreach (var item in requiredProp.EnumerateArray()) |
| | | 292 | | { |
| | 0 | 293 | | if (item.ValueKind == JsonValueKind.String) |
| | | 294 | | { |
| | 0 | 295 | | required.Add(item.GetString()!); |
| | | 296 | | } |
| | | 297 | | } |
| | | 298 | | } |
| | | 299 | | } |
| | 1 | 300 | | return required; |
| | | 301 | | } |
| | | 302 | | } |