// local-mcp-go — localhost MCP server delivering user instructions to agents. package main import ( "embed" "fmt" "io/fs" "log/slog" "net" "net/http" "os" "os/signal" "runtime" "syscall" "github.com/mark3labs/mcp-go/server" "github.com/local-mcp/local-mcp-go/internal/api" "github.com/local-mcp/local-mcp-go/internal/config" "github.com/local-mcp/local-mcp-go/internal/db" "github.com/local-mcp/local-mcp-go/internal/events" "github.com/local-mcp/local-mcp-go/internal/mcp" "github.com/local-mcp/local-mcp-go/internal/store" ) //go:embed static var staticFS embed.FS func main() { cfg := config.Load() // Logger level := slog.LevelInfo switch cfg.LogLevel { case "DEBUG": level = slog.LevelDebug case "WARN", "WARNING": level = slog.LevelWarn case "ERROR": level = slog.LevelError } slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level}))) // Database database, err := db.Open(cfg.DBPath) if err != nil { slog.Error("Failed to open database", "error", err) os.Exit(1) } defer database.Close() slog.Info("Database initialised", "path", cfg.DBPath) // Stores instStore := store.NewInstructionStore(database) settStore := store.NewSettingsStore(database) agentStore := store.NewAgentStore(database) // Event broker broker := events.NewBroker() // MCP server mcpHandler := mcp.New(instStore, settStore, agentStore, broker) sseServer := server.NewStreamableHTTPServer( mcpHandler.MCP, server.WithEndpointPath("/mcp"), server.WithStateLess(cfg.MCPStateless), ) // HTTP router staticSubFS, _ := fs.Sub(staticFS, "static") router := api.NewRouter( api.Stores{ Instructions: instStore, Settings: settStore, Agents: agentStore, }, broker, cfg.APIToken, staticSubFS, ) // Combined router: /mcp → MCP StreamableHTTP, /* → REST API mux := http.NewServeMux() mux.Handle("/mcp", sseServer) mux.Handle("/mcp/", http.StripPrefix("/mcp", sseServer)) mux.Handle("/", router) // Server with auto port switching port := cfg.HTTPPort maxAttempts := 10 var srv *http.Server var addr string for attempt := 0; attempt < maxAttempts; attempt++ { addr = fmt.Sprintf("%s:%s", cfg.Host, port) srv = &http.Server{Addr: addr, Handler: mux} // Try to listen ln, err := net.Listen("tcp", addr) if err == nil { ln.Close() // Close test listener break } // Port taken, try next portNum := 8000 + attempt port = fmt.Sprintf("%d", portNum+1) if attempt == maxAttempts-1 { slog.Error("Could not find available port", "tried", maxAttempts) os.Exit(1) } } if cfg.APIToken != "" { slog.Info("Token authentication enabled") } else { slog.Info("Token authentication disabled (set API_TOKEN to enable)") } httpURL := fmt.Sprintf("http://%s", addr) mcpURL := fmt.Sprintf("http://%s/mcp", addr) slog.Info("local-mcp-go ready", "version", config.AppVersion, "http", httpURL, "mcp", mcpURL, "stateless", cfg.MCPStateless, ) // On Windows, show interactive prompt if runtime.GOOS == "windows" { fmt.Println() fmt.Println("╔══════════════════════════════════════════════════════════╗") fmt.Printf("║ local-mcp-go v%-6s ready on port %s%-14s║\n", config.AppVersion, port, "") fmt.Println("║ ║") fmt.Printf("║ Web UI: %-46s║\n", httpURL) fmt.Printf("║ MCP: %-46s║\n", mcpURL) fmt.Println("║ ║") fmt.Println("║ Press Ctrl+C to stop the server ║") fmt.Println("╚══════════════════════════════════════════════════════════╝") fmt.Println() } // Graceful shutdown on SIGINT / SIGTERM stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) go func() { <-stop fmt.Println() slog.Info("Shutting down gracefully...") _ = srv.Close() }() if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { slog.Error("Server error", "error", err) os.Exit(1) } }