diff --git a/cmd/mcp.go b/cmd/mcp.go index b380868..9273ca8 100644 --- a/cmd/mcp.go +++ b/cmd/mcp.go @@ -2,6 +2,10 @@ package root import ( "fmt" + "os" + "os/exec" + "os/signal" + "syscall" "github.com/inercia/MCPShell/pkg/common" "github.com/inercia/MCPShell/pkg/config" @@ -13,6 +17,7 @@ import ( var ( useHTTP bool httpPort int + daemon bool ) // mcpCommand represents the run command which starts the MCP server @@ -25,8 +30,11 @@ Run an MCP server that provides tools to LLM applications. This command starts a server that communicates using the Model Context Protocol (MCP) and expooses the tools defined in a MCP configuration file. -The server loads tool definitions from a YAML configuration file and makes them +The server loads tool definitions from a MCP configuration file and makes them available to AI applications via the MCP protocol. + +When using --http mode, you can also use --daemon to run the server in the background +and ignore SIGHUP signals. `, PreRunE: func(cmd *cobra.Command, args []string) error { // Initialize logger @@ -51,12 +59,27 @@ available to AI applications via the MCP protocol. return fmt.Errorf("failed to ensure tools directory: %w", err) } + // Daemon mode is only supported with HTTP mode + if daemon && !useHTTP { + logger.Error("Daemon mode is only supported with HTTP mode") + return fmt.Errorf("daemon mode is only supported with HTTP mode (use --http flag)") + } + return nil }, RunE: func(cmd *cobra.Command, args []string) error { logger := common.GetLogger() defer common.RecoverPanic() + // Daemonize if requested + if daemon { + if err := daemonize(); err != nil { + logger.Error("Failed to daemonize: %v", err) + return fmt.Errorf("failed to daemonize: %w", err) + } + logger.Info("Daemonized successfully") + } + // Load the configuration file(s) (local or remote) localConfigPath, cleanup, err := config.ResolveMultipleConfigPaths(toolsFiles, logger) if err != nil { @@ -78,12 +101,82 @@ available to AI applications via the MCP protocol. }) if useHTTP { + // Set up SIGHUP handling for daemon mode + if daemon { + setupSIGHUPHandler(logger) + } return srv.StartHTTP(httpPort) } return srv.Start() }, } +// daemonize forks the process to run in the background +func daemonize() error { + // Get the current executable path + executable, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + + // Get current working directory + workDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } + + // Build command arguments, excluding the daemon flag + args := os.Args[1:] + var newArgs []string + for i, arg := range args { + if arg == "--daemon" { + // Skip the daemon flag + continue + } + if i > 0 && args[i-1] == "--daemon" { + // Skip the value if it was a separate argument + continue + } + newArgs = append(newArgs, arg) + } + + // Create the command + cmd := exec.Command(executable, newArgs...) + cmd.Dir = workDir + cmd.Env = os.Environ() + + // Set up process attributes for daemon behavior + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, // Create new session + } + + // Start the process + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start daemon process: %w", err) + } + + // Exit the parent process + os.Exit(0) + + // This line should never be reached, but Go requires it + return nil +} + +// setupSIGHUPHandler sets up signal handling to ignore SIGHUP in daemon mode +func setupSIGHUPHandler(logger *common.Logger) { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGHUP) + + go func() { + for { + sig := <-sigChan + if sig == syscall.SIGHUP { + logger.Info("Received SIGHUP, ignoring in daemon mode") + } + } + }() +} + // init adds flags to the run command func init() { rootCmd.AddCommand(mcpCommand) @@ -96,6 +189,7 @@ func init() { // Add HTTP server flags mcpCommand.Flags().BoolVar(&useHTTP, "http", false, "Enable HTTP server mode (serve MCP over HTTP/SSE instead of stdio)") mcpCommand.Flags().IntVar(&httpPort, "port", 8080, "Port for HTTP server (default: 8080, only used with --http)") + mcpCommand.Flags().BoolVar(&daemon, "daemon", false, "Run in daemon mode (background process, ignores SIGHUP, only works with --http)") // Mark required flags _ = mcpCommand.MarkFlagRequired("tools") diff --git a/docs/usage.md b/docs/usage.md index 92b4b95..bfcde75 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -103,11 +103,12 @@ Runs an MCP server that communicates using the Model Context Protocol and expose - `--http`: Enable HTTP server mode (serve MCP over HTTP/SSE instead of stdio) - `--port`: Port for HTTP server (default: 8080, only used with --http) +- `--daemon`: Run in daemon mode (background process, ignores SIGHUP, only works with --http) **Example**: ```console -mcpshell mcp --tools=examples/config.yaml --log-level=debug +mcpshell mcp --tools=examples/config.yaml --http --port=9090 --daemon --log-level=debug ``` ### EXE Command