Skip to content

Commit a3608bc

Browse files
author
Tui
committed
feat(service): add cross-platform service management support
Implement service installation and management for Windows (sc.exe), Linux (systemd), and macOS (launchd) Add platform-specific stubs for unsupported platforms Include service configuration handling and default config generation Add service installation flags to main application
1 parent 792d093 commit a3608bc

8 files changed

Lines changed: 438 additions & 0 deletions

File tree

cmd/mcp-server/main.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,86 @@ package main
33
import (
44
"context"
55
"flag"
6+
"fmt"
67
"log"
78
"net/http"
89
"os"
910
"time"
1011

1112
"github.com/ba0f3/MCP-Kali-Server/pkg/executor"
1213
"github.com/ba0f3/MCP-Kali-Server/pkg/handlers"
14+
"github.com/ba0f3/MCP-Kali-Server/pkg/service"
1315
"github.com/modelcontextprotocol/go-sdk/mcp"
1416
)
1517

18+
// handleServiceInstall installs the server as a system service
19+
func handleServiceInstall(serviceName, servicePort string) error {
20+
config := service.GetDefaultConfig()
21+
config.Name = serviceName
22+
23+
// Set service arguments to use HTTP mode with the specified port
24+
config.Args = []string{
25+
"-http", servicePort,
26+
"-timeout", "900",
27+
}
28+
29+
fmt.Printf("Installing %s service...\n", serviceName)
30+
fmt.Printf("Service will listen on port: %s\n", servicePort)
31+
fmt.Printf("Executable: %s\n", config.Executable)
32+
fmt.Printf("Working directory: %s\n", config.WorkingDir)
33+
34+
if err := service.InstallService(config); err != nil {
35+
return fmt.Errorf("installation failed: %w", err)
36+
}
37+
38+
fmt.Printf("\nService '%s' installed successfully!\n", serviceName)
39+
fmt.Println("\nTo start the service:")
40+
fmt.Printf(" Windows: sc start %s\n", serviceName)
41+
fmt.Printf(" Linux: sudo systemctl start %s\n", serviceName)
42+
fmt.Printf(" macOS: sudo launchctl start %s\n", serviceName)
43+
44+
return nil
45+
}
46+
47+
// handleServiceUninstall removes the installed service
48+
func handleServiceUninstall(serviceName string) error {
49+
fmt.Printf("Uninstalling %s service...\n", serviceName)
50+
51+
if err := service.UninstallService(serviceName); err != nil {
52+
return fmt.Errorf("uninstallation failed: %w", err)
53+
}
54+
55+
fmt.Printf("Service '%s' uninstalled successfully!\n", serviceName)
56+
return nil
57+
}
1658

1759
func main() {
1860
var (
1961
debug = flag.Bool("debug", false, "Enable debug logging")
2062
httpAddr = flag.String("http", "", "if set, use streamable HTTP at this address, instead of stdin/stdout")
2163
timeout = flag.Int("timeout", 900, "Command execution timeout in seconds")
64+
installService = flag.Bool("install-service", false, "Install the server as a system service")
65+
uninstallService = flag.Bool("uninstall-service", false, "Uninstall the system service")
66+
serviceName = flag.String("service-name", "mcp-kali-server", "Name of the service")
67+
servicePort = flag.String("service-port", ":8080", "Port for the service to listen on (used with -install-service)")
2268
)
2369
flag.Parse()
2470

71+
// Handle service installation/uninstallation
72+
if *installService {
73+
if err := handleServiceInstall(*serviceName, *servicePort); err != nil {
74+
log.Fatalf("Failed to install service: %v", err)
75+
}
76+
return
77+
}
78+
79+
if *uninstallService {
80+
if err := handleServiceUninstall(*serviceName); err != nil {
81+
log.Fatalf("Failed to uninstall service: %v", err)
82+
}
83+
return
84+
}
85+
2586
// Set the global command timeout
2687
executor.SetGlobalTimeout(time.Duration(*timeout) * time.Second)
2788

pkg/service/service.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package service
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"runtime"
8+
)
9+
10+
// ServiceConfig holds the configuration for the service
11+
type ServiceConfig struct {
12+
Name string
13+
DisplayName string
14+
Description string
15+
Executable string
16+
Args []string
17+
WorkingDir string
18+
}
19+
20+
// InstallService installs the application as a system service
21+
func InstallService(config ServiceConfig) error {
22+
switch runtime.GOOS {
23+
case "windows":
24+
return installWindowsService(config)
25+
case "linux":
26+
return installLinuxService(config)
27+
case "darwin":
28+
return installDarwinService(config)
29+
default:
30+
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
31+
}
32+
}
33+
34+
// UninstallService removes the installed service
35+
func UninstallService(name string) error {
36+
switch runtime.GOOS {
37+
case "windows":
38+
return uninstallWindowsService(name)
39+
case "linux":
40+
return uninstallLinuxService(name)
41+
case "darwin":
42+
return uninstallDarwinService(name)
43+
default:
44+
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
45+
}
46+
}
47+
48+
// GetDefaultConfig returns a default service configuration
49+
func GetDefaultConfig() ServiceConfig {
50+
executable, _ := os.Executable()
51+
workingDir := filepath.Dir(executable)
52+
53+
return ServiceConfig{
54+
Name: "mcp-kali-server",
55+
DisplayName: "MCP Kali Server",
56+
Description: "Model Context Protocol server for Kali Linux security tools",
57+
Executable: executable,
58+
WorkingDir: workingDir,
59+
}
60+
}

pkg/service/service_darwin.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//go:build darwin
2+
// +build darwin
3+
4+
package service
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"text/template"
12+
)
13+
14+
const launchdTemplate = `<?xml version="1.0" encoding="UTF-8"?>
15+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
16+
<plist version="1.0">
17+
<dict>
18+
<key>Label</key>
19+
<string>{{.Name}}</string>
20+
<key>ProgramArguments</key>
21+
<array>
22+
<string>{{.Executable}}</string>
23+
{{range .Args}}<string>{{.}}</string>
24+
{{end}}
25+
</array>
26+
<key>WorkingDirectory</key>
27+
<string>{{.WorkingDir}}</string>
28+
<key>RunAtLoad</key>
29+
<true/>
30+
<key>KeepAlive</key>
31+
<true/>
32+
<key>StandardOutPath</key>
33+
<string>/var/log/{{.Name}}.log</string>
34+
<key>StandardErrorPath</key>
35+
<string>/var/log/{{.Name}}.error.log</string>
36+
</dict>
37+
</plist>
38+
`
39+
40+
// installDarwinService installs the service on macOS using launchd
41+
func installDarwinService(config ServiceConfig) error {
42+
// Create plist file
43+
plistFile := filepath.Join("/Library/LaunchDaemons", config.Name+".plist")
44+
45+
file, err := os.Create(plistFile)
46+
if err != nil {
47+
return fmt.Errorf("failed to create plist file: %v", err)
48+
}
49+
defer file.Close()
50+
51+
// Write plist file
52+
tmpl, err := template.New("plist").Parse(launchdTemplate)
53+
if err != nil {
54+
return fmt.Errorf("failed to parse template: %v", err)
55+
}
56+
57+
if err := tmpl.Execute(file, config); err != nil {
58+
return fmt.Errorf("failed to write plist file: %v", err)
59+
}
60+
61+
// Set proper permissions
62+
if err := os.Chmod(plistFile, 0644); err != nil {
63+
return fmt.Errorf("failed to set permissions: %v", err)
64+
}
65+
66+
// Load the service
67+
cmd := exec.Command("launchctl", "load", plistFile)
68+
if err := cmd.Run(); err != nil {
69+
return fmt.Errorf("failed to load service: %v", err)
70+
}
71+
72+
return nil
73+
}
74+
75+
// uninstallDarwinService removes the service on macOS
76+
func uninstallDarwinService(name string) error {
77+
plistFile := filepath.Join("/Library/LaunchDaemons", name+".plist")
78+
79+
// Unload the service
80+
cmd := exec.Command("launchctl", "unload", plistFile)
81+
cmd.Run() // Ignore error if service is not loaded
82+
83+
// Remove plist file
84+
if err := os.Remove(plistFile); err != nil && !os.IsNotExist(err) {
85+
return fmt.Errorf("failed to remove plist file: %v", err)
86+
}
87+
88+
return nil
89+
}

pkg/service/service_darwin_stub.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//go:build !darwin
2+
// +build !darwin
3+
4+
package service
5+
6+
import "fmt"
7+
8+
// installDarwinService is a stub for non-Darwin platforms
9+
func installDarwinService(config ServiceConfig) error {
10+
return fmt.Errorf("macOS service installation is not supported on this platform")
11+
}
12+
13+
// uninstallDarwinService is a stub for non-Darwin platforms
14+
func uninstallDarwinService(name string) error {
15+
return fmt.Errorf("macOS service uninstallation is not supported on this platform")
16+
}

pkg/service/service_linux.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//go:build linux
2+
// +build linux
3+
4+
package service
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"text/template"
12+
)
13+
14+
const systemdTemplate = `[Unit]
15+
Description={{.Description}}
16+
After=network.target
17+
18+
[Service]
19+
Type=simple
20+
User={{.User}}
21+
WorkingDirectory={{.WorkingDir}}
22+
ExecStart={{.Executable}}{{range .Args}} {{.}}{{end}}
23+
Restart=on-failure
24+
RestartSec=10
25+
26+
[Install]
27+
WantedBy=multi-user.target
28+
`
29+
30+
// installLinuxService installs the service on Linux using systemd
31+
func installLinuxService(config ServiceConfig) error {
32+
// Check if systemd is available
33+
if _, err := os.Stat("/usr/bin/systemctl"); os.IsNotExist(err) {
34+
return fmt.Errorf("systemd is not available on this system")
35+
}
36+
37+
// Create service file
38+
serviceFile := filepath.Join("/etc/systemd/system", config.Name+".service")
39+
40+
file, err := os.Create(serviceFile)
41+
if err != nil {
42+
return fmt.Errorf("failed to create service file: %v", err)
43+
}
44+
defer file.Close()
45+
46+
// Prepare template data
47+
data := struct {
48+
Description string
49+
User string
50+
WorkingDir string
51+
Executable string
52+
Args []string
53+
}{
54+
Description: config.Description,
55+
User: os.Getenv("USER"),
56+
WorkingDir: config.WorkingDir,
57+
Executable: config.Executable,
58+
Args: config.Args,
59+
}
60+
61+
// If running as root, use nobody user
62+
if data.User == "root" || data.User == "" {
63+
data.User = "nobody"
64+
}
65+
66+
// Write service file
67+
tmpl, err := template.New("service").Parse(systemdTemplate)
68+
if err != nil {
69+
return fmt.Errorf("failed to parse template: %v", err)
70+
}
71+
72+
if err := tmpl.Execute(file, data); err != nil {
73+
return fmt.Errorf("failed to write service file: %v", err)
74+
}
75+
76+
// Reload systemd
77+
cmd := exec.Command("systemctl", "daemon-reload")
78+
if err := cmd.Run(); err != nil {
79+
return fmt.Errorf("failed to reload systemd: %v", err)
80+
}
81+
82+
// Enable the service
83+
cmd = exec.Command("systemctl", "enable", config.Name)
84+
if err := cmd.Run(); err != nil {
85+
return fmt.Errorf("failed to enable service: %v", err)
86+
}
87+
88+
return nil
89+
}
90+
91+
// uninstallLinuxService removes the service on Linux
92+
func uninstallLinuxService(name string) error {
93+
// Stop the service
94+
cmd := exec.Command("systemctl", "stop", name)
95+
cmd.Run() // Ignore error if service is not running
96+
97+
// Disable the service
98+
cmd = exec.Command("systemctl", "disable", name)
99+
cmd.Run() // Ignore error if service is not enabled
100+
101+
// Remove service file
102+
serviceFile := filepath.Join("/etc/systemd/system", name+".service")
103+
if err := os.Remove(serviceFile); err != nil && !os.IsNotExist(err) {
104+
return fmt.Errorf("failed to remove service file: %v", err)
105+
}
106+
107+
// Reload systemd
108+
cmd = exec.Command("systemctl", "daemon-reload")
109+
if err := cmd.Run(); err != nil {
110+
return fmt.Errorf("failed to reload systemd: %v", err)
111+
}
112+
113+
return nil
114+
}

pkg/service/service_linux_stub.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//go:build !linux
2+
// +build !linux
3+
4+
package service
5+
6+
import "fmt"
7+
8+
// installLinuxService is a stub for non-Linux platforms
9+
func installLinuxService(config ServiceConfig) error {
10+
return fmt.Errorf("Linux service installation is not supported on this platform")
11+
}
12+
13+
// uninstallLinuxService is a stub for non-Linux platforms
14+
func uninstallLinuxService(name string) error {
15+
return fmt.Errorf("Linux service uninstallation is not supported on this platform")
16+
}

0 commit comments

Comments
 (0)