From 9b7d892807240410f5dba4eb91e7940d4ba69639 Mon Sep 17 00:00:00 2001 From: csa7mdm Date: Thu, 24 Sep 2026 06:38:06 +0300 Subject: [PATCH] Fix affected tests ignoring non-C# changes; correct docs (0.3.3) Checked an external evaluation against the code: - dotnet_test_affected dropped every changed file that wasn't a compiled .cs file (.csproj, .razor, appsettings.json, deleted files) and ran nothing. They now use the project fallback and are listed in untracedFiles; a changed .props/.targets/global.json/nuget.config/.editorconfig runs the whole solution. - A new test confirms calls through an interface or base class are followed (the report said they weren't). - Removed system-overview.md, which described components that don't exist; labeled the BenchmarkDotNet suite as orchestration overhead only; dropped CONTRIBUTING's reference to a missing ai-context file. - README: fuller known gaps and a "Using it at work?" line. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 19 + CONTRIBUTING.md | 14 +- Directory.Build.props | 4 +- README.md | 7 +- benchmarks/DotNetDevMCP.Benchmarks/README.md | 9 +- .../polly/__pycache__/mcpc.cpython-311.pyc | Bin 0 -> 4508 bytes docs/architecture/system-overview.md | 448 ------------------ src/DotNetDevMCP.Server/.mcp/server.json | 4 +- .../AffectedTestFinder.cs | 28 +- .../Mcp/Tools/TestingTools.cs | 46 +- .../AffectedTestFinderTests.cs | 46 ++ 11 files changed, 137 insertions(+), 488 deletions(-) create mode 100644 benchmarks/polly/__pycache__/mcpc.cpython-311.pyc delete mode 100644 docs/architecture/system-overview.md diff --git a/CHANGELOG.md b/CHANGELOG.md index cc1c855..24e8939 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to DotNetDevMCP are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [SemVer](https://semver.org/). +## [0.3.3] - 2026-09-24 + +Prompted by an external evaluation; each claim was checked against the code first. + +### Fixed +- `dotnet_test_affected`: a change to a file the reference walk can't trace (a `.csproj`, `.razor`, `appsettings.json`, + resources, or a deleted `.cs` file) returned "No changed .cs files" and ran nothing. It now runs the test projects that + reference the project holding the file, and lists the file in `untracedFiles`. A changed `.props`, `.targets`, + `global.json`, `nuget.config` or `.editorconfig` runs the whole solution. Documentation files and files outside every + project (CI workflows) are still ignored. + +### Documentation +- Removed `docs/architecture/system-overview.md`, which described components that were never built (`MergeAnalyzer`, + `CodeReviewEngine`, `AgentCoordinator`, `DependencyAnalyzer`...). The wiki's Architecture page describes the code as it is. +- The BenchmarkDotNet suite now says what it measures: orchestration overhead with simulated work, not Roslyn or test runs. +- README: known gaps list the NuGet package reference gap and the lack of a sandbox; a test confirms calls through an + interface or base class are followed. New "Using it at work?" line. +- CONTRIBUTING no longer points at a `docs/ai-context` file that doesn't exist. + ## [0.3.2] - 2026-09-24 Security release, prompted by two external reviews; each claim was checked against the code first. Upgrade from 0.3.0/0.3.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d361c8c..c053010 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -97,10 +97,8 @@ public class FeatureTests **Required documentation updates:** 1. **Code Comments**: XML docs for public APIs -2. **AI Context**: Update `docs/ai-context/project-context.json` -3. **Architecture**: Update relevant docs in `docs/architecture/` -4. **ADRs**: Create ADR for significant architectural decisions -5. **README**: Update if adding major features +2. **ADRs**: Create an ADR in `docs/architecture/adr/` for significant architectural decisions +3. **README and wiki**: Update if you add or change a tool, a parameter or a behavior users can see ### 5. Test Your Changes @@ -335,14 +333,6 @@ What is the change we're proposing? What becomes easier or more difficult? ``` -### AI-Friendly Documentation - -Update `docs/ai-context/project-context.json` when: -- Adding new features or layers -- Making architectural changes -- Changing design decisions -- Updating dependencies - ## Pull Request Process 1. **Self-Review**: Review your own code first diff --git a/Directory.Build.props b/Directory.Build.props index 7645871..9529455 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,10 +9,10 @@ $(NoWarn);CS1591 - 0.3.2 + 0.3.3 false 0.2.0.0 - 0.3.2.0 + 0.3.3.0 Ahmed Mustafa diff --git a/README.md b/README.md index 1610b96..dca924a 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Measured with BenchmarkDotNet on an i7-10750H, .NET 10.0.9. The orchestration be After an edit, the agent usually reruns the whole suite. `dotnet_test_affected` asks Roslyn instead: take the symbols declared in the changed files, follow references (up to `maxDepth` hops, default 8) until you land in a method with `[Fact]`, `[Theory]`, `[Test]`, `[TestCase]` or `[TestMethod]`, then run exactly those. Changed files default to the git working tree, or `gitBase: "main"` for a branch. `dryRun: true` lists the tests without running them; `framework: "net10.0"` runs one target framework of multi-targeted test projects. -The walk has a time budget (`maxSelectionSeconds`, default 10). A change to code that everything depends on reaches too much to trace cheaply; then the whole solution runs instead and the response says so (`selectionComplete: false`). The same happens when the selection is more than 20% of all tests (`maxSelectedFraction`), where a filtered run is no faster. You never get a silently partial selection. Runs are killed after `timeoutSeconds` (default 600) so a hanging test cannot hang the agent; the response names the test modules that never finished. `maxDepth: 3` narrows more changes but misses more tests. Works with VSTest and with Microsoft.Testing.Platform (`"test": { "runner": "Microsoft.Testing.Platform" }` in global.json). +The walk has a time budget (`maxSelectionSeconds`, default 10). A change to code that everything depends on reaches too much to trace cheaply; then the test projects that reference the changed projects run instead (the whole solution if that's all of them), and the response says so (`selectionComplete: false`, `ranScope`). The same happens when the selection is more than 20% of all tests (`maxSelectedFraction`), where a filtered run is no faster. Changed files the walk can't trace (a `.csproj`, `.razor`, `appsettings.json`, a deleted file) switch to the same project fallback and are listed in `untracedFiles`; a changed `Directory.Build.props`, `global.json` or `.editorconfig` runs the whole solution. You never get a silently partial selection. Runs are killed after `timeoutSeconds` (default 600) so a hanging test cannot hang the agent; the response names the test modules that never finished. `maxDepth: 3` narrows more changes but misses more tests. Works with VSTest and with Microsoft.Testing.Platform (`"test": { "runner": "Microsoft.Testing.Platform" }` in global.json). On this repository, editing `ConcurrentExecutor.cs` selects 22 of 44 tests (the `ConcurrentExecutorTests` plus the `OrchestrationServiceTests` that reach it through `OrchestrationService`). Measured through the MCP tool, build included, i7-10750H: @@ -156,6 +156,7 @@ trust, run the agent and the server in a container with no credentials. Don't ex - **Want to help?** Start with a [good first issue](https://github.com/csa7mdm/DotNetDevMCP/labels/good%20first%20issue) or read [CONTRIBUTING](https://github.com/csa7mdm/DotNetDevMCP/blob/main/CONTRIBUTING.md). Running DotNetDevMCP on your own solution and reporting what happened helps just as much. - **Docs**: the [wiki](https://github.com/csa7mdm/DotNetDevMCP/wiki) (tutorial, tool reference, troubleshooting) is open to edits. - **Security**: report [privately](https://github.com/csa7mdm/DotNetDevMCP/security/advisories/new). +- **Using it at work?** [Tell me in Discussions](https://github.com/csa7mdm/DotNetDevMCP/discussions/categories/show-and-tell): it decides what gets built next. If your team wants help setting it up on your solution (affected-test tuning, `--clean-env` for private feeds, CI), or wants early input on team features (shared config, audit logging, sandboxed runs), say so there. - Everyone here follows the [Code of Conduct](https://github.com/csa7mdm/DotNetDevMCP/blob/main/CODE_OF_CONDUCT.md). If DotNetDevMCP saves you time, you can [sponsor its development](https://github.com/sponsors/csa7mdm). ## Build from source @@ -192,9 +193,9 @@ Built on the official [MCP C# SDK](https://github.com/modelcontextprotocol/cshar ## Status -0.3.2. The Roslyn tools are mature (they come from SharpTools). Testing, build, git and orchestration are newer and have been exercised on this repository and a few others; expect rough edges on unusual project layouts. Issues and PRs welcome, see [CONTRIBUTING](https://github.com/csa7mdm/DotNetDevMCP/blob/main/CONTRIBUTING.md). +0.3.3. The Roslyn tools are mature (they come from SharpTools). Testing, build, git and orchestration are newer and have been exercised on this repository and a few others; expect rough edges on unusual project layouts. Issues and PRs welcome, see [CONTRIBUTING](https://github.com/csa7mdm/DotNetDevMCP/blob/main/CONTRIBUTING.md). -Known gaps: `dotnet_test_affected` follows C# references only (no reflection, no DI-by-convention, no string-keyed lookups), so a change reached only through those paths will not select the test; use `dryRun` to check what it picks. Tests that hang instead of failing are only caught by a run that finishes. Test attribute detection covers xUnit, NUnit and MSTest by attribute name. Past the command-line length limit the filter widens from methods to classes, then to the whole project (more tests, never fewer). +Known gaps: `dotnet_test_affected` follows C# references only (no reflection, no DI-by-convention, no string-keyed lookups), so a change reached only through those paths will not select the test; use `dryRun` to check what it picks. Calls through an interface or base class are followed. The project fallback follows `ProjectReference`s only: a test project that uses the changed code through a NuGet package is not found. Builds and tests are not sandboxed (see Security). Tests that hang instead of failing are only caught by a run that finishes. Test attribute detection covers xUnit, NUnit and MSTest by attribute name. Past the command-line length limit the filter widens from methods to classes, then to the whole project (more tests, never fewer). ## Credits and license diff --git a/benchmarks/DotNetDevMCP.Benchmarks/README.md b/benchmarks/DotNetDevMCP.Benchmarks/README.md index dbf090b..cf1dbd7 100644 --- a/benchmarks/DotNetDevMCP.Benchmarks/README.md +++ b/benchmarks/DotNetDevMCP.Benchmarks/README.md @@ -1,10 +1,13 @@ # DotNetDevMCP Performance Benchmarks -This project contains comprehensive performance benchmarks for the DotNetDevMCP orchestration components using BenchmarkDotNet. +BenchmarkDotNet micro-benchmarks for the orchestration layer (`ConcurrentExecutor`, `WorkflowEngine`, `ResourceManager`). -## Purpose +## What these measure, and what they don't -Measure and validate the performance improvements achieved through concurrent operations and orchestration. Target: **measured 3.8x on this repository's test suite; see README for the benchmark tables** over sequential execution. +Every operation here is a simulated `Task.Delay`. The numbers show the scheduling and throttling overhead of the orchestration +classes compared with a sequential loop and `Task.WhenAll`; they say nothing about Roslyn, `dotnet build` or `dotnet test`. +For real workloads (affected-test selection on the Polly repository, build and test wall time, response sizes), see +[benchmarks/polly](../polly/README.md). ## Benchmark Categories diff --git a/benchmarks/polly/__pycache__/mcpc.cpython-311.pyc b/benchmarks/polly/__pycache__/mcpc.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d9bb171a734bf02a34d59810ef82f35d173941f GIT binary patch literal 4508 zcmbUkZEO=qcJ|9(u?Zw3IS#Jbwn@vjO)xFjR>7BCnuL}X90-9zE?^nYI@z$^HM8rG zxXvj$RceYNHM)yLR8iIH4@eI%-psst^XAQaZ{FSvg#rjl{_+nKm%Rx6lXc2P+(aHn0l9`S!kmsqN-a0SxqW_w zcUxgZa9eRiEoLwG- zcA_i=dd?_lsa$Md8H06F8OL1hxpO?`v9KcEfR(o^t8*yksjQHoS2dHb;#;Oc2z#(s zL)eG?IB=cEb=RNLFNbm{&rZCp;2;j+9oP9AaOT^Ub8~Q$a1-@W%3T@GN*O!Z@vh>b zl4WDfpdM(DVzFKBVhu~7-71i0nN~CEv{=>H5+&{{N`^v!f$sR ze3wdTVqR1!7gl&G8u64$y^|`jG}BN7S`U-9nK1P;r6C$j#MAMMnhvZ~iv|+9rW*E9 zBWXSn-s}u#_ZX(FCACD{hV|Bgtt~Nd_fA-*LDC88NvZa@iIo5iSec8kMMAYKDxEOX zszJpQM^6k;(ZzYwqF$^{8W~-uT$=J4rUFqfQ_)fl4Dvy%C#e`GV-{f^Vlgw4W%&1NAcM~h--zV;x z50#GZF0M?hzFbhci%R!e-#zS{#~T9IkmPbKpa#;UlO8-?5_am{YEg zK|qwKxD0fd8@Mia3y^DIk`vID%9hLJt5=k#kzH{CaHn$cK{>A0Y%!OG6}4l`^q;v; z#VISBu(j~NT^ZPP13vnM3n(7)0!$Dl2$il7%~q+%jEhP*lS+drCH0IouJGg~M!}^l zb`V)6ZEV$x0J1)IiOh)Vpl9_0m_-Zr;^boPL5p&~Mft96<+VE}zwKD*SZyo3)?H}n zDYo=DEj??y?>67v_n`OG{oYfB-qXe2(@wdPSAa`RTx0UNO6~-2W=fvowvG=93b9Lx z0a2+yT&Q?`d5pD~uU1=*%kgOrB8+fdd|JJJrr^q@O|ZS)lCNA-d4#pK3Z4;if>m!b zeQXvi)$;!nXM@D$GEp&uoKQT_Lk7&K zAtoWOu}Mic3)s0Z;fzl>@+GK+X1&;~JI(qAFZ#pljV+6B zFaGX9qjJAdDKx%bYha3A`f@&UJNW9D(sYtJLS3F_4=J( zWUgiwhClyg{*xtqee8aHyHnr39M4Z}_wSLOBsj zTQoBs?8!efykPIADCfz0%G|13>>ES1C+UC6TWa^7@#duG^77Zx5*`N(4%g+pWr>ww zG#ju@Q@0Kz;<^rDYbOXp{qU+~Qt-S&JcFm*Bw6pk(9qyemrOlGwKBR*_5)p_I%cqw zz6Jls?*aG-ac zH&q=`pZFtVs-aG$Nq2THa|U*b&WLV8Yjt;4CYk42%%lDVzggt$KL>U7^75|ln{G8N zPu?K~rK_lPmBy=~YrWK)Fbx~jLA{#gx^6b`hfVCfOb#>YypZ~UW7A+-CCOOA1V0DO z$uQ-*Ie8s0cqvTHD4oVva&@L|@-my_Y|0VJV`vgou%~Q`#SZ51thnz771{Ah4+z5= zfxub=02x4VXEFGjLa?bdRu0r@oF?^)ZcEpj7ID3w)2San? z^B>J|OZL~(Urv{lwATBNfxivhQ=HQu7y8c@`_IB~VgI=zz}9oc)^kgZ>-D?xy*CHG z8CX^dZHJ0&hn(G!g<}iHR$p2Z3XOeEeV^m&E5(RvYhv$*`+iLV^1meswUMLt;m=pz zC16`AV27~xw!D{*{sh5+JpoluenK}bc=E5rH5&}`C78FE+?x;lF+j6^*AN*U3kDXu zd3fQ67tbt?K4^ONe$%UkCZ*Vyn+1gRoWm0^LTt=n5Lu8{2u;W`QfZ57#gZD_s zA)DIN)@h=~0;VT?Dp zF~s61#Hv7c8XHq`!c3*&29}d1k?nC+-b@fO28J}jRxP_r?jIbE4h;7XoPmsA-jCH} z9D=D!oX%X-HEUeOa=7!Y&am=^oQ6zN?mUoGVN6TP(`H77uZBy|GYwUCW1K$S>3(8G zsRx_3q1sdcL1?ggn`}e%XU6Yc?pdj$V;a7qM8-TD?C6MN++mp+l29ST-3OZDqUt>zU9M^xsyx?EC-# literal 0 HcmV?d00001 diff --git a/docs/architecture/system-overview.md b/docs/architecture/system-overview.md deleted file mode 100644 index e51dcbf..0000000 --- a/docs/architecture/system-overview.md +++ /dev/null @@ -1,448 +0,0 @@ -# DotNetDevMCP System Architecture Overview - -**Version**: 0.1.0-alpha -**Last Updated**: 2025-12-30 -**Status**: Initial Design - -## Executive Summary - -DotNetDevMCP is a comprehensive Model Context Protocol (MCP) server designed to be the ultimate one-stop shop for .NET developers. Built by forking and extending [SharpTools](https://github.com/kooshi/SharpToolsMCP), it combines deep Roslyn-based code intelligence with advanced orchestration capabilities, testing frameworks, Git integration, and performance monitoring. - -## Design Philosophy - -### Core Principles - -1. **Performance Through Concurrency**: Maximize parallel operations for tests, builds, and analysis -2. **Context Efficiency**: Optimize for AI consumption with token-efficient responses -3. **Test-Driven Development**: All features backed by comprehensive tests -4. **Living Documentation**: Keep docs synchronized with code through automation -5. **Simplicity Over Complexity**: Prefer straightforward solutions that work -6. **Extensibility**: Plugin architecture for future enhancements - -### Target Audience - -- **Solo Developers**: Boost productivity with AI-assisted workflows -- **Development Teams**: Collaborative features and code review automation -- **Legacy Codebases**: Analysis and modernization tools -- **Greenfield Projects**: Scaffolding and best practice enforcement - -## System Architecture - -### High-Level Architecture Diagram - -```mermaid -graph TB - subgraph "Client Layer" - Claude[Claude Desktop/CLI] - IDE[IDE Extensions] - Custom[Custom MCP Clients] - end - - subgraph "DotNetDevMCP Server" - Server[MCP Server
stdio/SSE] - - subgraph "Orchestration Layer" - Orch[Orchestration Service] - Concurrent[Concurrent Executor] - Agent[Agent Coordinator] - end - - subgraph "Service Layer" - CodeInt[Code Intelligence
SharpTools] - Git[Advanced Git] - Test[Testing Orchestration] - Build[Build Intelligence] - Monitor[Monitoring & Analysis] - Docs[Documentation Generator] - end - - subgraph "Core Layer" - Core[Core Abstractions] - Models[Shared Models] - Utils[Utilities] - end - end - - subgraph "External Systems" - Roslyn[Roslyn API] - MSBuild[MSBuild] - GitLib[LibGit2Sharp] - FileSystem[File System] - Process[Process Execution] - end - - Claude --> Server - IDE --> Server - Custom --> Server - - Server --> Orch - Orch --> Concurrent - Orch --> Agent - - Concurrent --> CodeInt - Concurrent --> Git - Concurrent --> Test - Concurrent --> Build - Concurrent --> Monitor - Concurrent --> Docs - - CodeInt --> Core - Git --> Core - Test --> Core - Build --> Core - Monitor --> Core - Docs --> Core - - CodeInt --> Roslyn - Build --> MSBuild - Git --> GitLib - CodeInt --> FileSystem - Test --> Process -``` - -### Layer Responsibilities - -#### 1. MCP Server Layer (`DotNetDevMCP.Server`) - -**Purpose**: Protocol handling and request routing - -**Responsibilities**: -- Implement MCP protocol (stdio and SSE transports) -- Route tool requests to appropriate services -- Manage sessions and state -- Handle authentication (if needed) -- Logging and diagnostics - -**Key Classes**: -- `McpServer`: Main server implementation -- `StdioTransport`: Standard I/O transport -- Streamable HTTP transport (`--http`) -- `ToolRegistry`: Tool discovery and routing -- `SessionManager`: Client session management - -#### 2. Orchestration Layer (`DotNetDevMCP.Orchestration`) - -**Purpose**: Concurrent operations and workflow management - -**Responsibilities**: -- Coordinate parallel operations across services -- Manage resource allocation -- Handle complex multi-step workflows -- Agent-based task distribution -- Error recovery and retry logic - -**Key Classes**: -- `OrchestrationService`: Main orchestration coordinator -- `ConcurrentExecutor`: Parallel task execution engine -- `AgentCoordinator`: Multi-agent workflow management -- `WorkflowEngine`: Complex workflow orchestration -- `ResourceManager`: Resource allocation and throttling - -**Key Features**: -- **Parallel Test Execution**: Run tests across projects simultaneously -- **Multi-Solution Analysis**: Analyze multiple solutions concurrently -- **Batch Operations**: Process multiple files/symbols in parallel -- **Smart Throttling**: Prevent resource exhaustion - -#### 3. Service Layer - -##### 3.1 Code Intelligence (`DotNetDevMCP.CodeIntelligence`) - -**Purpose**: Roslyn-based code analysis (SharpTools integration) - -**Inherited from SharpTools**: -- Solution/Project loading and analysis -- Symbol navigation (FQN fuzzy matching) -- Type hierarchy and implementations -- Find references -- Code modifications (add, rename, move members) -- Complexity analysis - -**Extensions**: -- Enhanced batch operations -- Performance profiling integration -- Concurrent symbol resolution - -##### 3.2 Source Control (`DotNetDevMCP.SourceControl`) - -**Purpose**: Advanced Git integration (Level C - Deep) - -**Features**: -- **Basic Operations**: Status, diff, log, blame -- **Merge Analysis**: Conflict detection and resolution strategies -- **Code Review**: Automated review suggestions based on changes -- **Branch Management**: Strategy recommendations, cleanup -- **History Analysis**: Impact analysis, dependency tracking -- **Pull Request Intelligence**: Affected tests, risk assessment - -**Key Classes**: -- `GitService`: Core Git operations -- `MergeAnalyzer`: Merge conflict analysis -- `CodeReviewEngine`: Automated review generation -- `BranchStrategyAdvisor`: Branch management recommendations -- `HistoryAnalyzer`: Commit history insights - -##### 3.3 Testing (`DotNetDevMCP.Testing`) - -**Purpose**: Test orchestration and execution - -**Features**: -- **Multi-Framework Support**: xUnit, NUnit, MSTest -- **Parallel Execution**: Run tests concurrently across projects -- **Coverage Analysis**: Aggregate and analyze coverage -- **Smart Test Selection**: Run only affected tests -- **Result Aggregation**: Unified test results across frameworks - -**Key Classes**: -- `TestDiscoveryService`: Find tests across solutions -- `TestExecutor`: Parallel test execution -- `CoverageAnalyzer`: Coverage analysis and reporting -- `TestSelector`: Intelligent test selection -- `ResultAggregator`: Unified result reporting - -##### 3.4 Build Intelligence (`DotNetDevMCP.Build`) - -**Purpose**: MSBuild integration and build analysis - -**Features**: -- **Build Diagnostics**: Parse and analyze build output -- **Dependency Analysis**: Project and package dependencies -- **Pipeline Intelligence**: CI/CD pipeline recommendations -- **Build Optimization**: Identify bottlenecks -- **Warning/Error Analysis**: Categorize and prioritize issues - -**Key Classes**: -- `BuildService`: MSBuild execution and analysis -- `DiagnosticsParser`: Parse build diagnostics -- `DependencyAnalyzer`: Dependency graph analysis -- `PipelineAdvisor`: CI/CD recommendations -- `BuildOptimizer`: Build performance optimization - -##### 3.5 Monitoring & Analysis (`DotNetDevMCP.Monitoring`) - -**Purpose**: Log analysis and performance profiling - -**Features**: -- **Log Pattern Detection**: Identify error patterns -- **Performance Profiling**: Analyze performance bottlenecks -- **Error Aggregation**: Group and prioritize errors -- **Metrics Analysis**: Track code metrics over time -- **Anomaly Detection**: Identify unusual patterns - -**Key Classes**: -- `LogAnalyzer`: Log parsing and pattern detection -- `PerformanceProfiler`: Performance analysis -- `ErrorAggregator`: Error grouping and prioritization -- `MetricsTracker`: Code metrics tracking -- `AnomalyDetector`: Pattern anomaly detection - -##### 3.6 Documentation (planned) - -**Purpose**: AI-friendly documentation generation - -**Features**: -- **XML Doc Extraction**: Extract and format XML documentation -- **Markdown Generation**: Generate API documentation -- **Architecture Diagrams**: Auto-generate Mermaid diagrams -- **Context File Updates**: Keep project-context.json synchronized -- **API Documentation**: Generate comprehensive API docs - -**Key Classes**: -- `DocumentationGenerator`: Main doc generation -- `XmlDocExtractor`: XML documentation extraction -- `MarkdownFormatter`: Markdown generation -- `DiagramGenerator`: Architecture diagram generation -- `ContextUpdater`: project-context.json maintenance - -#### 4. Core Layer (`DotNetDevMCP.Core`) - -**Purpose**: Shared abstractions and utilities - -**Contents**: -- `Interfaces`: Core service interfaces -- `Models`: Shared data models -- `Extensions`: Extension methods -- `Utilities`: Common utilities -- `Exceptions`: Custom exception types - -## Concurrent Operations Architecture - -### Strategy - -DotNetDevMCP prioritizes concurrent operations to maximize performance: - -1. **Parallel Task Execution**: Use `Task.WhenAll` for independent operations -2. **Async/Await Throughout**: All I/O operations are async -3. **Resource Pooling**: Shared resources (e.g., Roslyn workspaces) pooled -4. **Smart Throttling**: Limit parallelism to prevent resource exhaustion -5. **Cancellation Support**: All operations support cancellation tokens - -### Example: Parallel Test Execution - -```csharp -// Discover tests across all projects -var testProjects = await testDiscovery.DiscoverAsync(solution); - -// Execute tests in parallel with throttling -var results = await Parallel.ForEachAsync( - testProjects, - new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, - async (project, ct) => await testExecutor.RunAsync(project, ct) -); - -// Aggregate results -var summary = resultAggregator.Aggregate(results); -``` - -### Example: Multi-Solution Analysis - -```csharp -// Load multiple solutions concurrently -var solutions = await Task.WhenAll( - solutionPaths.Select(path => solutionLoader.LoadAsync(path)) -); - -// Analyze each solution in parallel -var analyses = await Task.WhenAll( - solutions.Select(sln => analyzer.AnalyzeAsync(sln)) -); - -// Merge results -var mergedAnalysis = analysisMerger.Merge(analyses); -``` - -## Data Flow - -### Request Flow - -``` -1. MCP Client sends tool request - ↓ -2. McpServer receives and validates request - ↓ -3. ToolRegistry routes to appropriate service - ↓ -4. OrchestrationService coordinates operation - ↓ -5. Service layer executes (potentially in parallel) - ↓ -6. Results aggregated and formatted - ↓ -7. Response sent back to MCP Client -``` - -### Error Handling Flow - -``` -1. Exception thrown in service - ↓ -2. Caught by OrchestrationService - ↓ -3. Logged with context - ↓ -4. Retry logic applied (if applicable) - ↓ -5. User-friendly error message generated - ↓ -6. Partial results returned if available -``` - -## Technology Stack - -### Core Technologies - -- **.NET 9.0**: Runtime and SDK -- **C# 13**: Language version -- **Roslyn**: Code analysis engine -- **MSBuild**: Build system -- **LibGit2Sharp**: Git operations -- **xUnit**: Testing framework (for DotNetDevMCP itself) - -### Key NuGet Packages - -- `Microsoft.CodeAnalysis.CSharp`: Roslyn C# analysis -- `Microsoft.Build`: MSBuild integration -- `LibGit2Sharp`: Git operations -- `System.Text.Json`: JSON serialization -- `Microsoft.Extensions.DependencyInjection`: DI container -- `Microsoft.Extensions.Logging`: Logging abstraction - -## Deployment - -### MCP Server Deployment - -**Stdio Mode** (Default): -```json -{ - "mcpServers": { - "dotnetdev": { - "command": "dotnet", - "args": [ - "/path/to/DotNetDevMCP.Server.dll", - "--mode", "stdio" - ] - } - } -} -``` - -**SSE Mode** (Remote): -```json -{ - "mcpServers": { - "dotnetdev": { - "url": "http://localhost:5000/mcp", - "transport": "sse" - } - } -} -``` - -## Performance Characteristics - -### Expected Performance - -- **Symbol Resolution**: < 100ms for most symbols -- **Solution Loading**: 1-5 seconds (depends on size) -- **Test Execution**: Parallel execution reduced wall time 3.8x on this repository's test suite (see README) -- **Code Analysis**: Concurrent analysis across files -- **Memory Usage**: ~500MB baseline + ~100MB per loaded solution - -### Scalability - -- **Small Projects** (< 100 files): Instant response -- **Medium Projects** (100-1000 files): 1-2 second response -- **Large Projects** (1000-10000 files): 2-10 second response -- **Very Large Projects** (> 10000 files): Streaming responses, progressive loading - -## Security Considerations - -- **Local Trust Model**: MCP server runs locally, trusts local environment -- **No Network Exposure** (stdio mode): No external attack surface -- **Process Isolation**: Runs in separate process from client -- **File System Access**: Limited to workspace directory (configurable) -- **Code Execution**: Only via dotnet CLI (no arbitrary code execution) - -## Future Enhancements - -See [project-context.json](../ai-context/project-context.json) for detailed roadmap. - -### Potential Additions - -- **F# Support**: Extend to F# codebases -- **VB.NET Support**: Legacy VB.NET support -- **Remote Workspaces**: Support for remote dev environments -- **Cloud Integration**: Azure DevOps, GitHub Actions integration -- **AI Code Generation**: Integrated code generation -- **Real-time Collaboration**: Multi-user support - -## References - -- [MCP Protocol Specification](https://modelcontextprotocol.io) -- [Roslyn Documentation](https://github.com/dotnet/roslyn) -- [SharpTools Original Project](https://github.com/kooshi/SharpToolsMCP) -- [Architecture Decision Records](./adr/) - ---- - -**Next**: See [ADR Index](./adr/README.md) for detailed architectural decisions. diff --git a/src/DotNetDevMCP.Server/.mcp/server.json b/src/DotNetDevMCP.Server/.mcp/server.json index 52207e7..9b09e2e 100644 --- a/src/DotNetDevMCP.Server/.mcp/server.json +++ b/src/DotNetDevMCP.Server/.mcp/server.json @@ -2,7 +2,7 @@ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.csa7mdm/dotnetdevmcp", "description": "MCP server for .NET: Roslyn code navigation and refactoring, build, and affected-test selection.", - "version": "0.3.2", + "version": "0.3.3", "websiteUrl": "https://github.com/csa7mdm/DotNetDevMCP/wiki", "repository": { "url": "https://github.com/csa7mdm/DotNetDevMCP", @@ -13,7 +13,7 @@ "registryType": "nuget", "registryBaseUrl": "https://api.nuget.org/v3/index.json", "identifier": "DotNetDevMCP", - "version": "0.3.2", + "version": "0.3.3", "transport": { "type": "stdio" }, "packageArguments": [ { diff --git a/src/DotNetDevMCP.Testing/AffectedTestFinder.cs b/src/DotNetDevMCP.Testing/AffectedTestFinder.cs index 3da18c6..1771886 100644 --- a/src/DotNetDevMCP.Testing/AffectedTestFinder.cs +++ b/src/DotNetDevMCP.Testing/AffectedTestFinder.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using DotNetDevMCP.CodeIntelligence.Interfaces; +using DotNetDevMCP.Core; using DotNetDevMCP.Core.Models; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -215,12 +216,7 @@ private static bool IsTestProject(Project p) => p.MetadataReferences.Any(r => /// public static IReadOnlyList FindAffectedTestProjects(Solution solution, IEnumerable changedFiles) { - var changedProjectIds = new HashSet(); - foreach (var file in changedFiles) - { - var full = Path.GetFullPath(file); - foreach (var id in solution.GetDocumentIdsWithFilePath(full)) changedProjectIds.Add(id.ProjectId); - } + var changedProjectIds = changedFiles.SelectMany(f => OwningProjects(solution, f)).ToHashSet(); if (changedProjectIds.Count == 0) return []; // Reverse ProjectReference edges (referenced -> referencing projects), across all TFM variants. @@ -246,6 +242,26 @@ public static IReadOnlyList FindAffectedTestProjects(Solution solution, .Select(p => p.FilePath).OfType().Distinct(StringComparer.OrdinalIgnoreCase).ToList(); } + /// + /// The projects a changed file belongs to: those that compile it, or else those whose folder holds it (the deepest such + /// folder, for nested projects). That covers files the reference walk can't see: .csproj, .razor, .json, resources, and + /// deleted files. Empty when the file is outside every project folder (Directory.Build.props, global.json). + /// + public static IReadOnlyList OwningProjects(Solution solution, string file) + { + var full = Path.GetFullPath(file); + var ids = solution.GetDocumentIdsWithFilePath(full); + if (!ids.IsEmpty) return ids.Select(id => id.ProjectId).Distinct().ToList(); + + var owners = solution.Projects + .Select(p => (p.Id, Dir: Path.GetDirectoryName(p.FilePath))) + .Where(p => p.Dir != null && PathBoundary.IsWithin(full, p.Dir)) + .ToList(); + if (owners.Count == 0) return []; + var deepest = owners.Max(p => p.Dir!.Length); + return owners.Where(p => p.Dir!.Length == deepest).Select(p => p.Id).ToList(); + } + /// /// A project `dotnet test` can run: references a test framework AND declares a test method. Helper libraries such as /// Polly.TestUtils reference xUnit without containing tests, and `dotnet test` on them fails with "No test projects were found". diff --git a/src/DotNetDevMCP.Testing/Mcp/Tools/TestingTools.cs b/src/DotNetDevMCP.Testing/Mcp/Tools/TestingTools.cs index 6fa5129..2e90d2d 100644 --- a/src/DotNetDevMCP.Testing/Mcp/Tools/TestingTools.cs +++ b/src/DotNetDevMCP.Testing/Mcp/Tools/TestingTools.cs @@ -84,10 +84,16 @@ public static async Task RunAffected( var sw = System.Diagnostics.Stopwatch.StartNew(); var files = changedFiles is { Length: > 0 } ? changedFiles : await GitChangedFilesAsync(solutionDir, gitBase, cancellationToken); logger.LogDebug("dotnet_test_affected: resolved {Count} changed files in {Ms} ms", files.Length, sw.ElapsedMilliseconds); - files = files.Where(f => f.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)).Select(f => Path.GetFullPath(Path.Combine(solutionDir, f))).ToArray(); - if (files.Length == 0) + var solution = solutions.CurrentSolution; + var changed = files.Select(f => Path.GetFullPath(Path.Combine(solutionDir, f))).Where(f => !DocumentationExtensions.Contains(Path.GetExtension(f))).ToArray(); + // The walk traces only C# the solution compiles. Anything else (a .csproj, .razor, appsettings.json, a deleted file) + // can still break tests, so it switches to the project fallback below instead of being dropped. + files = changed.Where(f => f.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) && !solution.GetDocumentIdsWithFilePath(f).IsEmpty).ToArray(); + var untraced = changed.Where(f => !files.Contains(f) && (AffectedTestFinder.OwningProjects(solution, f).Count > 0 || IsBuildWideFile(f))).ToArray(); + var buildWideChange = untraced.Any(IsBuildWideFile); + if (files.Length == 0 && untraced.Length == 0) { - return new { Success = true, ChangedFiles = Array.Empty(), AffectedTests = Array.Empty(), Message = "No changed .cs files." }; + return new { Success = true, ChangedFiles = Array.Empty(), AffectedTests = Array.Empty(), Message = "No changed code or project files." }; } sw.Restart(); @@ -108,24 +114,28 @@ public static async Task RunAffected( AffectedRunScope scope; IReadOnlyList projectsToRun = []; IReadOnlyList allTestProjects = []; - if (selection.Complete && selectedFraction <= maxSelectedFraction) + if (untraced.Length == 0 && selection.Complete && selectedFraction <= maxSelectedFraction) { scope = AffectedRunScope.Selection; note = null; } else { - var reason = !selection.Complete + var reason = untraced.Length > 0 + ? $"Changed files the reference walk can't trace ({string.Join(", ", untraced.Select(Path.GetFileName))}): tests that depend on them can't be picked by name." + : !selection.Complete ? $"Selection stopped after {maxSelectionSeconds}s and {selection.SymbolsSearched} symbols: this change reaches too much code to trace cheaply." : $"Selected {affected.Count} of {selection.TotalTestMethods} test methods ({selectedFraction:P0}), above the {maxSelectedFraction:P0} threshold: a selection this large is likely slower filtered than running the whole solution."; - var reachableProjects = AffectedTestFinder.FindAffectedTestProjects(solutions.CurrentSolution, files); - allTestProjects = AffectedTestFinder.AllTestProjectFilePaths(solutions.CurrentSolution); + var reachableProjects = AffectedTestFinder.FindAffectedTestProjects(solution, files.Concat(untraced)); + allTestProjects = AffectedTestFinder.AllTestProjectFilePaths(solution); - if (reachableProjects.Count == 0 || reachableProjects.Count >= allTestProjects.Count) + if (buildWideChange || reachableProjects.Count == 0 || reachableProjects.Count >= allTestProjects.Count) { scope = AffectedRunScope.Solution; - note = $"{reason} Every test project in the solution is reachable from the change (or none could be resolved to a project); running the whole solution instead."; + note = buildWideChange + ? $"{reason} A build-wide file changed, which can affect every project; running the whole solution instead." + : $"{reason} Every test project in the solution is reachable from the change (or none could be resolved to a project); running the whole solution instead."; } else { @@ -140,7 +150,7 @@ public static async Task RunAffected( } var ranWholeSolution = scope == AffectedRunScope.Solution; // kept for compatibility; true only when the whole solution ran. - if (dryRun || (selection.Complete && affected.Count == 0)) + if (dryRun || (scope == AffectedRunScope.Selection && affected.Count == 0)) { var plannedProjects = scope switch { @@ -148,7 +158,7 @@ public static async Task RunAffected( AffectedRunScope.Solution => allTestProjects, _ => (IReadOnlyList)[], }; - return new { Success = true, ChangedFiles = files, SelectionComplete = selection.Complete, selection.SymbolsSearched, selection.TotalTestMethods, Note = note, RanWholeSolution = ranWholeSolution, RanScope = ScopeName(scope), TestProjectsRun = plannedProjects.Select(Path.GetFileName), AffectedTests = list, Ran = false }; + return new { Success = true, ChangedFiles = files, UntracedFiles = untraced, SelectionComplete = selection.Complete, selection.SymbolsSearched, selection.TotalTestMethods, Note = note, RanWholeSolution = ranWholeSolution, RanScope = ScopeName(scope), TestProjectsRun = plannedProjects.Select(Path.GetFileName), AffectedTests = list, Ran = false }; } TestRunSummary summary; @@ -190,7 +200,19 @@ public static async Task RunAffected( break; } - return new { Success = summary.Success, ChangedFiles = files, SelectionComplete = selection.Complete, selection.SymbolsSearched, selection.TotalTestMethods, Note = note, RanWholeSolution = ranWholeSolution, RanScope = ScopeName(scope), TestProjectsRun = testProjectsRun.Select(Path.GetFileName), AffectedTests = list, Ran = true, Run = Shape(summary) }; + return new { Success = summary.Success, ChangedFiles = files, UntracedFiles = untraced, SelectionComplete = selection.Complete, selection.SymbolsSearched, selection.TotalTestMethods, Note = note, RanWholeSolution = ranWholeSolution, RanScope = ScopeName(scope), TestProjectsRun = testProjectsRun.Select(Path.GetFileName), AffectedTests = list, Ran = true, Run = Shape(summary) }; + } + + /// Changes to these can't break a test. ponytail: extension list, not content sniffing. + private static readonly HashSet DocumentationExtensions = new(StringComparer.OrdinalIgnoreCase) { ".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg" }; + + /// Files outside any project folder that still feed every build. + private static bool IsBuildWideFile(string path) + { + var name = Path.GetFileName(path); + return name.EndsWith(".props", StringComparison.OrdinalIgnoreCase) || name.EndsWith(".targets", StringComparison.OrdinalIgnoreCase) + || name.Equals("global.json", StringComparison.OrdinalIgnoreCase) || name.Equals("nuget.config", StringComparison.OrdinalIgnoreCase) + || name.Equals(".editorconfig", StringComparison.OrdinalIgnoreCase); } private static string ScopeName(AffectedRunScope scope) => scope.ToString().ToLowerInvariant(); diff --git a/tests/DotNetDevMCP.Testing.Tests/AffectedTestFinderTests.cs b/tests/DotNetDevMCP.Testing.Tests/AffectedTestFinderTests.cs index d246e51..5caf0c2 100644 --- a/tests/DotNetDevMCP.Testing.Tests/AffectedTestFinderTests.cs +++ b/tests/DotNetDevMCP.Testing.Tests/AffectedTestFinderTests.cs @@ -95,6 +95,52 @@ public async Task Reports_an_incomplete_selection_instead_of_dropping_tests_when Assert.False(affected.Complete); } + [Fact] + public async Task Finds_tests_that_call_the_changed_implementation_only_through_its_interface() + { + // The test never names Impl: it calls IService.Run on an instance it gets from somewhere else (DI, a mock setup). + var solution = BuildSolution( + lib: new() + { + ["IService.cs"] = "namespace Lib; public interface IService { int Run(); }", + ["Impl.cs"] = "namespace Lib; public class Impl : IService { public int Run() => 1; }", + ["Base.cs"] = "namespace Lib; public abstract class Base { public abstract int Go(); }", + ["Derived.cs"] = "namespace Lib; public class Derived : Base { public override int Go() => 2; }", + }, + tests: """ + using Xunit; + namespace Lib.Tests; + public class LibTests + { + private readonly Lib.IService _service = null!; + private readonly Lib.Base _base = null!; + [Fact] public void Through_interface() { _ = _service.Run(); } + [Fact] public void Through_base_class() { _ = _base.Go(); } + [Fact] public void Unrelated() { } + } + """); + + var affected = await AffectedTestFinder.FindAsync(solution, [Path.Combine(Root, "src", "Impl.cs"), Path.Combine(Root, "src", "Derived.cs")], + maxDepth: 3, AffectedTestFinder.DefaultBudget, NullLogger.Instance, default); + + Assert.Equal( + ["Lib.Tests.LibTests.Through_base_class", "Lib.Tests.LibTests.Through_interface"], + affected.Tests.Select(t => t.FullyQualifiedName).Order()); + } + + [Fact] + public void Project_fallback_maps_a_file_the_solution_does_not_compile_to_the_project_folder_holding_it() + { + var solution = BuildSolution(lib: new() { ["A.cs"] = "namespace Lib; public class A { }" }, + tests: "using Xunit; namespace Lib.Tests; public class LibTests { [Fact] public void T() { } }"); + + // appsettings.json, a .razor file, the .csproj itself, or a deleted .cs file: none is a document the walk can trace. + var affected = AffectedTestFinder.FindAffectedTestProjects(solution, [Path.Combine(Root, "src", "appsettings.json")]); + + Assert.Equal([Path.Combine(Root, "test", "Lib.Tests.csproj")], affected); + Assert.Empty(AffectedTestFinder.OwningProjects(solution, Path.Combine(Root, "Directory.Build.props"))); + } + [Fact] public void Project_fallback_finds_only_the_test_project_that_references_the_changed_library() {