300 lines
7.3 KiB
C#
300 lines
7.3 KiB
C#
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<McpToolDefinition> _tools = new List<McpToolDefinition>();
|
|
|
|
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<McpToolDefinition> Tools => _tools;
|
|
|
|
public McpClientService(McpServerEntry config)
|
|
{
|
|
_config = config;
|
|
}
|
|
|
|
public async Task<bool> 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<string> CallToolAsync(string toolName, Dictionary<string, object> 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<JsonElement?> 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
|
|
{
|
|
}
|
|
}
|
|
}
|