packages feed

mcp-hoogle-0.2.0: src/McpHoogle.hs

{-# LANGUAGE TemplateHaskell #-}
-- | Entry point for the MCP server.
--
-- This module wires together the Hoogle database, the tool definitions from
-- "McpHoogle.Tools", and the @mcp-server@ library's stdio transport.
-- It loads the Hoogle database into an 'IORef' so it can be swapped at
-- runtime (via the @regenerate_database@ or @reload_database@ tools),
-- then enters the MCP request\/response loop reading JSON-RPC from stdin
-- and writing to stdout.
--
-- The server starts even without an existing database — searches return
-- "No database loaded" until one is generated or reloaded.
module McpHoogle
  ( runServer
  , runServerWithDb
  )
where

import Control.Concurrent.STM (TVar, newTVarIO)
import Data.IORef (IORef, newIORef)
import Data.Text qualified as Text
import Data.Version (showVersion)
import Hoogle qualified (Database)
import Hoogle (withDatabase, defaultDatabaseLocation)
import MCP.Server (runMcpServerStdio)
import MCP.Server.Types
  ( McpServerInfo(..)
  , McpServerHandlers(..)
  , Content(..)
  )
import MCP.Server.Derive (deriveToolHandlerWithDescription)
import McpHoogle.Tools (HoogleTool(..), RegenState(..), handleTool, toolDescriptions)
import Paths_mcp_hoogle (version)
import System.Directory (doesFileExist)

-- | Run the MCP server using the default Hoogle database location
-- (@~\/.hoogle\/default-haskell-*.hoo@).
--
-- If no database exists, the server starts anyway with an empty database
-- ref — the agent can call @regenerate_database@ or @reload_database@ to
-- populate it without restarting.
runServer :: IO ()
runServer = do
  defaultPath <- defaultDatabaseLocation
  exists <- doesFileExist defaultPath
  if exists
    then runServerWithDb defaultPath
    else runServerEmpty

-- | Run the MCP server with an explicit database path.
--
-- Loads the database, stores it in an 'IORef' (for hot-reload support),
-- registers the tool handlers via TH-derived dispatch, and enters the
-- stdio MCP loop. This function blocks until stdin is closed.
runServerWithDb :: FilePath -> IO ()
runServerWithDb databasePath =
  withDatabase databasePath $ \database -> do
    databaseRef <- newIORef (Just database)
    regenStateVar <- newTVarIO RegenIdle
    runMcpServerStdio serverInfo (handlers databaseRef regenStateVar)

-- | Run the MCP server without a database.
--
-- The server starts and exposes tools, but searches return "No database
-- loaded" until the agent calls @regenerate_database@ or @reload_database@.
runServerEmpty :: IO ()
runServerEmpty = do
  databaseRef <- newIORef Nothing
  regenStateVar <- newTVarIO RegenIdle
  runMcpServerStdio serverInfo (handlers databaseRef regenStateVar)

-- | Server metadata sent during MCP initialization.
serverInfo :: McpServerInfo
serverInfo = McpServerInfo
  { serverName = "mcp-hoogle"
  , serverVersion = Text.pack (showVersion version)
  , serverInstructions = Text.unlines
      [ "Hoogle search for Haskell types, functions, and modules."
      , ""
      , "USE THESE TOOLS INSTEAD OF:"
      , "- Web searching for Haskell documentation"
      , "- Running `hoogle` or `mcp-hoogle` CLI commands"
      , "- Fetching Hackage pages with curl/w3m"
      , ""
      , "AVAILABLE TOOLS:"
      , "- search: Find functions by name, keyword, or type signature (e.g. \"map\", \"[a] -> Int\")"
      , "- search_type: Search specifically by type signature"
      , "- lookup_module: Browse all exports of a module (e.g. \"Data.Map\")"
      , "- regenerate_database: Re-index packages. Pass ghcBinPath (find it with: nix-shell --run 'dirname $(which ghc-pkg)')"
      , "- reload_database: Reload database from disk after generating externally"
      , ""
      , "DATABASE SETUP:"
      , "If searches return 'No database loaded', call regenerate_database with"
      , "the ghcBinPath from the project's nix-shell. Find it by running:"
      , "  nix-shell --run 'dirname $(which ghc-pkg)'"
      , "Then pass that path to regenerate_database."
      , "Alternatively, run `nix-shell --run 'mcp-hoogle generate'` as a bash"
      , "command, then call reload_database to pick up the new file."
      ]
  }

-- | Build MCP handlers from a database ref and regeneration state.
handlers :: IORef (Maybe Hoogle.Database) -> TVar RegenState -> McpServerHandlers IO
handlers databaseRef regenStateVar = McpServerHandlers
  { prompts = Nothing
  , resources = Nothing
  , tools = Just $(deriveToolHandlerWithDescription ''HoogleTool 'toolHandler toolDescriptions)
  }
  where
    toolHandler :: HoogleTool -> IO Content
    toolHandler tool = ContentText <$> handleTool databaseRef regenStateVar tool