Skip to content

Commit 74c0733

Browse files
Add installaer to run it as service
1 parent 157c6a9 commit 74c0733

File tree

7 files changed

+1028
-29
lines changed

7 files changed

+1028
-29
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,41 @@ dotnet run
4040
dotnet run -- --http
4141
```
4242

43+
### Windows Service Installation
44+
45+
Install MCP Nexus as a Windows service for persistent operation:
46+
47+
```bash
48+
# Install as Windows service (requires administrator privileges)
49+
dotnet run -- --install
50+
51+
# Uninstall the Windows service
52+
dotnet run -- --uninstall
53+
54+
# Manual service mode testing
55+
dotnet run -- --service
56+
```
57+
58+
**Service Features:**
59+
- **Auto-start**: Service starts automatically on system boot
60+
- **HTTP Mode**: Service runs in HTTP transport mode
61+
- **Program Files**: Installed to `C:\Program Files\MCP-Nexus`
62+
- **Event Logging**: Logs to Windows Event Log and files
63+
- **Management**: Use Windows Services console or command line
64+
65+
**Service Management:**
66+
```bash
67+
# Check service status
68+
sc query "MCP-Nexus"
69+
70+
# Start/stop service manually
71+
sc start "MCP-Nexus"
72+
sc stop "MCP-Nexus"
73+
74+
# Access HTTP endpoint when service is running
75+
# http://localhost:5000/mcp
76+
```
77+
4378
### Basic Usage
4479

4580
The server automatically exposes all available tools through the MCP protocol. Connect using any MCP-compatible client or integrate directly with AI tools like Cursor.

mcp_nexus/Controllers/McpController.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ public async Task<IActionResult> HandleMcpRequest()
2424
var sessionId = Request.Headers["Mcp-Session-Id"].FirstOrDefault() ?? Guid.NewGuid().ToString();
2525
Response.Headers["Mcp-Session-Id"] = sessionId;
2626

27-
_logger.LogInformation("=== NEW MCP REQUEST (Session: {SessionId}) ===", sessionId);
28-
_logger.LogDebug("Request Headers: {Headers}", string.Join(", ", Request.Headers.Select(h => $"{h.Key}={string.Join(",", h.Value.ToArray())}")));
27+
OperationLogger.LogInfo(_logger, OperationLogger.Operations.Http, "NEW MCP REQUEST (Session: {SessionId})", sessionId);
28+
OperationLogger.LogDebug(_logger, OperationLogger.Operations.Http, "Request Headers: {Headers}", string.Join(", ", Request.Headers.Select(h => $"{h.Key}={string.Join(",", h.Value.ToArray())}")));
2929

3030
// Set up standard JSON response headers (NOT SSE)
3131
Response.Headers["Access-Control-Allow-Origin"] = "*";
@@ -43,11 +43,11 @@ public async Task<IActionResult> HandleMcpRequest()
4343
var requestElement = JsonSerializer.Deserialize<JsonElement>(requestBody);
4444

4545
// Extract and log key request details
46-
var method = requestElement.TryGetProperty("method", out var methodProp) ? methodProp.GetString() : "unknown";
47-
var id = requestElement.TryGetProperty("id", out var idProp) ? idProp.ToString() : "unknown";
48-
var jsonrpc = requestElement.TryGetProperty("jsonrpc", out var jsonrpcProp) ? jsonrpcProp.GetString() : "unknown";
46+
var method = requestElement.TryGetProperty("method", out var methodProp) ? methodProp.GetString() ?? "unknown" : "unknown";
47+
var id = requestElement.TryGetProperty("id", out var idProp) ? idProp.ToString() ?? "unknown" : "unknown";
48+
var jsonrpc = requestElement.TryGetProperty("jsonrpc", out var jsonrpcProp) ? jsonrpcProp.GetString() ?? "unknown" : "unknown";
4949

50-
_logger.LogInformation("Parsed JSON successfully - Method: '{Method}', ID: '{Id}', JsonRPC: '{JsonRpc}'", method, id, jsonrpc);
50+
OperationLogger.LogInfo(_logger, OperationLogger.Operations.MCP, "Parsed JSON successfully - Method: '{Method}', ID: '{Id}', JsonRPC: '{JsonRpc}'", method, id, jsonrpc);
5151

5252
if (requestElement.TryGetProperty("params", out var paramsProp))
5353
{

mcp_nexus/Program.cs

Lines changed: 163 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
using NLog.Web;
22
using mcp_nexus.Tools;
33
using mcp_nexus.Helper;
4+
using mcp_nexus.Services;
45
using System.CommandLine;
56
using System.CommandLine.Parsing;
7+
using System.Runtime.Versioning;
68
using System.Text.Json;
9+
using Microsoft.Extensions.Logging;
710

811
namespace mcp_nexus
912
{
@@ -12,68 +15,186 @@ internal class Program
1215
private static async Task Main(string[] args)
1316
{
1417
// Parse command line arguments
15-
var (customCdbPath, useHttp) = ParseCommandLineArguments(args);
18+
var commandLineArgs = ParseCommandLineArguments(args);
19+
20+
// Handle special commands first (Windows only)
21+
if (commandLineArgs.Install)
22+
{
23+
if (OperatingSystem.IsWindows())
24+
{
25+
// Create a logger using NLog configuration for the installation process
26+
using var loggerFactory = LoggerFactory.Create(builder =>
27+
{
28+
builder.ClearProviders();
29+
builder.AddNLogWeb();
30+
builder.SetMinimumLevel(LogLevel.Information);
31+
});
32+
var logger = loggerFactory.CreateLogger("MCP.Nexus.ServiceInstaller");
33+
34+
var success = await WindowsServiceInstaller.InstallServiceAsync(logger);
35+
Environment.Exit(success ? 0 : 1);
36+
}
37+
else
38+
{
39+
Console.Error.WriteLine("ERROR: Service installation is only supported on Windows.");
40+
Environment.Exit(1);
41+
}
42+
return;
43+
}
44+
45+
if (commandLineArgs.Uninstall)
46+
{
47+
if (OperatingSystem.IsWindows())
48+
{
49+
// Create a logger using NLog configuration for the uninstallation process
50+
using var loggerFactory = LoggerFactory.Create(builder =>
51+
{
52+
builder.ClearProviders();
53+
builder.AddNLogWeb();
54+
builder.SetMinimumLevel(LogLevel.Information);
55+
});
56+
var logger = loggerFactory.CreateLogger("MCP.Nexus.ServiceInstaller");
57+
58+
var success = await WindowsServiceInstaller.UninstallServiceAsync(logger);
59+
Environment.Exit(success ? 0 : 1);
60+
}
61+
else
62+
{
63+
Console.Error.WriteLine("ERROR: Service uninstallation is only supported on Windows.");
64+
Environment.Exit(1);
65+
}
66+
return;
67+
}
68+
69+
if (commandLineArgs.ForceUninstall)
70+
{
71+
if (OperatingSystem.IsWindows())
72+
{
73+
// Create a logger using NLog configuration for the force uninstallation process
74+
using var loggerFactory = LoggerFactory.Create(builder =>
75+
{
76+
builder.ClearProviders();
77+
builder.AddNLogWeb();
78+
builder.SetMinimumLevel(LogLevel.Information);
79+
});
80+
var logger = loggerFactory.CreateLogger("MCP.Nexus.ServiceInstaller");
81+
82+
var success = await WindowsServiceInstaller.ForceUninstallServiceAsync(logger);
83+
Environment.Exit(success ? 0 : 1);
84+
}
85+
else
86+
{
87+
Console.Error.WriteLine("ERROR: Service uninstallation is only supported on Windows.");
88+
Environment.Exit(1);
89+
}
90+
return;
91+
}
92+
93+
// Determine transport mode
94+
bool useHttp = commandLineArgs.UseHttp || commandLineArgs.ServiceMode;
95+
96+
// Validate service mode is only used on Windows
97+
if (commandLineArgs.ServiceMode && !OperatingSystem.IsWindows())
98+
{
99+
Console.Error.WriteLine("ERROR: Service mode is only supported on Windows.");
100+
Environment.Exit(1);
101+
return;
102+
}
16103

17104
if (useHttp)
18105
{
19-
await RunHttpServer(args, customCdbPath);
106+
await RunHttpServer(args, commandLineArgs);
20107
}
21108
else
22109
{
23-
await RunStdioServer(args, customCdbPath);
110+
await RunStdioServer(args, commandLineArgs);
24111
}
25112
}
26113

27-
private static (string? customCdbPath, bool useHttp) ParseCommandLineArguments(string[] args)
114+
private static CommandLineArguments ParseCommandLineArguments(string[] args)
28115
{
29-
string? customCdbPath = null;
30-
bool useHttp = false;
116+
var result = new CommandLineArguments();
31117

32118
var cdbPathOption = new Option<string?>("--cdb-path", "Custom path to CDB.exe debugger executable");
33119
var httpOption = new Option<bool>("--http", "Use HTTP transport instead of stdio");
34-
var rootCommand = new RootCommand("MCP Nexus - Windows Debugging MCP Server")
120+
var serviceOption = new Option<bool>("--service", "Run in Windows service mode (implies --http)");
121+
var installOption = new Option<bool>("--install", "Install MCP Nexus as Windows service");
122+
var uninstallOption = new Option<bool>("--uninstall", "Uninstall MCP Nexus Windows service");
123+
var forceUninstallOption = new Option<bool>("--force-uninstall", "Force uninstall MCP Nexus service (removes registry entries)");
124+
125+
var rootCommand = new RootCommand("MCP Nexus - Comprehensive MCP Server Platform")
35126
{
36127
cdbPathOption,
37-
httpOption
128+
httpOption,
129+
serviceOption,
130+
installOption,
131+
uninstallOption,
132+
forceUninstallOption
38133
};
39134

40135
var parseResult = rootCommand.Parse(args);
41136
if (parseResult.Errors.Count == 0)
42137
{
43-
customCdbPath = parseResult.GetValueForOption(cdbPathOption);
44-
useHttp = parseResult.GetValueForOption(httpOption);
138+
result.CustomCdbPath = parseResult.GetValueForOption(cdbPathOption);
139+
result.UseHttp = parseResult.GetValueForOption(httpOption);
140+
result.ServiceMode = parseResult.GetValueForOption(serviceOption);
141+
result.Install = parseResult.GetValueForOption(installOption);
142+
result.Uninstall = parseResult.GetValueForOption(uninstallOption);
143+
result.ForceUninstall = parseResult.GetValueForOption(forceUninstallOption);
45144
}
46145

47-
return (customCdbPath, useHttp);
146+
return result;
48147
}
49148

50-
private static async Task RunHttpServer(string[] args, string? customCdbPath)
149+
private class CommandLineArguments
51150
{
52-
Console.WriteLine("Configuring for HTTP transport...");
151+
public string? CustomCdbPath { get; set; }
152+
public bool UseHttp { get; set; }
153+
public bool ServiceMode { get; set; }
154+
public bool Install { get; set; }
155+
public bool Uninstall { get; set; }
156+
public bool ForceUninstall { get; set; }
157+
}
158+
159+
private static async Task RunHttpServer(string[] args, CommandLineArguments commandLineArgs)
160+
{
161+
var logMessage = commandLineArgs.ServiceMode ?
162+
"Configuring for Windows service mode (HTTP)..." :
163+
"Configuring for HTTP transport...";
164+
Console.WriteLine(logMessage);
165+
53166
var webBuilder = WebApplication.CreateBuilder(args);
54167

55-
ConfigureLogging(webBuilder.Logging);
56-
RegisterServices(webBuilder.Services, customCdbPath);
168+
// Add Windows service support if in service mode
169+
if (commandLineArgs.ServiceMode && OperatingSystem.IsWindows())
170+
{
171+
webBuilder.Host.UseWindowsService();
172+
}
173+
174+
ConfigureLogging(webBuilder.Logging, commandLineArgs.ServiceMode);
175+
RegisterServices(webBuilder.Services, commandLineArgs.CustomCdbPath);
57176
ConfigureHttpServices(webBuilder.Services);
58177

59178
var app = webBuilder.Build();
60179
ConfigureHttpPipeline(app);
61180

62-
Console.WriteLine("Starting MCP Nexus HTTP server on {0}...",
63-
string.Join(", ", app.Urls.DefaultIfEmpty("default URLs")));
181+
var startMessage = commandLineArgs.ServiceMode ?
182+
"Starting MCP Nexus as Windows service..." :
183+
$"Starting MCP Nexus HTTP server on {string.Join(", ", app.Urls.DefaultIfEmpty("default URLs"))}...";
184+
Console.WriteLine(startMessage);
64185

65186
await app.RunAsync();
66187
}
67188

68-
private static async Task RunStdioServer(string[] args, string? customCdbPath)
189+
private static async Task RunStdioServer(string[] args, CommandLineArguments commandLineArgs)
69190
{
70191
// CRITICAL: In stdio mode, stdout is reserved for MCP protocol
71192
// All console output must go to stderr
72193
Console.Error.WriteLine("Configuring for stdio transport...");
73194
var builder = Host.CreateApplicationBuilder(args);
74195

75-
ConfigureLogging(builder.Logging);
76-
RegisterServices(builder.Services, customCdbPath);
196+
ConfigureLogging(builder.Logging, false);
197+
RegisterServices(builder.Services, commandLineArgs.CustomCdbPath);
77198
ConfigureStdioServices(builder.Services);
78199

79200
Console.Error.WriteLine("Building application host...");
@@ -83,13 +204,32 @@ private static async Task RunStdioServer(string[] args, string? customCdbPath)
83204
await host.RunAsync();
84205
}
85206

86-
private static void ConfigureLogging(ILoggingBuilder logging)
207+
private static void ConfigureLogging(ILoggingBuilder logging, bool isServiceMode)
87208
{
88209
// Note: We use Console.Error for stdio mode compatibility
89-
Console.Error.WriteLine("Configuring logging...");
210+
var logMessage = "Configuring logging...";
211+
if (isServiceMode)
212+
{
213+
// For service mode, logging will go to Windows Event Log and files
214+
Console.WriteLine(logMessage);
215+
}
216+
else
217+
{
218+
Console.Error.WriteLine(logMessage);
219+
}
220+
90221
logging.ClearProviders();
91222
logging.AddNLogWeb();
92-
Console.Error.WriteLine("Logging configured with NLog");
223+
224+
var completeMessage = "Logging configured with NLog";
225+
if (isServiceMode)
226+
{
227+
Console.WriteLine(completeMessage);
228+
}
229+
else
230+
{
231+
Console.Error.WriteLine(completeMessage);
232+
}
93233
}
94234

95235
private static void RegisterServices(IServiceCollection services, string? customCdbPath)

0 commit comments

Comments
 (0)