Skip to content

Commit fc518c8

Browse files
sharpninjaclaude
andcommitted
Integrate MCP Server REPL workflows into Director hosted agent
Adds rich TODO, Requirements, and generic client passthrough workflows from McpServer.Repl.Core to the Director CLI's hosted agent mode, exposing them both as repl_* AI tools and as /todo, /requirements, and /client slash commands alongside the existing mcp_* tools. Also includes supporting work on this branch: - ResolveAdbPath() in Nuke build so Android targets find adb via ANDROID_HOME / default SDK location when PATH lookup fails - HealthCommandTests now spins up a real MCP server fixture on a random port instead of relying on an ambient server - DirectorRunner corrects the director.dll path and supports a custom working directory for test isolation - DesktopAppServiceFactory resolves a full-access workspace API key from AGENTS-README-FIRST.yaml via McpServerRestClientFactory so voice sessions and workspace listing succeed against the desktop app Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9e7bcbb commit fc518c8

12 files changed

Lines changed: 819 additions & 31 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
}

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)