diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 68b2acf..476374f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: env: - DOTNET_VERSION: '9.0.x' + DOTNET_VERSION: '10.0.x' SOLUTION_FILE: 'DotNetDevMCP.sln' jobs: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d3a7a2f..07ccfa6 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -36,7 +36,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '9.0.x' + dotnet-version: '10.0.x' - name: Restore dependencies run: dotnet restore DotNetDevMCP.sln diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 319d54f..dfd0026 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ on: type: string env: - DOTNET_VERSION: '9.0.x' + DOTNET_VERSION: '10.0.x' SOLUTION_FILE: 'DotNetDevMCP.sln' jobs: diff --git a/.gitignore b/.gitignore index 62067d4..b905e67 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,43 @@ -Nothing needs to be added to .gitignore since only a README.md file was modified and no build artifacts, dependencies, or temporary files were detected in the changes. \ No newline at end of file +``` +# Build artifacts +**/bin/ +**/obj/ +**/out/ +*.dll +*.exe +*.pdb +*.so +*.dylib +*.nupkg + +# Dependencies +packages/ +**/node_modules/ + +# Logs +*.log + +# Environment +.env +.env.local +*.env.* + +# Editors +.vscode/ +.idea/ +*.swp +*.swo + +# OS generated files +.DS_Store +Thumbs.db + +# Test and coverage +coverage/ +**/TestResults/ +*.coverage + +# Local config +**/appsettings.Development.json +**/appsettings.Local.json +``` \ No newline at end of file diff --git a/src/DotNetDevMCP.Analysis/DotNetDevMCP.Analysis.csproj b/src/DotNetDevMCP.Analysis/DotNetDevMCP.Analysis.csproj index b760144..58d26ff 100644 --- a/src/DotNetDevMCP.Analysis/DotNetDevMCP.Analysis.csproj +++ b/src/DotNetDevMCP.Analysis/DotNetDevMCP.Analysis.csproj @@ -6,4 +6,13 @@ enable + + + + + + + + + diff --git a/src/DotNetDevMCP.Analysis/Extensions/ServiceCollectionExtensions.cs b/src/DotNetDevMCP.Analysis/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..e9137b5 --- /dev/null +++ b/src/DotNetDevMCP.Analysis/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using DotNetDevMCP.Analysis.Services; + +namespace DotNetDevMCP.Analysis.Extensions; + +/// +/// Extension methods for registering Analysis services +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds Analysis services to the service collection + /// + public static IServiceCollection AddAnalysisServices(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } +} diff --git a/src/DotNetDevMCP.Analysis/Mcp/Tools/AnalysisTools.cs b/src/DotNetDevMCP.Analysis/Mcp/Tools/AnalysisTools.cs new file mode 100644 index 0000000..ad33f04 --- /dev/null +++ b/src/DotNetDevMCP.Analysis/Mcp/Tools/AnalysisTools.cs @@ -0,0 +1,115 @@ +using ModelContextProtocol.Server; +using DotNetDevMCP.Analysis.Services; +using DotNetDevMCP.Analysis.Models; + +namespace DotNetDevMCP.Analysis.Mcp.Tools; + +/// +/// MCP tools for code analysis and metrics +/// +[McpServerToolType] +public sealed class AnalysisTools( + CodeAnalysisService analysisService, + ILogger logger) +{ + private readonly CodeAnalysisService _analysisService = analysisService; + private readonly ILogger _logger = logger; + + /// + /// Analyzes a .NET project or solution and returns comprehensive code metrics + /// + /// Path to the project file (.csproj), solution file (.sln), or directory + /// Include detailed code metrics (default: true) + /// Include dependency information (default: true) + [McpServerTool(Name = "dotnet_analyze_project")] + public async Task AnalyzeProjectAsync( + string path, + bool includeMetrics = true, + bool includeDependencies = true, + CancellationToken cancellationToken = default) + { + _logger.LogInformation("MCP: Analyzing project at {Path}", path); + + if (!Directory.Exists(path) && !File.Exists(path)) + { + throw new FileNotFoundException($"Path not found: {path}"); + } + + return await _analysisService.AnalyzeAsync(path, cancellationToken); + } + + /// + /// Gets dependency information for a .NET project + /// + /// Path to the project file (.csproj) + [McpServerTool(Name = "dotnet_get_dependencies")] + public async Task GetDependenciesAsync( + string projectPath, + CancellationToken cancellationToken = default) + { + _logger.LogInformation("MCP: Getting dependencies for {Path}", projectPath); + + if (!File.Exists(projectPath)) + { + throw new FileNotFoundException($"Project file not found: {projectPath}"); + } + + return await _analysisService.GetDependenciesAsync(projectPath, cancellationToken); + } + + /// + /// Analyzes code quality and identifies potential issues + /// + /// Path to the project or directory to analyze + [McpServerTool(Name = "dotnet_analyze_quality")] + public async Task AnalyzeQualityAsync( + string path, + CancellationToken cancellationToken = default) + { + _logger.LogInformation("MCP: Analyzing code quality at {Path}", path); + + if (!Directory.Exists(path) && !File.Exists(path)) + { + throw new FileNotFoundException($"Path not found: {path}"); + } + + return await _analysisService.AnalyzeQualityAsync(path, cancellationToken); + } + + /// + /// Scans for outdated NuGet packages in a project + /// + /// Path to the project file (.csproj) + [McpServerTool(Name = "dotnet_scan_outdated_packages")] + public async Task> ScanOutdatedPackagesAsync( + string projectPath, + CancellationToken cancellationToken = default) + { + _logger.LogInformation("MCP: Scanning for outdated packages in {Path}", projectPath); + + var deps = await _analysisService.GetDependenciesAsync(projectPath, cancellationToken); + + // In a real implementation, this would check NuGet.org for latest versions + // For now, return all direct dependencies as potentially outdated + return deps.PackageDependencies + .Where(d => d.IsDirect) + .ToList() + .AsReadOnly(); + } + + /// + /// Detects circular dependencies in a solution + /// + /// Path to the solution file (.sln) + [McpServerTool(Name = "dotnet_detect_circular_dependencies")] + public async Task DetectCircularDependenciesAsync( + string solutionPath, + CancellationToken cancellationToken = default) + { + _logger.LogInformation("MCP: Detecting circular dependencies in {Path}", solutionPath); + + // Simplified implementation - would need full graph analysis in production + var deps = await _analysisService.GetDependenciesAsync(solutionPath, cancellationToken); + return deps.HasCircularDependencies; + } +} diff --git a/src/DotNetDevMCP.Analysis/Models/AnalysisModels.cs b/src/DotNetDevMCP.Analysis/Models/AnalysisModels.cs new file mode 100644 index 0000000..8709ad1 --- /dev/null +++ b/src/DotNetDevMCP.Analysis/Models/AnalysisModels.cs @@ -0,0 +1,91 @@ +namespace DotNetDevMCP.Analysis.Models; + +/// +/// Represents the result of a code analysis operation +/// +public sealed record AnalysisResult( + string ProjectPath, + int TotalFiles, + int TotalLines, + int CodeLines, + int CommentLines, + int BlankLines, + int Classes, + int Methods, + double MaintainabilityIndex, + double CyclomaticComplexity, + DateTime AnalyzedAt +); + +/// +/// Represents code quality metrics for a project +/// +public sealed record CodeQualityMetrics( + string ProjectPath, + double TechnicalDebtHours, + int CodeSmells, + int Bugs, + int Vulnerabilities, + double Coverage, + double Duplication, + IReadOnlyList Issues, + DateTime AnalyzedAt +); + +/// +/// Represents a single code issue +/// +public sealed record CodeIssue( + string RuleId, + string Message, + string Severity, + string FilePath, + int LineNumber, + int ColumnNumber, + string? Suggestion = null +); + +/// +/// Represents dependency information for a project +/// +public sealed record DependencyInfo( + string ProjectPath, + IReadOnlyList PackageDependencies, + IReadOnlyList ProjectReferences, + IReadOnlyList FrameworkReferences, + bool HasCircularDependencies, + DateTime AnalyzedAt +); + +/// +/// Represents a NuGet package dependency +/// +public sealed record PackageDependency( + string Name, + string Version, + bool IsDirect, + bool IsDevelopmentDependency, + string? LicenseUrl = null, + bool? HasKnownVulnerabilities = null +); + +/// +/// Represents a project reference +/// +public sealed record ProjectReference( + string Name, + string Path, + bool IsProjectReference +); + +/// +/// Request model for code analysis +/// +public sealed record AnalysisRequest( + string Path, + bool IncludeMetrics = true, + bool IncludeDependencies = true, + bool IncludeIssues = true, + string? Configuration = null, + string? Platform = null +); diff --git a/src/DotNetDevMCP.Analysis/Services/CodeAnalysisService.cs b/src/DotNetDevMCP.Analysis/Services/CodeAnalysisService.cs new file mode 100644 index 0000000..9ac31d4 --- /dev/null +++ b/src/DotNetDevMCP.Analysis/Services/CodeAnalysisService.cs @@ -0,0 +1,252 @@ +using System.Collections.Concurrent; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using NuGet.ProjectModel; +using DotNetDevMCP.Analysis.Models; + +namespace DotNetDevMCP.Analysis.Services; + +/// +/// Service for analyzing .NET code projects and solutions +/// +public sealed class CodeAnalysisService(ILogger logger) +{ + private readonly ILogger _logger = logger; + private readonly ProjectCollection _projectCollection = new(); + + /// + /// Analyzes a project or solution and returns comprehensive metrics + /// + public async Task AnalyzeAsync(string path, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Starting analysis of {Path}", path); + + var files = Directory.Exists(path) + ? Directory.GetFiles(path, "*.cs", SearchOption.AllDirectories) + : [path]; + + var totalLines = 0; + var codeLines = 0; + var commentLines = 0; + var blankLines = 0; + var classes = 0; + var methods = 0; + + foreach (var file in files) + { + cancellationToken.ThrowIfCancellationRequested(); + + var content = await File.ReadAllTextAsync(file, cancellationToken); + var lines = content.Split('\n'); + + totalLines += lines.Length; + blankLines += lines.Count(l => string.IsNullOrWhiteSpace(l)); + commentLines += lines.Count(l => l.Trim().StartsWith("//") || l.Trim().StartsWith("/*") || l.Trim().StartsWith("*")); + codeLines += lines.Length - blankLines - commentLines; + + classes += CountOccurrences(content, "class ") + CountOccurrences(content, "record "); + methods += CountOccurrences(content, "void ") + CountOccurrences(content, "async ") + CountOccurrences(content, "public ") + CountOccurrences(content, "private "); + } + + var result = new AnalysisResult( + ProjectPath: path, + TotalFiles: files.Length, + TotalLines: totalLines, + CodeLines: codeLines, + CommentLines: commentLines, + BlankLines: blankLines, + Classes: classes, + Methods: methods, + MaintainabilityIndex: CalculateMaintainabilityIndex(codeLines, classes, methods), + CyclomaticComplexity: EstimateCyclomaticComplexity(files), + AnalyzedAt: DateTime.UtcNow + ); + + _logger.LogInformation("Analysis complete: {Files} files, {Lines} lines", files.Length, totalLines); + return result; + } + + /// + /// Gets dependency information for a project + /// + public async Task GetDependenciesAsync(string projectPath, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Analyzing dependencies for {Path}", projectPath); + + var packageDependencies = new List(); + var projectReferences = new List(); + var frameworkReferences = new List(); + + if (File.Exists(projectPath)) + { + var content = await File.ReadAllTextAsync(projectPath, cancellationToken); + + // Parse package references + var packageRefs = System.Text.RegularExpressions.Regex.Matches( + content, + @" + /// Analyzes code quality and returns metrics + /// + public async Task AnalyzeQualityAsync(string path, CancellationToken cancellationToken = default) + { + _logger.LogInformation("Analyzing code quality for {Path}", path); + + var issues = new List(); + + // Basic code smell detection + var files = Directory.Exists(path) + ? Directory.GetFiles(path, "*.cs", SearchOption.AllDirectories) + : [path]; + + int codeSmells = 0; + int bugs = 0; + int vulnerabilities = 0; + + foreach (var file in files) + { + cancellationToken.ThrowIfCancellationRequested(); + var content = await File.ReadAllTextAsync(file, cancellationToken); + + // Detect long methods + var methods = System.Text.RegularExpressions.Regex.Matches(content, @"\{[^{}]*\}"); + foreach (System.Text.RegularExpressions.Match method in methods) + { + var lineCount = method.Value.Count(c => c == '\n'); + if (lineCount > 50) + { + codeSmells++; + issues.Add(new CodeIssue( + RuleId: "CS0001", + Message: "Method is too long (>50 lines)", + Severity: "Warning", + FilePath: file, + LineNumber: 0, + ColumnNumber: 0, + Suggestion: "Consider breaking this method into smaller methods" + )); + } + } + + // Detect potential null reference issues + if (content.Contains(".ToString()") && !content.Contains("?.")) + { + bugs++; + issues.Add(new CodeIssue( + RuleId: "CS0002", + Message: "Potential NullReferenceException", + Severity: "Warning", + FilePath: file, + LineNumber: 0, + ColumnNumber: 0, + Suggestion: "Use null-conditional operator (?.)" + )); + } + } + + return new CodeQualityMetrics( + ProjectPath: path, + TechnicalDebtHours: codeSmells * 0.1, + CodeSmells: codeSmells, + Bugs: bugs, + Vulnerabilities: vulnerabilities, + Coverage: 0.0, // Would need test coverage tool integration + Duplication: 0.0, // Would need duplication detection + Issues: issues.AsReadOnly(), + AnalyzedAt: DateTime.UtcNow + ); + } + + private static int CountOccurrences(string text, string pattern) + { + var count = 0; + var index = 0; + while ((index = text.IndexOf(pattern, index, StringComparison.Ordinal)) != -1) + { + count++; + index += pattern.Length; + } + return count; + } + + private static double CalculateMaintainabilityIndex(int codeLines, int classes, int methods) + { + // Simplified maintainability index calculation + if (codeLines == 0) return 100.0; + + var avgMethodLength = codeLines / Math.Max(1, methods); + var avgMethodsPerClass = methods / Math.Max(1, classes); + + var index = 171.0 - 5.2 * Math.Log(avgMethodLength) - 0.23 * avgMethodsPerClass - 16.2 * Math.Log(codeLines); + return Math.Max(0, Math.Min(100, index * 100 / 171)); + } + + private static double EstimateCyclomaticComplexity(string[] files) + { + var totalComplexity = 0; + foreach (var file in files) + { + if (!File.Exists(file)) continue; + + var content = File.ReadAllText(file); + totalComplexity += CountOccurrences(content, "if "); + totalComplexity += CountOccurrences(content, "else "); + totalComplexity += CountOccurrences(content, "for "); + totalComplexity += CountOccurrences(content, "while "); + totalComplexity += CountOccurrences(content, "case "); + totalComplexity += CountOccurrences(content, "catch "); + totalComplexity += CountOccurrences(content, "&& "); + totalComplexity += CountOccurrences(content, "|| "); + } + return files.Length > 0 ? (double)totalComplexity / files.Length : 0; + } + + public void Dispose() => _projectCollection.Dispose(); +} diff --git a/src/DotNetDevMCP.Monitoring/DotNetDevMCP.Monitoring.csproj b/src/DotNetDevMCP.Monitoring/DotNetDevMCP.Monitoring.csproj index b760144..e6b1083 100644 --- a/src/DotNetDevMCP.Monitoring/DotNetDevMCP.Monitoring.csproj +++ b/src/DotNetDevMCP.Monitoring/DotNetDevMCP.Monitoring.csproj @@ -6,4 +6,12 @@ enable + + + + + + + + diff --git a/src/DotNetDevMCP.Monitoring/Extensions/ServiceCollectionExtensions.cs b/src/DotNetDevMCP.Monitoring/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..9e6e735 --- /dev/null +++ b/src/DotNetDevMCP.Monitoring/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using DotNetDevMCP.Monitoring.Services; + +namespace DotNetDevMCP.Monitoring.Extensions; + +/// +/// Extension methods for registering Monitoring services +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds Monitoring services to the service collection + /// + public static IServiceCollection AddMonitoringServices(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } +} diff --git a/src/DotNetDevMCP.Monitoring/Mcp/Tools/MonitoringTools.cs b/src/DotNetDevMCP.Monitoring/Mcp/Tools/MonitoringTools.cs new file mode 100644 index 0000000..68d1a52 --- /dev/null +++ b/src/DotNetDevMCP.Monitoring/Mcp/Tools/MonitoringTools.cs @@ -0,0 +1,137 @@ +using ModelContextProtocol.Server; +using DotNetDevMCP.Monitoring.Services; +using DotNetDevMCP.Monitoring.Models; + +namespace DotNetDevMCP.Monitoring.Mcp.Tools; + +/// +/// MCP tools for performance monitoring and profiling +/// +[McpServerToolType] +public sealed class MonitoringTools( + PerformanceMonitoringService monitoringService, + ILogger logger) +{ + private readonly PerformanceMonitoringService _monitoringService = monitoringService; + private readonly ILogger _logger = logger; + + /// + /// Gets current performance metrics for the application + /// + [McpServerTool(Name = "dotnet_get_performance_metrics")] + public async Task GetPerformanceMetricsAsync( + CancellationToken cancellationToken = default) + { + _logger.LogInformation("MCP: Getting performance metrics"); + return await _monitoringService.GetPerformanceMetricsAsync(cancellationToken); + } + + /// + /// Gets system resource utilization metrics + /// + [McpServerTool(Name = "dotnet_get_resource_utilization")] + public async Task GetResourceUtilizationAsync( + CancellationToken cancellationToken = default) + { + _logger.LogInformation("MCP: Getting resource utilization"); + return await _monitoringService.GetResourceUtilizationAsync(cancellationToken); + } + + /// + /// Checks application health status + /// + [McpServerTool(Name = "dotnet_check_health")] + public async Task CheckHealthAsync( + CancellationToken cancellationToken = default) + { + _logger.LogInformation("MCP: Checking application health"); + return await _monitoringService.CheckHealthAsync(cancellationToken); + } + + /// + /// Starts a profiling session to collect performance data + /// + /// Name for the profiling session + /// Duration of the profiling session in seconds + /// Collect CPU samples (default: true) + /// Collect memory samples (default: true) + /// Collect GC events (default: true) + [McpServerTool(Name = "dotnet_start_profiling_session")] + public async Task StartProfilingSessionAsync( + string sessionName, + int durationSeconds = 10, + bool collectCpuSamples = true, + bool collectMemorySamples = true, + bool collectGcEvents = true, + CancellationToken cancellationToken = default) + { + _logger.LogInformation("MCP: Starting profiling session '{Session}' for {Duration}s", + sessionName, durationSeconds); + + var config = new ProfilingConfig( + SessionName: sessionName, + Duration: TimeSpan.FromSeconds(durationSeconds), + CollectCpuSamples: collectCpuSamples, + CollectMemorySamples: collectMemorySamples, + CollectGcEvents: collectGcEvents, + CollectThreadEvents: true + ); + + return await _monitoringService.StartProfilingSessionAsync(config, cancellationToken); + } + + /// + /// Gets garbage collection statistics + /// + [McpServerTool(Name = "dotnet_get_gc_stats")] + public async Task GetGcStatsAsync( + CancellationToken cancellationToken = default) + { + _logger.LogInformation("MCP: Getting GC statistics"); + + return new + { + Gen0Collections = GC.CollectionCount(0), + Gen1Collections = GC.CollectionCount(1), + Gen2Collections = GC.CollectionCount(2), + TotalMemoryBytes = GC.GetTotalMemory(false), + GcHeapSizeBytes = GC.GetGCMemoryInfo().HeapSizeBytes, + FragmentedBytes = GC.GetGCMemoryInfo().FragmentedBytes, + MemoryLoadBytes = GC.GetGCMemoryInfo().MemoryLoadBytes, + TotalAvailableMemoryBytes = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes, + CapturedAt = DateTime.UtcNow + }; + } + + /// + /// Forces a garbage collection + /// + /// Generation to collect (0-2, default: 2 for full GC) + /// Whether to block until collection completes (default: true) + [McpServerTool(Name = "dotnet_force_gc")] + public Task ForceGcAsync( + int generation = 2, + bool blocking = true, + CancellationToken cancellationToken = default) + { + _logger.LogInformation("MCP: Forcing GC collection gen {Generation}", generation); + + if (blocking) + { + GC.Collect(generation, GCCollectionMode.Forced, blocking: true, compacting: true); + GC.WaitForPendingFinalizers(); + } + else + { + GC.Collect(generation, GCCollectionMode.Forced, blocking: false, compacting: true); + } + + return Task.FromResult(new + { + Generation = generation, + Blocking = blocking, + TotalMemoryBytes = GC.GetTotalMemory(false), + Timestamp = DateTime.UtcNow + }); + } +} diff --git a/src/DotNetDevMCP.Monitoring/Models/MonitoringModels.cs b/src/DotNetDevMCP.Monitoring/Models/MonitoringModels.cs new file mode 100644 index 0000000..918590b --- /dev/null +++ b/src/DotNetDevMCP.Monitoring/Models/MonitoringModels.cs @@ -0,0 +1,115 @@ +namespace DotNetDevMCP.Monitoring.Models; + +/// +/// Represents performance metrics for a process or application +/// +public sealed record PerformanceMetrics( + string ProcessName, + int ProcessId, + double CpuUsagePercent, + long MemoryUsageBytes, + long PeakMemoryUsageBytes, + int ThreadCount, + int HandleCount, + double GcHeapSizeBytes, + int Gen0Collections, + int Gen1Collections, + int Gen2Collections, + DateTime CapturedAt +); + +/// +/// Represents application health status +/// +public sealed record HealthStatus( + string ApplicationName, + bool IsHealthy, + string Status, + TimeSpan Uptime, + IReadOnlyList Checks, + DateTime CapturedAt +); + +/// +/// Represents a single health check result +/// +public sealed record HealthCheckResult( + string Name, + bool IsHealthy, + string Status, + string? Description = null, + TimeSpan? Duration = null, + Exception? Exception = null +); + +/// +/// Represents resource utilization metrics +/// +public sealed record ResourceUtilization( + double CpuUsagePercent, + double MemoryUsagePercent, + double DiskUsagePercent, + double NetworkInBytesPerSecond, + double NetworkOutBytesPerSecond, + int ActiveConnections, + DateTime CapturedAt +); + +/// +/// Represents profiling session configuration +/// +public sealed record ProfilingConfig( + string SessionName, + TimeSpan Duration, + bool CollectCpuSamples = true, + bool CollectMemorySamples = true, + bool CollectGcEvents = true, + bool CollectThreadEvents = true, + int SamplingIntervalMs = 100 +); + +/// +/// Represents profiling results summary +/// +public sealed record ProfilingResults( + string SessionName, + TimeSpan Duration, + IReadOnlyList CpuHotSpots, + IReadOnlyList TopAllocations, + IReadOnlyList GcEvents, + DateTime CompletedAt +); + +/// +/// Represents a CPU hot spot in profiling results +/// +public sealed record HotSpot( + string MethodName, + string FileName, + int LineNumber, + double CpuPercent, + int CallCount, + double AvgDurationMs +); + +/// +/// Represents a memory allocation site +/// +public sealed record AllocationSite( + string TypeName, + string MethodName, + long TotalBytesAllocated, + int AllocationCount, + double AvgBytesPerAllocation +); + +/// +/// Represents a garbage collection event +/// +public sealed record GcEvent( + int Generation, + long DurationMs, + long BytesAllocated, + long BytesPromoted, + DateTime Timestamp +); diff --git a/src/DotNetDevMCP.Monitoring/Services/PerformanceMonitoringService.cs b/src/DotNetDevMCP.Monitoring/Services/PerformanceMonitoringService.cs new file mode 100644 index 0000000..db1af19 --- /dev/null +++ b/src/DotNetDevMCP.Monitoring/Services/PerformanceMonitoringService.cs @@ -0,0 +1,243 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using DotNetDevMCP.Monitoring.Models; + +namespace DotNetDevMCP.Monitoring.Services; + +/// +/// Service for monitoring application performance and resource usage +/// +public sealed class PerformanceMonitoringService(ILogger logger) +{ + private readonly ILogger _logger = logger; + private readonly Process _currentProcess = Process.GetCurrentProcess(); + private readonly Stopwatch _uptimeStopwatch = Stopwatch.StartNew(); + + /// + /// Captures current performance metrics for the application + /// + public Task GetPerformanceMetricsAsync(CancellationToken cancellationToken = default) + { + _logger.LogDebug("Capturing performance metrics"); + + _currentProcess.Refresh(); + + var cpuUsage = CalculateCpuUsage(); + var memoryUsage = _currentProcess.WorkingSet64; + var peakMemory = _currentProcess.PeakWorkingSet64; + + var metrics = new PerformanceMetrics( + ProcessName: _currentProcess.ProcessName, + ProcessId: _currentProcess.Id, + CpuUsagePercent: cpuUsage, + MemoryUsageBytes: memoryUsage, + PeakMemoryUsageBytes: peakMemory, + ThreadCount: _currentProcess.Threads.Count, + HandleCount: _currentProcess.HandleCount, + GcHeapSizeBytes: GC.GetTotalMemory(false), + Gen0Collections: GC.CollectionCount(0), + Gen1Collections: GC.CollectionCount(1), + Gen2Collections: GC.CollectionCount(2), + CapturedAt: DateTime.UtcNow + ); + + return Task.FromResult(metrics); + } + + /// + /// Gets resource utilization metrics for the system + /// + public async Task GetResourceUtilizationAsync(CancellationToken cancellationToken = default) + { + _logger.LogDebug("Capturing resource utilization"); + + var cpuUsage = await GetSystemCpuUsageAsync(cancellationToken); + var memoryUsage = GetSystemMemoryUsage(); + + return new ResourceUtilization( + CpuUsagePercent: cpuUsage, + MemoryUsagePercent: memoryUsage.memoryPercent, + DiskUsagePercent: 0.0, // Would need platform-specific implementation + NetworkInBytesPerSecond: 0.0, // Would need network monitoring + NetworkOutBytesPerSecond: 0.0, + ActiveConnections: 0, // Would need network stack access + CapturedAt: DateTime.UtcNow + ); + } + + /// + /// Checks application health status + /// + public async Task CheckHealthAsync(CancellationToken cancellationToken = default) + { + _logger.LogDebug("Checking application health"); + + var checks = new List(); + + // Memory check + var memoryOk = GC.GetTotalMemory(false) < 1_000_000_000; // 1GB threshold + checks.Add(new HealthCheckResult( + Name: "Memory", + IsHealthy: memoryOk, + Status: memoryOk ? "Healthy" : "Degraded", + Description: $"Current memory: {GC.GetTotalMemory(false):N0} bytes" + )); + + // Thread check + var threadCount = _currentProcess.Threads.Count; + var threadsOk = threadCount < 100; + checks.Add(new HealthCheckResult( + Name: "Threads", + IsHealthy: threadsOk, + Status: threadsOk ? "Healthy" : "Warning", + Description: $"Active threads: {threadCount}" + )); + + // GC check + var gen2Collections = GC.CollectionCount(2); + var gcOk = gen2Collections < 100; + checks.Add(new HealthCheckResult( + Name: "GarbageCollection", + IsHealthy: gcOk, + Status: gcOk ? "Healthy" : "Warning", + Description: $"Gen2 collections: {gen2Collections}" + )); + + var isHealthy = checks.All(c => c.IsHealthy); + + return new HealthStatus( + ApplicationName: AppDomain.CurrentDomain.FriendlyName, + IsHealthy: isHealthy, + Status: isHealthy ? "Healthy" : "Unhealthy", + Uptime: _uptimeStopwatch.Elapsed, + Checks: checks.AsReadOnly(), + CapturedAt: DateTime.UtcNow + ); + } + + /// + /// Starts a profiling session + /// + public async Task StartProfilingSessionAsync( + ProfilingConfig config, + CancellationToken cancellationToken = default) + { + _logger.LogInformation("Starting profiling session: {SessionName}", config.SessionName); + + var startTime = DateTime.UtcNow; + var cpuHotSpots = new List(); + var allocations = new List(); + var gcEvents = new List(); + + // Capture initial GC stats + var initialGen0 = GC.CollectionCount(0); + var initialGen1 = GC.CollectionCount(1); + var initialGen2 = GC.CollectionCount(2); + var initialMemory = GC.GetTotalMemory(false); + + // Wait for the specified duration or until cancellation + try + { + await Task.Delay(config.Duration, cancellationToken); + } + catch (TaskCanceledException) + { + _logger.LogWarning("Profiling session cancelled: {SessionName}", config.SessionName); + } + + // Capture final stats + var finalGen0 = GC.CollectionCount(0); + var finalGen1 = GC.CollectionCount(1); + var finalGen2 = GC.CollectionCount(2); + var finalMemory = GC.GetTotalMemory(false); + + // Generate synthetic hot spots from current stack trace + var stackTrace = new StackTrace(true); + foreach (var frame in stackTrace.GetFrames().Take(10)) + { + var method = frame.GetMethod(); + if (method != null) + { + cpuHotSpots.Add(new HotSpot( + MethodName: method.Name, + FileName: frame.GetFileName() ?? "unknown", + LineNumber: frame.GetFileLineNumber(), + CpuPercent: 0.0, // Would need actual sampling + CallCount: 1, + AvgDurationMs: 0.0 + )); + } + } + + // Record GC events that occurred during profiling + if (config.CollectGcEvents) + { + var gcCount0 = finalGen0 - initialGen0; + var gcCount1 = finalGen1 - initialGen1; + var gcCount2 = finalGen2 - initialGen2; + + for (int i = 0; i < gcCount0; i++) + { + gcEvents.Add(new GcEvent(0, 1, 0, 0, DateTime.UtcNow)); + } + for (int i = 0; i < gcCount1; i++) + { + gcEvents.Add(new GcEvent(1, 5, 0, 0, DateTime.UtcNow)); + } + for (int i = 0; i < gcCount2; i++) + { + gcEvents.Add(new GcEvent(2, 50, 0, 0, DateTime.UtcNow)); + } + } + + var results = new ProfilingResults( + SessionName: config.SessionName, + Duration: DateTime.UtcNow - startTime, + CpuHotSpots: cpuHotSpots.AsReadOnly(), + TopAllocations: allocations.AsReadOnly(), + GcEvents: gcEvents.AsReadOnly(), + CompletedAt: DateTime.UtcNow + ); + + _logger.LogInformation("Profiling session completed: {SessionName}", config.SessionName); + return results; + } + + private static double CalculateCpuUsage() + { + // Simplified CPU calculation - would need more sophisticated approach in production + return 0.0; + } + + private static async Task GetSystemCpuUsageAsync(CancellationToken ct) + { + // Platform-specific CPU usage calculation + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Would use PerformanceCounter on Windows + return await Task.FromResult(0.0); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + // Would read /proc/stat on Linux + return await Task.FromResult(0.0); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // Would use sysctl on macOS + return await Task.FromResult(0.0); + } + + return 0.0; + } + + private static (double totalMemory, double memoryPercent) GetSystemMemoryUsage() + { + // Simplified memory calculation + var totalMemory = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes; + var usedMemory = GC.GetTotalMemory(false); + var percent = totalMemory > 0 ? (usedMemory * 100.0 / totalMemory) : 0.0; + + return (totalMemory, percent); + } +} diff --git a/src/DotNetDevMCP.Server.Sse/Program.cs b/src/DotNetDevMCP.Server.Sse/Program.cs index c5c7db4..fefbfb2 100644 --- a/src/DotNetDevMCP.Server.Sse/Program.cs +++ b/src/DotNetDevMCP.Server.Sse/Program.cs @@ -2,6 +2,8 @@ using DotNetDevMCP.CodeIntelligence.Interfaces; using DotNetDevMCP.CodeIntelligence.Mcp.Tools; using DotNetDevMCP.CodeIntelligence.Extensions; +using DotNetDevMCP.Analysis.Extensions; +using DotNetDevMCP.Monitoring.Extensions; using System.CommandLine; using System.CommandLine.Parsing; using Microsoft.AspNetCore.HttpLogging; @@ -14,13 +16,15 @@ namespace DotNetDevMCP.Server.Sse; public class Program { // --- Application --- public const string ApplicationName = "DotNetDevMCP.SseServer"; - public const string ApplicationVersion = "0.0.1"; + public const string ApplicationVersion = "0.2.0"; public const string LogOutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"; public static async Task Main(string[] args) { // Ensure tool assemblies are loaded for MCP SDK's WithToolsFromAssembly _ = typeof(SolutionTools); _ = typeof(AnalysisTools); _ = typeof(ModificationTools); + _ = typeof(DotNetDevMCP.Analysis.Mcp.Tools.AnalysisTools); + _ = typeof(DotNetDevMCP.Monitoring.Mcp.Tools.MonitoringTools); var portOption = new Option("--port") { Description = "The port number for the MCP server to listen on.", @@ -134,7 +138,9 @@ public static async Task Main(string[] args) { // Can be configured: logging.RootPath = ... }); - builder.Services.WithSharpToolsServices(!disableGit, buildConfiguration); + builder.Services.WithCodeIntelligenceServices(!disableGit, buildConfiguration); + builder.Services.AddAnalysisServices(); + builder.Services.AddMonitoringServices(); builder.Services .AddMcpServer(options => { @@ -146,7 +152,7 @@ public static async Task Main(string[] args) { // but ModelContextProtocol's own Debug logging should be sufficient. }) .WithHttpTransport() - .WithSharpTools(); + .WithCodeIntelligence(); var app = builder.Build(); diff --git a/src/DotNetDevMCP.Server.Stdio/Program.cs b/src/DotNetDevMCP.Server.Stdio/Program.cs index 436369f..e306f51 100644 --- a/src/DotNetDevMCP.Server.Stdio/Program.cs +++ b/src/DotNetDevMCP.Server.Stdio/Program.cs @@ -2,6 +2,8 @@ using DotNetDevMCP.CodeIntelligence.Interfaces; using DotNetDevMCP.CodeIntelligence.Mcp.Tools; using DotNetDevMCP.CodeIntelligence.Extensions; +using DotNetDevMCP.Analysis.Extensions; +using DotNetDevMCP.Monitoring.Extensions; using Serilog; using System.CommandLine; using System.CommandLine.Parsing; @@ -19,12 +21,14 @@ namespace DotNetDevMCP.Server.Stdio; public static class Program { public const string ApplicationName = "DotNetDevMCP.StdioServer"; - public const string ApplicationVersion = "0.0.1"; + public const string ApplicationVersion = "0.2.0"; public const string LogOutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"; public static async Task Main(string[] args) { _ = typeof(SolutionTools); _ = typeof(AnalysisTools); _ = typeof(ModificationTools); + _ = typeof(DotNetDevMCP.Analysis.Mcp.Tools.AnalysisTools); + _ = typeof(DotNetDevMCP.Monitoring.Mcp.Tools.MonitoringTools); var logDirOption = new Option("--log-directory") { Description = "Optional path to a log directory. If not specified, logs only go to console." @@ -119,7 +123,9 @@ public static async Task Main(string[] args) { var builder = Host.CreateApplicationBuilder(args); builder.Logging.ClearProviders(); builder.Logging.AddSerilog(); - builder.Services.WithSharpToolsServices(!disableGit, buildConfiguration); + builder.Services.WithCodeIntelligenceServices(!disableGit, buildConfiguration); + builder.Services.AddAnalysisServices(); + builder.Services.AddMonitoringServices(); builder.Services .AddMcpServer(options => { @@ -129,7 +135,7 @@ public static async Task Main(string[] args) { }; }) .WithStdioServerTransport() - .WithSharpTools(); + .WithCodeIntelligence(); try { Log.Information("Starting {AppName} v{AppVersion}", ApplicationName, ApplicationVersion); diff --git a/src/DotNetDevMCP.SourceControl/DotNetDevMCP.SourceControl.csproj b/src/DotNetDevMCP.SourceControl/DotNetDevMCP.SourceControl.csproj index 008911b..e0ce06f 100644 --- a/src/DotNetDevMCP.SourceControl/DotNetDevMCP.SourceControl.csproj +++ b/src/DotNetDevMCP.SourceControl/DotNetDevMCP.SourceControl.csproj @@ -8,7 +8,6 @@ -