Skip to content

Commit c9267fc

Browse files
authored
Merge pull request #31 from jongalloway/copilot/add-mcp-resources-support
Add MCP Resources support for .NET environment metadata
2 parents d9e06dd + 6663422 commit c9267fc

File tree

6 files changed

+387
-85
lines changed

6 files changed

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

0 commit comments

Comments
 (0)