using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using AxCopilot.Models; namespace AxCopilot.Services; public class McpClientService : IDisposable { private readonly McpServerEntry _config; private Process? _process; private StreamWriter? _writer; private StreamReader? _reader; private int _requestId; internal bool _initialized; private readonly List _tools = new List(); private static readonly JsonSerializerOptions _jsonOpts = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; public string ServerName => _config.Name; public bool IsConnected { get { Process process = _process; return process != null && !process.HasExited; } } public IReadOnlyList Tools => _tools; public McpClientService(McpServerEntry config) { _config = config; } public async Task ConnectAsync(CancellationToken ct = default(CancellationToken)) { try { if (string.IsNullOrEmpty(_config.Command)) { LogService.Warn("MCP '" + _config.Name + "': command가 비어있습니다."); return false; } ProcessStartInfo psi = new ProcessStartInfo { FileName = _config.Command, UseShellExecute = false, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; foreach (string arg in _config.Args) { psi.ArgumentList.Add(arg); } foreach (var (key, value) in _config.Env) { psi.Environment[key] = value; } _process = Process.Start(psi); if (_process == null) { LogService.Warn("MCP '" + _config.Name + "': 프로세스 시작 실패."); return false; } _writer = _process.StandardInput; _reader = _process.StandardOutput; if (!(await SendRequestAsync("initialize", new { protocolVersion = "2024-11-05", capabilities = new { }, clientInfo = new { name = "AX Copilot", version = "1.7.2" } }, ct)).HasValue) { return false; } await SendNotificationAsync("notifications/initialized", ct); _initialized = true; await RefreshToolsAsync(ct); LogService.Info($"MCP '{_config.Name}': 연결 성공 ({_tools.Count}개 도구)."); return true; } catch (Exception ex) { Exception ex2 = ex; LogService.Warn("MCP '" + _config.Name + "' 연결 실패: " + ex2.Message); return false; } } public async Task RefreshToolsAsync(CancellationToken ct = default(CancellationToken)) { _tools.Clear(); JsonElement? result = await SendRequestAsync("tools/list", new { }, ct); if (!result.HasValue) { return; } try { if (!result.Value.TryGetProperty("tools", out var toolsArr)) { return; } foreach (JsonElement tool in toolsArr.EnumerateArray()) { JsonElement desc; McpToolDefinition def = new McpToolDefinition { Name = (tool.GetProperty("name").GetString() ?? ""), Description = (tool.TryGetProperty("description", out desc) ? (desc.GetString() ?? "") : ""), ServerName = _config.Name }; if (tool.TryGetProperty("inputSchema", out var schema) && schema.TryGetProperty("properties", out var props)) { foreach (JsonProperty prop in props.EnumerateObject()) { def.Parameters[prop.Name] = new McpParameterDef { Type = (prop.Value.TryGetProperty("type", out var t) ? (t.GetString() ?? "string") : "string"), Description = (prop.Value.TryGetProperty("description", out var d) ? (d.GetString() ?? "") : "") }; t = default(JsonElement); d = default(JsonElement); } if (schema.TryGetProperty("required", out var reqArr)) { foreach (JsonElement item in reqArr.EnumerateArray()) { string reqName = item.GetString(); if (reqName != null && def.Parameters.TryGetValue(reqName, out McpParameterDef p)) { p.Required = true; } p = null; } } reqArr = default(JsonElement); } _tools.Add(def); desc = default(JsonElement); schema = default(JsonElement); props = default(JsonElement); } } catch (Exception ex) { LogService.Warn("MCP '" + _config.Name + "' 도구 파싱 실패: " + ex.Message); } } public async Task CallToolAsync(string toolName, Dictionary arguments, CancellationToken ct = default(CancellationToken)) { JsonElement? result = await SendRequestAsync("tools/call", new { name = toolName, arguments = arguments }, ct); if (!result.HasValue) { return "[MCP 호출 실패]"; } try { if (result.Value.TryGetProperty("content", out var contentArr)) { StringBuilder sb = new StringBuilder(); foreach (JsonElement item2 in contentArr.EnumerateArray()) { if (item2.TryGetProperty("text", out var text)) { sb.AppendLine(text.GetString()); } text = default(JsonElement); } return sb.ToString().TrimEnd(); } if (result.Value.TryGetProperty("isError", out var isErr) && isErr.GetBoolean()) { return $"[MCP 오류] {result}"; } } catch { } return result?.ToString() ?? "[MCP 빈 응답]"; } private async Task SendRequestAsync(string method, object parameters, CancellationToken ct) { if (_writer == null || _reader == null) { return null; } int id = Interlocked.Increment(ref _requestId); var request = new { jsonrpc = "2.0", id = id, method = method, @params = parameters }; string json = JsonSerializer.Serialize(request, _jsonOpts); await _writer.WriteLineAsync(json); await _writer.FlushAsync(); using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(TimeSpan.FromSeconds(10.0)); while (!cts.Token.IsCancellationRequested) { string line = await _reader.ReadLineAsync(cts.Token); if (string.IsNullOrWhiteSpace(line)) { continue; } try { JsonDocument doc = JsonDocument.Parse(line); JsonElement root = doc.RootElement; if (root.TryGetProperty("id", out var _)) { if (root.TryGetProperty("result", out var result)) { return result; } if (root.TryGetProperty("error", out var error)) { LogService.Warn(string.Concat(str3: error.TryGetProperty("message", out var m) ? m.GetString() : "Unknown error", str0: "MCP '", str1: _config.Name, str2: "' RPC 오류: ")); return null; } result = default(JsonElement); error = default(JsonElement); } } catch { } } return null; } private async Task SendNotificationAsync(string method, CancellationToken ct) { if (_writer != null) { var notification = new { jsonrpc = "2.0", method = method }; string json = JsonSerializer.Serialize(notification, _jsonOpts); await _writer.WriteLineAsync(json); await _writer.FlushAsync(); } } public void Dispose() { try { _writer?.Dispose(); _reader?.Dispose(); Process process = _process; if (process != null && !process.HasExited) { _process.Kill(entireProcessTree: true); _process.Dispose(); } } catch { } } }