cli/internal/cmd/daemon/cmd.go
Vi bdbcc4acfd feat(daemon): add MCP daemon mode with multi-transport support (#334)
Implements the daemon mode feature for running core as a background service
with MCP server capabilities.

New features:
- `core daemon` command with configurable MCP transport
- Support for stdio, TCP, and Unix socket transports
- Environment variable configuration (CORE_MCP_TRANSPORT, CORE_MCP_ADDR)
- CLI flags for runtime configuration
- Integration with existing daemon infrastructure (PID file, health checks)

Files added:
- internal/cmd/daemon/cmd.go - daemon command implementation
- pkg/mcp/transport_stdio.go - stdio transport wrapper
- pkg/mcp/transport_unix.go - Unix domain socket transport

Files modified:
- pkg/mcp/mcp.go - added log import
- pkg/mcp/transport_tcp.go - added log import
- pkg/mcp/transport_tcp_test.go - fixed port binding test

Usage:
  core daemon                           # TCP on 127.0.0.1:9100
  core daemon --mcp-transport=socket --mcp-addr=/tmp/core.sock
  CORE_MCP_TRANSPORT=stdio core daemon  # for Claude Code integration

Fixes #119

Co-authored-by: Claude <developers@lethean.io>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Snider <snider@host.uk.com>
2026-02-05 17:42:35 +00:00

180 lines
4.8 KiB
Go

// Package daemon provides the `core daemon` command for running as a background service.
package daemon
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/io"
"github.com/host-uk/core/pkg/log"
"github.com/host-uk/core/pkg/mcp"
)
func init() {
cli.RegisterCommands(AddDaemonCommand)
}
// Transport types for MCP server.
const (
TransportStdio = "stdio"
TransportTCP = "tcp"
TransportSocket = "socket"
)
// Config holds daemon configuration.
type Config struct {
// MCPTransport is the MCP server transport type (stdio, tcp, socket).
MCPTransport string
// MCPAddr is the address/path for tcp or socket transports.
MCPAddr string
// HealthAddr is the address for health check endpoints.
HealthAddr string
// PIDFile is the path for the PID file.
PIDFile string
}
// DefaultConfig returns the default daemon configuration.
func DefaultConfig() Config {
home, _ := os.UserHomeDir()
return Config{
MCPTransport: TransportTCP,
MCPAddr: mcp.DefaultTCPAddr,
HealthAddr: "127.0.0.1:9101",
PIDFile: filepath.Join(home, ".core", "daemon.pid"),
}
}
// ConfigFromEnv loads configuration from environment variables.
// Environment variables override default values.
func ConfigFromEnv() Config {
cfg := DefaultConfig()
if v := os.Getenv("CORE_MCP_TRANSPORT"); v != "" {
cfg.MCPTransport = v
}
if v := os.Getenv("CORE_MCP_ADDR"); v != "" {
cfg.MCPAddr = v
}
if v := os.Getenv("CORE_HEALTH_ADDR"); v != "" {
cfg.HealthAddr = v
}
if v := os.Getenv("CORE_PID_FILE"); v != "" {
cfg.PIDFile = v
}
return cfg
}
// AddDaemonCommand adds the 'daemon' command to the root.
func AddDaemonCommand(root *cli.Command) {
cfg := ConfigFromEnv()
daemonCmd := cli.NewCommand(
"daemon",
"Start the core daemon",
"Starts the core daemon which provides long-running services like MCP.\n\n"+
"The daemon can be configured via environment variables or flags:\n"+
" CORE_MCP_TRANSPORT - MCP transport type (stdio, tcp, socket)\n"+
" CORE_MCP_ADDR - MCP address/path (e.g., :9100, /tmp/mcp.sock)\n"+
" CORE_HEALTH_ADDR - Health check endpoint address\n"+
" CORE_PID_FILE - PID file path for single-instance enforcement",
func(cmd *cli.Command, args []string) error {
return runDaemon(cfg)
},
)
// Flags override environment variables
cli.StringFlag(daemonCmd, &cfg.MCPTransport, "mcp-transport", "t", cfg.MCPTransport,
"MCP transport type (stdio, tcp, socket)")
cli.StringFlag(daemonCmd, &cfg.MCPAddr, "mcp-addr", "a", cfg.MCPAddr,
"MCP listen address (e.g., :9100 or /tmp/mcp.sock)")
cli.StringFlag(daemonCmd, &cfg.HealthAddr, "health-addr", "", cfg.HealthAddr,
"Health check endpoint address (empty to disable)")
cli.StringFlag(daemonCmd, &cfg.PIDFile, "pid-file", "", cfg.PIDFile,
"PID file path (empty to disable)")
root.AddCommand(daemonCmd)
}
// runDaemon starts the daemon with the given configuration.
func runDaemon(cfg Config) error {
// Set daemon mode environment for child processes
os.Setenv("CORE_DAEMON", "1")
log.Info("Starting daemon",
"transport", cfg.MCPTransport,
"addr", cfg.MCPAddr,
"health", cfg.HealthAddr,
)
// Create MCP service
mcpSvc, err := mcp.New()
if err != nil {
return fmt.Errorf("failed to create MCP service: %w", err)
}
// Create daemon with health checks
daemon := cli.NewDaemon(cli.DaemonOptions{
Medium: io.Local,
PIDFile: cfg.PIDFile,
HealthAddr: cfg.HealthAddr,
ShutdownTimeout: 30,
})
// Start daemon (acquires PID, starts health server)
if err := daemon.Start(); err != nil {
return fmt.Errorf("failed to start daemon: %w", err)
}
// Get context that cancels on SIGINT/SIGTERM
ctx := cli.Context()
// Start MCP server in background
mcpErrCh := make(chan error, 1)
go func() {
mcpErrCh <- startMCP(ctx, mcpSvc, cfg)
}()
// Mark as ready
daemon.SetReady(true)
log.Info("Daemon ready",
"pid", os.Getpid(),
"health", daemon.HealthAddr(),
)
// Wait for shutdown signal or MCP error
select {
case err := <-mcpErrCh:
if err != nil && ctx.Err() == nil {
log.Error("MCP server error", "err", err)
return err
}
case <-ctx.Done():
log.Info("Shutting down daemon")
}
return daemon.Stop()
}
// startMCP starts the MCP server with the configured transport.
func startMCP(ctx context.Context, svc *mcp.Service, cfg Config) error {
switch cfg.MCPTransport {
case TransportStdio:
log.Info("Starting MCP server", "transport", "stdio")
return svc.ServeStdio(ctx)
case TransportTCP:
log.Info("Starting MCP server", "transport", "tcp", "addr", cfg.MCPAddr)
return svc.ServeTCP(ctx, cfg.MCPAddr)
case TransportSocket:
log.Info("Starting MCP server", "transport", "unix", "path", cfg.MCPAddr)
return svc.ServeUnix(ctx, cfg.MCPAddr)
default:
return fmt.Errorf("unknown MCP transport: %s (valid: stdio, tcp, socket)", cfg.MCPTransport)
}
}