Skip to content

Commit 5bc0fb0

Browse files
Copilotjongalloway
andcommitted
Refactor: Extract dotnet command execution to shared helper class
Remove code duplication between DotNetCliTools and DotNetResources by extracting command execution logic to DotNetCommandExecutor. This provides two methods: - ExecuteCommandAsync: Full output with logging and truncation (for tools) - ExecuteCommandForResourceAsync: Simple output-only version (for resources) Both classes now use the shared implementation, eliminating the duplicate ExecuteDotNetCommandAsync method. Co-authored-by: jongalloway <[email protected]>
1 parent 1d6109c commit 5bc0fb0

File tree

3 files changed

+147
-119
lines changed

3 files changed

+147
-119
lines changed

DotNetMcp/DotNetCliTools.cs

Lines changed: 1 addition & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -448,90 +448,8 @@ public async Task<string> DotnetNugetLocals(
448448
return await ExecuteDotNetCommand(args);
449449
}
450450

451-
// Limit output to 1 million characters (~1-4MB depending on encoding)
452-
private const int MaxOutputCharacters = 1_000_000;
453-
454451
private async Task<string> ExecuteDotNetCommand(string arguments)
455-
{
456-
_logger.LogDebug("Executing: dotnet {Arguments}", arguments);
457-
458-
var psi = new ProcessStartInfo
459-
{
460-
FileName = "dotnet",
461-
Arguments = arguments,
462-
RedirectStandardOutput = true,
463-
RedirectStandardError = true,
464-
UseShellExecute = false,
465-
CreateNoWindow = true
466-
};
467-
468-
using var process = new Process { StartInfo = psi };
469-
var output = new StringBuilder();
470-
var error = new StringBuilder();
471-
var outputTruncated = false;
472-
var errorTruncated = false;
473-
474-
process.OutputDataReceived += (_, e) =>
475-
{
476-
if (e.Data != null)
477-
{
478-
// Check if adding this line would exceed the limit
479-
int projectedLength = output.Length + e.Data.Length + Environment.NewLine.Length;
480-
if (projectedLength < MaxOutputCharacters)
481-
{
482-
output.AppendLine(e.Data);
483-
}
484-
else if (!outputTruncated)
485-
{
486-
output.AppendLine("[Output truncated - exceeded maximum character limit]");
487-
outputTruncated = true;
488-
}
489-
}
490-
};
491-
492-
process.ErrorDataReceived += (_, e) =>
493-
{
494-
if (e.Data != null)
495-
{
496-
// Check if adding this line would exceed the limit
497-
int projectedLength = error.Length + e.Data.Length + Environment.NewLine.Length;
498-
if (projectedLength < MaxOutputCharacters)
499-
{
500-
error.AppendLine(e.Data);
501-
}
502-
else if (!errorTruncated)
503-
{
504-
error.AppendLine("[Error output truncated - exceeded maximum character limit]");
505-
errorTruncated = true;
506-
}
507-
}
508-
};
509-
510-
process.Start();
511-
process.BeginOutputReadLine();
512-
process.BeginErrorReadLine();
513-
await process.WaitForExitAsync();
514-
515-
_logger.LogDebug("Command completed with exit code: {ExitCode}", process.ExitCode);
516-
if (outputTruncated)
517-
{
518-
_logger.LogWarning("Output was truncated due to size limit");
519-
}
520-
if (errorTruncated)
521-
{
522-
_logger.LogWarning("Error output was truncated due to size limit");
523-
}
524-
525-
var result = new StringBuilder();
526-
if (output.Length > 0) result.AppendLine(output.ToString());
527-
if (error.Length > 0)
528-
{
529-
result.AppendLine("Errors:");
530-
result.AppendLine(error.ToString());
531-
}
532-
result.AppendLine($"Exit Code: {process.ExitCode}");
533-
return result.ToString();
534-
}
452+
=> await DotNetCommandExecutor.ExecuteCommandAsync(arguments, _logger);
535453

536454
private static bool IsValidAdditionalOptions(string options)
537455
{

DotNetMcp/DotNetCommandExecutor.cs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
using System.Diagnostics;
2+
using System.Text;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace DotNetMcp;
6+
7+
/// <summary>
8+
/// Helper class for executing dotnet CLI commands.
9+
/// Provides a centralized implementation to avoid code duplication between tools and resources.
10+
/// </summary>
11+
public static class DotNetCommandExecutor
12+
{
13+
private const int MaxOutputCharacters = 1_000_000;
14+
15+
/// <summary>
16+
/// Execute a dotnet command with full output handling, logging, and truncation support.
17+
/// </summary>
18+
/// <param name="arguments">The command-line arguments to pass to dotnet.exe</param>
19+
/// <param name="logger">Optional logger for debug/warning messages</param>
20+
/// <returns>Combined output, error, and exit code information</returns>
21+
public static async Task<string> ExecuteCommandAsync(string arguments, ILogger? logger = null)
22+
{
23+
logger?.LogDebug("Executing: dotnet {Arguments}", arguments);
24+
25+
var psi = new ProcessStartInfo
26+
{
27+
FileName = "dotnet",
28+
Arguments = arguments,
29+
RedirectStandardOutput = true,
30+
RedirectStandardError = true,
31+
UseShellExecute = false,
32+
CreateNoWindow = true
33+
};
34+
35+
using var process = new Process { StartInfo = psi };
36+
var output = new StringBuilder();
37+
var error = new StringBuilder();
38+
var outputTruncated = false;
39+
var errorTruncated = false;
40+
41+
process.OutputDataReceived += (_, e) =>
42+
{
43+
if (e.Data != null)
44+
{
45+
// Check if adding this line would exceed the limit
46+
int projectedLength = output.Length + e.Data.Length + Environment.NewLine.Length;
47+
if (projectedLength < MaxOutputCharacters)
48+
{
49+
output.AppendLine(e.Data);
50+
}
51+
else if (!outputTruncated)
52+
{
53+
output.AppendLine("[Output truncated - exceeded maximum character limit]");
54+
outputTruncated = true;
55+
}
56+
}
57+
};
58+
59+
process.ErrorDataReceived += (_, e) =>
60+
{
61+
if (e.Data != null)
62+
{
63+
// Check if adding this line would exceed the limit
64+
int projectedLength = error.Length + e.Data.Length + Environment.NewLine.Length;
65+
if (projectedLength < MaxOutputCharacters)
66+
{
67+
error.AppendLine(e.Data);
68+
}
69+
else if (!errorTruncated)
70+
{
71+
error.AppendLine("[Error output truncated - exceeded maximum character limit]");
72+
errorTruncated = true;
73+
}
74+
}
75+
};
76+
77+
process.Start();
78+
process.BeginOutputReadLine();
79+
process.BeginErrorReadLine();
80+
await process.WaitForExitAsync();
81+
82+
logger?.LogDebug("Command completed with exit code: {ExitCode}", process.ExitCode);
83+
if (outputTruncated)
84+
{
85+
logger?.LogWarning("Output was truncated due to size limit");
86+
}
87+
if (errorTruncated)
88+
{
89+
logger?.LogWarning("Error output was truncated due to size limit");
90+
}
91+
92+
var result = new StringBuilder();
93+
if (output.Length > 0) result.AppendLine(output.ToString());
94+
if (error.Length > 0)
95+
{
96+
result.AppendLine("Errors:");
97+
result.AppendLine(error.ToString());
98+
}
99+
result.AppendLine($"Exit Code: {process.ExitCode}");
100+
return result.ToString();
101+
}
102+
103+
/// <summary>
104+
/// Execute a dotnet command and return only the standard output.
105+
/// Throws an exception if the command fails with a non-zero exit code.
106+
/// </summary>
107+
/// <param name="arguments">The command-line arguments to pass to dotnet.exe</param>
108+
/// <param name="logger">Optional logger for debug messages</param>
109+
/// <returns>Standard output only (no error or exit code information)</returns>
110+
/// <exception cref="InvalidOperationException">Thrown if the command fails</exception>
111+
public static async Task<string> ExecuteCommandForResourceAsync(string arguments, ILogger? logger = null)
112+
{
113+
logger?.LogDebug("Executing: dotnet {Arguments}", arguments);
114+
115+
var startInfo = new ProcessStartInfo
116+
{
117+
FileName = "dotnet",
118+
Arguments = arguments,
119+
RedirectStandardOutput = true,
120+
RedirectStandardError = true,
121+
UseShellExecute = false,
122+
CreateNoWindow = true
123+
};
124+
125+
using var process = Process.Start(startInfo);
126+
if (process == null)
127+
{
128+
throw new InvalidOperationException("Failed to start dotnet process");
129+
}
130+
131+
var output = await process.StandardOutput.ReadToEndAsync();
132+
var error = await process.StandardError.ReadToEndAsync();
133+
await process.WaitForExitAsync();
134+
135+
if (process.ExitCode != 0 && !string.IsNullOrEmpty(error))
136+
{
137+
logger?.LogError("Command failed with exit code {ExitCode}: {Error}", process.ExitCode, error);
138+
throw new InvalidOperationException($"dotnet command failed: {error}");
139+
}
140+
141+
logger?.LogDebug("Command completed successfully");
142+
return output;
143+
}
144+
}

DotNetMcp/DotNetResources.cs

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Diagnostics;
21
using System.Text.Json;
32
using Microsoft.Extensions.Logging;
43
using ModelContextProtocol.Server;
@@ -29,7 +28,7 @@ public async Task<string> GetSdkInfo()
2928
_logger.LogDebug("Reading SDK information");
3029
try
3130
{
32-
var result = await ExecuteDotNetCommandAsync("--list-sdks");
31+
var result = await DotNetCommandExecutor.ExecuteCommandForResourceAsync("--list-sdks", _logger);
3332

3433
// Parse the SDK list output
3534
var sdks = new List<object>();
@@ -72,7 +71,7 @@ public async Task<string> GetRuntimeInfo()
7271
_logger.LogDebug("Reading runtime information");
7372
try
7473
{
75-
var result = await ExecuteDotNetCommandAsync("--list-runtimes");
74+
var result = await DotNetCommandExecutor.ExecuteCommandForResourceAsync("--list-runtimes", _logger);
7675

7776
// Parse the runtime list output
7877
var runtimes = new List<object>();
@@ -199,37 +198,4 @@ public Task<string> GetFrameworks()
199198
return Task.FromResult(JsonSerializer.Serialize(new { error = ex.Message }));
200199
}
201200
}
202-
203-
/// <summary>
204-
/// Execute a dotnet command and return the output.
205-
/// </summary>
206-
private async Task<string> ExecuteDotNetCommandAsync(string args)
207-
{
208-
var startInfo = new ProcessStartInfo
209-
{
210-
FileName = "dotnet",
211-
Arguments = args,
212-
RedirectStandardOutput = true,
213-
RedirectStandardError = true,
214-
UseShellExecute = false,
215-
CreateNoWindow = true
216-
};
217-
218-
using var process = Process.Start(startInfo);
219-
if (process == null)
220-
{
221-
throw new InvalidOperationException("Failed to start dotnet process");
222-
}
223-
224-
var output = await process.StandardOutput.ReadToEndAsync();
225-
var error = await process.StandardError.ReadToEndAsync();
226-
await process.WaitForExitAsync();
227-
228-
if (process.ExitCode != 0 && !string.IsNullOrEmpty(error))
229-
{
230-
throw new InvalidOperationException($"dotnet command failed: {error}");
231-
}
232-
233-
return output;
234-
}
235201
}

0 commit comments

Comments
 (0)