# Directory Structure ``` ├── .gitignore ├── build-on-windows.bat ├── Build-OnWindows.ps1 ├── claude-mcp-config-wsl.json ├── CLAUDE.md ├── DotNetFrameworkMCP.sln ├── LICENSE ├── mcp-config-example.json ├── ProjectPlan.md ├── README.md ├── run-tcp-server-vs2022.bat ├── run-tcp-server.bat ├── src │ └── DotNetFrameworkMCP.Server │ ├── appsettings.json │ ├── Configuration │ │ └── McpServerConfiguration.cs │ ├── DotNetFrameworkMCP.Server.csproj │ ├── Executors │ │ ├── BaseTestExecutor.cs │ │ ├── DotNetBuildExecutor.cs │ │ ├── DotNetTestExecutor.cs │ │ ├── ExecutorFactory.cs │ │ ├── IBuildExecutor.cs │ │ ├── ITestExecutor.cs │ │ ├── MSBuildExecutor.cs │ │ └── VSTestExecutor.cs │ ├── Models │ │ ├── AnalyzeSolutionRequest.cs │ │ ├── BuildProjectRequest.cs │ │ ├── RunProjectRequest.cs │ │ └── RunTestsRequest.cs │ ├── Program.cs │ ├── Protocol │ │ ├── McpMessage.cs │ │ └── ToolDefinition.cs │ ├── Services │ │ ├── ProcessBasedBuildService.cs │ │ ├── TcpMcpServer.cs │ │ └── TestRunnerService.cs │ └── Tools │ ├── BuildProjectHandler.cs │ ├── IToolHandler.cs │ └── RunTestsHandler.cs ├── start-tcp-server.bat ├── test-dotnet-cli.bat ├── test-messages.json ├── test-tcp-server.sh ├── tests │ └── DotNetFrameworkMCP.Server.Tests │ ├── DotNetFrameworkMCP.Server.Tests.csproj │ ├── Executors │ │ ├── ExecutorFactoryTests.cs │ │ └── MSBuildExecutorTests.cs │ ├── GlobalUsings.cs │ ├── Services │ │ ├── ProcessBasedBuildServiceTests.cs │ │ └── TestRunnerServiceTests.cs │ └── Tools │ ├── BuildProjectHandlerTests.cs │ └── RunTestsHandlerTests.cs └── wsl-mcp-bridge.sh ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | .idea/ 400 | 401 | # macOS 402 | .DS_Store 403 | 404 | # Published output 405 | publish/ ``` -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- ```markdown 1 | # Claude Development Guidelines 2 | 3 | ## Project Information 4 | 5 | This is a .NET Framework MCP (Model Context Protocol) server for Windows that enables building, testing, and running .NET Framework projects remotely. 6 | 7 | ## Testing Framework 8 | 9 | **Use NUnit for all tests** - This project uses NUnit, not xUnit or MSTest. 10 | 11 | ### Test Structure: 12 | ```csharp 13 | using NUnit.Framework; 14 | 15 | [TestFixture] 16 | public class MyServiceTests 17 | { 18 | [SetUp] 19 | public void SetUp() 20 | { 21 | // Initialize test dependencies 22 | } 23 | 24 | [TearDown] 25 | public void TearDown() 26 | { 27 | // Cleanup resources 28 | } 29 | 30 | [Test] 31 | public void MyMethod_WithValidInput_ReturnsExpectedResult() 32 | { 33 | // Arrange 34 | // Act 35 | // Assert 36 | Assert.That(result, Is.EqualTo(expected)); 37 | } 38 | 39 | [TestCase("value1", "expected1")] 40 | [TestCase("value2", "expected2")] 41 | public void MyMethod_WithDifferentInputs_ReturnsExpectedResults(string input, string expected) 42 | { 43 | // Test implementation 44 | } 45 | } 46 | ``` 47 | 48 | ### NUnit Assertions: 49 | - Use `Assert.That(actual, Is.EqualTo(expected))` instead of `Assert.Equal()` 50 | - Use `Assert.That(collection, Has.Count.EqualTo(3))` for collections 51 | - Use `Assert.That(text, Does.Contain("substring"))` for string checks 52 | - Use `Times.Once` with Moq for verifications 53 | 54 | ## Build Commands 55 | 56 | ### Development: 57 | ```bash 58 | # Build the server 59 | dotnet build src/DotNetFrameworkMCP.Server 60 | 61 | # Run tests 62 | dotnet test 63 | 64 | # Run with dotnet CLI enabled 65 | set MCPSERVER__UseDotNetCli=true 66 | dotnet run --project src/DotNetFrameworkMCP.Server -- --port 3001 67 | ``` 68 | 69 | ### IMPORTANT: Pre-Commit Workflow 70 | **ALWAYS build and test before committing!** 71 | 72 | ```bash 73 | # 1. Build to ensure no compilation errors 74 | dotnet build 75 | 76 | # 2. Run tests to ensure functionality works 77 | dotnet test 78 | 79 | # 3. Only commit if both succeed 80 | git add -A 81 | git commit -m "Your commit message" 82 | ``` 83 | 84 | If build or tests fail, fix the issues before committing. Never commit broken code. 85 | 86 | ### Production: 87 | ```bash 88 | # Build self-contained executable 89 | build-on-windows.bat 90 | 91 | # Run the compiled executable 92 | run-tcp-server.bat 93 | ``` 94 | 95 | ## Architecture Notes 96 | 97 | - Uses Strategy pattern for build/test executors 98 | - MSBuild vs dotnet CLI selection via configuration 99 | - Factory pattern for executor creation 100 | - Services delegate to executors, no conditional logic 101 | - All executors implement `IBuildExecutor` or `ITestExecutor` 102 | 103 | ## Configuration 104 | 105 | Key settings in `appsettings.json`: 106 | - `UseDotNetCli`: Switch between MSBuild and dotnet CLI 107 | - `PreferredVSVersion`: VS version preference for MSBuild 108 | - `BuildTimeout`: Build operation timeout 109 | - `TestTimeout`: Test operation timeout ``` -------------------------------------------------------------------------------- /tests/DotNetFrameworkMCP.Server.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- ```csharp 1 | global using NUnit.Framework; ``` -------------------------------------------------------------------------------- /claude-mcp-config-wsl.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "mcpServers": { 3 | "dotnet-framework": { 4 | "command": "/mnt/c/Users/work-tower/Projects/Open/MCP For .Net Framework/wsl-mcp-bridge.sh", 5 | "env": { 6 | "MCP_DEBUG": "true" 7 | } 8 | } 9 | } 10 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Tools/IToolHandler.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Text.Json; 2 | using DotNetFrameworkMCP.Server.Protocol; 3 | 4 | namespace DotNetFrameworkMCP.Server.Tools; 5 | 6 | public interface IToolHandler 7 | { 8 | string Name { get; } 9 | ToolDefinition GetDefinition(); 10 | Task<object> ExecuteAsync(JsonElement arguments); 11 | } ``` -------------------------------------------------------------------------------- /mcp-config-example.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "mcpServers": { 3 | "dotnet-framework": { 4 | "command": "dotnet", 5 | "args": [ 6 | "run", 7 | "--project", 8 | "/mnt/c/Users/work-tower/Projects/Open/MCP For .Net Framework/src/DotNetFrameworkMCP.Server" 9 | ], 10 | "env": { 11 | "MCPSERVER_EnableDetailedLogging": "true" 12 | } 13 | } 14 | } 15 | } ``` -------------------------------------------------------------------------------- /test-dotnet-cli.bat: -------------------------------------------------------------------------------- ``` 1 | @echo off 2 | echo Testing dotnet CLI support... 3 | echo. 4 | echo Starting server with dotnet CLI enabled... 5 | 6 | REM Build the server first 7 | cd src\DotNetFrameworkMCP.Server 8 | dotnet build 9 | 10 | REM Run with dotnet CLI enabled 11 | set MCPSERVER__UseDotNetCli=true 12 | set MCPSERVER__EnableDetailedLogging=true 13 | 14 | echo. 15 | echo Running server with dotnet CLI support enabled... 16 | dotnet run -- --port 3001 ``` -------------------------------------------------------------------------------- /run-tcp-server.bat: -------------------------------------------------------------------------------- ``` 1 | @echo off 2 | echo Starting .NET Framework MCP Server in TCP mode... 3 | echo Server will listen on port 3001 4 | echo Press Ctrl+C to stop 5 | echo. 6 | 7 | cd /d "%~dp0" 8 | 9 | if exist "publish\DotNetFrameworkMCP.Server.exe" ( 10 | publish\DotNetFrameworkMCP.Server.exe --port 3001 11 | ) else ( 12 | echo ERROR: Server executable not found! 13 | echo Please run build-on-windows.bat first to build the project. 14 | pause 15 | exit /b 1 16 | ) ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/appsettings.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "McpServer": { 3 | "MsBuildPath": "auto", 4 | "DefaultConfiguration": "Debug", 5 | "DefaultPlatform": "Any CPU", 6 | "TestTimeout": 300000, 7 | "BuildTimeout": 1200000, 8 | "EnableDetailedLogging": true, 9 | "PreferredVSVersion": "2022", 10 | "UseDotNetCli": false, 11 | "DotNetPath": "dotnet" 12 | }, 13 | "Logging": { 14 | "LogLevel": { 15 | "Default": "Information", 16 | "DotNetFrameworkMCP.Server": "Debug" 17 | } 18 | } 19 | } ``` -------------------------------------------------------------------------------- /start-tcp-server.bat: -------------------------------------------------------------------------------- ``` 1 | @echo off 2 | echo Starting .NET Framework MCP Server in TCP mode... 3 | echo Server will listen on port 3001 4 | echo Press Ctrl+C to stop 5 | echo. 6 | 7 | cd /d "%~dp0" 8 | 9 | if exist "publish\DotNetFrameworkMCP.Server.exe" ( 10 | echo Using compiled executable... 11 | publish\DotNetFrameworkMCP.Server.exe --port 3001 12 | ) else ( 13 | echo Using dotnet run (building if needed)... 14 | dotnet run --project "src\DotNetFrameworkMCP.Server" -- --port 3001 15 | ) 16 | 17 | pause ``` -------------------------------------------------------------------------------- /wsl-mcp-bridge.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # MCP Bridge Script for WSL 4 | # This script connects to the Windows TCP MCP server from WSL 5 | 6 | # Configuration 7 | WINDOWS_HOST="localhost" # Or use the actual Windows IP if needed 8 | MCP_PORT="3001" 9 | 10 | # Check if netcat is available 11 | if ! command -v nc &> /dev/null; then 12 | echo "Error: netcat (nc) is required but not installed." >&2 13 | echo "Install it with: sudo apt install netcat-openbsd" >&2 14 | exit 1 15 | fi 16 | 17 | # Connect to the Windows MCP server via TCP 18 | exec nc "$WINDOWS_HOST" "$MCP_PORT" ``` -------------------------------------------------------------------------------- /test-messages.json: -------------------------------------------------------------------------------- ```json 1 | // Example MCP messages to test the server 2 | 3 | // 1. Initialize the server 4 | {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18"}} 5 | 6 | // 2. List available tools 7 | {"jsonrpc": "2.0", "id": 2, "method": "tools/list"} 8 | 9 | // 3. Call the build_project tool 10 | {"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "build_project", "arguments": {"path": "./src/DotNetFrameworkMCP.Server/DotNetFrameworkMCP.Server.csproj", "configuration": "Debug", "restore": true}}} ``` -------------------------------------------------------------------------------- /build-on-windows.bat: -------------------------------------------------------------------------------- ``` 1 | @echo off 2 | echo Building .NET Framework MCP Server... 3 | echo. 4 | 5 | cd /d "%~dp0" 6 | 7 | echo Restoring packages... 8 | dotnet restore 9 | 10 | echo. 11 | echo Building Release configuration... 12 | dotnet build -c Release 13 | 14 | echo. 15 | echo Publishing self-contained executable... 16 | dotnet publish src\DotNetFrameworkMCP.Server -c Release -r win-x64 --self-contained -o publish 17 | 18 | echo. 19 | echo Build complete! 20 | echo Executable location: publish\DotNetFrameworkMCP.Server.exe 21 | echo. 22 | echo You can now run the server with: 23 | echo publish\DotNetFrameworkMCP.Server.exe --tcp --port 3001 24 | echo. 25 | pause ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Models/RunProjectRequest.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNetFrameworkMCP.Server.Models; 4 | 5 | public class RunProjectRequest 6 | { 7 | [JsonPropertyName("path")] 8 | public string Path { get; set; } = string.Empty; 9 | 10 | [JsonPropertyName("args")] 11 | public List<string>? Args { get; set; } 12 | 13 | [JsonPropertyName("workingDirectory")] 14 | public string? WorkingDirectory { get; set; } 15 | } 16 | 17 | public class RunResult 18 | { 19 | [JsonPropertyName("exitCode")] 20 | public int ExitCode { get; set; } 21 | 22 | [JsonPropertyName("output")] 23 | public string Output { get; set; } = string.Empty; 24 | 25 | [JsonPropertyName("error")] 26 | public string Error { get; set; } = string.Empty; 27 | 28 | [JsonPropertyName("duration")] 29 | public double Duration { get; set; } 30 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Configuration/McpServerConfiguration.cs: -------------------------------------------------------------------------------- ```csharp 1 | namespace DotNetFrameworkMCP.Server.Configuration; 2 | 3 | public class McpServerConfiguration 4 | { 5 | public string MsBuildPath { get; set; } = "auto"; 6 | public string DefaultConfiguration { get; set; } = "Debug"; 7 | public string DefaultPlatform { get; set; } = "Any CPU"; 8 | public int TestTimeout { get; set; } = 300000; 9 | public int BuildTimeout { get; set; } = 1200000; // 20 minutes for large solutions 10 | public bool EnableDetailedLogging { get; set; } = false; 11 | public string PreferredVSVersion { get; set; } = "2022"; // Options: "2022", "2019", "auto" 12 | public bool UseDotNetCli { get; set; } = false; // Use dotnet CLI instead of MSBuild 13 | public string DotNetPath { get; set; } = "dotnet"; // Path to dotnet CLI executable 14 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Executors/ITestExecutor.cs: -------------------------------------------------------------------------------- ```csharp 1 | using DotNetFrameworkMCP.Server.Models; 2 | 3 | namespace DotNetFrameworkMCP.Server.Executors; 4 | 5 | /// <summary> 6 | /// Interface for test execution strategies 7 | /// </summary> 8 | public interface ITestExecutor 9 | { 10 | /// <summary> 11 | /// Executes tests for the specified project 12 | /// </summary> 13 | /// <param name="projectPath">Path to the test project</param> 14 | /// <param name="filter">Optional test filter expression</param> 15 | /// <param name="verbose">Whether to enable verbose output</param> 16 | /// <param name="cancellationToken">Cancellation token</param> 17 | /// <returns>Test result with total, passed, failed, skipped counts and details</returns> 18 | Task<TestResult> ExecuteTestsAsync( 19 | string projectPath, 20 | string? filter, 21 | bool verbose, 22 | CancellationToken cancellationToken = default); 23 | } ``` -------------------------------------------------------------------------------- /test-tcp-server.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | echo "Testing TCP MCP Server..." 4 | 5 | # Start the server in background 6 | cd "/mnt/c/Users/work-tower/Projects/Open/MCP For .Net Framework" 7 | dotnet run --project src/DotNetFrameworkMCP.Server -- --port 3001 & 8 | SERVER_PID=$! 9 | 10 | # Wait a moment for server to start 11 | sleep 2 12 | 13 | echo "Server started with PID $SERVER_PID" 14 | echo "Testing connection..." 15 | 16 | # Test the server 17 | { 18 | echo '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18"}}' 19 | sleep 0.1 20 | echo '{"jsonrpc": "2.0", "id": 2, "method": "tools/list"}' 21 | sleep 0.1 22 | echo '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "build_project", "arguments": {"path": "./src/DotNetFrameworkMCP.Server/DotNetFrameworkMCP.Server.csproj", "configuration": "Debug", "restore": true}}}' 23 | sleep 1 24 | } | nc localhost 3001 25 | 26 | echo "Test completed. Stopping server..." 27 | kill $SERVER_PID 28 | wait $SERVER_PID 2>/dev/null 29 | echo "Server stopped." ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Executors/IBuildExecutor.cs: -------------------------------------------------------------------------------- ```csharp 1 | using DotNetFrameworkMCP.Server.Models; 2 | 3 | namespace DotNetFrameworkMCP.Server.Executors; 4 | 5 | /// <summary> 6 | /// Interface for build execution strategies 7 | /// </summary> 8 | public interface IBuildExecutor 9 | { 10 | /// <summary> 11 | /// Executes a build for the specified project 12 | /// </summary> 13 | /// <param name="projectPath">Path to the project or solution file</param> 14 | /// <param name="configuration">Build configuration (e.g., Debug, Release)</param> 15 | /// <param name="platform">Target platform (e.g., Any CPU, x86, x64)</param> 16 | /// <param name="restore">Whether to restore NuGet packages</param> 17 | /// <param name="cancellationToken">Cancellation token</param> 18 | /// <returns>Build result with success status, errors, warnings, and output</returns> 19 | Task<BuildResult> ExecuteBuildAsync( 20 | string projectPath, 21 | string configuration, 22 | string platform, 23 | bool restore, 24 | CancellationToken cancellationToken = default); 25 | } ``` -------------------------------------------------------------------------------- /Build-OnWindows.ps1: -------------------------------------------------------------------------------- ``` 1 | #!/usr/bin/env pwsh 2 | 3 | Write-Host "Building .NET Framework MCP Server..." -ForegroundColor Green 4 | Write-Host "" 5 | 6 | # Change to script directory 7 | Set-Location $PSScriptRoot 8 | 9 | Write-Host "Restoring packages..." -ForegroundColor Yellow 10 | dotnet restore 11 | 12 | Write-Host "" 13 | Write-Host "Building Release configuration..." -ForegroundColor Yellow 14 | dotnet build -c Release 15 | 16 | if ($LASTEXITCODE -ne 0) { 17 | Write-Host "Build failed!" -ForegroundColor Red 18 | exit 1 19 | } 20 | 21 | Write-Host "" 22 | Write-Host "Publishing self-contained executable..." -ForegroundColor Yellow 23 | dotnet publish src\DotNetFrameworkMCP.Server -c Release -r win-x64 --self-contained -o publish 24 | 25 | if ($LASTEXITCODE -ne 0) { 26 | Write-Host "Publish failed!" -ForegroundColor Red 27 | exit 1 28 | } 29 | 30 | Write-Host "" 31 | Write-Host "Build complete!" -ForegroundColor Green 32 | Write-Host "Executable location: publish\DotNetFrameworkMCP.Server.exe" -ForegroundColor Cyan 33 | Write-Host "" 34 | Write-Host "You can now run the server with:" -ForegroundColor Yellow 35 | Write-Host " .\publish\DotNetFrameworkMCP.Server.exe --tcp --port 3001" -ForegroundColor White 36 | Write-Host "" ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Executors/ExecutorFactory.cs: -------------------------------------------------------------------------------- ```csharp 1 | using DotNetFrameworkMCP.Server.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace DotNetFrameworkMCP.Server.Executors; 6 | 7 | /// <summary> 8 | /// Factory for creating build and test executors based on configuration 9 | /// </summary> 10 | public interface IExecutorFactory 11 | { 12 | IBuildExecutor CreateBuildExecutor(); 13 | ITestExecutor CreateTestExecutor(); 14 | } 15 | 16 | public class ExecutorFactory : IExecutorFactory 17 | { 18 | private readonly IServiceProvider _serviceProvider; 19 | private readonly McpServerConfiguration _configuration; 20 | 21 | public ExecutorFactory(IServiceProvider serviceProvider, IOptions<McpServerConfiguration> configuration) 22 | { 23 | _serviceProvider = serviceProvider; 24 | _configuration = configuration.Value; 25 | } 26 | 27 | public IBuildExecutor CreateBuildExecutor() 28 | { 29 | return _configuration.UseDotNetCli 30 | ? _serviceProvider.GetRequiredService<DotNetBuildExecutor>() 31 | : _serviceProvider.GetRequiredService<MSBuildExecutor>(); 32 | } 33 | 34 | public ITestExecutor CreateTestExecutor() 35 | { 36 | return _configuration.UseDotNetCli 37 | ? _serviceProvider.GetRequiredService<DotNetTestExecutor>() 38 | : _serviceProvider.GetRequiredService<VSTestExecutor>(); 39 | } 40 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Protocol/ToolDefinition.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNetFrameworkMCP.Server.Protocol; 4 | 5 | public class ToolDefinition 6 | { 7 | [JsonPropertyName("name")] 8 | public string Name { get; set; } = string.Empty; 9 | 10 | [JsonPropertyName("description")] 11 | public string Description { get; set; } = string.Empty; 12 | 13 | [JsonPropertyName("inputSchema")] 14 | public JsonSchema InputSchema { get; set; } = new(); 15 | } 16 | 17 | public class JsonSchema 18 | { 19 | [JsonPropertyName("type")] 20 | public string Type { get; set; } = "object"; 21 | 22 | [JsonPropertyName("properties")] 23 | public Dictionary<string, SchemaProperty> Properties { get; set; } = new(); 24 | 25 | [JsonPropertyName("required")] 26 | public List<string> Required { get; set; } = new(); 27 | } 28 | 29 | public class SchemaProperty 30 | { 31 | [JsonPropertyName("type")] 32 | public string Type { get; set; } = string.Empty; 33 | 34 | [JsonPropertyName("description")] 35 | public string? Description { get; set; } 36 | 37 | [JsonPropertyName("enum")] 38 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 39 | public List<string>? Enum { get; set; } 40 | 41 | [JsonPropertyName("default")] 42 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 43 | public object? Default { get; set; } 44 | 45 | [JsonPropertyName("items")] 46 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 47 | public SchemaProperty? Items { get; set; } 48 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Models/BuildProjectRequest.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNetFrameworkMCP.Server.Models; 4 | 5 | public class BuildProjectRequest 6 | { 7 | [JsonPropertyName("path")] 8 | public string Path { get; set; } = string.Empty; 9 | 10 | [JsonPropertyName("configuration")] 11 | public string Configuration { get; set; } = "Debug"; 12 | 13 | [JsonPropertyName("platform")] 14 | public string Platform { get; set; } = "Any CPU"; 15 | 16 | [JsonPropertyName("restore")] 17 | public bool Restore { get; set; } = true; 18 | } 19 | 20 | public class BuildResult 21 | { 22 | [JsonPropertyName("success")] 23 | public bool Success { get; set; } 24 | 25 | [JsonPropertyName("errors")] 26 | public List<BuildMessage> Errors { get; set; } = new(); 27 | 28 | [JsonPropertyName("warnings")] 29 | public List<BuildMessage> Warnings { get; set; } = new(); 30 | 31 | [JsonPropertyName("buildTime")] 32 | public double BuildTime { get; set; } 33 | 34 | [JsonPropertyName("output")] 35 | public string Output { get; set; } = string.Empty; 36 | } 37 | 38 | public class BuildMessage 39 | { 40 | [JsonPropertyName("file")] 41 | public string? File { get; set; } 42 | 43 | [JsonPropertyName("line")] 44 | public int Line { get; set; } 45 | 46 | [JsonPropertyName("column")] 47 | public int Column { get; set; } 48 | 49 | [JsonPropertyName("code")] 50 | public string? Code { get; set; } 51 | 52 | [JsonPropertyName("message")] 53 | public string Message { get; set; } = string.Empty; 54 | 55 | [JsonPropertyName("project")] 56 | public string? Project { get; set; } 57 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Protocol/McpMessage.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNetFrameworkMCP.Server.Protocol; 4 | 5 | public class McpMessage 6 | { 7 | [JsonPropertyName("jsonrpc")] 8 | public string JsonRpc { get; set; } = "2.0"; 9 | 10 | [JsonPropertyName("id")] 11 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 12 | public object? Id { get; set; } 13 | 14 | [JsonPropertyName("method")] 15 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 16 | public string? Method { get; set; } 17 | 18 | [JsonPropertyName("params")] 19 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 20 | public object? Params { get; set; } 21 | 22 | [JsonPropertyName("result")] 23 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 24 | public object? Result { get; set; } 25 | 26 | [JsonPropertyName("error")] 27 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 28 | public McpError? Error { get; set; } 29 | } 30 | 31 | public class McpError 32 | { 33 | [JsonPropertyName("code")] 34 | public int Code { get; set; } 35 | 36 | [JsonPropertyName("message")] 37 | public string Message { get; set; } = string.Empty; 38 | 39 | [JsonPropertyName("data")] 40 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 41 | public object? Data { get; set; } 42 | } 43 | 44 | public static class McpErrorCodes 45 | { 46 | public const int ParseError = -32700; 47 | public const int InvalidRequest = -32600; 48 | public const int MethodNotFound = -32601; 49 | public const int InvalidParams = -32602; 50 | public const int InternalError = -32603; 51 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Models/RunTestsRequest.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNetFrameworkMCP.Server.Models; 4 | 5 | public class RunTestsRequest 6 | { 7 | [JsonPropertyName("path")] 8 | public string Path { get; set; } = string.Empty; 9 | 10 | [JsonPropertyName("filter")] 11 | public string? Filter { get; set; } 12 | 13 | [JsonPropertyName("verbose")] 14 | public bool Verbose { get; set; } = false; 15 | } 16 | 17 | public class TestResult 18 | { 19 | [JsonPropertyName("totalTests")] 20 | public int TotalTests { get; set; } 21 | 22 | [JsonPropertyName("passedTests")] 23 | public int PassedTests { get; set; } 24 | 25 | [JsonPropertyName("failedTests")] 26 | public int FailedTests { get; set; } 27 | 28 | [JsonPropertyName("skippedTests")] 29 | public int SkippedTests { get; set; } 30 | 31 | [JsonPropertyName("duration")] 32 | public double Duration { get; set; } 33 | 34 | [JsonPropertyName("testDetails")] 35 | public List<TestDetail> TestDetails { get; set; } = new(); 36 | 37 | [JsonPropertyName("output")] 38 | public string Output { get; set; } = string.Empty; 39 | } 40 | 41 | public class TestDetail 42 | { 43 | [JsonPropertyName("name")] 44 | public string Name { get; set; } = string.Empty; 45 | 46 | [JsonPropertyName("className")] 47 | public string ClassName { get; set; } = string.Empty; 48 | 49 | [JsonPropertyName("result")] 50 | public string Result { get; set; } = string.Empty; 51 | 52 | [JsonPropertyName("duration")] 53 | public double Duration { get; set; } 54 | 55 | [JsonPropertyName("errorMessage")] 56 | public string? ErrorMessage { get; set; } 57 | 58 | [JsonPropertyName("stackTrace")] 59 | public string? StackTrace { get; set; } 60 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Models/AnalyzeSolutionRequest.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Text.Json.Serialization; 2 | 3 | namespace DotNetFrameworkMCP.Server.Models; 4 | 5 | public class AnalyzeSolutionRequest 6 | { 7 | [JsonPropertyName("path")] 8 | public string Path { get; set; } = string.Empty; 9 | } 10 | 11 | public class SolutionAnalysis 12 | { 13 | [JsonPropertyName("solutionName")] 14 | public string SolutionName { get; set; } = string.Empty; 15 | 16 | [JsonPropertyName("projects")] 17 | public List<ProjectInfo> Projects { get; set; } = new(); 18 | 19 | [JsonPropertyName("totalProjects")] 20 | public int TotalProjects { get; set; } 21 | } 22 | 23 | public class ProjectInfo 24 | { 25 | [JsonPropertyName("name")] 26 | public string Name { get; set; } = string.Empty; 27 | 28 | [JsonPropertyName("path")] 29 | public string Path { get; set; } = string.Empty; 30 | 31 | [JsonPropertyName("type")] 32 | public string Type { get; set; } = string.Empty; 33 | 34 | [JsonPropertyName("targetFramework")] 35 | public string TargetFramework { get; set; } = string.Empty; 36 | 37 | [JsonPropertyName("outputType")] 38 | public string OutputType { get; set; } = string.Empty; 39 | 40 | [JsonPropertyName("dependencies")] 41 | public List<string> Dependencies { get; set; } = new(); 42 | 43 | [JsonPropertyName("packages")] 44 | public List<PackageInfo> Packages { get; set; } = new(); 45 | } 46 | 47 | public class ListPackagesRequest 48 | { 49 | [JsonPropertyName("path")] 50 | public string Path { get; set; } = string.Empty; 51 | } 52 | 53 | public class PackageInfo 54 | { 55 | [JsonPropertyName("name")] 56 | public string Name { get; set; } = string.Empty; 57 | 58 | [JsonPropertyName("version")] 59 | public string Version { get; set; } = string.Empty; 60 | } ``` -------------------------------------------------------------------------------- /run-tcp-server-vs2022.bat: -------------------------------------------------------------------------------- ``` 1 | @echo off 2 | echo Starting .NET Framework MCP Server with Visual Studio 2022 MSBuild... 3 | echo Server will listen on port 3001 4 | echo Press Ctrl+C to stop 5 | echo. 6 | 7 | cd /d "%~dp0" 8 | 9 | REM Set MSBuild path to VS2022 (adjust path as needed) 10 | setlocal EnableDelayedExpansion 11 | 12 | set "paths[0]=C:\Program Files (x86)\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin" 13 | set "paths[1]=C:\Program Files (x86)\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin" 14 | set "paths[2]=C:\Program Files (x86)\Microsoft Visual Studio\2022\Preview\MSBuild\Current\Bin" 15 | set "paths[3]=C:\Program Files (x86)\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin" 16 | set "paths[4]=C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin" 17 | set "paths[5]=C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin" 18 | set "paths[6]=C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin" 19 | set "paths[7]=C:\Program Files\Microsoft Visual Studio\2022\Preview\MSBuild\Current\Bin" 20 | set "paths[8]=C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin" 21 | set "paths[9]=C:\Program Files\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin" 22 | 23 | set "MSBUILD_PATH=" 24 | for /L %%i in (0,1,9) do ( 25 | call set "currentPath=%%paths[%%i]%%" 26 | if exist "!currentPath!" ( 27 | set "MSBUILD_PATH=!currentPath!" 28 | goto :found 29 | ) 30 | ) 31 | 32 | :found 33 | if defined MSBUILD_PATH ( 34 | echo Using MSBuild from: %MSBUILD_PATH% 35 | ) else ( 36 | echo Error: MSBuild path not found. 37 | pause 38 | ) 39 | 40 | echo. 41 | 42 | REM Run DotNetFramework MCP Server 43 | if exist "publish\DotNetFrameworkMCP.Server.exe" ( 44 | echo Using compiled executable... 45 | "publish\DotNetFrameworkMCP.Server.exe" --port 3001 46 | ) else ( 47 | echo Using dotnet run, building if needed... 48 | dotnet run --project "src\DotNetFrameworkMCP.Server" -- --port 3001 49 | ) 50 | 51 | pause ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Services/ProcessBasedBuildService.cs: -------------------------------------------------------------------------------- ```csharp 1 | using DotNetFrameworkMCP.Server.Executors; 2 | using DotNetFrameworkMCP.Server.Models; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace DotNetFrameworkMCP.Server.Services; 6 | 7 | public interface IProcessBasedBuildService 8 | { 9 | Task<Models.BuildResult> BuildProjectAsync(string projectPath, string configuration, string platform, bool restore, CancellationToken cancellationToken = default); 10 | } 11 | 12 | public class ProcessBasedBuildService : IProcessBasedBuildService 13 | { 14 | private readonly ILogger<ProcessBasedBuildService> _logger; 15 | private readonly IExecutorFactory _executorFactory; 16 | 17 | public ProcessBasedBuildService( 18 | ILogger<ProcessBasedBuildService> logger, 19 | IExecutorFactory executorFactory) 20 | { 21 | _logger = logger; 22 | _executorFactory = executorFactory; 23 | } 24 | 25 | public async Task<Models.BuildResult> BuildProjectAsync( 26 | string projectPath, 27 | string configuration, 28 | string platform, 29 | bool restore, 30 | CancellationToken cancellationToken = default) 31 | { 32 | try 33 | { 34 | _logger.LogInformation("Starting build for project: {ProjectPath}", projectPath); 35 | 36 | var buildExecutor = _executorFactory.CreateBuildExecutor(); 37 | return await buildExecutor.ExecuteBuildAsync(projectPath, configuration, platform, restore, cancellationToken); 38 | } 39 | catch (Exception ex) 40 | { 41 | _logger.LogError(ex, "Failed to create build executor or execute build for project: {ProjectPath}", projectPath); 42 | 43 | return new Models.BuildResult 44 | { 45 | Success = false, 46 | Errors = new List<BuildMessage> 47 | { 48 | new BuildMessage 49 | { 50 | Message = ex.Message, 51 | File = projectPath 52 | } 53 | }, 54 | Warnings = new List<BuildMessage>(), 55 | BuildTime = 0, 56 | Output = $"Build service failed: {ex.Message}" 57 | }; 58 | } 59 | } 60 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Services/TestRunnerService.cs: -------------------------------------------------------------------------------- ```csharp 1 | using DotNetFrameworkMCP.Server.Executors; 2 | using DotNetFrameworkMCP.Server.Models; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace DotNetFrameworkMCP.Server.Services; 6 | 7 | public interface ITestRunnerService 8 | { 9 | Task<TestResult> RunTestsAsync(string projectPath, string? filter, bool verbose, CancellationToken cancellationToken = default); 10 | } 11 | 12 | public class TestRunnerService : ITestRunnerService 13 | { 14 | private readonly ILogger<TestRunnerService> _logger; 15 | private readonly IExecutorFactory _executorFactory; 16 | 17 | public TestRunnerService( 18 | ILogger<TestRunnerService> logger, 19 | IExecutorFactory executorFactory) 20 | { 21 | _logger = logger; 22 | _executorFactory = executorFactory; 23 | } 24 | 25 | public async Task<TestResult> RunTestsAsync( 26 | string projectPath, 27 | string? filter, 28 | bool verbose, 29 | CancellationToken cancellationToken = default) 30 | { 31 | try 32 | { 33 | _logger.LogInformation("Starting test run for project: {ProjectPath}", projectPath); 34 | 35 | var testExecutor = _executorFactory.CreateTestExecutor(); 36 | return await testExecutor.ExecuteTestsAsync(projectPath, filter, verbose, cancellationToken); 37 | } 38 | catch (Exception ex) 39 | { 40 | _logger.LogError(ex, "Failed to create test executor or execute tests for project: {ProjectPath}", projectPath); 41 | 42 | return new TestResult 43 | { 44 | TotalTests = 0, 45 | PassedTests = 0, 46 | FailedTests = 0, 47 | SkippedTests = 0, 48 | Duration = 0, 49 | TestDetails = new List<TestDetail> 50 | { 51 | new TestDetail 52 | { 53 | Name = "Test Execution Error", 54 | ClassName = "System", 55 | Result = "Failed", 56 | Duration = 0, 57 | ErrorMessage = ex.Message, 58 | StackTrace = ex.StackTrace 59 | } 60 | }, 61 | Output = $"Test service failed: {ex.Message}" 62 | }; 63 | } 64 | } 65 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Tools/RunTestsHandler.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Text.Json; 2 | using DotNetFrameworkMCP.Server.Configuration; 3 | using DotNetFrameworkMCP.Server.Models; 4 | using DotNetFrameworkMCP.Server.Protocol; 5 | using DotNetFrameworkMCP.Server.Services; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace DotNetFrameworkMCP.Server.Tools; 10 | 11 | public class RunTestsHandler : IToolHandler 12 | { 13 | private readonly ILogger<RunTestsHandler> _logger; 14 | private readonly McpServerConfiguration _configuration; 15 | private readonly ITestRunnerService _testRunnerService; 16 | 17 | public RunTestsHandler( 18 | ILogger<RunTestsHandler> logger, 19 | IOptions<McpServerConfiguration> configuration, 20 | ITestRunnerService testRunnerService) 21 | { 22 | _logger = logger; 23 | _configuration = configuration.Value; 24 | _testRunnerService = testRunnerService; 25 | } 26 | 27 | public string Name => "run_tests"; 28 | 29 | public ToolDefinition GetDefinition() 30 | { 31 | return new ToolDefinition 32 | { 33 | Name = Name, 34 | Description = "Run tests in a .NET test project", 35 | InputSchema = new JsonSchema 36 | { 37 | Type = "object", 38 | Properties = new Dictionary<string, SchemaProperty> 39 | { 40 | ["path"] = new SchemaProperty 41 | { 42 | Type = "string", 43 | Description = "Path to test project (.csproj file)" 44 | }, 45 | ["filter"] = new SchemaProperty 46 | { 47 | Type = "string", 48 | Description = "Test filter expression (optional)" 49 | }, 50 | ["verbose"] = new SchemaProperty 51 | { 52 | Type = "boolean", 53 | Description = "Enable verbose output", 54 | Default = false 55 | } 56 | }, 57 | Required = new List<string> { "path" } 58 | } 59 | }; 60 | } 61 | 62 | public async Task<object> ExecuteAsync(JsonElement arguments) 63 | { 64 | var request = JsonSerializer.Deserialize<RunTestsRequest>(arguments.GetRawText()); 65 | if (request == null || string.IsNullOrEmpty(request.Path)) 66 | { 67 | throw new ArgumentException("Invalid test run request"); 68 | } 69 | 70 | _logger.LogInformation("Running tests for project: {Path}", request.Path); 71 | 72 | if (!string.IsNullOrEmpty(request.Filter)) 73 | { 74 | _logger.LogInformation("Using test filter: {Filter}", request.Filter); 75 | } 76 | 77 | // Create cancellation token with timeout 78 | using var cts = new CancellationTokenSource(_configuration.TestTimeout); 79 | 80 | try 81 | { 82 | return await _testRunnerService.RunTestsAsync( 83 | request.Path, 84 | request.Filter, 85 | request.Verbose, 86 | cts.Token); 87 | } 88 | catch (OperationCanceledException) 89 | { 90 | throw new TimeoutException($"Test run timed out after {_configuration.TestTimeout}ms"); 91 | } 92 | } 93 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Program.cs: -------------------------------------------------------------------------------- ```csharp 1 | using DotNetFrameworkMCP.Server.Configuration; 2 | using DotNetFrameworkMCP.Server.Executors; 3 | using DotNetFrameworkMCP.Server.Services; 4 | using DotNetFrameworkMCP.Server.Tools; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | var host = Host.CreateDefaultBuilder(args) 11 | .ConfigureAppConfiguration((context, config) => 12 | { 13 | config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); 14 | config.AddEnvironmentVariables(prefix: "MCPSERVER_"); 15 | }) 16 | .ConfigureServices((context, services) => 17 | { 18 | // Configuration 19 | services.Configure<McpServerConfiguration>( 20 | context.Configuration.GetSection("McpServer")); 21 | 22 | // Register executors 23 | services.AddSingleton<MSBuildExecutor>(); 24 | services.AddSingleton<DotNetBuildExecutor>(); 25 | services.AddSingleton<VSTestExecutor>(); 26 | services.AddSingleton<DotNetTestExecutor>(); 27 | services.AddSingleton<IExecutorFactory, ExecutorFactory>(); 28 | 29 | // Register services 30 | services.AddSingleton<IProcessBasedBuildService, ProcessBasedBuildService>(); 31 | services.AddSingleton<ITestRunnerService, TestRunnerService>(); 32 | 33 | // Register tool handlers 34 | services.AddSingleton<IToolHandler, BuildProjectHandler>(); 35 | services.AddSingleton<IToolHandler, RunTestsHandler>(); 36 | 37 | // Register the TCP MCP server 38 | services.AddSingleton<TcpMcpServer>(); 39 | 40 | // Configure logging 41 | services.AddLogging(builder => 42 | { 43 | builder.ClearProviders(); 44 | 45 | // Only log to stderr to keep stdout clean for MCP messages 46 | builder.AddConsole(options => 47 | { 48 | options.LogToStandardErrorThreshold = LogLevel.Trace; 49 | }); 50 | 51 | builder.SetMinimumLevel(LogLevel.Information); 52 | 53 | if (context.Configuration.GetValue<bool>("McpServer:EnableDetailedLogging")) 54 | { 55 | builder.SetMinimumLevel(LogLevel.Debug); 56 | } 57 | }); 58 | }) 59 | .UseConsoleLifetime() 60 | .Build(); 61 | 62 | var logger = host.Services.GetRequiredService<ILogger<Program>>(); 63 | 64 | try 65 | { 66 | logger.LogInformation("Starting .NET Framework MCP Server"); 67 | 68 | // Create cancellation token that responds to Ctrl+C 69 | using var cts = new CancellationTokenSource(); 70 | Console.CancelKeyPress += (sender, e) => 71 | { 72 | e.Cancel = true; 73 | cts.Cancel(); 74 | }; 75 | 76 | // Parse port from command line arguments 77 | var port = 3001; 78 | if (args.Contains("--port")) 79 | { 80 | var portIndex = Array.IndexOf(args, "--port"); 81 | if (portIndex + 1 < args.Length && int.TryParse(args[portIndex + 1], out var parsedPort)) 82 | { 83 | port = parsedPort; 84 | } 85 | } 86 | 87 | var tcpServer = host.Services.GetRequiredService<TcpMcpServer>(); 88 | logger.LogInformation("Starting TCP MCP Server on port {Port}", port); 89 | await tcpServer.RunAsync(port, cts.Token); 90 | } 91 | catch (Exception ex) 92 | { 93 | logger.LogCritical(ex, "Server failed to start"); 94 | Environment.Exit(1); 95 | } 96 | ``` -------------------------------------------------------------------------------- /tests/DotNetFrameworkMCP.Server.Tests/Executors/MSBuildExecutorTests.cs: -------------------------------------------------------------------------------- ```csharp 1 | using DotNetFrameworkMCP.Server.Configuration; 2 | using DotNetFrameworkMCP.Server.Executors; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Options; 5 | using Moq; 6 | using NUnit.Framework; 7 | 8 | namespace DotNetFrameworkMCP.Server.Tests.Executors; 9 | 10 | [TestFixture] 11 | public class MSBuildExecutorTests 12 | { 13 | private Mock<ILogger<MSBuildExecutor>> _mockLogger; 14 | private Mock<IOptions<McpServerConfiguration>> _mockOptions; 15 | private McpServerConfiguration _configuration; 16 | private MSBuildExecutor _executor; 17 | 18 | [SetUp] 19 | public void SetUp() 20 | { 21 | _mockLogger = new Mock<ILogger<MSBuildExecutor>>(); 22 | _mockOptions = new Mock<IOptions<McpServerConfiguration>>(); 23 | _configuration = new McpServerConfiguration 24 | { 25 | BuildTimeout = 60000, 26 | PreferredVSVersion = "2022" 27 | }; 28 | _mockOptions.Setup(x => x.Value).Returns(_configuration); 29 | _executor = new MSBuildExecutor(_mockLogger.Object, _mockOptions.Object); 30 | } 31 | 32 | [Test] 33 | public async Task ExecuteBuildAsync_WithNonExistentProject_ReturnsFailedResult() 34 | { 35 | // Arrange 36 | var nonExistentPath = "/path/to/nonexistent/project.csproj"; 37 | 38 | // Act 39 | var result = await _executor.ExecuteBuildAsync(nonExistentPath, "Debug", "Any CPU", true); 40 | 41 | // Assert 42 | Assert.That(result.Success, Is.False); 43 | Assert.That(result.Errors, Has.Count.EqualTo(1)); 44 | Assert.That(result.Errors[0].Message, Does.Contain("Project file not found")); 45 | } 46 | 47 | [Test] 48 | public async Task ExecuteBuildAsync_WithTimeout_ReturnsFailedResult() 49 | { 50 | // Arrange 51 | _configuration.BuildTimeout = 1; // 1ms timeout to force timeout 52 | var tempProjectFile = Path.GetTempFileName(); 53 | File.WriteAllText(tempProjectFile, "<Project></Project>"); 54 | 55 | try 56 | { 57 | // Act 58 | var result = await _executor.ExecuteBuildAsync(tempProjectFile, "Debug", "Any CPU", true); 59 | 60 | // Assert 61 | // Should return a failed result due to timeout or MSBuild not found 62 | Assert.That(result.Success, Is.False); 63 | } 64 | finally 65 | { 66 | // Cleanup 67 | if (File.Exists(tempProjectFile)) 68 | File.Delete(tempProjectFile); 69 | } 70 | } 71 | 72 | [Test] 73 | public void Constructor_WithValidParameters_CreatesInstance() 74 | { 75 | // Act & Assert 76 | Assert.That(_executor, Is.Not.Null); 77 | } 78 | 79 | [TestCase("Debug", "Any CPU", true)] 80 | [TestCase("Release", "x64", false)] 81 | [TestCase("Debug", "x86", true)] 82 | public async Task ExecuteBuildAsync_WithDifferentConfigurations_HandlesGracefully( 83 | string configuration, string platform, bool restore) 84 | { 85 | // Arrange 86 | var tempProjectFile = Path.GetTempFileName(); 87 | File.WriteAllText(tempProjectFile, "<Project></Project>"); 88 | 89 | try 90 | { 91 | // Act 92 | var result = await _executor.ExecuteBuildAsync(tempProjectFile, configuration, platform, restore); 93 | 94 | // Assert 95 | // Result should be non-null (even if build fails due to no MSBuild) 96 | Assert.That(result, Is.Not.Null); 97 | } 98 | finally 99 | { 100 | // Cleanup 101 | if (File.Exists(tempProjectFile)) 102 | File.Delete(tempProjectFile); 103 | } 104 | } 105 | } ``` -------------------------------------------------------------------------------- /tests/DotNetFrameworkMCP.Server.Tests/Executors/ExecutorFactoryTests.cs: -------------------------------------------------------------------------------- ```csharp 1 | using DotNetFrameworkMCP.Server.Configuration; 2 | using DotNetFrameworkMCP.Server.Executors; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Options; 6 | using Moq; 7 | using NUnit.Framework; 8 | 9 | namespace DotNetFrameworkMCP.Server.Tests.Executors; 10 | 11 | [TestFixture] 12 | public class ExecutorFactoryTests 13 | { 14 | private IServiceProvider _serviceProvider; 15 | private Mock<IOptions<McpServerConfiguration>> _mockOptions; 16 | private McpServerConfiguration _configuration; 17 | 18 | [SetUp] 19 | public void SetUp() 20 | { 21 | _configuration = new McpServerConfiguration(); 22 | _mockOptions = new Mock<IOptions<McpServerConfiguration>>(); 23 | _mockOptions.Setup(x => x.Value).Returns(_configuration); 24 | 25 | var services = new ServiceCollection(); 26 | 27 | // Register executors 28 | services.AddSingleton<MSBuildExecutor>(); 29 | services.AddSingleton<DotNetBuildExecutor>(); 30 | services.AddSingleton<VSTestExecutor>(); 31 | services.AddSingleton<DotNetTestExecutor>(); 32 | 33 | // Register configuration 34 | services.AddSingleton(_mockOptions.Object); 35 | 36 | // Register loggers 37 | services.AddSingleton<ILogger<MSBuildExecutor>>(Mock.Of<ILogger<MSBuildExecutor>>()); 38 | services.AddSingleton<ILogger<DotNetBuildExecutor>>(Mock.Of<ILogger<DotNetBuildExecutor>>()); 39 | services.AddSingleton<ILogger<VSTestExecutor>>(Mock.Of<ILogger<VSTestExecutor>>()); 40 | services.AddSingleton<ILogger<DotNetTestExecutor>>(Mock.Of<ILogger<DotNetTestExecutor>>()); 41 | 42 | _serviceProvider = services.BuildServiceProvider(); 43 | } 44 | 45 | [TearDown] 46 | public void TearDown() 47 | { 48 | if (_serviceProvider is IDisposable disposable) 49 | { 50 | disposable.Dispose(); 51 | } 52 | } 53 | 54 | [Test] 55 | public void CreateBuildExecutor_WhenUseDotNetCliIsFalse_ReturnsMSBuildExecutor() 56 | { 57 | // Arrange 58 | _configuration.UseDotNetCli = false; 59 | var factory = new ExecutorFactory(_serviceProvider, _mockOptions.Object); 60 | 61 | // Act 62 | var executor = factory.CreateBuildExecutor(); 63 | 64 | // Assert 65 | Assert.That(executor, Is.TypeOf<MSBuildExecutor>()); 66 | } 67 | 68 | [Test] 69 | public void CreateBuildExecutor_WhenUseDotNetCliIsTrue_ReturnsDotNetBuildExecutor() 70 | { 71 | // Arrange 72 | _configuration.UseDotNetCli = true; 73 | var factory = new ExecutorFactory(_serviceProvider, _mockOptions.Object); 74 | 75 | // Act 76 | var executor = factory.CreateBuildExecutor(); 77 | 78 | // Assert 79 | Assert.That(executor, Is.TypeOf<DotNetBuildExecutor>()); 80 | } 81 | 82 | [Test] 83 | public void CreateTestExecutor_WhenUseDotNetCliIsFalse_ReturnsVSTestExecutor() 84 | { 85 | // Arrange 86 | _configuration.UseDotNetCli = false; 87 | var factory = new ExecutorFactory(_serviceProvider, _mockOptions.Object); 88 | 89 | // Act 90 | var executor = factory.CreateTestExecutor(); 91 | 92 | // Assert 93 | Assert.That(executor, Is.TypeOf<VSTestExecutor>()); 94 | } 95 | 96 | [Test] 97 | public void CreateTestExecutor_WhenUseDotNetCliIsTrue_ReturnsDotNetTestExecutor() 98 | { 99 | // Arrange 100 | _configuration.UseDotNetCli = true; 101 | var factory = new ExecutorFactory(_serviceProvider, _mockOptions.Object); 102 | 103 | // Act 104 | var executor = factory.CreateTestExecutor(); 105 | 106 | // Assert 107 | Assert.That(executor, Is.TypeOf<DotNetTestExecutor>()); 108 | } 109 | 110 | [Test] 111 | public void Constructor_WithValidParameters_CreatesInstance() 112 | { 113 | // Act 114 | var factory = new ExecutorFactory(_serviceProvider, _mockOptions.Object); 115 | 116 | // Assert 117 | Assert.That(factory, Is.Not.Null); 118 | } 119 | } ``` -------------------------------------------------------------------------------- /tests/DotNetFrameworkMCP.Server.Tests/Tools/BuildProjectHandlerTests.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Text.Json; 2 | using DotNetFrameworkMCP.Server.Configuration; 3 | using DotNetFrameworkMCP.Server.Models; 4 | using DotNetFrameworkMCP.Server.Services; 5 | using DotNetFrameworkMCP.Server.Tools; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace DotNetFrameworkMCP.Server.Tests.Tools; 10 | 11 | [TestFixture] 12 | public class BuildProjectHandlerTests 13 | { 14 | private BuildProjectHandler _handler; 15 | private ILogger<BuildProjectHandler> _logger; 16 | private IOptions<McpServerConfiguration> _configuration; 17 | private MockProcessBasedBuildService _buildService; 18 | 19 | [SetUp] 20 | public void Setup() 21 | { 22 | _logger = new TestLogger<BuildProjectHandler>(); 23 | _configuration = Options.Create(new McpServerConfiguration 24 | { 25 | DefaultConfiguration = "Debug", 26 | DefaultPlatform = "Any CPU", 27 | BuildTimeout = 600000 28 | }); 29 | _buildService = new MockProcessBasedBuildService(); 30 | _handler = new BuildProjectHandler(_logger, _configuration, _buildService); 31 | } 32 | 33 | [Test] 34 | public void GetDefinition_ReturnsCorrectToolDefinition() 35 | { 36 | var definition = _handler.GetDefinition(); 37 | 38 | Assert.That(definition.Name, Is.EqualTo("build_project")); 39 | Assert.That(definition.Description, Is.EqualTo("Build a .NET project or solution")); 40 | Assert.That(definition.InputSchema, Is.Not.Null); 41 | Assert.That(definition.InputSchema.Type, Is.EqualTo("object")); 42 | Assert.That(definition.InputSchema.Properties, Has.Count.EqualTo(4)); 43 | Assert.That(definition.InputSchema.Properties.ContainsKey("path"), Is.True); 44 | Assert.That(definition.InputSchema.Required, Contains.Item("path")); 45 | } 46 | 47 | [Test] 48 | public async Task ExecuteAsync_WithValidRequest_ReturnsSuccessResult() 49 | { 50 | var request = new BuildProjectRequest 51 | { 52 | Path = "test.csproj", 53 | Configuration = "Debug", 54 | Platform = "Any CPU", 55 | Restore = true 56 | }; 57 | 58 | var json = JsonSerializer.Serialize(request); 59 | var element = JsonDocument.Parse(json).RootElement; 60 | 61 | var result = await _handler.ExecuteAsync(element); 62 | 63 | Assert.That(result, Is.Not.Null); 64 | Assert.That(result, Is.TypeOf<BuildResult>()); 65 | 66 | var buildResult = (BuildResult)result; 67 | Assert.That(buildResult.Success, Is.True); 68 | Assert.That(buildResult.Errors, Is.Empty); 69 | Assert.That(buildResult.Warnings, Is.Empty); 70 | Assert.That(buildResult.Output, Is.EqualTo("Build succeeded.")); 71 | } 72 | 73 | [Test] 74 | public void ExecuteAsync_WithInvalidRequest_ThrowsArgumentException() 75 | { 76 | var invalidJson = "{}"; 77 | var element = JsonDocument.Parse(invalidJson).RootElement; 78 | 79 | Assert.ThrowsAsync<ArgumentException>(async () => await _handler.ExecuteAsync(element)); 80 | } 81 | 82 | private class TestLogger<T> : ILogger<T> 83 | { 84 | public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null; 85 | public bool IsEnabled(LogLevel logLevel) => true; 86 | public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { } 87 | } 88 | 89 | private class MockProcessBasedBuildService : IProcessBasedBuildService 90 | { 91 | public Task<Models.BuildResult> BuildProjectAsync(string projectPath, string configuration, string platform, bool restore, CancellationToken cancellationToken = default) 92 | { 93 | return Task.FromResult(new Models.BuildResult 94 | { 95 | Success = true, 96 | Errors = new List<BuildMessage>(), 97 | Warnings = new List<BuildMessage>(), 98 | BuildTime = 2.45, 99 | Output = "Build succeeded." 100 | }); 101 | } 102 | } 103 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Executors/DotNetTestExecutor.cs: -------------------------------------------------------------------------------- ```csharp 1 | using DotNetFrameworkMCP.Server.Configuration; 2 | using DotNetFrameworkMCP.Server.Models; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace DotNetFrameworkMCP.Server.Executors; 7 | 8 | /// <summary> 9 | /// dotnet test-based test executor 10 | /// </summary> 11 | public class DotNetTestExecutor : BaseTestExecutor 12 | { 13 | public DotNetTestExecutor( 14 | ILogger<DotNetTestExecutor> logger, 15 | IOptions<McpServerConfiguration> configuration) 16 | : base(logger, configuration) 17 | { 18 | } 19 | 20 | public override async Task<TestResult> ExecuteTestsAsync( 21 | string projectPath, 22 | string? filter, 23 | bool verbose, 24 | CancellationToken cancellationToken = default) 25 | { 26 | var stopwatch = System.Diagnostics.Stopwatch.StartNew(); 27 | 28 | try 29 | { 30 | // Validate project path 31 | if (!File.Exists(projectPath)) 32 | { 33 | throw new FileNotFoundException($"Test project file not found: {projectPath}"); 34 | } 35 | 36 | var arguments = new List<string> 37 | { 38 | "test", 39 | $"\"{projectPath}\"", 40 | "--no-build" // Assume project is already built 41 | }; 42 | 43 | if (!string.IsNullOrEmpty(filter)) 44 | { 45 | arguments.Add("--filter"); 46 | arguments.Add($"\"{filter}\""); 47 | } 48 | 49 | if (verbose) 50 | { 51 | arguments.Add("--verbosity"); 52 | arguments.Add("detailed"); 53 | } 54 | else 55 | { 56 | arguments.Add("--verbosity"); 57 | arguments.Add("normal"); 58 | } 59 | 60 | // Add logger for structured output 61 | var trxFileName = $"TestResults_{Guid.NewGuid():N}.trx"; 62 | var trxFilePath = Path.Combine(Path.GetTempPath(), trxFileName); 63 | arguments.Add($"--logger"); 64 | arguments.Add($"trx;LogFileName=\"{trxFilePath}\""); 65 | 66 | var argumentString = string.Join(" ", arguments); 67 | _logger.LogInformation("Running dotnet test: {Arguments}", argumentString); 68 | 69 | var result = await RunProcessAsync(_configuration.DotNetPath, argumentString, cancellationToken); 70 | 71 | // Parse results from TRX file 72 | var testResult = await ParseTrxFileAsync(trxFilePath, result.Output); 73 | 74 | // Clean up TRX file 75 | try 76 | { 77 | if (File.Exists(trxFilePath)) 78 | File.Delete(trxFilePath); 79 | } 80 | catch (Exception ex) 81 | { 82 | _logger.LogDebug("Failed to delete TRX file: {Error}", ex.Message); 83 | } 84 | 85 | stopwatch.Stop(); 86 | testResult.Duration = stopwatch.Elapsed.TotalSeconds; 87 | 88 | return testResult; 89 | } 90 | catch (Exception ex) 91 | { 92 | _logger.LogError(ex, "Error running dotnet test for project: {ProjectPath}", projectPath); 93 | stopwatch.Stop(); 94 | 95 | return new TestResult 96 | { 97 | TotalTests = 0, 98 | PassedTests = 0, 99 | FailedTests = 0, 100 | SkippedTests = 0, 101 | Duration = stopwatch.Elapsed.TotalSeconds, 102 | TestDetails = new List<TestDetail> 103 | { 104 | new TestDetail 105 | { 106 | Name = "Test Execution Error", 107 | ClassName = "DotNetTest", 108 | Result = "Failed", 109 | Duration = 0, 110 | ErrorMessage = ex.Message, 111 | StackTrace = ex.StackTrace 112 | } 113 | }, 114 | Output = $"Dotnet test execution failed: {ex.Message}\n{ex.StackTrace}" 115 | }; 116 | } 117 | } 118 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Tools/BuildProjectHandler.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Text.Json; 2 | using DotNetFrameworkMCP.Server.Configuration; 3 | using DotNetFrameworkMCP.Server.Models; 4 | using DotNetFrameworkMCP.Server.Protocol; 5 | using DotNetFrameworkMCP.Server.Services; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace DotNetFrameworkMCP.Server.Tools; 10 | 11 | public class BuildProjectHandler : IToolHandler 12 | { 13 | private readonly ILogger<BuildProjectHandler> _logger; 14 | private readonly McpServerConfiguration _configuration; 15 | private readonly IProcessBasedBuildService _buildService; 16 | 17 | public BuildProjectHandler( 18 | ILogger<BuildProjectHandler> logger, 19 | IOptions<McpServerConfiguration> configuration, 20 | IProcessBasedBuildService buildService) 21 | { 22 | _logger = logger; 23 | _configuration = configuration.Value; 24 | _buildService = buildService; 25 | } 26 | 27 | public string Name => "build_project"; 28 | 29 | public ToolDefinition GetDefinition() 30 | { 31 | return new ToolDefinition 32 | { 33 | Name = Name, 34 | Description = "Build a .NET project or solution", 35 | InputSchema = new JsonSchema 36 | { 37 | Type = "object", 38 | Properties = new Dictionary<string, SchemaProperty> 39 | { 40 | ["path"] = new SchemaProperty 41 | { 42 | Type = "string", 43 | Description = "Path to .csproj or .sln file" 44 | }, 45 | ["configuration"] = new SchemaProperty 46 | { 47 | Type = "string", 48 | Description = "Build configuration", 49 | Enum = new List<string> { "Debug", "Release" }, 50 | Default = "Debug" 51 | }, 52 | ["platform"] = new SchemaProperty 53 | { 54 | Type = "string", 55 | Description = "Target platform", 56 | Enum = new List<string> { "Any CPU", "x86", "x64" }, 57 | Default = "Any CPU" 58 | }, 59 | ["restore"] = new SchemaProperty 60 | { 61 | Type = "boolean", 62 | Description = "Restore NuGet packages", 63 | Default = true 64 | } 65 | }, 66 | Required = new List<string> { "path" } 67 | } 68 | }; 69 | } 70 | 71 | public async Task<object> ExecuteAsync(JsonElement arguments) 72 | { 73 | var request = JsonSerializer.Deserialize<BuildProjectRequest>(arguments.GetRawText()); 74 | if (request == null || string.IsNullOrEmpty(request.Path)) 75 | { 76 | throw new ArgumentException("Invalid build request"); 77 | } 78 | 79 | _logger.LogInformation("Building project: {Path}", request.Path); 80 | 81 | // Use default values from configuration if not specified 82 | var configuration = string.IsNullOrEmpty(request.Configuration) 83 | ? _configuration.DefaultConfiguration 84 | : request.Configuration; 85 | 86 | var platform = string.IsNullOrEmpty(request.Platform) 87 | ? _configuration.DefaultPlatform 88 | : request.Platform; 89 | 90 | // Create cancellation token with timeout 91 | using var cts = new CancellationTokenSource(_configuration.BuildTimeout); 92 | 93 | try 94 | { 95 | _logger.LogDebug("Building project using process-based MSBuild approach"); 96 | return await _buildService.BuildProjectAsync( 97 | request.Path, 98 | configuration, 99 | platform, 100 | request.Restore, 101 | cts.Token); 102 | } 103 | catch (OperationCanceledException) 104 | { 105 | throw new TimeoutException($"Build timed out after {_configuration.BuildTimeout}ms"); 106 | } 107 | } 108 | } ``` -------------------------------------------------------------------------------- /tests/DotNetFrameworkMCP.Server.Tests/Tools/RunTestsHandlerTests.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Text.Json; 2 | using DotNetFrameworkMCP.Server.Configuration; 3 | using DotNetFrameworkMCP.Server.Models; 4 | using DotNetFrameworkMCP.Server.Services; 5 | using DotNetFrameworkMCP.Server.Tools; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Options; 8 | using Moq; 9 | using NUnit.Framework; 10 | 11 | namespace DotNetFrameworkMCP.Server.Tests.Tools; 12 | 13 | [TestFixture] 14 | public class RunTestsHandlerTests 15 | { 16 | private Mock<ILogger<RunTestsHandler>> _mockLogger; 17 | private Mock<ITestRunnerService> _mockTestRunnerService; 18 | private IOptions<McpServerConfiguration> _configuration; 19 | private RunTestsHandler _handler; 20 | 21 | [SetUp] 22 | public void SetUp() 23 | { 24 | _mockLogger = new Mock<ILogger<RunTestsHandler>>(); 25 | _mockTestRunnerService = new Mock<ITestRunnerService>(); 26 | _configuration = Options.Create(new McpServerConfiguration 27 | { 28 | TestTimeout = 300000 29 | }); 30 | 31 | _handler = new RunTestsHandler(_mockLogger.Object, _configuration, _mockTestRunnerService.Object); 32 | } 33 | 34 | [Test] 35 | public void Name_ShouldReturnCorrectName() 36 | { 37 | Assert.That(_handler.Name, Is.EqualTo("run_tests")); 38 | } 39 | 40 | [Test] 41 | public void GetDefinition_ShouldReturnValidDefinition() 42 | { 43 | var definition = _handler.GetDefinition(); 44 | 45 | Assert.That(definition.Name, Is.EqualTo("run_tests")); 46 | Assert.That(definition.Description, Is.EqualTo("Run tests in a .NET test project")); 47 | Assert.That(definition.InputSchema.Type, Is.EqualTo("object")); 48 | Assert.That(definition.InputSchema.Properties, Contains.Key("path")); 49 | Assert.That(definition.InputSchema.Properties["path"].Type, Is.EqualTo("string")); 50 | Assert.That(definition.InputSchema.Required, Contains.Item("path")); 51 | } 52 | 53 | [Test] 54 | public async Task ExecuteAsync_WithValidRequest_ShouldCallTestRunnerService() 55 | { 56 | // Arrange 57 | var expectedResult = new TestResult 58 | { 59 | TotalTests = 5, 60 | PassedTests = 4, 61 | FailedTests = 1, 62 | SkippedTests = 0, 63 | Duration = 2.5, 64 | TestDetails = new List<TestDetail>() 65 | }; 66 | 67 | _mockTestRunnerService 68 | .Setup(x => x.RunTestsAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>())) 69 | .ReturnsAsync(expectedResult); 70 | 71 | var request = new RunTestsRequest 72 | { 73 | Path = "TestProject.csproj", 74 | Filter = "Category=Unit", 75 | Verbose = true 76 | }; 77 | 78 | var jsonElement = JsonSerializer.SerializeToElement(request); 79 | 80 | // Act 81 | var result = await _handler.ExecuteAsync(jsonElement); 82 | 83 | // Assert 84 | Assert.That(result, Is.EqualTo(expectedResult)); 85 | _mockTestRunnerService.Verify( 86 | x => x.RunTestsAsync("TestProject.csproj", "Category=Unit", true, It.IsAny<CancellationToken>()), 87 | Times.Once); 88 | } 89 | 90 | [Test] 91 | public async Task ExecuteAsync_WithMinimalRequest_ShouldUseDefaults() 92 | { 93 | // Arrange 94 | var expectedResult = new TestResult(); 95 | _mockTestRunnerService 96 | .Setup(x => x.RunTestsAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>())) 97 | .ReturnsAsync(expectedResult); 98 | 99 | var request = new RunTestsRequest 100 | { 101 | Path = "TestProject.csproj" 102 | }; 103 | 104 | var jsonElement = JsonSerializer.SerializeToElement(request); 105 | 106 | // Act 107 | var result = await _handler.ExecuteAsync(jsonElement); 108 | 109 | // Assert 110 | _mockTestRunnerService.Verify( 111 | x => x.RunTestsAsync("TestProject.csproj", null, false, It.IsAny<CancellationToken>()), 112 | Times.Once); 113 | } 114 | 115 | [Test] 116 | public void ExecuteAsync_WithNullRequest_ShouldThrowArgumentException() 117 | { 118 | // Arrange 119 | var jsonElement = JsonSerializer.SerializeToElement((object?)null); 120 | 121 | // Act & Assert 122 | Assert.ThrowsAsync<ArgumentException>(() => _handler.ExecuteAsync(jsonElement)); 123 | } 124 | 125 | [Test] 126 | public void ExecuteAsync_WithEmptyPath_ShouldThrowArgumentException() 127 | { 128 | // Arrange 129 | var request = new RunTestsRequest { Path = "" }; 130 | var jsonElement = JsonSerializer.SerializeToElement(request); 131 | 132 | // Act & Assert 133 | Assert.ThrowsAsync<ArgumentException>(() => _handler.ExecuteAsync(jsonElement)); 134 | } 135 | 136 | [Test] 137 | public void ExecuteAsync_WhenServiceTimesOut_ShouldThrowTimeoutException() 138 | { 139 | // Arrange 140 | _mockTestRunnerService 141 | .Setup(x => x.RunTestsAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>())) 142 | .Throws<OperationCanceledException>(); 143 | 144 | var request = new RunTestsRequest { Path = "TestProject.csproj" }; 145 | var jsonElement = JsonSerializer.SerializeToElement(request); 146 | 147 | // Act & Assert 148 | Assert.ThrowsAsync<TimeoutException>(() => _handler.ExecuteAsync(jsonElement)); 149 | } 150 | } ``` -------------------------------------------------------------------------------- /tests/DotNetFrameworkMCP.Server.Tests/Services/TestRunnerServiceTests.cs: -------------------------------------------------------------------------------- ```csharp 1 | using DotNetFrameworkMCP.Server.Executors; 2 | using DotNetFrameworkMCP.Server.Models; 3 | using DotNetFrameworkMCP.Server.Services; 4 | using Microsoft.Extensions.Logging; 5 | using Moq; 6 | using NUnit.Framework; 7 | 8 | namespace DotNetFrameworkMCP.Server.Tests.Services; 9 | 10 | [TestFixture] 11 | public class TestRunnerServiceTests 12 | { 13 | private Mock<ILogger<TestRunnerService>> _mockLogger; 14 | private Mock<IExecutorFactory> _mockExecutorFactory; 15 | private Mock<ITestExecutor> _mockTestExecutor; 16 | private TestRunnerService _service; 17 | private string _tempProjectFile; 18 | 19 | [SetUp] 20 | public void SetUp() 21 | { 22 | _mockLogger = new Mock<ILogger<TestRunnerService>>(); 23 | _mockExecutorFactory = new Mock<IExecutorFactory>(); 24 | _mockTestExecutor = new Mock<ITestExecutor>(); 25 | 26 | _mockExecutorFactory.Setup(x => x.CreateTestExecutor()) 27 | .Returns(_mockTestExecutor.Object); 28 | 29 | _service = new TestRunnerService(_mockLogger.Object, _mockExecutorFactory.Object); 30 | 31 | // Create a temporary project file for testing 32 | _tempProjectFile = Path.GetTempFileName(); 33 | File.WriteAllText(_tempProjectFile, @" 34 | <Project Sdk=""Microsoft.NET.Sdk""> 35 | <PropertyGroup> 36 | <TargetFramework>net48</TargetFramework> 37 | <IsPackable>false</IsPackable> 38 | </PropertyGroup> 39 | <ItemGroup> 40 | <PackageReference Include=""Microsoft.NET.Test.Sdk"" Version=""17.0.0"" /> 41 | <PackageReference Include=""MSTest.TestFramework"" Version=""2.2.7"" /> 42 | <PackageReference Include=""MSTest.TestAdapter"" Version=""2.2.7"" /> 43 | </ItemGroup> 44 | </Project>"); 45 | } 46 | 47 | [TearDown] 48 | public void TearDown() 49 | { 50 | if (File.Exists(_tempProjectFile)) 51 | { 52 | File.Delete(_tempProjectFile); 53 | } 54 | } 55 | 56 | [Test] 57 | public async Task RunTestsAsync_CallsExecutorFactory() 58 | { 59 | // Arrange 60 | var expectedResult = new TestResult 61 | { 62 | TotalTests = 5, 63 | PassedTests = 4, 64 | FailedTests = 1, 65 | SkippedTests = 0, 66 | Duration = 2.5, 67 | TestDetails = new List<TestDetail>(), 68 | Output = "Test output" 69 | }; 70 | 71 | _mockTestExecutor.Setup(x => x.ExecuteTestsAsync( 72 | It.IsAny<string>(), 73 | It.IsAny<string>(), 74 | It.IsAny<bool>(), 75 | It.IsAny<CancellationToken>())) 76 | .ReturnsAsync(expectedResult); 77 | 78 | // Act 79 | var result = await _service.RunTestsAsync(_tempProjectFile, null, false); 80 | 81 | // Assert 82 | Assert.That(result, Is.EqualTo(expectedResult)); 83 | _mockExecutorFactory.Verify(x => x.CreateTestExecutor(), Times.Once); 84 | _mockTestExecutor.Verify(x => x.ExecuteTestsAsync( 85 | _tempProjectFile, null, false, It.IsAny<CancellationToken>()), Times.Once); 86 | } 87 | 88 | [Test] 89 | public async Task RunTestsAsync_WithFilter_PassesFilterToExecutor() 90 | { 91 | // Arrange 92 | var filter = "TestCategory=Unit"; 93 | var expectedResult = new TestResult { TotalTests = 1, PassedTests = 1 }; 94 | 95 | _mockTestExecutor.Setup(x => x.ExecuteTestsAsync( 96 | It.IsAny<string>(), 97 | filter, 98 | It.IsAny<bool>(), 99 | It.IsAny<CancellationToken>())) 100 | .ReturnsAsync(expectedResult); 101 | 102 | // Act 103 | var result = await _service.RunTestsAsync(_tempProjectFile, filter, true); 104 | 105 | // Assert 106 | _mockTestExecutor.Verify(x => x.ExecuteTestsAsync( 107 | _tempProjectFile, filter, true, It.IsAny<CancellationToken>()), Times.Once); 108 | } 109 | 110 | [Test] 111 | public async Task RunTestsAsync_WhenExecutorThrows_ReturnsFailedResult() 112 | { 113 | // Arrange 114 | _mockTestExecutor.Setup(x => x.ExecuteTestsAsync( 115 | It.IsAny<string>(), 116 | It.IsAny<string>(), 117 | It.IsAny<bool>(), 118 | It.IsAny<CancellationToken>())) 119 | .ThrowsAsync(new InvalidOperationException("Test executor failed")); 120 | 121 | // Act 122 | var result = await _service.RunTestsAsync(_tempProjectFile, null, false); 123 | 124 | // Assert 125 | Assert.That(result.TotalTests, Is.EqualTo(0)); 126 | Assert.That(result.PassedTests, Is.EqualTo(0)); 127 | Assert.That(result.FailedTests, Is.EqualTo(0)); 128 | Assert.That(result.SkippedTests, Is.EqualTo(0)); 129 | Assert.That(result.TestDetails, Has.Count.EqualTo(1)); 130 | Assert.That(result.TestDetails[0].Result, Is.EqualTo("Failed")); 131 | Assert.That(result.TestDetails[0].ErrorMessage, Does.Contain("Test executor failed")); 132 | } 133 | 134 | [Test] 135 | public async Task RunTestsAsync_WhenFactoryThrows_ReturnsFailedResult() 136 | { 137 | // Arrange 138 | _mockExecutorFactory.Setup(x => x.CreateTestExecutor()) 139 | .Throws(new InvalidOperationException("Factory failed")); 140 | 141 | // Act 142 | var result = await _service.RunTestsAsync(_tempProjectFile, null, false); 143 | 144 | // Assert 145 | Assert.That(result.TotalTests, Is.EqualTo(0)); 146 | Assert.That(result.TestDetails, Has.Count.EqualTo(1)); 147 | Assert.That(result.TestDetails[0].Result, Is.EqualTo("Failed")); 148 | Assert.That(result.Output, Does.Contain("Test service failed")); 149 | } 150 | } ``` -------------------------------------------------------------------------------- /tests/DotNetFrameworkMCP.Server.Tests/Services/ProcessBasedBuildServiceTests.cs: -------------------------------------------------------------------------------- ```csharp 1 | using DotNetFrameworkMCP.Server.Executors; 2 | using DotNetFrameworkMCP.Server.Models; 3 | using DotNetFrameworkMCP.Server.Services; 4 | using Microsoft.Extensions.Logging; 5 | using Moq; 6 | using NUnit.Framework; 7 | 8 | namespace DotNetFrameworkMCP.Server.Tests.Services; 9 | 10 | [TestFixture] 11 | public class ProcessBasedBuildServiceTests 12 | { 13 | private Mock<ILogger<ProcessBasedBuildService>> _mockLogger; 14 | private Mock<IExecutorFactory> _mockExecutorFactory; 15 | private Mock<IBuildExecutor> _mockBuildExecutor; 16 | private ProcessBasedBuildService _service; 17 | private string _tempProjectFile; 18 | 19 | [SetUp] 20 | public void SetUp() 21 | { 22 | _mockLogger = new Mock<ILogger<ProcessBasedBuildService>>(); 23 | _mockExecutorFactory = new Mock<IExecutorFactory>(); 24 | _mockBuildExecutor = new Mock<IBuildExecutor>(); 25 | 26 | _mockExecutorFactory.Setup(x => x.CreateBuildExecutor()) 27 | .Returns(_mockBuildExecutor.Object); 28 | 29 | _service = new ProcessBasedBuildService(_mockLogger.Object, _mockExecutorFactory.Object); 30 | 31 | // Create a temporary project file for testing 32 | _tempProjectFile = Path.GetTempFileName(); 33 | File.WriteAllText(_tempProjectFile, @" 34 | <Project Sdk=""Microsoft.NET.Sdk""> 35 | <PropertyGroup> 36 | <TargetFramework>net48</TargetFramework> 37 | </PropertyGroup> 38 | </Project>"); 39 | } 40 | 41 | [TearDown] 42 | public void TearDown() 43 | { 44 | if (File.Exists(_tempProjectFile)) 45 | { 46 | File.Delete(_tempProjectFile); 47 | } 48 | } 49 | 50 | [Test] 51 | public async Task BuildProjectAsync_CallsExecutorFactory() 52 | { 53 | // Arrange 54 | var expectedResult = new BuildResult 55 | { 56 | Success = true, 57 | BuildTime = 5.2, 58 | Errors = new List<BuildMessage>(), 59 | Warnings = new List<BuildMessage>(), 60 | Output = "Build succeeded" 61 | }; 62 | 63 | _mockBuildExecutor.Setup(x => x.ExecuteBuildAsync( 64 | It.IsAny<string>(), 65 | It.IsAny<string>(), 66 | It.IsAny<string>(), 67 | It.IsAny<bool>(), 68 | It.IsAny<CancellationToken>())) 69 | .ReturnsAsync(expectedResult); 70 | 71 | // Act 72 | var result = await _service.BuildProjectAsync(_tempProjectFile, "Debug", "Any CPU", true); 73 | 74 | // Assert 75 | Assert.That(result, Is.EqualTo(expectedResult)); 76 | _mockExecutorFactory.Verify(x => x.CreateBuildExecutor(), Times.Once); 77 | _mockBuildExecutor.Verify(x => x.ExecuteBuildAsync( 78 | _tempProjectFile, "Debug", "Any CPU", true, It.IsAny<CancellationToken>()), Times.Once); 79 | } 80 | 81 | [Test] 82 | public async Task BuildProjectAsync_WithDifferentParameters_PassesToExecutor() 83 | { 84 | // Arrange 85 | var expectedResult = new BuildResult { Success = true }; 86 | var configuration = "Release"; 87 | var platform = "x64"; 88 | var restore = false; 89 | 90 | _mockBuildExecutor.Setup(x => x.ExecuteBuildAsync( 91 | It.IsAny<string>(), 92 | configuration, 93 | platform, 94 | restore, 95 | It.IsAny<CancellationToken>())) 96 | .ReturnsAsync(expectedResult); 97 | 98 | // Act 99 | var result = await _service.BuildProjectAsync(_tempProjectFile, configuration, platform, restore); 100 | 101 | // Assert 102 | _mockBuildExecutor.Verify(x => x.ExecuteBuildAsync( 103 | _tempProjectFile, configuration, platform, restore, It.IsAny<CancellationToken>()), Times.Once); 104 | } 105 | 106 | [Test] 107 | public async Task BuildProjectAsync_WhenExecutorThrows_ReturnsFailedResult() 108 | { 109 | // Arrange 110 | _mockBuildExecutor.Setup(x => x.ExecuteBuildAsync( 111 | It.IsAny<string>(), 112 | It.IsAny<string>(), 113 | It.IsAny<string>(), 114 | It.IsAny<bool>(), 115 | It.IsAny<CancellationToken>())) 116 | .ThrowsAsync(new InvalidOperationException("Build executor failed")); 117 | 118 | // Act 119 | var result = await _service.BuildProjectAsync(_tempProjectFile, "Debug", "Any CPU", true); 120 | 121 | // Assert 122 | Assert.That(result.Success, Is.False); 123 | Assert.That(result.Errors, Has.Count.EqualTo(1)); 124 | Assert.That(result.Errors[0].Message, Does.Contain("Build executor failed")); 125 | Assert.That(result.Output, Does.Contain("Build service failed")); 126 | } 127 | 128 | [Test] 129 | public async Task BuildProjectAsync_WhenFactoryThrows_ReturnsFailedResult() 130 | { 131 | // Arrange 132 | _mockExecutorFactory.Setup(x => x.CreateBuildExecutor()) 133 | .Throws(new InvalidOperationException("Factory failed")); 134 | 135 | // Act 136 | var result = await _service.BuildProjectAsync(_tempProjectFile, "Debug", "Any CPU", true); 137 | 138 | // Assert 139 | Assert.That(result.Success, Is.False); 140 | Assert.That(result.Errors, Has.Count.EqualTo(1)); 141 | Assert.That(result.Errors[0].Message, Does.Contain("Factory failed")); 142 | Assert.That(result.BuildTime, Is.EqualTo(0)); 143 | } 144 | 145 | [Test] 146 | public async Task BuildProjectAsync_WithCancellationToken_PassesToExecutor() 147 | { 148 | // Arrange 149 | var expectedResult = new BuildResult { Success = true }; 150 | var cancellationTokenSource = new CancellationTokenSource(); 151 | 152 | _mockBuildExecutor.Setup(x => x.ExecuteBuildAsync( 153 | It.IsAny<string>(), 154 | It.IsAny<string>(), 155 | It.IsAny<string>(), 156 | It.IsAny<bool>(), 157 | cancellationTokenSource.Token)) 158 | .ReturnsAsync(expectedResult); 159 | 160 | // Act 161 | var result = await _service.BuildProjectAsync(_tempProjectFile, "Debug", "Any CPU", true, cancellationTokenSource.Token); 162 | 163 | // Assert 164 | _mockBuildExecutor.Verify(x => x.ExecuteBuildAsync( 165 | _tempProjectFile, "Debug", "Any CPU", true, cancellationTokenSource.Token), Times.Once); 166 | } 167 | } ``` -------------------------------------------------------------------------------- /ProjectPlan.md: -------------------------------------------------------------------------------- ```markdown 1 | # .NET Framework MCP Service Project Plan 2 | 3 | ## Project Overview 4 | 5 | Build a Model Context Protocol (MCP) service that enables Claude Code to build, test, and run .NET Framework projects. The service will provide a standardized interface for interacting with MSBuild, test runners, and other .NET tooling. 6 | 7 | ## Core Requirements 8 | 9 | ### 1. Build Operations 10 | - Trigger MSBuild for solutions and projects 11 | - Support multiple build configurations (Debug/Release) 12 | - Support multiple platforms (Any CPU, x86, x64) 13 | - Handle NuGet package restoration 14 | - Parse and return structured build output (errors, warnings, success status) 15 | 16 | ### 2. Test Operations 17 | - Discover test projects and test methods 18 | - Execute all tests or specific test selections 19 | - Support multiple test frameworks (MSTest, NUnit, xUnit) 20 | - Parse and return test results with pass/fail counts and error details 21 | 22 | ### 3. Run Operations 23 | - Execute console applications with arguments 24 | - Capture and return console output 25 | - Handle process termination and timeouts 26 | 27 | ### 4. Project Analysis 28 | - List projects in a solution 29 | - Show project dependencies 30 | - Display project properties and configurations 31 | - List NuGet packages and versions 32 | 33 | ## Technical Architecture 34 | 35 | ### Technology Stack 36 | - **Language**: C# (.NET 6+ for the MCP service itself) 37 | - **MCP SDK**: Use official MCP SDK for C# (if available) or implement protocol directly 38 | - **Dependencies**: 39 | - Microsoft.Build for MSBuild integration 40 | - Microsoft.Build.Locator for finding MSBuild installations 41 | - Test framework APIs for test discovery/execution 42 | 43 | ### MCP Methods to Implement 44 | 45 | ``` 46 | tools: 47 | - name: build_project 48 | description: Build a .NET project or solution 49 | inputSchema: 50 | type: object 51 | properties: 52 | path: { type: string, description: "Path to .csproj or .sln file" } 53 | configuration: { type: string, enum: ["Debug", "Release"], default: "Debug" } 54 | platform: { type: string, enum: ["Any CPU", "x86", "x64"], default: "Any CPU" } 55 | restore: { type: boolean, default: true, description: "Restore NuGet packages" } 56 | 57 | - name: run_tests 58 | description: Run tests in a .NET test project 59 | inputSchema: 60 | type: object 61 | properties: 62 | path: { type: string, description: "Path to test project" } 63 | filter: { type: string, description: "Test filter expression" } 64 | verbose: { type: boolean, default: false } 65 | 66 | - name: run_project 67 | description: Execute a .NET console application 68 | inputSchema: 69 | type: object 70 | properties: 71 | path: { type: string, description: "Path to project" } 72 | args: { type: array, items: { type: string } } 73 | workingDirectory: { type: string } 74 | 75 | - name: analyze_solution 76 | description: Get information about a solution structure 77 | inputSchema: 78 | type: object 79 | properties: 80 | path: { type: string, description: "Path to .sln file" } 81 | 82 | - name: list_packages 83 | description: List NuGet packages in a project 84 | inputSchema: 85 | type: object 86 | properties: 87 | path: { type: string, description: "Path to project" } 88 | ``` 89 | 90 | ## Implementation Phases 91 | 92 | ### Phase 1: Core Infrastructure (Week 1) ✅ COMPLETED 93 | - [x] Set up C# project structure 94 | - [x] Implement MCP protocol handling with JSON-RPC support 95 | - [x] Create basic server lifecycle (start, stop, health check) 96 | - [x] Implement logging framework with configurable verbosity 97 | - [x] Add configuration management with environment variable support 98 | - [x] Switch test framework to NUnit 99 | - [x] Create initial unit tests 100 | 101 | ### Phase 2: Build Functionality (Week 2) ✅ COMPLETED 102 | - [x] Implement MSBuild locator logic with Visual Studio version selection 103 | - [x] Create build_project method 104 | - [x] Add build output parsing with intelligent truncation 105 | - [x] Implement error/warning extraction 106 | - [x] Add NuGet restore functionality 107 | - [x] Add TCP server support for cross-platform communication 108 | - [x] Create WSL-to-Windows bridge for Claude Code integration 109 | - [x] Implement build cancellation and timeout handling 110 | - [x] Add MCP token limit compliance (25k token limit) 111 | 112 | ### Phase 3: Test Runner Integration (Week 3) ✅ COMPLETED 113 | - [x] Implement test discovery logic 114 | - [x] Add support for MSTest runner 115 | - [x] Add support for NUnit runner 116 | - [x] Add support for xUnit runner 117 | - [x] Create test result parsing with TRX file support 118 | - [x] Implement test filtering 119 | - [x] Add comprehensive error message and stack trace extraction 120 | - [x] Implement test adapter discovery and integration 121 | - [x] Add solution-based building for proper test configuration 122 | 123 | ### Phase 4: Project Execution (Week 4) 124 | - [ ] Implement run_project method 125 | - [ ] Add process management 126 | - [ ] Implement output capture 127 | - [ ] Add timeout handling 128 | - [ ] Handle process termination 129 | - [ ] Write unit tests for execution operations 130 | 131 | ### Phase 5: Analysis Features (Week 5) 132 | - [ ] Implement solution analysis 133 | - [ ] Add project dependency mapping 134 | - [ ] Create package listing functionality 135 | - [ ] Add project property inspection 136 | - [ ] Write unit tests for analysis operations 137 | 138 | ### Phase 6: Polish & Documentation (Week 6) 139 | - [ ] Comprehensive error handling 140 | - [ ] Performance optimization 141 | - [ ] Add integration tests 142 | - [ ] Write user documentation 143 | - [ ] Create example usage scenarios 144 | - [ ] Package for distribution 145 | 146 | ## Key Implementation Details 147 | 148 | ### MSBuild Integration 149 | ```csharp 150 | // Use Microsoft.Build.Locator to find MSBuild 151 | MSBuildLocator.RegisterDefaults(); 152 | 153 | // Use Microsoft.Build API for building 154 | var projectCollection = new ProjectCollection(); 155 | var project = projectCollection.LoadProject(projectPath); 156 | ``` 157 | 158 | ### Output Parsing Strategy 159 | - Use MSBuild loggers to capture structured output 160 | - Implement custom logger for JSON-formatted results 161 | - Parse compiler error format: `file(line,col): error CODE: message` 162 | 163 | ### Test Runner Integration 164 | - Use VSTest.Console.exe for universal test execution 165 | - Parse TRX files for structured results 166 | - Support test filtering using standard syntax 167 | 168 | ### Error Handling 169 | - Graceful handling of missing MSBuild installations 170 | - Clear error messages for missing dependencies 171 | - Timeout handling for long-running operations 172 | - Process cleanup on service shutdown 173 | 174 | ## Configuration Schema 175 | 176 | ```json 177 | { 178 | "McpServer": { 179 | "MsBuildPath": "auto", 180 | "DefaultConfiguration": "Debug", 181 | "DefaultPlatform": "Any CPU", 182 | "TestTimeout": 300000, 183 | "BuildTimeout": 1200000, 184 | "EnableDetailedLogging": false, 185 | "PreferredVSVersion": "2022" 186 | } 187 | } 188 | ``` 189 | 190 | ## Testing Strategy 191 | 192 | ### Unit Tests 193 | - Mock MSBuild API calls 194 | - Test output parsing logic 195 | - Verify error handling paths 196 | - Test configuration management 197 | 198 | ### Integration Tests 199 | - Use sample .NET projects 200 | - Test full build/test/run cycles 201 | - Verify cross-framework compatibility 202 | - Test error scenarios (missing files, bad syntax) 203 | 204 | ## Distribution Plan 205 | 206 | 1. **NuGet Package**: Primary distribution as a .NET tool 207 | 2. **GitHub Releases**: Compiled binaries with installation script 208 | 3. **Docker Image**: Containerized version with MSBuild pre-installed 209 | 210 | ## Success Criteria 211 | 212 | - Successfully builds complex multi-project solutions 213 | - Accurately reports build errors and warnings 214 | - Runs tests from all major test frameworks 215 | - Provides clear, actionable error messages 216 | - Performs operations within reasonable time limits 217 | - Maintains stability during long-running operations 218 | 219 | ## Future Enhancements 220 | 221 | ### Security & Authentication 222 | - API key or token-based authentication 223 | - Role-based access control for different operations 224 | - Audit logging for security compliance 225 | - Network access restrictions and whitelisting 226 | 227 | ### Extended Platform Support 228 | - Support for .NET Core/.NET 5+ projects 229 | - Web project launching with browser integration 230 | - Support for F# and VB.NET projects 231 | 232 | ### Advanced Development Features 233 | - Code coverage reporting 234 | - Incremental build support 235 | - Watch mode for continuous building/testing 236 | - Integration with code analyzers 237 | - Performance profiling and diagnostics 238 | 239 | ## Resources & References 240 | 241 | - [MCP Specification](https://modelcontextprotocol.io/docs) 242 | - [MSBuild API Documentation](https://docs.microsoft.com/en-us/dotnet/api/microsoft.build) 243 | - [VSTest Documentation](https://docs.microsoft.com/en-us/visualstudio/test/vstest-console-options) 244 | - [NuGet API Reference](https://docs.microsoft.com/en-us/nuget/api/overview) ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Executors/BaseTestExecutor.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Diagnostics; 2 | using System.Text; 3 | using System.Xml.Linq; 4 | using DotNetFrameworkMCP.Server.Configuration; 5 | using DotNetFrameworkMCP.Server.Models; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace DotNetFrameworkMCP.Server.Executors; 10 | 11 | /// <summary> 12 | /// Base class for test executors with common functionality 13 | /// </summary> 14 | public abstract class BaseTestExecutor : ITestExecutor 15 | { 16 | protected readonly ILogger _logger; 17 | protected readonly McpServerConfiguration _configuration; 18 | 19 | protected BaseTestExecutor(ILogger logger, IOptions<McpServerConfiguration> configuration) 20 | { 21 | _logger = logger; 22 | _configuration = configuration.Value; 23 | } 24 | 25 | public abstract Task<TestResult> ExecuteTestsAsync( 26 | string projectPath, 27 | string? filter, 28 | bool verbose, 29 | CancellationToken cancellationToken = default); 30 | 31 | protected async Task<(string Output, int ExitCode)> RunProcessAsync( 32 | string fileName, 33 | string arguments, 34 | CancellationToken cancellationToken) 35 | { 36 | _logger.LogDebug("Running: {FileName} {Arguments}", fileName, arguments); 37 | 38 | using var process = new Process(); 39 | process.StartInfo.FileName = fileName; 40 | process.StartInfo.Arguments = arguments; 41 | process.StartInfo.UseShellExecute = false; 42 | process.StartInfo.RedirectStandardOutput = true; 43 | process.StartInfo.RedirectStandardError = true; 44 | process.StartInfo.CreateNoWindow = true; 45 | 46 | var output = new StringBuilder(); 47 | process.OutputDataReceived += (sender, e) => 48 | { 49 | if (e.Data != null) 50 | { 51 | output.AppendLine(e.Data); 52 | } 53 | }; 54 | 55 | process.ErrorDataReceived += (sender, e) => 56 | { 57 | if (e.Data != null) 58 | { 59 | output.AppendLine(e.Data); 60 | } 61 | }; 62 | 63 | process.Start(); 64 | process.BeginOutputReadLine(); 65 | process.BeginErrorReadLine(); 66 | 67 | // Wait for completion with timeout and cancellation support 68 | var timeoutMs = _configuration.TestTimeout; 69 | using var timeoutCts = new CancellationTokenSource(timeoutMs); 70 | using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); 71 | 72 | try 73 | { 74 | await process.WaitForExitAsync(combinedCts.Token); 75 | } 76 | catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) 77 | { 78 | _logger.LogWarning("Test execution timed out after {TimeoutMs}ms, killing process", timeoutMs); 79 | process.Kill(); 80 | throw new TimeoutException($"Test execution timed out after {timeoutMs}ms"); 81 | } 82 | catch (OperationCanceledException) 83 | { 84 | _logger.LogInformation("Test execution cancelled by user, killing process"); 85 | process.Kill(); 86 | throw; 87 | } 88 | 89 | return (output.ToString(), process.ExitCode); 90 | } 91 | 92 | protected async Task<TestResult> ParseTrxFileAsync(string trxFilePath, string consoleOutput) 93 | { 94 | var testDetails = new List<TestDetail>(); 95 | 96 | try 97 | { 98 | if (File.Exists(trxFilePath)) 99 | { 100 | var trxContent = await File.ReadAllTextAsync(trxFilePath); 101 | _logger.LogDebug("Parsing TRX file: {TrxFilePath}", trxFilePath); 102 | 103 | var doc = XDocument.Parse(trxContent); 104 | var ns = doc.Root?.GetDefaultNamespace(); 105 | 106 | if (ns != null) 107 | { 108 | // Parse test results 109 | var unitTestResults = doc.Descendants(ns + "UnitTestResult"); 110 | 111 | foreach (var result in unitTestResults) 112 | { 113 | var testId = result.Attribute("testId")?.Value; 114 | var testName = result.Attribute("testName")?.Value ?? "Unknown Test"; 115 | var outcome = result.Attribute("outcome")?.Value ?? "Unknown"; 116 | var duration = result.Attribute("duration")?.Value; 117 | 118 | // Try to find the test definition to get class information 119 | var className = "Unknown"; 120 | if (!string.IsNullOrEmpty(testId)) 121 | { 122 | var testDefinition = doc.Descendants(ns + "UnitTest") 123 | .FirstOrDefault(t => t.Attribute("id")?.Value == testId); 124 | 125 | if (testDefinition != null) 126 | { 127 | var testMethod = testDefinition.Descendants(ns + "TestMethod").FirstOrDefault(); 128 | className = testMethod?.Attribute("className")?.Value ?? "Unknown"; 129 | } 130 | } 131 | 132 | // Parse duration 133 | var durationSeconds = 0.0; 134 | if (!string.IsNullOrEmpty(duration) && TimeSpan.TryParse(duration, out var durationTimeSpan)) 135 | { 136 | durationSeconds = durationTimeSpan.TotalSeconds; 137 | } 138 | 139 | // Get error message and stack trace for failed tests 140 | string? errorMessage = null; 141 | string? stackTrace = null; 142 | 143 | if (outcome == "Failed") 144 | { 145 | var output = result.Element(ns + "Output"); 146 | var errorInfo = output?.Element(ns + "ErrorInfo"); 147 | errorMessage = errorInfo?.Element(ns + "Message")?.Value; 148 | stackTrace = errorInfo?.Element(ns + "StackTrace")?.Value; 149 | } 150 | 151 | testDetails.Add(new TestDetail 152 | { 153 | Name = testName, 154 | ClassName = className, 155 | Result = outcome, 156 | Duration = durationSeconds, 157 | ErrorMessage = errorMessage, 158 | StackTrace = stackTrace 159 | }); 160 | } 161 | } 162 | } 163 | 164 | // If no tests were parsed from TRX, fall back to console output parsing 165 | if (testDetails.Count == 0) 166 | { 167 | _logger.LogWarning("No tests found in TRX file, falling back to console output parsing"); 168 | ParseConsoleOutput(consoleOutput, testDetails); 169 | } 170 | } 171 | catch (Exception ex) 172 | { 173 | _logger.LogError(ex, "Error parsing TRX file: {TrxFilePath}", trxFilePath); 174 | // Fall back to console output parsing 175 | ParseConsoleOutput(consoleOutput, testDetails); 176 | } 177 | 178 | return new TestResult 179 | { 180 | TotalTests = testDetails.Count, 181 | PassedTests = testDetails.Count(t => t.Result == "Passed"), 182 | FailedTests = testDetails.Count(t => t.Result == "Failed"), 183 | SkippedTests = testDetails.Count(t => t.Result == "Skipped"), 184 | TestDetails = testDetails, 185 | Output = consoleOutput 186 | }; 187 | } 188 | 189 | private void ParseConsoleOutput(string output, List<TestDetail> testDetails) 190 | { 191 | if (string.IsNullOrEmpty(output)) 192 | return; 193 | 194 | var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries); 195 | 196 | foreach (var line in lines) 197 | { 198 | // Simple pattern matching for common test output formats 199 | if (line.Contains("Passed") || line.Contains("Failed") || line.Contains("Skipped")) 200 | { 201 | var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); 202 | if (parts.Length >= 2) 203 | { 204 | var result = parts.FirstOrDefault(p => p == "Passed" || p == "Failed" || p == "Skipped"); 205 | if (result != null) 206 | { 207 | testDetails.Add(new TestDetail 208 | { 209 | Name = line.Trim(), 210 | ClassName = "ParsedFromConsole", 211 | Result = result, 212 | Duration = 0, 213 | ErrorMessage = result == "Failed" ? "Failed (parsed from console output)" : null, 214 | StackTrace = null 215 | }); 216 | } 217 | } 218 | } 219 | } 220 | } 221 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Services/TcpMcpServer.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Net; 2 | using System.Net.Sockets; 3 | using System.Text; 4 | using System.Text.Json; 5 | using DotNetFrameworkMCP.Server.Configuration; 6 | using DotNetFrameworkMCP.Server.Protocol; 7 | using DotNetFrameworkMCP.Server.Tools; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Extensions.Options; 10 | 11 | namespace DotNetFrameworkMCP.Server.Services; 12 | 13 | public class TcpMcpServer 14 | { 15 | private readonly ILogger<TcpMcpServer> _logger; 16 | private readonly McpServerConfiguration _configuration; 17 | private readonly Dictionary<string, IToolHandler> _toolHandlers; 18 | private readonly JsonSerializerOptions _jsonOptions; 19 | private TcpListener? _listener; 20 | 21 | public TcpMcpServer( 22 | ILogger<TcpMcpServer> logger, 23 | IOptions<McpServerConfiguration> configuration, 24 | IEnumerable<IToolHandler> toolHandlers) 25 | { 26 | _logger = logger; 27 | _configuration = configuration.Value; 28 | _toolHandlers = toolHandlers.ToDictionary(h => h.Name); 29 | _jsonOptions = new JsonSerializerOptions 30 | { 31 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 32 | WriteIndented = false 33 | }; 34 | } 35 | 36 | public async Task RunAsync(int port = 3001, CancellationToken cancellationToken = default) 37 | { 38 | _listener = new TcpListener(IPAddress.Any, port); 39 | _listener.Start(); 40 | 41 | _logger.LogInformation("TCP MCP Server listening on port {Port}", port); 42 | 43 | // Register cancellation callback to stop the listener 44 | using var registration = cancellationToken.Register(() => 45 | { 46 | _logger.LogInformation("Cancellation requested, stopping TCP listener"); 47 | _listener?.Stop(); 48 | }); 49 | 50 | try 51 | { 52 | while (!cancellationToken.IsCancellationRequested) 53 | { 54 | var client = await _listener.AcceptTcpClientAsync().WaitAsync(cancellationToken); 55 | _logger.LogInformation("Client connected from {RemoteEndPoint}", client.Client.RemoteEndPoint); 56 | 57 | // Handle each client in a separate task 58 | _ = Task.Run(async () => await HandleClientAsync(client, cancellationToken), cancellationToken); 59 | } 60 | } 61 | catch (OperationCanceledException) 62 | { 63 | // Expected when cancellation occurs 64 | _logger.LogInformation("TCP server shutdown requested"); 65 | } 66 | catch (ObjectDisposedException) 67 | { 68 | // Expected when cancellation occurs 69 | } 70 | finally 71 | { 72 | _listener?.Stop(); 73 | _listener = null; 74 | _logger.LogInformation("TCP MCP Server stopped"); 75 | } 76 | } 77 | 78 | private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken) 79 | { 80 | try 81 | { 82 | using (client) 83 | { 84 | var stream = client.GetStream(); 85 | var buffer = new byte[4096]; 86 | var messageBuffer = new List<byte>(); 87 | 88 | while (!cancellationToken.IsCancellationRequested && client.Connected) 89 | { 90 | var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken); 91 | if (bytesRead == 0) 92 | break; 93 | 94 | messageBuffer.AddRange(buffer.Take(bytesRead)); 95 | 96 | // Process complete messages (look for newline delimiters) 97 | while (true) 98 | { 99 | var newlineIndex = messageBuffer.IndexOf((byte)'\n'); 100 | if (newlineIndex == -1) 101 | break; 102 | 103 | var messageBytes = messageBuffer.Take(newlineIndex).ToArray(); 104 | messageBuffer.RemoveRange(0, newlineIndex + 1); 105 | 106 | if (messageBytes.Length > 0) 107 | { 108 | var response = await ProcessMessageAsync(messageBytes); 109 | if (response != null) 110 | { 111 | var responseJson = JsonSerializer.Serialize(response, _jsonOptions); 112 | var responseBytes = Encoding.UTF8.GetBytes(responseJson + "\n"); 113 | await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken); 114 | await stream.FlushAsync(cancellationToken); 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | catch (Exception ex) 122 | { 123 | _logger.LogError(ex, "Error handling client"); 124 | } 125 | } 126 | 127 | private async Task<McpMessage?> ProcessMessageAsync(byte[] messageBytes) 128 | { 129 | try 130 | { 131 | var message = JsonSerializer.Deserialize<McpMessage>(messageBytes, _jsonOptions); 132 | if (message == null) 133 | { 134 | return CreateErrorResponse(null, McpErrorCodes.ParseError, "Failed to parse message"); 135 | } 136 | 137 | _logger.LogDebug("Received message: {Method}", message.Method); 138 | 139 | // Handle different MCP methods 140 | switch (message.Method) 141 | { 142 | case "initialize": 143 | return await HandleInitializeAsync(message); 144 | 145 | case "tools/list": 146 | return await HandleToolsListAsync(message); 147 | 148 | case "tools/call": 149 | return await HandleToolCallAsync(message); 150 | 151 | case "notifications/cancelled": 152 | // Handle cancellation notification from client 153 | _logger.LogInformation("Received cancellation notification from client"); 154 | return null; // Notifications don't require responses 155 | 156 | case null when message.Id != null: 157 | // This is likely a response to a request we made 158 | return null; 159 | 160 | default: 161 | _logger.LogWarning("Unknown method received: {Method}", message.Method); 162 | return CreateErrorResponse( 163 | message.Id, 164 | McpErrorCodes.MethodNotFound, 165 | $"Method '{message.Method}' not found"); 166 | } 167 | } 168 | catch (JsonException ex) 169 | { 170 | _logger.LogError(ex, "Failed to parse JSON message"); 171 | return CreateErrorResponse(null, McpErrorCodes.ParseError, "Invalid JSON"); 172 | } 173 | catch (Exception ex) 174 | { 175 | _logger.LogError(ex, "Error processing message"); 176 | return CreateErrorResponse(null, McpErrorCodes.InternalError, ex.Message); 177 | } 178 | } 179 | 180 | private Task<McpMessage> HandleInitializeAsync(McpMessage request) 181 | { 182 | var response = new McpMessage 183 | { 184 | Id = request.Id, 185 | Result = new 186 | { 187 | protocolVersion = "2025-06-18", 188 | capabilities = new 189 | { 190 | tools = new { } 191 | }, 192 | serverInfo = new 193 | { 194 | name = "dotnet-framework-mcp-tcp", 195 | version = "1.0.0" 196 | } 197 | } 198 | }; 199 | 200 | return Task.FromResult(response); 201 | } 202 | 203 | private Task<McpMessage> HandleToolsListAsync(McpMessage request) 204 | { 205 | var tools = _toolHandlers.Values.Select(h => h.GetDefinition()).ToList(); 206 | 207 | var response = new McpMessage 208 | { 209 | Id = request.Id, 210 | Result = new { tools } 211 | }; 212 | 213 | return Task.FromResult(response); 214 | } 215 | 216 | private async Task<McpMessage> HandleToolCallAsync(McpMessage request) 217 | { 218 | try 219 | { 220 | var toolCallParams = JsonSerializer.Deserialize<ToolCallParams>( 221 | JsonSerializer.Serialize(request.Params), 222 | _jsonOptions); 223 | 224 | if (toolCallParams == null || string.IsNullOrEmpty(toolCallParams.Name)) 225 | { 226 | return CreateErrorResponse( 227 | request.Id, 228 | McpErrorCodes.InvalidParams, 229 | "Invalid tool call parameters"); 230 | } 231 | 232 | if (!_toolHandlers.TryGetValue(toolCallParams.Name, out var handler)) 233 | { 234 | return CreateErrorResponse( 235 | request.Id, 236 | McpErrorCodes.InvalidParams, 237 | $"Tool '{toolCallParams.Name}' not found"); 238 | } 239 | 240 | var result = await handler.ExecuteAsync(toolCallParams.Arguments); 241 | 242 | return new McpMessage 243 | { 244 | Id = request.Id, 245 | Result = new 246 | { 247 | content = new[] 248 | { 249 | new 250 | { 251 | type = "text", 252 | text = JsonSerializer.Serialize(result, _jsonOptions) 253 | } 254 | } 255 | } 256 | }; 257 | } 258 | catch (Exception ex) 259 | { 260 | _logger.LogError(ex, "Error executing tool"); 261 | return CreateErrorResponse( 262 | request.Id, 263 | McpErrorCodes.InternalError, 264 | $"Tool execution failed: {ex.Message}"); 265 | } 266 | } 267 | 268 | private McpMessage CreateErrorResponse(object? id, int code, string message) 269 | { 270 | return new McpMessage 271 | { 272 | Id = id, 273 | Error = new McpError 274 | { 275 | Code = code, 276 | Message = message 277 | } 278 | }; 279 | } 280 | 281 | private class ToolCallParams 282 | { 283 | public string Name { get; set; } = string.Empty; 284 | public JsonElement Arguments { get; set; } 285 | } 286 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Executors/DotNetBuildExecutor.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Diagnostics; 2 | using System.Text; 3 | using System.Text.RegularExpressions; 4 | using DotNetFrameworkMCP.Server.Configuration; 5 | using DotNetFrameworkMCP.Server.Models; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace DotNetFrameworkMCP.Server.Executors; 10 | 11 | /// <summary> 12 | /// dotnet CLI-based build executor 13 | /// </summary> 14 | public class DotNetBuildExecutor : IBuildExecutor 15 | { 16 | private readonly ILogger<DotNetBuildExecutor> _logger; 17 | private readonly McpServerConfiguration _configuration; 18 | 19 | public DotNetBuildExecutor( 20 | ILogger<DotNetBuildExecutor> logger, 21 | IOptions<McpServerConfiguration> configuration) 22 | { 23 | _logger = logger; 24 | _configuration = configuration.Value; 25 | } 26 | 27 | public async Task<BuildResult> ExecuteBuildAsync( 28 | string projectPath, 29 | string configuration, 30 | string platform, 31 | bool restore, 32 | CancellationToken cancellationToken = default) 33 | { 34 | var stopwatch = Stopwatch.StartNew(); 35 | var errors = new List<BuildMessage>(); 36 | var warnings = new List<BuildMessage>(); 37 | 38 | try 39 | { 40 | // Validate project path 41 | if (!File.Exists(projectPath)) 42 | { 43 | throw new FileNotFoundException($"Project file not found: {projectPath}"); 44 | } 45 | 46 | _logger.LogInformation("Using dotnet CLI for build"); 47 | 48 | // Build the project using dotnet CLI 49 | var result = await RunDotNetBuildAsync(projectPath, configuration, platform, restore, cancellationToken); 50 | 51 | // Parse the output for errors and warnings 52 | ParseBuildOutput(result.Output, errors, warnings); 53 | 54 | stopwatch.Stop(); 55 | 56 | return new BuildResult 57 | { 58 | Success = result.ExitCode == 0, 59 | Errors = errors, 60 | Warnings = warnings, 61 | BuildTime = stopwatch.Elapsed.TotalSeconds, 62 | Output = TruncateOutput(result.Output, result.ExitCode != 0) 63 | }; 64 | } 65 | catch (Exception ex) 66 | { 67 | _logger.LogError(ex, "Build failed for project: {ProjectPath}", projectPath); 68 | stopwatch.Stop(); 69 | 70 | return new BuildResult 71 | { 72 | Success = false, 73 | Errors = new List<BuildMessage> 74 | { 75 | new BuildMessage 76 | { 77 | Message = ex.Message, 78 | File = projectPath 79 | } 80 | }, 81 | Warnings = warnings, 82 | BuildTime = stopwatch.Elapsed.TotalSeconds, 83 | Output = TruncateOutput($"Build failed with exception: {ex.Message}\n{ex.StackTrace}", true) 84 | }; 85 | } 86 | } 87 | 88 | private async Task<(int ExitCode, string Output)> RunDotNetBuildAsync( 89 | string projectPath, 90 | string configuration, 91 | string platform, 92 | bool restore, 93 | CancellationToken cancellationToken) 94 | { 95 | var arguments = new List<string> 96 | { 97 | "build", 98 | $"\"{projectPath}\"", 99 | $"--configuration", configuration, 100 | "--verbosity", "normal" 101 | }; 102 | 103 | // Platform is typically handled differently in dotnet CLI 104 | // For .NET Framework projects, it's often part of the runtime identifier 105 | if (!string.IsNullOrEmpty(platform) && platform != "Any CPU") 106 | { 107 | arguments.Add($"-p:Platform=\"{platform}\""); 108 | } 109 | 110 | if (!restore) 111 | { 112 | arguments.Add("--no-restore"); 113 | } 114 | 115 | var argumentString = string.Join(" ", arguments); 116 | _logger.LogDebug("Running: {DotNetPath} {Arguments}", _configuration.DotNetPath, argumentString); 117 | 118 | var psi = new ProcessStartInfo 119 | { 120 | FileName = _configuration.DotNetPath, 121 | Arguments = argumentString, 122 | UseShellExecute = false, 123 | RedirectStandardOutput = true, 124 | RedirectStandardError = true, 125 | CreateNoWindow = true, 126 | WorkingDirectory = Path.GetDirectoryName(projectPath) ?? Environment.CurrentDirectory 127 | }; 128 | 129 | var output = new StringBuilder(); 130 | 131 | using var process = new Process { StartInfo = psi }; 132 | 133 | process.OutputDataReceived += (sender, e) => 134 | { 135 | if (e.Data != null) 136 | { 137 | output.AppendLine(e.Data); 138 | } 139 | }; 140 | 141 | process.ErrorDataReceived += (sender, e) => 142 | { 143 | if (e.Data != null) 144 | { 145 | output.AppendLine(e.Data); 146 | } 147 | }; 148 | 149 | try 150 | { 151 | process.Start(); 152 | } 153 | catch (Exception ex) 154 | { 155 | throw new InvalidOperationException($"Failed to start dotnet CLI. Make sure .NET SDK is installed and '{_configuration.DotNetPath}' is in PATH.", ex); 156 | } 157 | 158 | process.BeginOutputReadLine(); 159 | process.BeginErrorReadLine(); 160 | 161 | // Wait for completion with timeout and cancellation support 162 | var timeoutMs = _configuration.BuildTimeout; 163 | using var timeoutCts = new CancellationTokenSource(timeoutMs); 164 | using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); 165 | 166 | try 167 | { 168 | await process.WaitForExitAsync(combinedCts.Token); 169 | } 170 | catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) 171 | { 172 | _logger.LogWarning("Build timed out after {TimeoutMs}ms, killing process", timeoutMs); 173 | process.Kill(); 174 | throw new TimeoutException($"Build timed out after {timeoutMs}ms"); 175 | } 176 | catch (OperationCanceledException) 177 | { 178 | _logger.LogInformation("Build cancelled by user, killing process"); 179 | process.Kill(); 180 | throw; 181 | } 182 | 183 | return (process.ExitCode, output.ToString()); 184 | } 185 | 186 | private void ParseBuildOutput(string output, List<BuildMessage> errors, List<BuildMessage> warnings) 187 | { 188 | if (string.IsNullOrEmpty(output)) 189 | return; 190 | 191 | var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries); 192 | 193 | // Regex patterns for dotnet build error and warning messages 194 | var errorPattern = new Regex(@"^(.+?)\((\d+),(\d+)\):\s+error\s+([A-Z]+\d+):\s+(.+)$", RegexOptions.Multiline); 195 | var warningPattern = new Regex(@"^(.+?)\((\d+),(\d+)\):\s+warning\s+([A-Z]+\d+):\s+(.+)$", RegexOptions.Multiline); 196 | var generalErrorPattern = new Regex(@"^(.+?):\s+error\s+([A-Z]+\d+):\s+(.+)$", RegexOptions.Multiline); 197 | 198 | foreach (var line in lines) 199 | { 200 | var trimmedLine = line.Trim(); 201 | 202 | // Try specific error pattern first (file with line/column) 203 | var errorMatch = errorPattern.Match(trimmedLine); 204 | if (errorMatch.Success) 205 | { 206 | errors.Add(new BuildMessage 207 | { 208 | File = errorMatch.Groups[1].Value, 209 | Line = int.TryParse(errorMatch.Groups[2].Value, out var errorLine) ? errorLine : 0, 210 | Column = int.TryParse(errorMatch.Groups[3].Value, out var errorCol) ? errorCol : 0, 211 | Code = errorMatch.Groups[4].Value, 212 | Message = errorMatch.Groups[5].Value 213 | }); 214 | continue; 215 | } 216 | 217 | // Try warning pattern 218 | var warningMatch = warningPattern.Match(trimmedLine); 219 | if (warningMatch.Success) 220 | { 221 | warnings.Add(new BuildMessage 222 | { 223 | File = warningMatch.Groups[1].Value, 224 | Line = int.TryParse(warningMatch.Groups[2].Value, out var warningLine) ? warningLine : 0, 225 | Column = int.TryParse(warningMatch.Groups[3].Value, out var warningCol) ? warningCol : 0, 226 | Code = warningMatch.Groups[4].Value, 227 | Message = warningMatch.Groups[5].Value 228 | }); 229 | continue; 230 | } 231 | 232 | // Try general error pattern (no line/column) 233 | var generalErrorMatch = generalErrorPattern.Match(trimmedLine); 234 | if (generalErrorMatch.Success) 235 | { 236 | errors.Add(new BuildMessage 237 | { 238 | File = generalErrorMatch.Groups[1].Value, 239 | Code = generalErrorMatch.Groups[2].Value, 240 | Message = generalErrorMatch.Groups[3].Value 241 | }); 242 | } 243 | } 244 | } 245 | 246 | private string TruncateOutput(string output, bool isFailed) 247 | { 248 | const int maxChars = 15000; // Conservative limit to stay under 25k tokens 249 | 250 | if (string.IsNullOrEmpty(output) || output.Length <= maxChars) 251 | { 252 | return output; 253 | } 254 | 255 | if (isFailed) 256 | { 257 | // For failed builds, prioritize the end of the output (where errors typically appear) 258 | var lines = output.Split('\n'); 259 | var importantLines = new List<string>(); 260 | var currentLength = 0; 261 | 262 | // Add summary line if present 263 | for (int i = 0; i < Math.Min(10, lines.Length); i++) 264 | { 265 | if (lines[i].Contains("Build FAILED") || lines[i].Contains("error") || lines[i].Contains("Error")) 266 | { 267 | importantLines.Add(lines[i]); 268 | currentLength += lines[i].Length + 1; 269 | break; 270 | } 271 | } 272 | 273 | // Add errors from the end 274 | for (int i = lines.Length - 1; i >= 0 && currentLength < maxChars - 100; i--) 275 | { 276 | var line = lines[i]; 277 | if (currentLength + line.Length + 1 > maxChars - 100) break; 278 | 279 | if (line.Contains("error") || line.Contains("Error") || 280 | line.Contains("warning") || line.Contains("Warning") || 281 | line.Contains("Build FAILED") || line.Contains("Time Elapsed")) 282 | { 283 | importantLines.Insert(importantLines.Count == 0 ? 0 : 1, line); 284 | currentLength += line.Length + 1; 285 | } 286 | } 287 | 288 | var result = string.Join("\n", importantLines); 289 | if (result.Length < maxChars - 200) 290 | { 291 | // Add some context from the end 292 | var remaining = maxChars - result.Length - 100; 293 | var endPortion = output.Substring(Math.Max(0, output.Length - remaining)); 294 | result += "\n...\n" + endPortion; 295 | } 296 | 297 | return $"[Output truncated - showing errors and summary]\n{result}"; 298 | } 299 | else 300 | { 301 | // For successful builds, show beginning and end 302 | var halfMax = maxChars / 2 - 50; 303 | var start = output.Substring(0, Math.Min(halfMax, output.Length)); 304 | var end = output.Length > halfMax ? output.Substring(output.Length - halfMax) : ""; 305 | 306 | return start + "\n\n[... middle portion truncated ...]\n\n" + end; 307 | } 308 | } 309 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Executors/VSTestExecutor.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Text; 2 | using DotNetFrameworkMCP.Server.Configuration; 3 | using DotNetFrameworkMCP.Server.Models; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace DotNetFrameworkMCP.Server.Executors; 8 | 9 | /// <summary> 10 | /// VSTest.Console.exe-based test executor 11 | /// </summary> 12 | public class VSTestExecutor : BaseTestExecutor 13 | { 14 | public VSTestExecutor( 15 | ILogger<VSTestExecutor> logger, 16 | IOptions<McpServerConfiguration> configuration) 17 | : base(logger, configuration) 18 | { 19 | } 20 | 21 | public override async Task<TestResult> ExecuteTestsAsync( 22 | string projectPath, 23 | string? filter, 24 | bool verbose, 25 | CancellationToken cancellationToken = default) 26 | { 27 | var stopwatch = System.Diagnostics.Stopwatch.StartNew(); 28 | 29 | try 30 | { 31 | // Validate project path 32 | if (!File.Exists(projectPath)) 33 | { 34 | throw new FileNotFoundException($"Test project file not found: {projectPath}"); 35 | } 36 | 37 | var vstestPath = FindVSTestConsoleExecutable(); 38 | if (string.IsNullOrEmpty(vstestPath)) 39 | { 40 | throw new InvalidOperationException("VSTest.Console.exe not found. Please install Visual Studio or Build Tools."); 41 | } 42 | 43 | // Find test assembly 44 | var testAssembly = FindTestAssembly(projectPath); 45 | if (string.IsNullOrEmpty(testAssembly)) 46 | { 47 | return new TestResult 48 | { 49 | TotalTests = 0, 50 | PassedTests = 0, 51 | FailedTests = 0, 52 | SkippedTests = 0, 53 | Duration = 0, 54 | TestDetails = new List<TestDetail> 55 | { 56 | new TestDetail 57 | { 58 | Name = "Assembly Not Found", 59 | ClassName = "VSTest", 60 | Result = "Failed", 61 | Duration = 0, 62 | ErrorMessage = $"No test assembly found for project {Path.GetFileNameWithoutExtension(projectPath)}", 63 | StackTrace = null 64 | } 65 | }, 66 | Output = $"No test assemblies found for project {projectPath} in bin directories" 67 | }; 68 | } 69 | 70 | _logger.LogInformation("Running tests from assembly: {TestAssembly}", testAssembly); 71 | 72 | var args = new StringBuilder($"\"{testAssembly}\""); 73 | 74 | if (!string.IsNullOrEmpty(filter)) 75 | { 76 | args.Append($" /TestCaseFilter:\"{filter}\""); 77 | } 78 | 79 | // Always use detailed console output to capture error messages 80 | args.Append(" /logger:console;verbosity=detailed"); 81 | 82 | // Try to find test adapters for better framework support 83 | var testAdapterPath = FindTestAdapterPath(projectPath); 84 | if (!string.IsNullOrEmpty(testAdapterPath)) 85 | { 86 | args.Append($" /TestAdapterPath:\"{testAdapterPath}\""); 87 | _logger.LogDebug("Using test adapter path: {TestAdapterPath}", testAdapterPath); 88 | } 89 | 90 | // Add TRX logger for structured output 91 | var trxFileName = $"TestResults_{Guid.NewGuid():N}.trx"; 92 | var trxFilePath = Path.Combine(Path.GetTempPath(), trxFileName); 93 | args.Append($" /logger:trx;LogFileName=\"{trxFilePath}\""); 94 | 95 | var result = await RunProcessAsync(vstestPath, args.ToString(), cancellationToken); 96 | 97 | // Parse results from TRX file 98 | var testResult = await ParseTrxFileAsync(trxFilePath, result.Output); 99 | 100 | // Clean up TRX file 101 | try 102 | { 103 | if (File.Exists(trxFilePath)) 104 | File.Delete(trxFilePath); 105 | } 106 | catch (Exception ex) 107 | { 108 | _logger.LogDebug("Failed to delete TRX file: {Error}", ex.Message); 109 | } 110 | 111 | stopwatch.Stop(); 112 | testResult.Duration = stopwatch.Elapsed.TotalSeconds; 113 | 114 | return testResult; 115 | } 116 | catch (Exception ex) 117 | { 118 | _logger.LogError(ex, "Error running VSTest for project: {ProjectPath}", projectPath); 119 | stopwatch.Stop(); 120 | 121 | return new TestResult 122 | { 123 | TotalTests = 0, 124 | PassedTests = 0, 125 | FailedTests = 0, 126 | SkippedTests = 0, 127 | Duration = stopwatch.Elapsed.TotalSeconds, 128 | TestDetails = new List<TestDetail> 129 | { 130 | new TestDetail 131 | { 132 | Name = "Test Execution Error", 133 | ClassName = "VSTest", 134 | Result = "Failed", 135 | Duration = 0, 136 | ErrorMessage = ex.Message, 137 | StackTrace = ex.StackTrace 138 | } 139 | }, 140 | Output = $"VSTest execution failed: {ex.Message}\n{ex.StackTrace}" 141 | }; 142 | } 143 | } 144 | 145 | private string? FindVSTestConsoleExecutable() 146 | { 147 | // Check environment variable first 148 | var envPath = Environment.GetEnvironmentVariable("VSTEST_CONSOLE_PATH"); 149 | if (!string.IsNullOrEmpty(envPath) && File.Exists(envPath)) 150 | { 151 | return envPath; 152 | } 153 | 154 | // Get preferred VS version from configuration 155 | var preferredVersion = _configuration.PreferredVSVersion?.ToLower() ?? "2022"; 156 | 157 | var possiblePaths = new List<string>(); 158 | 159 | // Add paths based on preferred version first 160 | if (preferredVersion == "2022" || preferredVersion == "auto") 161 | { 162 | possiblePaths.AddRange(new[] 163 | { 164 | @"C:\Program Files (x86)\Microsoft Visual Studio\2022\Professional\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", 165 | @"C:\Program Files (x86)\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", 166 | @"C:\Program Files (x86)\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", 167 | @"C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", 168 | @"C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", 169 | @"C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", 170 | @"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", 171 | @"C:\Program Files\Microsoft Visual Studio\2022\BuildTools\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe" 172 | }); 173 | } 174 | 175 | if (preferredVersion == "2019" || preferredVersion == "auto") 176 | { 177 | possiblePaths.AddRange(new[] 178 | { 179 | @"C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", 180 | @"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", 181 | @"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe", 182 | @"C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe" 183 | }); 184 | } 185 | 186 | var foundPath = possiblePaths.FirstOrDefault(File.Exists); 187 | if (foundPath != null) 188 | { 189 | var version = foundPath.Contains("2022") ? "2022" : foundPath.Contains("2019") ? "2019" : "Unknown"; 190 | _logger.LogInformation("Found VSTest.Console.exe version {Version} at: {Path}", version, foundPath); 191 | } 192 | else 193 | { 194 | _logger.LogWarning("VSTest.Console.exe not found in standard locations"); 195 | } 196 | 197 | return foundPath; 198 | } 199 | 200 | private string? FindTestAssembly(string projectPath) 201 | { 202 | var projectDir = Path.GetDirectoryName(projectPath); 203 | var projectName = Path.GetFileNameWithoutExtension(projectPath); 204 | 205 | // Look for test assemblies in bin directories 206 | var possiblePaths = new[] 207 | { 208 | Path.Combine(projectDir!, "bin", "Debug", $"{projectName}.dll"), 209 | Path.Combine(projectDir!, "bin", "Release", $"{projectName}.dll"), 210 | Path.Combine(projectDir!, "bin", "Debug", "net48", $"{projectName}.dll"), 211 | Path.Combine(projectDir!, "bin", "Release", "net48", $"{projectName}.dll"), 212 | Path.Combine(projectDir!, "bin", "Debug", "net472", $"{projectName}.dll"), 213 | Path.Combine(projectDir!, "bin", "Release", "net472", $"{projectName}.dll") 214 | }; 215 | 216 | var testAssembly = possiblePaths.FirstOrDefault(File.Exists); 217 | 218 | if (string.IsNullOrEmpty(testAssembly)) 219 | { 220 | // Fallback: search recursively 221 | var assemblyFiles = Directory.GetFiles(Path.Combine(projectDir!, "bin"), "*.dll", SearchOption.AllDirectories) 222 | .Where(f => Path.GetFileName(f).Equals($"{projectName}.dll", StringComparison.OrdinalIgnoreCase)) 223 | .ToList(); 224 | 225 | testAssembly = assemblyFiles.FirstOrDefault(); 226 | } 227 | 228 | return testAssembly; 229 | } 230 | 231 | private string? FindTestAdapterPath(string projectPath) 232 | { 233 | try 234 | { 235 | var projectDir = Path.GetDirectoryName(projectPath); 236 | if (string.IsNullOrEmpty(projectDir)) 237 | return null; 238 | 239 | // Look for test adapters in packages folder (packages.config style) 240 | var currentDir = new DirectoryInfo(projectDir); 241 | while (currentDir != null) 242 | { 243 | var packagesDir = Path.Combine(currentDir.FullName, "packages"); 244 | if (Directory.Exists(packagesDir)) 245 | { 246 | _logger.LogDebug("Searching for test adapters in packages directory: {PackagesDir}", packagesDir); 247 | 248 | // Look for NUnit test adapter (prioritize NUnit3TestAdapter) 249 | var nunitAdapterPatterns = new[] { "NUnit3TestAdapter.*", "NUnitTestAdapter.*" }; 250 | foreach (var pattern in nunitAdapterPatterns) 251 | { 252 | var nunitAdapterDirs = Directory.GetDirectories(packagesDir, pattern, SearchOption.TopDirectoryOnly); 253 | foreach (var adapterDir in nunitAdapterDirs) 254 | { 255 | var possiblePaths = new[] 256 | { 257 | Path.Combine(adapterDir, "build"), 258 | Path.Combine(adapterDir, "build", "net35"), 259 | Path.Combine(adapterDir, "build", "net40"), 260 | adapterDir 261 | }; 262 | 263 | foreach (var path in possiblePaths) 264 | { 265 | if (Directory.Exists(path)) 266 | { 267 | var adapterDlls = Directory.GetFiles(path, "*TestAdapter*.dll", SearchOption.AllDirectories); 268 | if (adapterDlls.Length > 0) 269 | { 270 | _logger.LogInformation("Found NUnit test adapter at: {Path}", path); 271 | return path; 272 | } 273 | } 274 | } 275 | } 276 | } 277 | 278 | // Look for xUnit test adapter as fallback 279 | var xunitAdapterDirs = Directory.GetDirectories(packagesDir, "xunit.runner.visualstudio.*", SearchOption.TopDirectoryOnly); 280 | foreach (var adapterDir in xunitAdapterDirs) 281 | { 282 | var buildPath = Path.Combine(adapterDir, "build"); 283 | if (Directory.Exists(buildPath)) 284 | { 285 | _logger.LogInformation("Found xUnit test adapter at: {Path}", buildPath); 286 | return buildPath; 287 | } 288 | } 289 | 290 | break; // Only check the first packages directory found 291 | } 292 | 293 | currentDir = currentDir.Parent; 294 | } 295 | 296 | return null; 297 | } 298 | catch (Exception ex) 299 | { 300 | _logger.LogWarning(ex, "Error searching for test adapter path for project: {ProjectPath}", projectPath); 301 | return null; 302 | } 303 | } 304 | } ``` -------------------------------------------------------------------------------- /src/DotNetFrameworkMCP.Server/Executors/MSBuildExecutor.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System.Diagnostics; 2 | using System.Text; 3 | using System.Text.RegularExpressions; 4 | using DotNetFrameworkMCP.Server.Configuration; 5 | using DotNetFrameworkMCP.Server.Models; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace DotNetFrameworkMCP.Server.Executors; 10 | 11 | /// <summary> 12 | /// MSBuild-based build executor 13 | /// </summary> 14 | public class MSBuildExecutor : IBuildExecutor 15 | { 16 | private readonly ILogger<MSBuildExecutor> _logger; 17 | private readonly McpServerConfiguration _configuration; 18 | 19 | public MSBuildExecutor( 20 | ILogger<MSBuildExecutor> logger, 21 | IOptions<McpServerConfiguration> configuration) 22 | { 23 | _logger = logger; 24 | _configuration = configuration.Value; 25 | } 26 | 27 | public async Task<BuildResult> ExecuteBuildAsync( 28 | string projectPath, 29 | string configuration, 30 | string platform, 31 | bool restore, 32 | CancellationToken cancellationToken = default) 33 | { 34 | var stopwatch = Stopwatch.StartNew(); 35 | var errors = new List<BuildMessage>(); 36 | var warnings = new List<BuildMessage>(); 37 | 38 | try 39 | { 40 | // Validate project path 41 | if (!File.Exists(projectPath)) 42 | { 43 | throw new FileNotFoundException($"Project file not found: {projectPath}"); 44 | } 45 | 46 | // Find MSBuild.exe 47 | var msbuildPath = FindMSBuildExecutable(); 48 | if (string.IsNullOrEmpty(msbuildPath)) 49 | { 50 | _logger.LogError("Could not find MSBuild.exe in any standard locations"); 51 | throw new InvalidOperationException("Could not find MSBuild.exe. Please install Visual Studio or Build Tools for Visual Studio, or set MSBUILD_EXE_PATH environment variable."); 52 | } 53 | 54 | _logger.LogInformation("Using MSBuild.exe: {MSBuildPath}", msbuildPath); 55 | 56 | // Build the project using MSBuild.exe process 57 | var result = await RunMSBuildAsync(msbuildPath, projectPath, configuration, platform, restore, cancellationToken); 58 | 59 | // Parse the output for errors and warnings 60 | ParseBuildOutput(result.Output, errors, warnings); 61 | 62 | stopwatch.Stop(); 63 | 64 | return new BuildResult 65 | { 66 | Success = result.ExitCode == 0, 67 | Errors = errors, 68 | Warnings = warnings, 69 | BuildTime = stopwatch.Elapsed.TotalSeconds, 70 | Output = TruncateOutput(result.Output, result.ExitCode != 0) 71 | }; 72 | } 73 | catch (Exception ex) 74 | { 75 | _logger.LogError(ex, "Build failed for project: {ProjectPath}", projectPath); 76 | stopwatch.Stop(); 77 | 78 | return new BuildResult 79 | { 80 | Success = false, 81 | Errors = new List<BuildMessage> 82 | { 83 | new BuildMessage 84 | { 85 | Message = ex.Message, 86 | File = projectPath 87 | } 88 | }, 89 | Warnings = warnings, 90 | BuildTime = stopwatch.Elapsed.TotalSeconds, 91 | Output = TruncateOutput($"Build failed with exception: {ex.Message}\n{ex.StackTrace}", true) 92 | }; 93 | } 94 | } 95 | 96 | private string? FindMSBuildExecutable() 97 | { 98 | // Check environment variable first 99 | var envPath = Environment.GetEnvironmentVariable("MSBUILD_EXE_PATH"); 100 | if (!string.IsNullOrEmpty(envPath) && File.Exists(envPath)) 101 | { 102 | return envPath; 103 | } 104 | 105 | // Get preferred VS version from configuration 106 | var preferredVersion = _configuration.PreferredVSVersion?.ToLower() ?? "2022"; 107 | 108 | // Look for MSBuild.exe in standard Visual Studio locations 109 | var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); 110 | var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); 111 | 112 | var possiblePaths = new List<string>(); 113 | 114 | // Add paths based on preferred version first 115 | if (preferredVersion == "2022" || preferredVersion == "auto") 116 | { 117 | possiblePaths.AddRange(new[] 118 | { 119 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"), 120 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe"), 121 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"), 122 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2022\Preview\MSBuild\Current\Bin\MSBuild.exe"), 123 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe"), 124 | Path.Combine(programFiles, @"Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"), 125 | Path.Combine(programFiles, @"Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe"), 126 | Path.Combine(programFiles, @"Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"), 127 | Path.Combine(programFiles, @"Microsoft Visual Studio\2022\Preview\MSBuild\Current\Bin\MSBuild.exe"), 128 | Path.Combine(programFiles, @"Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe") 129 | }); 130 | } 131 | 132 | if (preferredVersion == "2019" || preferredVersion == "auto") 133 | { 134 | possiblePaths.AddRange(new[] 135 | { 136 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe"), 137 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe"), 138 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe"), 139 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe") 140 | }); 141 | } 142 | 143 | // If not auto mode and preferred version is not 2022 or 2019, add all versions as fallback 144 | if (preferredVersion != "auto" && preferredVersion != "2022" && preferredVersion != "2019") 145 | { 146 | _logger.LogWarning("Unknown PreferredVSVersion '{Version}', falling back to auto detection", preferredVersion); 147 | possiblePaths.AddRange(new[] 148 | { 149 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"), 150 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe"), 151 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"), 152 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2022\Preview\MSBuild\Current\Bin\MSBuild.exe"), 153 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe"), 154 | Path.Combine(programFiles, @"Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"), 155 | Path.Combine(programFiles, @"Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe"), 156 | Path.Combine(programFiles, @"Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"), 157 | Path.Combine(programFiles, @"Microsoft Visual Studio\2022\Preview\MSBuild\Current\Bin\MSBuild.exe"), 158 | Path.Combine(programFiles, @"Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe"), 159 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe"), 160 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe"), 161 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe"), 162 | Path.Combine(programFilesX86, @"Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe") 163 | }); 164 | } 165 | 166 | // Add legacy paths as final fallback 167 | possiblePaths.AddRange(new[] 168 | { 169 | Path.Combine(programFilesX86, @"MSBuild\14.0\Bin\MSBuild.exe"), 170 | Path.Combine(programFilesX86, @"MSBuild\15.0\Bin\MSBuild.exe"), 171 | @"C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe", 172 | @"C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe" 173 | }); 174 | 175 | var foundPath = possiblePaths.FirstOrDefault(File.Exists); 176 | if (foundPath != null) 177 | { 178 | var version = foundPath.Contains("2022") ? "2022" : foundPath.Contains("2019") ? "2019" : "Legacy"; 179 | _logger.LogInformation("Found MSBuild.exe version {Version} at: {Path}", version, foundPath); 180 | } 181 | 182 | return foundPath; 183 | } 184 | 185 | private async Task<(int ExitCode, string Output)> RunMSBuildAsync( 186 | string msbuildPath, 187 | string projectPath, 188 | string configuration, 189 | string platform, 190 | bool restore, 191 | CancellationToken cancellationToken) 192 | { 193 | var arguments = new List<string> 194 | { 195 | $"\"{projectPath}\"", 196 | $"/p:Configuration={configuration}", 197 | $"/p:Platform=\"{platform}\"", 198 | "/v:normal", // Normal verbosity 199 | "/nologo" 200 | }; 201 | 202 | if (restore) 203 | { 204 | arguments.Add("/restore"); 205 | } 206 | 207 | var argumentString = string.Join(" ", arguments); 208 | _logger.LogDebug("Running: {MSBuildPath} {Arguments}", msbuildPath, argumentString); 209 | 210 | var psi = new ProcessStartInfo 211 | { 212 | FileName = msbuildPath, 213 | Arguments = argumentString, 214 | UseShellExecute = false, 215 | RedirectStandardOutput = true, 216 | RedirectStandardError = true, 217 | CreateNoWindow = true, 218 | WorkingDirectory = Path.GetDirectoryName(projectPath) ?? Environment.CurrentDirectory 219 | }; 220 | 221 | var output = new StringBuilder(); 222 | 223 | using var process = new Process { StartInfo = psi }; 224 | 225 | process.OutputDataReceived += (sender, e) => 226 | { 227 | if (e.Data != null) 228 | { 229 | output.AppendLine(e.Data); 230 | } 231 | }; 232 | 233 | process.ErrorDataReceived += (sender, e) => 234 | { 235 | if (e.Data != null) 236 | { 237 | output.AppendLine(e.Data); 238 | } 239 | }; 240 | 241 | process.Start(); 242 | process.BeginOutputReadLine(); 243 | process.BeginErrorReadLine(); 244 | 245 | // Wait for completion with timeout and cancellation support 246 | var timeoutMs = _configuration.BuildTimeout; 247 | using var timeoutCts = new CancellationTokenSource(timeoutMs); 248 | using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); 249 | 250 | try 251 | { 252 | await process.WaitForExitAsync(combinedCts.Token); 253 | } 254 | catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested) 255 | { 256 | _logger.LogWarning("Build timed out after {TimeoutMs}ms, killing process", timeoutMs); 257 | process.Kill(); 258 | throw new TimeoutException($"Build timed out after {timeoutMs}ms"); 259 | } 260 | catch (OperationCanceledException) 261 | { 262 | _logger.LogInformation("Build cancelled by user, killing process"); 263 | process.Kill(); 264 | throw; 265 | } 266 | 267 | return (process.ExitCode, output.ToString()); 268 | } 269 | 270 | private void ParseBuildOutput(string output, List<BuildMessage> errors, List<BuildMessage> warnings) 271 | { 272 | if (string.IsNullOrEmpty(output)) 273 | return; 274 | 275 | var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries); 276 | 277 | // Regex patterns for MSBuild error and warning messages 278 | var errorPattern = new Regex(@"^(.+?)\((\d+),(\d+)\):\s+error\s+([A-Z]+\d+):\s+(.+)$", RegexOptions.Multiline); 279 | var warningPattern = new Regex(@"^(.+?)\((\d+),(\d+)\):\s+warning\s+([A-Z]+\d+):\s+(.+)$", RegexOptions.Multiline); 280 | var generalErrorPattern = new Regex(@"^(.+?):\s+error\s+([A-Z]+\d+):\s+(.+)$", RegexOptions.Multiline); 281 | 282 | foreach (var line in lines) 283 | { 284 | var trimmedLine = line.Trim(); 285 | 286 | // Try specific error pattern first (file with line/column) 287 | var errorMatch = errorPattern.Match(trimmedLine); 288 | if (errorMatch.Success) 289 | { 290 | errors.Add(new BuildMessage 291 | { 292 | File = errorMatch.Groups[1].Value, 293 | Line = int.TryParse(errorMatch.Groups[2].Value, out var errorLine) ? errorLine : 0, 294 | Column = int.TryParse(errorMatch.Groups[3].Value, out var errorCol) ? errorCol : 0, 295 | Code = errorMatch.Groups[4].Value, 296 | Message = errorMatch.Groups[5].Value 297 | }); 298 | continue; 299 | } 300 | 301 | // Try warning pattern 302 | var warningMatch = warningPattern.Match(trimmedLine); 303 | if (warningMatch.Success) 304 | { 305 | warnings.Add(new BuildMessage 306 | { 307 | File = warningMatch.Groups[1].Value, 308 | Line = int.TryParse(warningMatch.Groups[2].Value, out var warningLine) ? warningLine : 0, 309 | Column = int.TryParse(warningMatch.Groups[3].Value, out var warningCol) ? warningCol : 0, 310 | Code = warningMatch.Groups[4].Value, 311 | Message = warningMatch.Groups[5].Value 312 | }); 313 | continue; 314 | } 315 | 316 | // Try general error pattern (no line/column) 317 | var generalErrorMatch = generalErrorPattern.Match(trimmedLine); 318 | if (generalErrorMatch.Success) 319 | { 320 | errors.Add(new BuildMessage 321 | { 322 | File = generalErrorMatch.Groups[1].Value, 323 | Code = generalErrorMatch.Groups[2].Value, 324 | Message = generalErrorMatch.Groups[3].Value 325 | }); 326 | } 327 | } 328 | } 329 | 330 | private string TruncateOutput(string output, bool isFailed) 331 | { 332 | const int maxChars = 15000; // Conservative limit to stay under 25k tokens 333 | 334 | if (string.IsNullOrEmpty(output) || output.Length <= maxChars) 335 | { 336 | return output; 337 | } 338 | 339 | if (isFailed) 340 | { 341 | // For failed builds, prioritize the end of the output (where errors typically appear) 342 | var lines = output.Split('\n'); 343 | var importantLines = new List<string>(); 344 | var currentLength = 0; 345 | 346 | // Add summary line if present 347 | for (int i = 0; i < Math.Min(10, lines.Length); i++) 348 | { 349 | if (lines[i].Contains("Build FAILED") || lines[i].Contains("error") || lines[i].Contains("Error")) 350 | { 351 | importantLines.Add(lines[i]); 352 | currentLength += lines[i].Length + 1; 353 | break; 354 | } 355 | } 356 | 357 | // Add errors from the end 358 | for (int i = lines.Length - 1; i >= 0 && currentLength < maxChars - 100; i--) 359 | { 360 | var line = lines[i]; 361 | if (currentLength + line.Length + 1 > maxChars - 100) break; 362 | 363 | if (line.Contains("error") || line.Contains("Error") || 364 | line.Contains("warning") || line.Contains("Warning") || 365 | line.Contains("Build FAILED") || line.Contains("Time Elapsed")) 366 | { 367 | importantLines.Insert(importantLines.Count == 0 ? 0 : 1, line); 368 | currentLength += line.Length + 1; 369 | } 370 | } 371 | 372 | var result = string.Join("\n", importantLines); 373 | if (result.Length < maxChars - 200) 374 | { 375 | // Add some context from the end 376 | var remaining = maxChars - result.Length - 100; 377 | var endPortion = output.Substring(Math.Max(0, output.Length - remaining)); 378 | result += "\n...\n" + endPortion; 379 | } 380 | 381 | return $"[Output truncated - showing errors and summary]\n{result}"; 382 | } 383 | else 384 | { 385 | // For successful builds, show beginning and end 386 | var halfMax = maxChars / 2 - 50; 387 | var start = output.Substring(0, Math.Min(halfMax, output.Length)); 388 | var end = output.Length > halfMax ? output.Substring(output.Length - halfMax) : ""; 389 | 390 | return start + "\n\n[... middle portion truncated ...]\n\n" + end; 391 | } 392 | } 393 | } ```