Skip to content

Commit 03d5de8

Browse files
feat: Add service update command with improved backup structure and logging
Major Features: - Add --update command for seamless service updates (stop, build, backup, update, start) - Implement proper --help command with comprehensive usage information - Add organized backup system using C:\Program Files\MCP-Nexus\backups\[timestamp] Improvements: - Refactor all logging to use consistent OperationLogger with operation prefixes - Add Update operation constant to OperationLogger.Operations - Improve command line argument parsing and validation - Add proper admin privilege checks for all service operations Backup System: - Create timestamped backups in organized subfolder structure - Prevent backup recursion by excluding backups folder from backup process - Provide clear backup location feedback to users - Maintain backup history for easy rollback Documentation: - Update README.md with new --update command - Add comprehensive help system with examples and notes - Document improved backup structure and service features - Include admin privilege requirements and backup locations Fixes: - Fix null reference warnings in McpController logging - Fix unused variable warnings in WindowsServiceInstaller - Ensure all logger calls use consistent OperationLogger format - Add missing using System.Linq directive User Experience: - Add emoji indicators for better visual feedback during operations - Provide clear error messages and recovery instructions - Show backup locations and service status in output - Include manual cleanup options for troubleshooting
1 parent 74c0733 commit 03d5de8

File tree

5 files changed

+233
-8
lines changed

5 files changed

+233
-8
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ Install MCP Nexus as a Windows service for persistent operation:
4848
# Install as Windows service (requires administrator privileges)
4949
dotnet run -- --install
5050

51+
# Update existing Windows service (stop, update files, restart)
52+
dotnet run -- --update
53+
5154
# Uninstall the Windows service
5255
dotnet run -- --uninstall
5356

@@ -61,6 +64,7 @@ dotnet run -- --service
6164
- **Program Files**: Installed to `C:\Program Files\MCP-Nexus`
6265
- **Event Logging**: Logs to Windows Event Log and files
6366
- **Management**: Use Windows Services console or command line
67+
- **Safe Updates**: Automatic backups to `C:\Program Files\MCP-Nexus\backups\[timestamp]`
6468

6569
**Service Management:**
6670
```bash
@@ -189,8 +193,13 @@ curl -X POST http://localhost:5000/mcp \
189193
### Command Line Options
190194

191195
- `--http`: Run in HTTP transport mode
196+
- `--service`: Run in Windows service mode (implies --http)
192197
- `--cdb-path <path>`: Custom path to CDB.exe for debugging tools
193-
- `--verbose`: Enable verbose logging
198+
- `--install`: Install MCP Nexus as Windows service (Windows only)
199+
- `--update`: Update existing Windows service files and restart (Windows only)
200+
- `--uninstall`: Uninstall MCP Nexus Windows service (Windows only)
201+
- `--force-uninstall`: Force uninstall service with registry cleanup (Windows only)
202+
- `--help`: Show command line help
194203

195204
### Environment Variables
196205

mcp_nexus/Program.cs

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ internal class Program
1414
{
1515
private static async Task Main(string[] args)
1616
{
17+
// Check if this is a help request first
18+
if (args.Length > 0 && (args[0] == "--help" || args[0] == "-h" || args[0] == "help"))
19+
{
20+
await ShowHelpAsync();
21+
return;
22+
}
23+
1724
// Parse command line arguments
1825
var commandLineArgs = ParseCommandLineArguments(args);
1926

@@ -90,6 +97,30 @@ private static async Task Main(string[] args)
9097
return;
9198
}
9299

100+
if (commandLineArgs.Update)
101+
{
102+
if (OperatingSystem.IsWindows())
103+
{
104+
// Create a logger using NLog configuration for the update process
105+
using var loggerFactory = LoggerFactory.Create(builder =>
106+
{
107+
builder.ClearProviders();
108+
builder.AddNLogWeb();
109+
builder.SetMinimumLevel(LogLevel.Information);
110+
});
111+
var logger = loggerFactory.CreateLogger("MCP.Nexus.ServiceInstaller");
112+
113+
var success = await WindowsServiceInstaller.UpdateServiceAsync(logger);
114+
Environment.Exit(success ? 0 : 1);
115+
}
116+
else
117+
{
118+
Console.Error.WriteLine("ERROR: Service update is only supported on Windows.");
119+
Environment.Exit(1);
120+
}
121+
return;
122+
}
123+
93124
// Determine transport mode
94125
bool useHttp = commandLineArgs.UseHttp || commandLineArgs.ServiceMode;
95126

@@ -111,6 +142,44 @@ private static async Task Main(string[] args)
111142
}
112143
}
113144

145+
private static async Task ShowHelpAsync()
146+
{
147+
Console.WriteLine("MCP Nexus - Comprehensive MCP Server Platform");
148+
Console.WriteLine();
149+
Console.WriteLine("USAGE:");
150+
Console.WriteLine(" mcp_nexus [OPTIONS]");
151+
Console.WriteLine();
152+
Console.WriteLine("DESCRIPTION:");
153+
Console.WriteLine(" MCP Nexus is a Model Context Protocol (MCP) server that provides various tools");
154+
Console.WriteLine(" and utilities for development and debugging. It supports both stdio and HTTP transports.");
155+
Console.WriteLine();
156+
Console.WriteLine("OPTIONS:");
157+
Console.WriteLine(" --http Use HTTP transport instead of stdio");
158+
Console.WriteLine(" --service Run in Windows service mode (implies --http)");
159+
Console.WriteLine(" --cdb-path <PATH> Custom path to CDB.exe debugger executable");
160+
Console.WriteLine();
161+
Console.WriteLine("SERVICE MANAGEMENT (Windows only):");
162+
Console.WriteLine(" --install Install MCP Nexus as Windows service");
163+
Console.WriteLine(" --uninstall Uninstall MCP Nexus Windows service");
164+
Console.WriteLine(" --update Update MCP Nexus service (stop, update files, restart)");
165+
Console.WriteLine(" --force-uninstall Force uninstall MCP Nexus service (removes registry entries)");
166+
Console.WriteLine();
167+
Console.WriteLine("EXAMPLES:");
168+
Console.WriteLine(" mcp_nexus # Run in stdio mode");
169+
Console.WriteLine(" mcp_nexus --http # Run HTTP server on localhost:5000");
170+
Console.WriteLine(" mcp_nexus --install # Install as Windows service");
171+
Console.WriteLine(" mcp_nexus --update # Update installed service");
172+
Console.WriteLine(" mcp_nexus --cdb-path \"C:\\WinDbg\" # Use custom debugger path");
173+
Console.WriteLine();
174+
Console.WriteLine("NOTES:");
175+
Console.WriteLine(" - Service commands require administrator privileges on Windows");
176+
Console.WriteLine(" - Updates create backups in: C:\\Program Files\\MCP-Nexus\\backups\\[timestamp]");
177+
Console.WriteLine(" - HTTP mode runs on localhost:5000/mcp");
178+
Console.WriteLine();
179+
Console.WriteLine("For more information, visit: https://github.com/your-repo/mcp_nexus");
180+
await Task.CompletedTask;
181+
}
182+
114183
private static CommandLineArguments ParseCommandLineArguments(string[] args)
115184
{
116185
var result = new CommandLineArguments();
@@ -121,6 +190,7 @@ private static CommandLineArguments ParseCommandLineArguments(string[] args)
121190
var installOption = new Option<bool>("--install", "Install MCP Nexus as Windows service");
122191
var uninstallOption = new Option<bool>("--uninstall", "Uninstall MCP Nexus Windows service");
123192
var forceUninstallOption = new Option<bool>("--force-uninstall", "Force uninstall MCP Nexus service (removes registry entries)");
193+
var updateOption = new Option<bool>("--update", "Update MCP Nexus service (stop, update files, restart)");
124194

125195
var rootCommand = new RootCommand("MCP Nexus - Comprehensive MCP Server Platform")
126196
{
@@ -129,7 +199,8 @@ private static CommandLineArguments ParseCommandLineArguments(string[] args)
129199
serviceOption,
130200
installOption,
131201
uninstallOption,
132-
forceUninstallOption
202+
forceUninstallOption,
203+
updateOption
133204
};
134205

135206
var parseResult = rootCommand.Parse(args);
@@ -141,6 +212,7 @@ private static CommandLineArguments ParseCommandLineArguments(string[] args)
141212
result.Install = parseResult.GetValueForOption(installOption);
142213
result.Uninstall = parseResult.GetValueForOption(uninstallOption);
143214
result.ForceUninstall = parseResult.GetValueForOption(forceUninstallOption);
215+
result.Update = parseResult.GetValueForOption(updateOption);
144216
}
145217

146218
return result;
@@ -154,6 +226,7 @@ private class CommandLineArguments
154226
public bool Install { get; set; }
155227
public bool Uninstall { get; set; }
156228
public bool ForceUninstall { get; set; }
229+
public bool Update { get; set; }
157230
}
158231

159232
private static async Task RunHttpServer(string[] args, CommandLineArguments commandLineArgs)

mcp_nexus/Services/OperationLogger.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public static class Operations
8989
public const string Install = "Install";
9090
public const string Uninstall = "Uninstall";
9191
public const string ForceUninstall = "ForceUninstall";
92+
public const string Update = "Update";
9293
public const string Build = "Build";
9394
public const string Copy = "Copy";
9495
public const string Registry = "Registry";

mcp_nexus/Services/WindowsServiceInstaller.cs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics;
2+
using System.Linq;
23
using System.Reflection;
34
using System.Runtime.Versioning;
45
using System.Security.Principal;
@@ -696,6 +697,152 @@ private static bool IsRunAsAdministrator()
696697
return false;
697698
}
698699
}
700+
701+
public static async Task<bool> UpdateServiceAsync(ILogger? logger = null)
702+
{
703+
try
704+
{
705+
if (!IsRunAsAdministrator())
706+
{
707+
var errorMsg = "Service update requires administrator privileges. Please run the command as administrator.";
708+
OperationLogger.LogError(logger, OperationLogger.Operations.Update, "{ErrorMsg}", errorMsg);
709+
Console.Error.WriteLine($"ERROR: {errorMsg}");
710+
return false;
711+
}
712+
713+
OperationLogger.LogInfo(logger, OperationLogger.Operations.Update, "Starting MCP Nexus service update");
714+
Console.WriteLine("🔄 Starting MCP Nexus service update...");
715+
716+
// Check if service exists
717+
if (!IsServiceInstalled())
718+
{
719+
var errorMsg = "MCP Nexus service is not installed. Use --install to install it first.";
720+
OperationLogger.LogError(logger, OperationLogger.Operations.Update, "{ErrorMsg}", errorMsg);
721+
Console.Error.WriteLine($"ERROR: {errorMsg}");
722+
return false;
723+
}
724+
725+
// Step 1: Stop the service
726+
OperationLogger.LogInfo(logger, OperationLogger.Operations.Update, "Stopping MCP Nexus service for update");
727+
Console.WriteLine("📱 Stopping MCP Nexus service...");
728+
var stopResult = await RunScCommandAsync($@"stop ""{ServiceName}""", logger, allowFailure: true);
729+
if (stopResult)
730+
{
731+
OperationLogger.LogInfo(logger, OperationLogger.Operations.Update, "Service stopped successfully");
732+
Console.WriteLine("✅ Service stopped successfully");
733+
}
734+
else
735+
{
736+
OperationLogger.LogInfo(logger, OperationLogger.Operations.Update, "Service was not running or already stopped");
737+
Console.WriteLine("ℹ️ Service was not running");
738+
}
739+
740+
// Wait for service to fully stop
741+
await Task.Delay(3000);
742+
743+
// Step 2: Build the project in Release mode
744+
OperationLogger.LogInfo(logger, OperationLogger.Operations.Update, "Building project for deployment");
745+
Console.WriteLine("🔨 Building project in Release mode...");
746+
if (!await BuildProjectForDeploymentAsync(logger))
747+
{
748+
OperationLogger.LogError(logger, OperationLogger.Operations.Update, "Failed to build project for update");
749+
Console.Error.WriteLine("❌ Failed to build project for update");
750+
return false;
751+
}
752+
753+
// Step 3: Update files
754+
OperationLogger.LogInfo(logger, OperationLogger.Operations.Update, "Updating application files");
755+
Console.WriteLine("📁 Updating application files...");
756+
757+
// Create backup of current installation
758+
var backupsBaseFolder = Path.Combine(InstallFolder, "backups");
759+
var backupFolder = Path.Combine(backupsBaseFolder, DateTime.Now.ToString("yyyyMMdd_HHmmss"));
760+
OperationLogger.LogInfo(logger, OperationLogger.Operations.Update, "Creating backup: {BackupFolder}", backupFolder);
761+
try
762+
{
763+
if (Directory.Exists(InstallFolder))
764+
{
765+
// Create backups directory if it doesn't exist
766+
if (!Directory.Exists(backupsBaseFolder))
767+
{
768+
Directory.CreateDirectory(backupsBaseFolder);
769+
}
770+
771+
// Create the specific backup folder
772+
Directory.CreateDirectory(backupFolder);
773+
774+
// Copy all files except the backups folder itself
775+
var sourceFiles = Directory.GetFiles(InstallFolder, "*", SearchOption.TopDirectoryOnly);
776+
foreach (var sourceFile in sourceFiles)
777+
{
778+
var fileName = Path.GetFileName(sourceFile);
779+
var targetFile = Path.Combine(backupFolder, fileName);
780+
File.Copy(sourceFile, targetFile, true);
781+
}
782+
783+
// Copy subdirectories except backups
784+
var sourceDirectories = Directory.GetDirectories(InstallFolder)
785+
.Where(dir => !string.Equals(Path.GetFileName(dir), "backups", StringComparison.OrdinalIgnoreCase))
786+
.ToArray();
787+
788+
foreach (var sourceDir in sourceDirectories)
789+
{
790+
var dirName = Path.GetFileName(sourceDir);
791+
var targetDir = Path.Combine(backupFolder, dirName);
792+
await CopyDirectoryAsync(sourceDir, targetDir, logger);
793+
}
794+
795+
OperationLogger.LogInfo(logger, OperationLogger.Operations.Update, "Backup created successfully");
796+
Console.WriteLine($"💾 Backup created: {backupFolder}");
797+
}
798+
}
799+
catch (Exception ex)
800+
{
801+
OperationLogger.LogWarning(logger, OperationLogger.Operations.Update, "Could not create backup: {Error}", ex.Message);
802+
Console.WriteLine($"⚠️ Warning: Could not create backup: {ex.Message}");
803+
}
804+
805+
// Copy new files
806+
await CopyApplicationFilesAsync(logger);
807+
OperationLogger.LogInfo(logger, OperationLogger.Operations.Update, "Application files updated successfully");
808+
Console.WriteLine("✅ Application files updated");
809+
810+
// Step 4: Start the service
811+
OperationLogger.LogInfo(logger, OperationLogger.Operations.Update, "Starting MCP Nexus service");
812+
Console.WriteLine("🚀 Starting MCP Nexus service...");
813+
var startResult = await RunScCommandAsync($@"start ""{ServiceName}""", logger);
814+
if (startResult)
815+
{
816+
OperationLogger.LogInfo(logger, OperationLogger.Operations.Update, "Service started successfully");
817+
Console.WriteLine("✅ Service started successfully");
818+
}
819+
else
820+
{
821+
OperationLogger.LogError(logger, OperationLogger.Operations.Update, "Failed to start service after update");
822+
Console.Error.WriteLine("❌ Failed to start service after update");
823+
Console.Error.WriteLine($"You can manually start it with: sc start \"{ServiceName}\"");
824+
Console.Error.WriteLine($"Or restore from backup: {backupFolder}");
825+
return false;
826+
}
827+
828+
OperationLogger.LogInfo(logger, OperationLogger.Operations.Update, "MCP Nexus service updated successfully");
829+
Console.WriteLine();
830+
Console.WriteLine("✅ MCP Nexus service updated successfully!");
831+
Console.WriteLine($" Service Name: {ServiceName}");
832+
Console.WriteLine($" Install Path: {InstallFolder}");
833+
Console.WriteLine(" HTTP Endpoint: http://localhost:5000/mcp");
834+
Console.WriteLine($" Backup Location: {backupFolder}");
835+
Console.WriteLine();
836+
837+
return true;
838+
}
839+
catch (Exception ex)
840+
{
841+
OperationLogger.LogError(logger, OperationLogger.Operations.Update, ex, "Service update failed");
842+
Console.Error.WriteLine($"❌ Service update failed: {ex.Message}");
843+
return false;
844+
}
845+
}
699846
}
700847
}
701848

@@ -707,3 +854,4 @@ private static bool IsRunAsAdministrator()
707854

708855

709856

857+

mcp_nexus/nlog.config

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,9 @@
1515
keepFileOpen="false"
1616
autoFlush="true"
1717
createDirs="true" />
18-
19-
<target xsi:type="EventLog" name="eventLog"
20-
source="MCP-Nexus"
21-
eventId="1000"
22-
layout="${message} ${exception:format=ToString}" />
2318
</targets>
2419

2520
<rules>
2621
<logger name="*" minlevel="Info" writeTo="mainFile" />
27-
<logger name="*" minlevel="Warn" writeTo="eventLog" />
2822
</rules>
2923
</nlog>

0 commit comments

Comments
 (0)