Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 95 additions & 1 deletion cmd/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -13,6 +17,7 @@ import (
var (
useHTTP bool
httpPort int
daemon bool
)

// mcpCommand represents the run command which starts the MCP server
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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")
Expand Down
3 changes: 2 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading