Skip to content

Commit 8b51fd1

Browse files
sharpninjaCopilot
andcommitted
refactor: extract factories + add infra interfaces, drop 35 violations (110→75)
Wave 1-2 of 8-phase remediation: - P1.1-P1.5: Add 5 infrastructure service interfaces to UI.Core submodule (IFileSystemService, IProcessLauncherService, ITimerService, IJsonParsingService, IFileSystemWatcherService) - P2.1-P2.2: Create AppMediatorFactory, move 31 handler registrations out of MainWindowViewModel (eliminates all VM002 violations) - P3.1-P3.2: Create McpServiceFactory, replace 4 direct service constructions in InitializeMcpEndpoint (eliminates all VM001 violations) - P8.1: Create McpServerManager.Core.Tests xunit project Violations: 110 → 75 (VM001: 4→0, VM002: 31→0) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b41207a commit 8b51fd1

6 files changed

Lines changed: 193 additions & 70 deletions

File tree

McpServerManager.slnx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@
55
<Project Path="src/McpServerManager.Desktop/McpServerManager.Desktop.csproj" />
66
<Project Path="src/McpServerManager.Android/McpServerManager.Android.csproj" />
77
</Folder>
8+
<Folder Name="/tests/">
9+
<Project Path="src/McpServerManager.Core.Tests/McpServerManager.Core.Tests.csproj" />
10+
</Folder>
811
</Solution>

lib/McpServer

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net9.0</TargetFramework>
4+
<Nullable>enable</Nullable>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<IsPackable>false</IsPackable>
7+
<IsTestProject>true</IsTestProject>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
12+
<PackageReference Include="xunit" Version="2.9.3" />
13+
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
14+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
15+
<PrivateAssets>all</PrivateAssets>
16+
</PackageReference>
17+
<PackageReference Include="Moq" Version="4.20.72" />
18+
<PackageReference Include="FluentAssertions" Version="7.1.0" />
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<ProjectReference Include="..\McpServerManager.Core\McpServerManager.Core.csproj" />
23+
</ItemGroup>
24+
</Project>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
using McpServerManager.Core.Cqrs;
3+
4+
namespace McpServerManager.Core.Services;
5+
6+
/// <summary>
7+
/// Factory for creating and registering all app-side CQRS mediator handlers.
8+
/// Extracts handler registration from MainWindowViewModel to the composition root.
9+
/// </summary>
10+
public static class AppMediatorFactory
11+
{
12+
/// <summary>
13+
/// Creates a new <see cref="Mediator"/> and registers all command/query handlers.
14+
/// </summary>
15+
/// <param name="onBusyChanged">Optional callback invoked when the mediator's IsBusy state changes.</param>
16+
public static Mediator CreateAndRegisterAllHandlers(Action<bool>? onBusyChanged = null)
17+
{
18+
var mediator = new Mediator();
19+
20+
if (onBusyChanged is not null)
21+
mediator.IsBusyChanged += onBusyChanged;
22+
23+
// Async operations (data loading)
24+
mediator.Register(new Commands.InitializeFromMcpHandler());
25+
mediator.Register(new Commands.RefreshAndLoadAllJsonHandler());
26+
mediator.Register(new Commands.RefreshAndLoadAgentJsonHandler());
27+
mediator.Register(new Commands.RefreshAndLoadSessionHandler());
28+
mediator.Register(new Commands.LoadJsonFileHandler());
29+
mediator.Register(new Commands.NavigateToNodeHandler());
30+
mediator.Register(new Commands.LoadMarkdownFileHandler());
31+
mediator.Register(new Commands.LoadSourceFileHandler());
32+
33+
// Navigation
34+
mediator.Register(new Commands.NavigateBackHandler());
35+
mediator.Register(new Commands.NavigateForwardHandler());
36+
mediator.Register(new Commands.RefreshViewHandler());
37+
mediator.Register(new Commands.PhoneNavigateSectionHandler());
38+
mediator.Register(new Commands.TreeItemTappedHandler());
39+
40+
// Request details
41+
mediator.Register(new Commands.ShowRequestDetailsHandler());
42+
mediator.Register(new Commands.CloseRequestDetailsHandler());
43+
mediator.Register(new Commands.NavigateToPreviousRequestHandler());
44+
mediator.Register(new Commands.NavigateToNextRequestHandler());
45+
46+
// Selection & interaction
47+
mediator.Register(new Commands.SelectSearchEntryHandler());
48+
mediator.Register(new Commands.JsonNodeDoubleTappedHandler());
49+
mediator.Register(new Commands.SearchRowTappedHandler());
50+
mediator.Register(new Commands.SearchRowDoubleTappedHandler());
51+
52+
// Clipboard
53+
mediator.Register(new Commands.CopyTextHandler());
54+
mediator.Register(new Commands.CopyOriginalJsonHandler());
55+
56+
// Preview/Markdown
57+
mediator.Register(new Commands.OpenPreviewInBrowserHandler());
58+
mediator.Register(new Commands.ToggleShowRawMarkdownHandler());
59+
60+
// Archive
61+
mediator.Register(new Commands.ArchiveCurrentHandler());
62+
mediator.Register(new Commands.ArchiveTreeItemHandler());
63+
64+
// Tree & config
65+
mediator.Register(new Commands.OpenTreeItemHandler());
66+
mediator.Register(new Commands.OpenAgentConfigHandler());
67+
mediator.Register(new Commands.OpenPromptTemplatesHandler());
68+
69+
// Refresh
70+
mediator.Register(new Commands.RefreshHandler());
71+
72+
return mediator;
73+
}
74+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System;
2+
using McpServer.Client;
3+
4+
namespace McpServerManager.Core.Services;
5+
6+
/// <summary>
7+
/// Factory for creating MCP service instances. Extracts service construction
8+
/// from MainWindowViewModel to the composition root.
9+
/// </summary>
10+
public sealed class McpServiceFactory
11+
{
12+
/// <summary>
13+
/// Creates a <see cref="McpSessionLogService"/> backed by the given client.
14+
/// </summary>
15+
public McpSessionLogService CreateSessionLogService(McpServerClient client)
16+
=> new(client);
17+
18+
/// <summary>
19+
/// Creates a <see cref="McpTodoService"/> backed by the given clients.
20+
/// </summary>
21+
/// <param name="client">Client for standard TODO operations.</param>
22+
/// <param name="promptClient">Client with extended timeout for prompt generation.</param>
23+
public McpTodoService CreateTodoService(McpServerClient client, McpServerClient promptClient)
24+
=> new(client, promptClient);
25+
26+
/// <summary>
27+
/// Creates a <see cref="McpWorkspaceService"/> backed by the given client.
28+
/// </summary>
29+
public McpWorkspaceService CreateWorkspaceService(McpServerClient client, Uri baseUri)
30+
=> new(client, baseUri);
31+
32+
/// <summary>
33+
/// Creates a <see cref="McpVoiceConversationService"/> with resolver functions
34+
/// for dynamic connection state.
35+
/// </summary>
36+
public McpVoiceConversationService CreateVoiceService(
37+
string baseUrl,
38+
string? apiKey,
39+
string? bearerToken,
40+
Func<string> resolveBaseUrl,
41+
Func<string?> resolveBearerToken,
42+
Func<string?> resolveApiKey,
43+
Func<string?> resolveWorkspacePath)
44+
{
45+
return new McpVoiceConversationService(baseUrl, apiKey: apiKey, bearerToken: bearerToken)
46+
{
47+
ResolveBaseUrl = resolveBaseUrl,
48+
ResolveBearerToken = resolveBearerToken,
49+
ResolveApiKey = resolveApiKey,
50+
ResolveWorkspacePath = resolveWorkspacePath
51+
};
52+
}
53+
54+
/// <summary>
55+
/// Creates a <see cref="McpAgentEventStreamService"/> via the existing factory.
56+
/// </summary>
57+
public McpAgentEventStreamService CreateEventStreamService(
58+
string baseUrl,
59+
string? apiKey,
60+
string? bearerToken,
61+
Func<string> resolveBaseUrl,
62+
Func<string?> resolveBearerToken,
63+
Func<string?> resolveApiKey,
64+
Func<string?> resolveWorkspacePath)
65+
{
66+
return AgentEventStreamFactory.Create(
67+
baseUrl,
68+
apiKey: apiKey,
69+
bearerToken: bearerToken,
70+
resolveBaseUrl: resolveBaseUrl,
71+
resolveBearerToken: resolveBearerToken,
72+
resolveApiKey: resolveApiKey,
73+
resolveWorkspacePath: resolveWorkspacePath);
74+
}
75+
}

src/McpServerManager.Core/ViewModels/MainWindowViewModel.cs

Lines changed: 16 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ public partial class MainWindowViewModel : ViewModelBase, Commands.ICommandTarge
7171
private UiCoreAppRuntime _uiCoreRuntime = null!;
7272
private bool _suppressWorkspaceSelectionChanged;
7373
private bool _hasCompletedInitialSwitch;
74-
internal readonly Mediator _mediator = new();
74+
internal readonly Mediator _mediator;
75+
private readonly McpServiceFactory _serviceFactory = new();
7576
private static readonly ILogger _logger = AppLogService.Instance.CreateLogger("ViewModel");
7677

7778
/// <summary>Raised when the active workspace path changes. Child VMs subscribe to refresh reactively.</summary>
@@ -377,8 +378,9 @@ public MainWindowViewModel(
377378
_clipboardService = clipboardService;
378379
_systemNotificationService = systemNotificationService ?? NoOpSystemNotificationService.Instance;
379380
_activeBearerToken = string.IsNullOrWhiteSpace(bearerToken) ? null : bearerToken.Trim();
381+
_mediator = AppMediatorFactory.CreateAndRegisterAllHandlers(
382+
busy => DispatchToUi(() => IsBusy = _mediator.IsBusy));
380383
InitializeMcpEndpoint(mcpBaseUrl, mcpApiKey);
381-
RegisterCqrsHandlers();
382384
}
383385

384386
private void InitializeMcpEndpoint(string mcpBaseUrl, string? initialApiKey = null)
@@ -403,23 +405,21 @@ private void InitializeMcpEndpoint(string mcpBaseUrl, string? initialApiKey = nu
403405
apiKey: _activeMcpApiKey,
404406
bearerToken: _activeBearerToken);
405407

406-
// Create services once — they share the pre-authenticated clients.
407-
McpSessionService = new McpSessionLogService(_mcpClient);
408-
_mcpTodoService = new McpTodoService(_mcpClient, _mcpPromptClient);
409-
_mcpWorkspaceService = new McpWorkspaceService(_mcpClient, _defaultMcpBaseUri);
410-
_mcpVoiceService = new McpVoiceConversationService(
408+
// Create services once via factory — they share the pre-authenticated clients.
409+
McpSessionService = _serviceFactory.CreateSessionLogService(_mcpClient);
410+
_mcpTodoService = _serviceFactory.CreateTodoService(_mcpClient, _mcpPromptClient);
411+
_mcpWorkspaceService = _serviceFactory.CreateWorkspaceService(_mcpClient, _defaultMcpBaseUri);
412+
_mcpVoiceService = _serviceFactory.CreateVoiceService(
411413
_defaultMcpBaseUrl,
412414
apiKey: _activeMcpApiKey,
413-
bearerToken: _activeBearerToken)
414-
{
415-
ResolveBaseUrl = () => _activeMcpBaseUrl,
416-
ResolveBearerToken = () => _activeBearerToken,
417-
ResolveApiKey = () => _activeMcpApiKey,
418-
ResolveWorkspacePath = () => SelectedWorkspaceConnection?.WorkspaceRootPath
419-
?? _mcpClient.WorkspacePath
420-
};
415+
bearerToken: _activeBearerToken,
416+
resolveBaseUrl: () => _activeMcpBaseUrl,
417+
resolveBearerToken: () => _activeBearerToken,
418+
resolveApiKey: () => _activeMcpApiKey,
419+
resolveWorkspacePath: () => SelectedWorkspaceConnection?.WorkspaceRootPath
420+
?? _mcpClient.WorkspacePath);
421421
_activeMcpBaseUrl = _defaultMcpBaseUrl;
422-
_agentEventStreamService = AgentEventStreamFactory.Create(
422+
_agentEventStreamService = _serviceFactory.CreateEventStreamService(
423423
_activeMcpBaseUrl,
424424
apiKey: _activeMcpApiKey,
425425
bearerToken: _activeBearerToken,
@@ -545,59 +545,6 @@ private void ApplyActiveMcpBaseUrl(string mcpBaseUrl, string? mcpApiKey = null,
545545
return null;
546546
}
547547

548-
private void RegisterCqrsHandlers()
549-
{
550-
_mediator.IsBusyChanged += _ => DispatchToUi(() => IsBusy = _mediator.IsBusy);
551-
// Async operations (data loading)
552-
_mediator.Register(new Commands.InitializeFromMcpHandler());
553-
_mediator.Register(new Commands.RefreshAndLoadAllJsonHandler());
554-
_mediator.Register(new Commands.RefreshAndLoadAgentJsonHandler());
555-
_mediator.Register(new Commands.RefreshAndLoadSessionHandler());
556-
_mediator.Register(new Commands.LoadJsonFileHandler());
557-
_mediator.Register(new Commands.NavigateToNodeHandler());
558-
_mediator.Register(new Commands.LoadMarkdownFileHandler());
559-
_mediator.Register(new Commands.LoadSourceFileHandler());
560-
561-
// Navigation
562-
_mediator.Register(new Commands.NavigateBackHandler());
563-
_mediator.Register(new Commands.NavigateForwardHandler());
564-
_mediator.Register(new Commands.RefreshViewHandler());
565-
_mediator.Register(new Commands.PhoneNavigateSectionHandler());
566-
_mediator.Register(new Commands.TreeItemTappedHandler());
567-
568-
// Request details
569-
_mediator.Register(new Commands.ShowRequestDetailsHandler());
570-
_mediator.Register(new Commands.CloseRequestDetailsHandler());
571-
_mediator.Register(new Commands.NavigateToPreviousRequestHandler());
572-
_mediator.Register(new Commands.NavigateToNextRequestHandler());
573-
574-
// Selection & interaction
575-
_mediator.Register(new Commands.SelectSearchEntryHandler());
576-
_mediator.Register(new Commands.JsonNodeDoubleTappedHandler());
577-
_mediator.Register(new Commands.SearchRowTappedHandler());
578-
_mediator.Register(new Commands.SearchRowDoubleTappedHandler());
579-
580-
// Clipboard
581-
_mediator.Register(new Commands.CopyTextHandler());
582-
_mediator.Register(new Commands.CopyOriginalJsonHandler());
583-
584-
// Preview/Markdown
585-
_mediator.Register(new Commands.OpenPreviewInBrowserHandler());
586-
_mediator.Register(new Commands.ToggleShowRawMarkdownHandler());
587-
588-
// Archive
589-
_mediator.Register(new Commands.ArchiveCurrentHandler());
590-
_mediator.Register(new Commands.ArchiveTreeItemHandler());
591-
592-
// Tree & config
593-
_mediator.Register(new Commands.OpenTreeItemHandler());
594-
_mediator.Register(new Commands.OpenAgentConfigHandler());
595-
_mediator.Register(new Commands.OpenPromptTemplatesHandler());
596-
597-
// Refresh
598-
_mediator.Register(new Commands.RefreshHandler());
599-
}
600-
601548
/// <summary>Command for tree item tap (handles directory expand/collapse and MCP node refresh).</summary>
602549
[RelayCommand]
603550
private void TreeItemTapped(FileNode? node)

0 commit comments

Comments
 (0)