Skip to content

Commit 2912973

Browse files
authored
Merge pull request #3 from sharpninja/claude/magical-ritchie
Integrate MCP Server REPL workflows into Director hosted agent
2 parents d4826a2 + fc518c8 commit 2912973

14 files changed

Lines changed: 821 additions & 33 deletions

File tree

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"Bash(grep -n \"BuildDesktopDebCore\" /f/GitHub/McpServerManager/build/*.cs)",
3030
"Bash(grep -n \"BuildDesktopMsixCore\" /f/GitHub/McpServerManager/build/*.cs)",
3131
"Bash(dotnet run:*)",
32-
"Bash(find F:GitHubMcpServerManager -type f -name *.yaml -o -name *.yml)"
32+
"Bash(find F:GitHubMcpServerManager -type f -name *.yaml -o -name *.yml)",
33+
"Bash(git submodule:*)"
3334
]
3435
}
3536
}

build/Build.BuildAndDeployTargets.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ private void DeployAndroidCore(string deviceSerial)
361361
return;
362362
}
363363

364-
var devices = InvokeProcess("adb", new List<string> { "devices", "-l" }, RepoRootPath, true);
364+
var devices = InvokeProcess(ResolveAdbPath(), new List<string> { "devices", "-l" }, RepoRootPath, true);
365365
foreach (var line in devices.StandardOutputLines)
366366
{
367367
Info(line);
@@ -662,9 +662,9 @@ private DeploymentResult DeployAndroidSelection(string targetName, bool expectEm
662662
{
663663
try
664664
{
665-
if (!CommandExists("adb"))
665+
if (!CommandExists("adb") && ResolveAdbPath() == "adb")
666666
{
667-
return CreateDeploymentResult(targetName, "Skipped", "adb was not found in PATH.");
667+
return CreateDeploymentResult(targetName, "Skipped", "adb was not found in PATH or Android SDK.");
668668
}
669669

670670
var resolution = ResolveAndroidDevice(expectEmulator, requestedSerial);

build/Build.Common.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,10 +472,41 @@ private void ApplyMarkdownAvaloniaLinuxPatchIfNeeded()
472472
File.WriteAllText(propsPath, updated);
473473
}
474474

475+
private string ResolveAdbPath()
476+
{
477+
// Check if adb is already on the system PATH.
478+
if (CommandExists("adb"))
479+
return "adb";
480+
481+
// Try ANDROID_HOME / ANDROID_SDK_ROOT environment variables.
482+
foreach (var envVar in new[] { "ANDROID_HOME", "ANDROID_SDK_ROOT" })
483+
{
484+
var sdkRoot = Environment.GetEnvironmentVariable(envVar);
485+
if (!string.IsNullOrWhiteSpace(sdkRoot))
486+
{
487+
var candidate = Path.Combine(sdkRoot, "platform-tools", "adb.exe");
488+
if (File.Exists(candidate))
489+
return candidate;
490+
}
491+
}
492+
493+
// Try the default Windows SDK install location.
494+
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
495+
if (!string.IsNullOrWhiteSpace(localAppData))
496+
{
497+
var candidate = Path.Combine(localAppData, "Android", "Sdk", "platform-tools", "adb.exe");
498+
if (File.Exists(candidate))
499+
return candidate;
500+
}
501+
502+
// Fallback — let the caller handle the missing-adb error.
503+
return "adb";
504+
}
505+
475506
private List<AndroidDeviceInfo> GetAndroidDevicesCore()
476507
{
477508
var devices = new List<AndroidDeviceInfo>();
478-
var adbCheck = InvokeProcess("adb", new List<string> { "devices", "-l" }, RepoRootPath, false);
509+
var adbCheck = InvokeProcess(ResolveAdbPath(), new List<string> { "devices", "-l" }, RepoRootPath, false);
479510
if (adbCheck.ExitCode != 0)
480511
{
481512
return devices;

build/Build.UtilityTargets.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ string InvokeAdbCapture(IReadOnlyList<string> arguments, bool allowFailure = fal
551551
{
552552
var commandArguments = new List<string> { "-s", serial };
553553
commandArguments.AddRange(arguments);
554-
var result = InvokeProcess("adb", commandArguments, RepoRootPath, false);
554+
var result = InvokeProcess(ResolveAdbPath(), commandArguments, RepoRootPath, false);
555555
if (!allowFailure && result.ExitCode != 0)
556556
{
557557
throw new InvalidOperationException($"adb {string.Join(" ", commandArguments)} failed.{Environment.NewLine}{result.GetCombinedOutput()}");
@@ -573,7 +573,7 @@ void WriteArtifact(string name, string content)
573573
}
574574

575575
EnsureDirectoryExists(effectiveOutputRoot);
576-
InvokeProcess("adb", new List<string> { "devices" }, RepoRootPath, true);
576+
InvokeProcess(ResolveAdbPath(), new List<string> { "devices" }, RepoRootPath, true);
577577

578578
WriteArtifact("session-metadata.json", JsonSerializer.Serialize(new
579579
{
@@ -584,7 +584,7 @@ void WriteArtifact(string name, string content)
584584
capturedAtUtc = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)
585585
}, new JsonSerializerOptions { WriteIndented = true }));
586586

587-
WriteArtifact("adb-devices.txt", InvokeProcess("adb", new List<string> { "devices", "-l" }, RepoRootPath, false).GetCombinedOutput());
587+
WriteArtifact("adb-devices.txt", InvokeProcess(ResolveAdbPath(), new List<string> { "devices", "-l" }, RepoRootPath, false).GetCombinedOutput());
588588
WriteArtifact("device-getprop.txt", InvokeAdbCapture(new List<string> { "shell", "getprop" }, true));
589589
WriteArtifact("device-build.txt", InvokeAdbCapture(new List<string> { "shell", "dumpsys", "package", PackageName }, true));
590590

@@ -646,7 +646,7 @@ dotnet run --project build/Build.csproj -- --target CollectAndroidCrashArtifacts
646646
if (IncludeBugreport)
647647
{
648648
var bugreportBase = Path.Combine(effectiveOutputRoot, "bugreport");
649-
var bugreportResult = InvokeProcess("adb", new List<string> { "-s", serial, "bugreport", bugreportBase }, RepoRootPath, false);
649+
var bugreportResult = InvokeProcess(ResolveAdbPath(), new List<string> { "-s", serial, "bugreport", bugreportBase }, RepoRootPath, false);
650650
WriteArtifact("bugreport-command-output.txt", bugreportResult.GetCombinedOutput());
651651
}
652652
}

lib/McpServer

Submodule McpServer updated 299 files

src/McpServer.Director/Commands/AgentHostCommand.cs

Lines changed: 151 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ public static void Register(RootCommand root)
6161
(services, directorContext) => ConfigureHostedAgentServices(services, directorContext, settings));
6262
application = new DirectorAgentConsoleApplication(
6363
serviceProvider.GetRequiredService<IMcpHostedAgentFactory>().CreateHostedAgent(),
64-
settings);
64+
settings,
65+
serviceProvider.GetRequiredService<ReplWorkflowToolAdapter>(),
66+
serviceProvider.GetRequiredService<McpServer.Repl.Core.ITodoWorkflow>(),
67+
serviceProvider.GetRequiredService<McpServer.Repl.Core.IRequirementsWorkflow>(),
68+
serviceProvider.GetRequiredService<McpServer.Repl.Core.IGenericClientPassthrough>());
6569
using (application)
6670
{
6771
var args = prompt.Length == 0
@@ -118,6 +122,22 @@ private static void ConfigureHostedAgentServices(
118122
options.Description = settings.AgentDescription;
119123
options.SourceType = settings.SourceType;
120124
});
125+
126+
// Register REPL workflow services for richer TODO, requirements, and client passthrough tools.
127+
services.AddSingleton<McpServer.Repl.Core.ISessionLogWorkflow>(sp =>
128+
new McpServer.Repl.Core.SessionLogWorkflow(
129+
sp.GetRequiredService<McpServer.Client.McpServerClient>().SessionLog,
130+
TimeProvider.System));
131+
services.AddSingleton<McpServer.Repl.Core.ITodoWorkflow>(sp =>
132+
new McpServer.Repl.Core.TodoWorkflow(
133+
sp.GetRequiredService<McpServer.Client.McpServerClient>().Todo));
134+
services.AddSingleton<McpServer.Repl.Core.IRequirementsWorkflow>(sp =>
135+
new McpServer.Repl.Core.RequirementsWorkflow(
136+
sp.GetRequiredService<McpServer.Client.McpServerClient>().Requirements));
137+
services.AddSingleton<McpServer.Repl.Core.IGenericClientPassthrough>(sp =>
138+
new McpServer.Repl.Core.GenericClientPassthrough(
139+
sp.GetRequiredService<McpServer.Client.McpServerClient>()));
140+
services.AddSingleton<ReplWorkflowToolAdapter>();
121141
}
122142
}
123143

@@ -129,6 +149,9 @@ internal sealed class DirectorAgentConsoleApplication : IDisposable
129149
private readonly ChatClientAgent _chatAgent;
130150
private readonly object _powerShellCommandSync = new();
131151
private readonly ChatClientAgentRunOptions _runOptions;
152+
private readonly McpServer.Repl.Core.ITodoWorkflow _replTodo;
153+
private readonly McpServer.Repl.Core.IRequirementsWorkflow _replRequirements;
154+
private readonly McpServer.Repl.Core.IGenericClientPassthrough _replPassthrough;
132155
private CancellationTokenSource? _activePowerShellCommandCancellationSource;
133156
private AgentSession? _agentSession;
134157
private string? _powerShellSessionId;
@@ -139,13 +162,29 @@ internal sealed class DirectorAgentConsoleApplication : IDisposable
139162
private int _historyBrowseIndex = -1;
140163
private string _historyScratchLine = "";
141164

142-
public DirectorAgentConsoleApplication(IMcpHostedAgent hostedAgent, DirectorAgentSettings settings)
165+
public DirectorAgentConsoleApplication(
166+
IMcpHostedAgent hostedAgent,
167+
DirectorAgentSettings settings,
168+
ReplWorkflowToolAdapter replAdapter,
169+
McpServer.Repl.Core.ITodoWorkflow replTodo,
170+
McpServer.Repl.Core.IRequirementsWorkflow replRequirements,
171+
McpServer.Repl.Core.IGenericClientPassthrough replPassthrough)
143172
{
144173
_hostedAgent = hostedAgent ?? throw new ArgumentNullException(nameof(hostedAgent));
145174
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
175+
_replTodo = replTodo ?? throw new ArgumentNullException(nameof(replTodo));
176+
_replRequirements = replRequirements ?? throw new ArgumentNullException(nameof(replRequirements));
177+
_replPassthrough = replPassthrough ?? throw new ArgumentNullException(nameof(replPassthrough));
146178
_chatClient = CreateChatClient(settings);
147179
_chatAgent = hostedAgent.CreateChatClientAgent(_chatClient);
148180
_runOptions = hostedAgent.CreateRunOptions();
181+
182+
// Merge REPL workflow tools into the agent's tool set
183+
var chatOptions = _runOptions.ChatOptions ??= new Microsoft.Extensions.AI.ChatOptions();
184+
chatOptions.Tools ??= new List<AITool>();
185+
foreach (var tool in replAdapter.CreateTools())
186+
chatOptions.Tools.Add(tool);
187+
149188
_powerShellCurrentLocation = settings.WorkspacePath;
150189
_verbosity = settings.Verbosity;
151190
LoadConsoleStateFromDisk();
@@ -326,6 +365,16 @@ private bool TryHandleCommand(
326365
return Task.CompletedTask;
327366
};
328367
return true;
368+
case "/todo":
369+
commandAction = ct => HandleTodoCommandAsync(input, ct);
370+
return true;
371+
case "/requirements":
372+
case "/reqs":
373+
commandAction = ct => HandleRequirementsCommandAsync(input, ct);
374+
return true;
375+
case "/client":
376+
commandAction = ct => HandleClientCommandAsync(input, ct);
377+
return true;
329378
default:
330379
commandAction = _ =>
331380
{
@@ -1028,12 +1077,21 @@ private void WriteBanner()
10281077
private void WriteHelp()
10291078
{
10301079
Console.WriteLine("Commands:");
1031-
Console.WriteLine(" /help Show this help text.");
1032-
Console.WriteLine(" /tools List the MCP-backed tools attached to the hosted agent.");
1033-
Console.WriteLine(" /session Show the current MCP session-log identifier.");
1034-
Console.WriteLine(" /v N Set verbosity level (1=concise, 2=balanced, 3=detailed).");
1035-
Console.WriteLine(" /new Start a fresh conversation and session log.");
1036-
Console.WriteLine(" /exit Exit the Director agent host.");
1080+
Console.WriteLine(" /help Show this help text.");
1081+
Console.WriteLine(" /tools List the MCP-backed tools attached to the hosted agent.");
1082+
Console.WriteLine(" /session Show the current MCP session-log identifier.");
1083+
Console.WriteLine(" /v N Set verbosity level (1=concise, 2=balanced, 3=detailed).");
1084+
Console.WriteLine(" /new Start a fresh conversation and session log.");
1085+
Console.WriteLine(" /exit Exit the Director agent host.");
1086+
Console.WriteLine();
1087+
Console.WriteLine("REPL workflow commands:");
1088+
Console.WriteLine(" /todo List all TODO items.");
1089+
Console.WriteLine(" /todo <keyword> Search TODOs by keyword.");
1090+
Console.WriteLine(" /todo select <id> Select a TODO as the active context.");
1091+
Console.WriteLine(" /todo get <id> Show TODO details.");
1092+
Console.WriteLine(" /requirements List functional requirements summary.");
1093+
Console.WriteLine(" /reqs Alias for /requirements.");
1094+
Console.WriteLine(" /client <c>.<m> Invoke McpServerClient sub-client method (e.g. /client context.SearchAsync).");
10371095
Console.WriteLine();
10381096
Console.WriteLine("Prompt behavior:");
10391097
Console.WriteLine(" - The prompt shows model [verbosity] <location>> (model id from MCP_AGENT_MODEL_NAME or default).");
@@ -1052,6 +1110,91 @@ private void WriteToolList()
10521110
Console.WriteLine("Attached MCP tools:");
10531111
foreach (var tool in _hostedAgent.Registration.Tools)
10541112
Console.WriteLine($" - {tool.Name}");
1113+
1114+
var replTools = _runOptions.ChatOptions?.Tools?
1115+
.Where(t => t.Name.StartsWith("repl_", StringComparison.Ordinal))
1116+
.ToList();
1117+
if (replTools is { Count: > 0 })
1118+
{
1119+
Console.WriteLine();
1120+
Console.WriteLine("REPL workflow tools:");
1121+
foreach (var tool in replTools)
1122+
Console.WriteLine($" - {tool.Name}");
1123+
}
1124+
1125+
Console.WriteLine();
1126+
}
1127+
1128+
private async Task HandleTodoCommandAsync(string input, CancellationToken cancellationToken)
1129+
{
1130+
var parts = input.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
1131+
var subCommand = parts.Length > 1 ? parts[1] : null;
1132+
1133+
if (string.Equals(subCommand, "select", StringComparison.OrdinalIgnoreCase) && parts.Length > 2)
1134+
{
1135+
await _replTodo.SelectAsync(parts[2], cancellationToken).ConfigureAwait(false);
1136+
var sel = _replTodo.CurrentSelection();
1137+
Console.WriteLine($"Selected: {sel?.Id}{sel?.Title} [{sel?.Priority}]");
1138+
Console.WriteLine();
1139+
return;
1140+
}
1141+
1142+
if (string.Equals(subCommand, "get", StringComparison.OrdinalIgnoreCase) && parts.Length > 2)
1143+
{
1144+
var item = await _replTodo.GetAsync(parts[2], cancellationToken).ConfigureAwait(false);
1145+
Console.WriteLine($"{item.Id} {item.Title}");
1146+
Console.WriteLine($" Section: {item.Section} Priority: {item.Priority} Done: {item.Done}");
1147+
if (!string.IsNullOrWhiteSpace(item.Estimate)) Console.WriteLine($" Estimate: {item.Estimate}");
1148+
if (item.Description.Count > 0) Console.WriteLine($" Description: {string.Join(" ", item.Description)}");
1149+
Console.WriteLine();
1150+
return;
1151+
}
1152+
1153+
// Default: query with optional keyword
1154+
var keyword = parts.Length > 1 ? string.Join(' ', parts.Skip(1)) : null;
1155+
var result = await _replTodo.QueryAsync(keyword: keyword, cancellationToken: cancellationToken).ConfigureAwait(false);
1156+
Console.WriteLine($"TODOs ({result.TotalCount} total):");
1157+
foreach (var todo in result.Items)
1158+
{
1159+
var done = todo.Done ? "[x]" : "[ ]";
1160+
Console.WriteLine($" {done} {todo.Id,-25} {todo.Priority,-8} {todo.Title}");
1161+
}
1162+
Console.WriteLine();
1163+
}
1164+
1165+
private async Task HandleRequirementsCommandAsync(string input, CancellationToken cancellationToken)
1166+
{
1167+
var result = await _replRequirements.ListFrAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
1168+
Console.WriteLine($"Functional Requirements ({result.TotalCount} total):");
1169+
foreach (var fr in result.Items)
1170+
Console.WriteLine($" {fr.Id,-20} {fr.Status,-12} {fr.Title}");
1171+
Console.WriteLine();
1172+
}
1173+
1174+
private async Task HandleClientCommandAsync(string input, CancellationToken cancellationToken)
1175+
{
1176+
// Format: /client <clientName>.<methodName> [json-args]
1177+
var parts = input.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
1178+
if (parts.Length < 2 || !parts[1].Contains('.'))
1179+
{
1180+
Console.Error.WriteLine("Usage: /client <clientName>.<methodName> [json-args]");
1181+
Console.Error.WriteLine("Example: /client context.SearchAsync {\"query\":\"auth\"}");
1182+
Console.Error.WriteLine();
1183+
return;
1184+
}
1185+
1186+
var dotIndex = parts[1].IndexOf('.');
1187+
var clientName = parts[1][..dotIndex];
1188+
var methodName = parts[1][(dotIndex + 1)..];
1189+
var argsJson = parts.Length > 2 ? parts[2] : null;
1190+
1191+
var arguments = string.IsNullOrWhiteSpace(argsJson)
1192+
? new Dictionary<string, object?>()
1193+
: JsonSerializer.Deserialize<Dictionary<string, object?>>(argsJson) ?? new Dictionary<string, object?>();
1194+
1195+
var result = await _replPassthrough.InvokeAsync(clientName, methodName, arguments, cancellationToken).ConfigureAwait(false);
1196+
var json = JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true });
1197+
Console.WriteLine(json);
10551198
Console.WriteLine();
10561199
}
10571200

src/McpServer.Director/McpServer.Director.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
<ItemGroup>
4040
<ProjectReference Include="..\..\lib\McpServer\src\McpServer.Client\McpServer.Client.csproj" />
4141
<ProjectReference Include="..\..\lib\McpServer\src\McpServer.McpAgent\McpServer.McpAgent.csproj" />
42+
<ProjectReference Include="..\..\lib\McpServer\src\McpServer.Repl.Core\McpServer.Repl.Core.csproj" />
4243
<ProjectReference Include="..\McpServer.UI.Core\McpServer.UI.Core.csproj" />
4344
</ItemGroup>
4445
<ItemGroup>

0 commit comments

Comments
 (0)