packages feed

mcp-hoogle-0.1.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@ tool), then enters the MCP
-- request\/response loop reading JSON-RPC from stdin and writing to stdout.
module McpHoogle
  ( runServer
  , runServerWithDb
  )
where

import Data.IORef (newIORef)
import Data.Text qualified as Text
import Hoogle (withDatabase, defaultDatabaseLocation)
import MCP.Server (runMcpServerStdio)
import MCP.Server.Types
  ( McpServerInfo(..)
  , McpServerHandlers(..)
  , Content(..)
  )
import MCP.Server.Derive (deriveToolHandlerWithDescription)
import McpHoogle.Tools (HoogleTool(..), handleTool, toolDescriptions)
import System.Directory (doesFileExist)

-- | Run the MCP server using the default Hoogle database location
-- (@~\/.hoogle\/default-haskell-*.hoo@).
--
-- Errors immediately if no database is found — the user should run
-- @mcp-hoogle generate@ first.
runServer :: IO ()
runServer = do
  defaultPath <- defaultDatabaseLocation
  exists <- doesFileExist defaultPath
  if exists
    then runServerWithDb defaultPath
    else error $ "Hoogle database not found at: " <> defaultPath
      <> "\nRun 'mcp-hoogle generate' to create it, or pass --database PATH."

-- | 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 database
    let serverInfo :: McpServerInfo
        serverInfo = McpServerInfo
          { serverName = "mcp-hoogle"
          , serverVersion = "0.1.0"
          , 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 after entering a different project's nix-shell"
              , ""
              , "DATABASE SETUP:"
              , "If no database exists, run `mcp-hoogle generate` from an"
              , "environment where `ghc-pkg` is on PATH (so it can discover"
              , "installed packages). For nix-based projects this means running"
              , "from inside the project's nix-shell:"
              , "  nix-shell --run 'mcp-hoogle generate'"
              , "For cabal/stack projects, just run `mcp-hoogle generate` directly"
              , "(GHC tools are already on PATH)."
              , "This only needs to be done once per project. The database persists"
              , "at ~/.hoogle/ and is reused across sessions."
              ]
          }

        toolHandler :: HoogleTool -> IO Content
        toolHandler tool = ContentText <$> handleTool databaseRef tool

        handlers :: McpServerHandlers IO
        handlers = McpServerHandlers
          { prompts = Nothing
          , resources = Nothing
          , tools = Just $(deriveToolHandlerWithDescription ''HoogleTool 'toolHandler toolDescriptions)
          }

    runMcpServerStdio serverInfo handlers