Skip to content

Commit a2cce4f

Browse files
authored
Merge pull request #22389 from unoplatform/dev/jela/devserver-startup
Stdio MCP fixes
2 parents 4afc71c + faf1546 commit a2cce4f

File tree

4 files changed

+356
-27
lines changed

4 files changed

+356
-27
lines changed

doc/articles/dev-server.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ You can manage the Dev Server from the command line using the dotnet tool `Uno.D
4343
- `--port | -p <int>`: Optional port value for MCP proxy mode
4444
- `--mcp-wait-tools-list`: Wait for the upstream Uno App tools to become available before responding to clients. Use this when working with MCP agents that do not react to `tool_list_changed` (for example, Codex or Claude Code).
4545
- `--force-roots-fallback`: Skip the MCP `roots` handshake and expose the `uno_app_set_roots` tool so agents that cannot send workspace roots can still initialize (required for Google Antigravity).
46+
- `--force-generate-tool-cache`: Immediately request the Uno App tool list once the Dev Server is online and persist it to the local cache. Use this to prime CI environments or agents that expect a tools cache before they can call `list_tools`.
47+
- `--solution-dir <path>`: Explicit solution directory Uno.DevServer should monitor. Useful when starting the DevServer manually (e.g., CI agents) or when priming tools via `--force-generate-tool-cache`. Defaults to the current working directory when omitted.
4648

4749
## Hot Reload
4850

src/Uno.UI.DevServer.Cli/CliManager.cs

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics;
2+
using System.IO;
23
using System.Net;
34
using System.Net.Sockets;
45
using System.Text.Json;
@@ -26,19 +27,34 @@ public async Task<int> RunAsync(string[] originalArgs)
2627
{
2728
try
2829
{
30+
var solutionDirParseResult = ExtractSolutionDirectory(originalArgs);
31+
// --solution-dir is applied uniformly so automation and CI environments can run any
32+
// command (start, stop, list, login, MCP) against a target solution even when the
33+
// current working directory differs from the solution root.
34+
if (!solutionDirParseResult.Success)
35+
{
36+
return 1;
37+
}
38+
39+
originalArgs = solutionDirParseResult.FilteredArgs;
40+
var workingDirectory = solutionDirParseResult.SolutionDirectory ?? Environment.CurrentDirectory;
41+
2942
if (originalArgs.Contains("--mcp-app"))
3043
{
31-
return await RunMcpProxyAsync(originalArgs.Where(a => a != "--mcp-app").ToArray());
44+
return await RunMcpProxyAsync(
45+
originalArgs.Where(a => a != "--mcp-app").ToArray(),
46+
workingDirectory,
47+
solutionDirParseResult.SolutionDirectory);
3248
}
3349

3450
ShowBanner();
3551

3652
if (originalArgs is { Length: > 0 } && string.Equals(originalArgs[0], "login", StringComparison.OrdinalIgnoreCase))
3753
{
38-
return await OpenSettings(originalArgs);
54+
return await OpenSettings(originalArgs, workingDirectory);
3955
}
4056

41-
var hostPath = await _unoToolsLocator.ResolveHostExecutableAsync(Environment.CurrentDirectory);
57+
var hostPath = await _unoToolsLocator.ResolveHostExecutableAsync(workingDirectory);
4258

4359
if (hostPath is null)
4460
{
@@ -50,7 +66,7 @@ public async Task<int> RunAsync(string[] originalArgs)
5066
string.Equals(originalArgs[0], "cleanup", StringComparison.OrdinalIgnoreCase)
5167
);
5268

53-
var startInfo = BuildHostArgs(hostPath, originalArgs, Environment.CurrentDirectory, redirectOutput: !isDirectOutputCommand);
69+
var startInfo = BuildHostArgs(hostPath, originalArgs, workingDirectory, redirectOutput: !isDirectOutputCommand);
5470

5571
var result = await DevServerProcessHelper.RunConsoleProcessAsync(startInfo, _logger);
5672
return result.ExitCode;
@@ -80,16 +96,16 @@ private void ShowBanner()
8096
}
8197
}
8298

83-
private async Task<int> OpenSettings(string[] originalArgs)
99+
private async Task<int> OpenSettings(string[] originalArgs, string workingDirectory)
84100
{
85-
var studioExecutable = await _unoToolsLocator.ResolveSettingsExecutableAsync(Environment.CurrentDirectory);
101+
var studioExecutable = await _unoToolsLocator.ResolveSettingsExecutableAsync(workingDirectory);
86102

87103
if (studioExecutable is null)
88104
{
89105
return 1; // errors already logged
90106
}
91107

92-
var startInfo = DevServerProcessHelper.CreateDotnetProcessStartInfo(studioExecutable, originalArgs, Environment.CurrentDirectory, redirectOutput: true);
108+
var startInfo = DevServerProcessHelper.CreateDotnetProcessStartInfo(studioExecutable, originalArgs, workingDirectory, redirectOutput: true);
93109

94110
var (exitCode, stdOut, stdErr) = await DevServerProcessHelper.RunGuiProcessAsync(startInfo, _logger, TimeSpan.FromSeconds(3));
95111

@@ -116,7 +132,7 @@ private async Task<int> OpenSettings(string[] originalArgs)
116132
}
117133
}
118134

119-
private async Task<int> RunMcpProxyAsync(string[] args)
135+
private async Task<int> RunMcpProxyAsync(string[] args, string workingDirectory, string? solutionDirectory)
120136
{
121137
try
122138
{
@@ -125,6 +141,7 @@ private async Task<int> RunMcpProxyAsync(string[] args)
125141
int requestedPort = 0;
126142
bool mcpWaitToolsList = false;
127143
bool forceRootsFallback = false;
144+
bool forceGenerateToolCache = false;
128145
var forwardedArgs = new List<string>();
129146

130147
for (int i = 0; i < args.Length; i++)
@@ -155,11 +172,26 @@ private async Task<int> RunMcpProxyAsync(string[] args)
155172
forceRootsFallback = true;
156173
continue; // do not forward mcp-specific arguments to controller
157174
}
175+
else if (a == "--force-generate-tool-cache")
176+
{
177+
forceGenerateToolCache = true;
178+
continue; // do not forward mcp-specific arguments to controller
179+
}
158180
forwardedArgs.Add(a);
159181
}
160182

183+
var normalizedSolutionDirectory = solutionDirectory ?? (forceGenerateToolCache ? workingDirectory : null);
184+
161185
var waitForTools = mcpWaitToolsList;
162-
return await _services.GetRequiredService<McpProxy>().RunAsync(Environment.CurrentDirectory, requestedPort, forwardedArgs, waitForTools, forceRootsFallback, CancellationToken.None);
186+
return await _services.GetRequiredService<McpProxy>().RunAsync(
187+
workingDirectory,
188+
requestedPort,
189+
forwardedArgs,
190+
waitForTools,
191+
forceRootsFallback,
192+
forceGenerateToolCache,
193+
normalizedSolutionDirectory,
194+
CancellationToken.None);
163195
}
164196
catch (Exception ex)
165197
{
@@ -168,6 +200,47 @@ private async Task<int> RunMcpProxyAsync(string[] args)
168200
}
169201
}
170202

203+
private (bool Success, string[] FilteredArgs, string? SolutionDirectory) ExtractSolutionDirectory(string[] args)
204+
{
205+
string? rawSolutionDirectory = null;
206+
var filteredArgs = new List<string>(args.Length);
207+
208+
for (int i = 0; i < args.Length; i++)
209+
{
210+
var arg = args[i];
211+
if (arg == "--solution-dir")
212+
{
213+
if (i + 1 >= args.Length)
214+
{
215+
_logger.LogError("Missing value for --solution-dir");
216+
return (false, Array.Empty<string>(), null);
217+
}
218+
219+
rawSolutionDirectory = args[i + 1];
220+
i++; // skip value
221+
continue;
222+
}
223+
224+
filteredArgs.Add(arg);
225+
}
226+
227+
string? normalizedSolutionDirectory = null;
228+
if (!string.IsNullOrWhiteSpace(rawSolutionDirectory))
229+
{
230+
try
231+
{
232+
normalizedSolutionDirectory = Path.GetFullPath(rawSolutionDirectory);
233+
}
234+
catch (Exception ex)
235+
{
236+
_logger.LogError(ex, "Invalid solution directory '{Directory}'", rawSolutionDirectory);
237+
return (false, Array.Empty<string>(), null);
238+
}
239+
}
240+
241+
return (true, filteredArgs.ToArray(), normalizedSolutionDirectory);
242+
}
243+
171244
private ProcessStartInfo BuildHostArgs(string hostPath, string[] originalArgs, string workingDirectory, bool redirectOutput = true)
172245
{
173246
var args = new List<string> { "--command" };

0 commit comments

Comments
 (0)