AX Agent MCP 명령 확장 및 에이전트 복구/슬래시 UX 보강
- /mcp 하위 명령 확장: add/remove/reset 지원, 도움말/상태 문구 동기화 - add 파서 추가: stdio(command+args), sse(url) 형식 검증 및 중복 서버명 방지 - remove all/단건 및 reset(세션 MCP 오버라이드 초기화) 실행 경로 구현 - Agentic loop 복구 프롬프트 강화: 미등록/비허용 도구 상황에서 tool_search 우선 가이드 적용 - 반복 실패 중단 응답에 재시도 루트 명시로 루프 복구 가능성 개선 - 슬래시 팝업 힌트 밀도 개선: agentUiExpressionLevel(rich/balanced/simple) 연동 - 테스트 보강: ChatWindowSlashPolicyTests(/mcp add/remove/reset, add 파서, 토크나이저), AgentLoopCodeQualityTests(tool_search 복구 가이드) - 문서 반영: docs/DEVELOPMENT.md, docs/AGENT_ROADMAP.md에 2026-04-04 추가 진행 이력 기록
This commit is contained in:
@@ -4901,9 +4901,15 @@ public partial class ChatWindow : Window
|
||||
var end = Math.Min(start + SlashPageSize, total);
|
||||
var totalSkills = _slashAllMatches.Count(x => x.IsSkill);
|
||||
var totalCommands = total - totalSkills;
|
||||
var expressionLevel = (_settings.Settings.Llm.AgentUiExpressionLevel ?? "balanced").Trim().ToLowerInvariant();
|
||||
|
||||
SlashPopupTitle.Text = "명령 팔레트";
|
||||
SlashPopupHint.Text = $"명령 {totalCommands}개 · 스킬 {totalSkills}개 · /compact 지원";
|
||||
SlashPopupHint.Text = expressionLevel switch
|
||||
{
|
||||
"simple" => $"명령 {totalCommands} · 스킬 {totalSkills}",
|
||||
"rich" => $"명령 {totalCommands}개 · 스킬 {totalSkills}개 · 추천: /compact /mcp /chrome",
|
||||
_ => $"명령 {totalCommands}개 · 스킬 {totalSkills}개",
|
||||
};
|
||||
|
||||
// 위 화살표
|
||||
if (start > 0)
|
||||
@@ -5523,7 +5529,7 @@ public partial class ChatWindow : Window
|
||||
("/doctor", "프로젝트/환경 점검 체크를 수행합니다."));
|
||||
|
||||
AddHelpSection(contentPanel, "연결/확장 명령어", "환경 연결, 플러그인, 에이전트 관련", fg, fg2, accent, itemBg, hoverBg,
|
||||
("/mcp", "외부 도구 연결 상태 점검"),
|
||||
("/mcp", "외부 도구 연결 상태 점검 및 add/remove/reset 관리"),
|
||||
("/agents", "에이전트 분담 전략 제시"),
|
||||
("/plugin", "플러그인 구성 점검"),
|
||||
("/reload-plugins", "플러그인 재로드 점검"),
|
||||
@@ -6177,11 +6183,106 @@ public partial class ChatWindow : Window
|
||||
"enable" => ("enable", target),
|
||||
"disable" => ("disable", target),
|
||||
"reconnect" => ("reconnect", target),
|
||||
"add" => ("add", target),
|
||||
"remove" => ("remove", target),
|
||||
"reset" => ("reset", target),
|
||||
"status" => ("status", target),
|
||||
_ => ("help", text),
|
||||
};
|
||||
}
|
||||
|
||||
internal static (bool success, McpServerEntry? entry, string error) ParseMcpAddTarget(string target)
|
||||
{
|
||||
var raw = (target ?? "").Trim();
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
return (false, null, "사용법: /mcp add <서버명> :: stdio <명령> [인자...] | /mcp add <서버명> :: sse <URL>");
|
||||
|
||||
var sep = raw.IndexOf("::", StringComparison.Ordinal);
|
||||
if (sep < 0)
|
||||
return (false, null, "추가 형식이 올바르지 않습니다. 구분자 `::` 를 사용하세요.\n예: /mcp add chrome :: stdio node server.js");
|
||||
|
||||
var name = raw[..sep].Trim();
|
||||
var spec = raw[(sep + 2)..].Trim();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return (false, null, "서버명이 비어 있습니다. /mcp add <서버명> :: ... 형식으로 입력하세요.");
|
||||
if (string.IsNullOrWhiteSpace(spec))
|
||||
return (false, null, "연결 정보가 비어 있습니다. stdio 또는 sse 설정을 입력하세요.");
|
||||
|
||||
var tokens = TokenizeCommand(spec);
|
||||
if (tokens.Count < 2)
|
||||
return (false, null, "연결 정보가 부족합니다. 예: stdio node server.js 또는 sse https://host/sse");
|
||||
|
||||
var transport = tokens[0].Trim().ToLowerInvariant();
|
||||
if (transport == "stdio")
|
||||
{
|
||||
var command = tokens[1].Trim();
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
return (false, null, "stdio 방식은 실행 명령(command)이 필요합니다.");
|
||||
|
||||
var args = tokens.Count > 2 ? tokens.Skip(2).ToList() : new List<string>();
|
||||
return (true, new McpServerEntry
|
||||
{
|
||||
Name = name,
|
||||
Transport = "stdio",
|
||||
Command = command,
|
||||
Args = args,
|
||||
Enabled = true,
|
||||
}, "");
|
||||
}
|
||||
|
||||
if (transport == "sse")
|
||||
{
|
||||
var url = string.Join(" ", tokens.Skip(1)).Trim();
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var parsed))
|
||||
return (false, null, $"유효하지 않은 SSE URL 입니다: {url}");
|
||||
if (!string.Equals(parsed.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(parsed.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
||||
return (false, null, "SSE URL은 http 또는 https 스킴만 지원합니다.");
|
||||
|
||||
return (true, new McpServerEntry
|
||||
{
|
||||
Name = name,
|
||||
Transport = "sse",
|
||||
Url = url,
|
||||
Enabled = true,
|
||||
}, "");
|
||||
}
|
||||
|
||||
return (false, null, $"지원하지 않는 transport 입니다: {transport}. 사용 가능 값: stdio, sse");
|
||||
}
|
||||
|
||||
internal static List<string> TokenizeCommand(string input)
|
||||
{
|
||||
var text = input ?? "";
|
||||
var tokens = new List<string>();
|
||||
var sb = new System.Text.StringBuilder();
|
||||
var inQuote = false;
|
||||
|
||||
foreach (var ch in text)
|
||||
{
|
||||
if (ch == '"')
|
||||
{
|
||||
inQuote = !inQuote;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inQuote && char.IsWhiteSpace(ch))
|
||||
{
|
||||
if (sb.Length == 0) continue;
|
||||
tokens.Add(sb.ToString());
|
||||
sb.Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append(ch);
|
||||
}
|
||||
|
||||
if (sb.Length > 0)
|
||||
tokens.Add(sb.ToString());
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private async Task<string> BuildMcpRuntimeStatusTextAsync(
|
||||
IEnumerable<McpServerEntry>? source = null,
|
||||
bool runtimeCheck = true,
|
||||
@@ -6235,7 +6336,7 @@ public partial class ChatWindow : Window
|
||||
lines.Add($"- {name} [{transport}] : {statusLabel}");
|
||||
}
|
||||
|
||||
lines.Add("명령: /mcp enable|disable <서버명|all>, /mcp reconnect <서버명|all> (enable/disable는 세션 한정)");
|
||||
lines.Add("명령: /mcp status | enable|disable <서버명|all> | reconnect <서버명|all> | add <서버명> :: stdio|sse ... | remove <서버명|all> | reset");
|
||||
return string.Join("\n", lines);
|
||||
}
|
||||
|
||||
@@ -6275,17 +6376,66 @@ public partial class ChatWindow : Window
|
||||
private async Task<string> HandleMcpSlashAsync(string displayText, CancellationToken ct = default)
|
||||
{
|
||||
var llm = _settings.Settings.Llm;
|
||||
var servers = llm.McpServers ?? [];
|
||||
llm.McpServers ??= new List<McpServerEntry>();
|
||||
var servers = llm.McpServers;
|
||||
var (action, target) = ParseMcpAction(displayText);
|
||||
if (action == "help")
|
||||
return "사용법: /mcp, /mcp status, /mcp enable|disable <서버명|all>, /mcp reconnect <서버명|all>";
|
||||
return "사용법: /mcp, /mcp status, /mcp enable|disable <서버명|all>, /mcp reconnect <서버명|all>, /mcp add <서버명> :: stdio <명령> [인자...] | sse <URL>, /mcp remove <서버명|all>, /mcp reset";
|
||||
|
||||
if (action == "status")
|
||||
return await BuildMcpRuntimeStatusTextAsync(servers, runtimeCheck: true, ct).ConfigureAwait(false);
|
||||
|
||||
if (action == "reset")
|
||||
{
|
||||
var changed = _sessionMcpEnabledOverrides.Count;
|
||||
_sessionMcpEnabledOverrides.Clear();
|
||||
return $"세션 MCP 오버라이드를 초기화했습니다. ({changed}개 해제)\n" +
|
||||
await BuildMcpRuntimeStatusTextAsync(servers, runtimeCheck: false, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (action == "add")
|
||||
{
|
||||
var (ok, entry, error) = ParseMcpAddTarget(target);
|
||||
if (!ok || entry == null)
|
||||
return error;
|
||||
|
||||
var duplicate = servers.Any(s => string.Equals(s.Name, entry.Name, StringComparison.OrdinalIgnoreCase));
|
||||
if (duplicate)
|
||||
return $"동일한 이름의 MCP 서버가 이미 존재합니다: {entry.Name}\n기존 항목을 수정하거나 /mcp remove {entry.Name} 후 다시 추가하세요.";
|
||||
|
||||
servers.Add(entry);
|
||||
_settings.Save();
|
||||
_sessionMcpEnabledOverrides.Remove(entry.Name);
|
||||
|
||||
return $"MCP 서버를 추가했습니다: {entry.Name} ({entry.Transport})\n" +
|
||||
await BuildMcpRuntimeStatusTextAsync(servers, runtimeCheck: false, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (servers.Count == 0)
|
||||
return "MCP 서버가 없습니다. 설정에서 서버를 추가한 뒤 다시 시도하세요.";
|
||||
|
||||
if (action == "remove")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(target) || string.Equals(target, "all", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var removed = servers.Count;
|
||||
servers.Clear();
|
||||
_sessionMcpEnabledOverrides.Clear();
|
||||
_settings.Save();
|
||||
return $"MCP 서버를 모두 제거했습니다. ({removed}개)";
|
||||
}
|
||||
|
||||
var resolved = ResolveMcpServerName(servers, target);
|
||||
if (string.IsNullOrWhiteSpace(resolved))
|
||||
return $"제거 대상 서버를 찾지 못했습니다: {target}";
|
||||
|
||||
var removedCount = servers.RemoveAll(s => string.Equals(s.Name, resolved, StringComparison.OrdinalIgnoreCase));
|
||||
_sessionMcpEnabledOverrides.Remove(resolved);
|
||||
_settings.Save();
|
||||
return $"MCP 서버 제거 완료: {resolved} ({removedCount}개)\n" +
|
||||
await BuildMcpRuntimeStatusTextAsync(servers, runtimeCheck: false, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (action is "enable" or "disable")
|
||||
{
|
||||
var newEnabled = action == "enable";
|
||||
@@ -6335,7 +6485,7 @@ public partial class ChatWindow : Window
|
||||
return await BuildMcpRuntimeStatusTextAsync(targetServers, runtimeCheck: true, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return "사용법: /mcp, /mcp status, /mcp enable|disable <서버명|all>, /mcp reconnect <서버명|all>";
|
||||
return "사용법: /mcp, /mcp status, /mcp enable|disable <서버명|all>, /mcp reconnect <서버명|all>, /mcp add <서버명> :: stdio <명령> [인자...] | sse <URL>, /mcp remove <서버명|all>, /mcp reset";
|
||||
}
|
||||
|
||||
private string BuildSlashHeapDumpText()
|
||||
|
||||
Reference in New Issue
Block a user