diff --git a/Changelog.md b/Changelog.md
new file mode 100644
--- /dev/null
+++ b/Changelog.md
@@ -0,0 +1,6 @@
+# Change log for template project
+
+## Version 0.0.0 
+
+import [template](https://github.com/jappeace/template).
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Jappie Klooster
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Readme.md b/Readme.md
new file mode 100644
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,75 @@
+[![Github actions build status](https://img.shields.io/github/actions/workflow/status/jappeace/mcp-hoogle/ci.yaml?branch=master)](https://github.com/jappeace/mcp-hoogle/actions)
+
+> Give me a type signature and I shall move the world.
+
+# mcp-hoogle
+
+An MCP (Model Context Protocol) server that exposes Hoogle search over your project's local Haskell dependencies.
+Run it from within your project's nix-shell to give AI assistants type-aware search across all your project's packages.
+
+## Usage
+
+### 1. Generate a Hoogle database from your project
+
+From within your project's nix-shell (where all dependencies are available):
+
+```bash
+mcp-hoogle generate
+```
+
+This indexes all packages in your local GHC package database.
+
+### 2. Run the MCP server
+
+```bash
+mcp-hoogle serve
+```
+
+Or with a specific database path:
+
+```bash
+mcp-hoogle serve /path/to/database.hoo
+```
+
+### 3. Configure Claude Code
+
+Add to your Claude Code MCP settings:
+
+```json
+{
+  "mcpServers": {
+    "hoogle": {
+      "command": "mcp-hoogle",
+      "args": ["serve"]
+    }
+  }
+}
+```
+
+## MCP Tools
+
+The server exposes four tools:
+
+- **search** — Search by function name, type signature, or keyword
+- **search_type** — Search specifically by type signature (e.g. `[a] -> Int`)
+- **lookup_module** — Browse exports of a module (e.g. `Data.Map`)
+- **regenerate_database** — Re-index packages and reload without restarting the server
+
+## Building
+
+```bash
+nix-shell
+cabal build
+```
+
+Or via nix:
+
+```bash
+nix-build
+```
+
+## How it works
+
+1. `mcp-hoogle generate` calls `hoogle generate --local` which indexes all packages registered in the current GHC package database
+2. `mcp-hoogle serve` loads the generated Hoogle database and exposes it via MCP stdio transport
+3. AI assistants connect via MCP and can search for types, functions, and modules
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,63 @@
+module Main where
+
+import McpHoogle (runServer, runServerWithDb)
+import Hoogle (defaultDatabaseLocation, hoogle)
+import Options.Applicative
+
+data Command
+  = Generate GenerateOpts
+  | Serve ServeOpts
+
+data GenerateOpts = GenerateOpts
+  { generateDatabase :: Maybe FilePath
+  }
+
+data ServeOpts = ServeOpts
+  { serveDatabase :: Maybe FilePath
+  }
+
+commandParser :: Parser Command
+commandParser = subparser
+  ( command "generate" (info (Generate <$> generateOptsParser) (progDesc "Generate Hoogle DB from local GHC packages"))
+  <> command "serve" (info (Serve <$> serveOptsParser) (progDesc "Run MCP server (stdio transport)"))
+  ) <|> (Serve <$> serveOptsParser)
+
+generateOptsParser :: Parser GenerateOpts
+generateOptsParser = GenerateOpts
+  <$> optional (strOption
+    ( long "database"
+    <> short 'd'
+    <> metavar "PATH"
+    <> help "Path to write the Hoogle database (default: ~/.hoogle/)"
+    ))
+
+serveOptsParser :: Parser ServeOpts
+serveOptsParser = ServeOpts
+  <$> optional (strOption
+    ( long "database"
+    <> short 'd'
+    <> metavar "PATH"
+    <> help "Path to the Hoogle database file"
+    ))
+
+opts :: ParserInfo Command
+opts = info (commandParser <**> helper)
+  ( fullDesc
+  <> progDesc "MCP server exposing Hoogle search over local Haskell dependencies"
+  <> header "mcp-hoogle - Hoogle search via Model Context Protocol"
+  )
+
+main :: IO ()
+main = do
+  cmd <- execParser opts
+  case cmd of
+    Generate (GenerateOpts mPath) -> do
+      databasePath <- maybe defaultDatabaseLocation pure mPath
+      putStrLn $ "Generating Hoogle database at: " <> databasePath
+      putStrLn "Indexing local packages from GHC package database..."
+      hoogle ["generate", "--local", "--database=" <> databasePath]
+      putStrLn "Done."
+    Serve (ServeOpts mPath) ->
+      case mPath of
+        Nothing -> runServer
+        Just path -> runServerWithDb path
diff --git a/mcp-hoogle.cabal b/mcp-hoogle.cabal
new file mode 100644
--- /dev/null
+++ b/mcp-hoogle.cabal
@@ -0,0 +1,112 @@
+cabal-version:      3.0
+
+name:           mcp-hoogle
+version:        0.1.0
+synopsis:       MCP server exposing Hoogle search over local project dependencies
+description:    An MCP (Model Context Protocol) server that exposes Hoogle search
+                over your project's local Haskell dependencies. Run from within a
+                nix-shell to give AI assistants type-aware search across all packages.
+category:       Development, IDE
+homepage:       https://github.com/jappeace/mcp-hoogle#readme
+bug-reports:    https://github.com/jappeace/mcp-hoogle/issues
+author:         Jappie Klooster
+maintainer:     hi@jappie.me
+copyright:      2025 Jappie Klooster
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    Readme.md
+    LICENSE
+extra-doc-files:
+    Changelog.md
+
+source-repository head
+  type: git
+  location: https://github.com/jappeace/mcp-hoogle
+
+flag werror
+  description: Enable -Werror during development
+  manual: True
+  default: False
+
+common common-options
+  default-extensions:
+      EmptyCase
+      FlexibleContexts
+      FlexibleInstances
+      InstanceSigs
+      MultiParamTypeClasses
+      LambdaCase
+      MultiWayIf
+      NamedFieldPuns
+      TupleSections
+      DeriveFoldable
+      DeriveFunctor
+      DeriveGeneric
+      DeriveLift
+      DeriveTraversable
+      DerivingStrategies
+      GeneralizedNewtypeDeriving
+      StandaloneDeriving
+      OverloadedStrings
+      TypeApplications
+      NumericUnderscores
+      ImportQualifiedPost
+      TemplateHaskell
+
+  ghc-options:
+    -Wall -Wincomplete-uni-patterns
+    -Wincomplete-record-updates -Widentities -Wredundant-constraints
+    -Wcpp-undef -fwarn-tabs -Wpartial-fields
+    -fdefer-diagnostics -Wunused-packages
+    -fenable-th-splice-warnings
+    -fno-omit-yields
+
+  if flag(werror)
+    ghc-options: -Werror
+
+  build-depends:
+      base >=4.9.1.0 && <4.22
+
+  default-language: Haskell2010
+
+library
+  import: common-options
+  exposed-modules:
+      McpHoogle
+      McpHoogle.Tools
+      McpHoogle.Format
+  hs-source-dirs:
+      src
+  build-depends:
+      hoogle >=5.0 && <5.1
+    , mcp-server >=0.1.0.17 && <0.2
+    , text >=1.2 && <2.2
+    , directory >=1.3 && <1.4
+
+executable mcp-hoogle
+  import: common-options
+  main-is: Main.hs
+  hs-source-dirs:
+      app
+  build-depends:
+      mcp-hoogle
+    , hoogle >=5.0 && <5.1
+    , optparse-applicative >=0.16 && <0.19
+  ghc-options: -Wno-unused-packages -threaded -rtsopts "-with-rtsopts=-N -M7G -T -Iw10"
+
+test-suite unit
+  import: common-options
+  type: exitcode-stdio-1.0
+  main-is: Test.hs
+  ghc-options: -Wno-unused-packages -threaded -rtsopts "-with-rtsopts=-N -M7G -T -Iw10"
+  hs-source-dirs:
+      test
+  build-depends:
+      tasty >=1.4 && <1.6
+    , tasty-hunit >=0.10 && <0.11
+    , mcp-hoogle
+    , hoogle >=5.0 && <5.1
+    , text >=1.2 && <2.2
+    , directory >=1.3 && <1.4
diff --git a/src/McpHoogle.hs b/src/McpHoogle.hs
new file mode 100644
--- /dev/null
+++ b/src/McpHoogle.hs
@@ -0,0 +1,92 @@
+{-# 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
diff --git a/src/McpHoogle/Format.hs b/src/McpHoogle/Format.hs
new file mode 100644
--- /dev/null
+++ b/src/McpHoogle/Format.hs
@@ -0,0 +1,60 @@
+-- | Rendering of Hoogle search results into human-readable text.
+--
+-- Hoogle's 'Target' type contains HTML markup (e.g. @\<s0\>map\<\/s0\>@)
+-- which is meaningless in a plain-text MCP response. This module strips
+-- the HTML and formats each result with its package, module, docs, and URL
+-- so an AI assistant can read them directly.
+module McpHoogle.Format
+  ( formatTarget
+  , formatTargets
+  , stripHtmlTags
+  )
+where
+
+import Data.Text (Text)
+import Data.Text qualified as Text
+import Hoogle (Target(..))
+
+-- | Format a single Hoogle 'Target' into a readable markdown-ish block.
+--
+-- Includes the item signature (HTML-stripped), package name, module name,
+-- documentation excerpt, and a link to the Hackage page.
+formatTarget :: Target -> Text
+formatTarget target = Text.unlines
+  [ "## " <> stripHtmlTags (Text.pack (targetItem target))
+  , case targetPackage target of
+      Just (package, _url) -> "Package: " <> Text.pack package
+      Nothing -> ""
+  , case targetModule target of
+      Just (moduleName, _url) -> "Module: " <> Text.pack moduleName
+      Nothing -> ""
+  , if null (targetDocs target)
+      then ""
+      else "\n" <> stripHtmlTags (Text.pack (targetDocs target))
+  , "URL: " <> Text.pack (targetURL target)
+  ]
+
+-- | Format a list of targets separated by horizontal rules.
+-- Returns a "No results found." message for an empty list.
+formatTargets :: [Target] -> Text
+formatTargets [] = "No results found."
+formatTargets targets =
+  Text.intercalate "\n---\n\n" (map formatTarget targets)
+
+-- | Strip HTML tags from text, keeping only the text content.
+--
+-- Hoogle wraps function names in @\<s0\>...\<\/s0\>@ spans and uses
+-- other HTML for formatting. This does a single-pass removal of all
+-- angle-bracketed sequences.
+stripHtmlTags :: Text -> Text
+stripHtmlTags = go False
+  where
+    go :: Bool -> Text -> Text
+    go _inTag input = case Text.uncons input of
+      Nothing -> Text.empty
+      Just ('<', rest) -> go True rest
+      Just (char, rest)
+        | _inTag -> case char of
+            '>' -> go False rest
+            _   -> go True rest
+        | otherwise -> Text.cons char (go False rest)
diff --git a/src/McpHoogle/Tools.hs b/src/McpHoogle/Tools.hs
new file mode 100644
--- /dev/null
+++ b/src/McpHoogle/Tools.hs
@@ -0,0 +1,100 @@
+-- | MCP tool definitions and their handlers.
+--
+-- Each constructor of 'HoogleTool' becomes an MCP tool that Claude (or any
+-- MCP client) can invoke. The @mcp-server@ library's Template Haskell
+-- derivation turns the ADT into a tool list + dispatcher automatically.
+--
+-- The handler reads from an 'IORef' 'Database' so the database can be
+-- hot-swapped via 'RegenerateDatabase' without restarting the server.
+module McpHoogle.Tools
+  ( HoogleTool(..)
+  , SearchParams(..)
+  , SearchTypeParams(..)
+  , LookupModuleParams(..)
+  , RegenerateDatabaseParams(..)
+  , handleTool
+  , toolDescriptions
+  )
+where
+
+import Data.Text (Text)
+import Data.Text qualified as Text
+import Data.IORef (IORef, readIORef, writeIORef)
+import Hoogle (Database, searchDatabase, withDatabase, hoogle)
+import McpHoogle.Format (formatTargets)
+
+-- | Parameters for a general search (name, keyword, or type signature).
+data SearchParams = SearchParams
+  { query :: Text
+  }
+
+-- | Parameters for a type-signature-specific search.
+data SearchTypeParams = SearchTypeParams
+  { typeSignature :: Text
+  }
+
+-- | Parameters for a module-name lookup.
+data LookupModuleParams = LookupModuleParams
+  { moduleName :: Text
+  }
+
+-- | Parameters for database regeneration.
+data RegenerateDatabaseParams = RegenerateDatabaseParams
+  { databasePath :: Text
+  }
+
+-- | The set of MCP tools this server exposes.
+--
+-- Each constructor maps to one callable tool. The nested parameter records
+-- provide named arguments (avoiding partial record selectors) which the
+-- TH derivation exposes as the tool's input schema.
+data HoogleTool
+  = Search SearchParams
+  | SearchType SearchTypeParams
+  | LookupModule LookupModuleParams
+  | RegenerateDatabase RegenerateDatabaseParams
+
+-- | Human-readable descriptions for each tool and its arguments.
+-- Fed to 'deriveToolHandlerWithDescription' so MCP clients know what
+-- each tool does and what arguments to pass.
+toolDescriptions :: [(String, String)]
+toolDescriptions =
+  [ ("Search", "Search Hoogle by function name, type signature, or keyword. Returns matching functions with their types, packages, and documentation.")
+  , ("SearchType", "Search Hoogle specifically by type signature. Example: 'a -> [a]' or '[a] -> Int'")
+  , ("LookupModule", "Search for all exports of a given module name. Example: 'Data.Map'")
+  , ("RegenerateDatabase", "Regenerate the Hoogle database from the current GHC package database. Call this after switching projects or nix-shells to re-index available packages. The databasePath should be the path to write the .hoo file.")
+  , ("query", "The search query: a function name, keyword, or type signature")
+  , ("typeSignature", "A Haskell type signature to search for, e.g. '[a] -> Int' or 'Map k v -> [(k,v)]'")
+  , ("moduleName", "A Haskell module name to look up, e.g. 'Data.Map' or 'Control.Monad'")
+  , ("databasePath", "Path to the Hoogle database file to regenerate and reload")
+  ]
+
+-- | Dispatch a tool call to the appropriate Hoogle operation.
+--
+-- Reads the current database from the 'IORef'. For 'RegenerateDatabase',
+-- shells out to @hoogle generate --local@, reloads the resulting file,
+-- and swaps the 'IORef' contents so subsequent searches use the new data.
+handleTool :: IORef Database -> HoogleTool -> IO Text
+handleTool databaseRef (Search (SearchParams searchQuery)) = do
+  database <- readIORef databaseRef
+  let results = take 20 (searchDatabase database (Text.unpack searchQuery))
+  pure (formatTargets results)
+handleTool databaseRef (SearchType (SearchTypeParams sig)) = do
+  database <- readIORef databaseRef
+  let results = take 20 (searchDatabase database (Text.unpack sig))
+  pure (formatTargets results)
+handleTool databaseRef (LookupModule (LookupModuleParams modName)) = do
+  database <- readIORef databaseRef
+  let queryString = "module:" <> Text.unpack modName
+      results = take 30 (searchDatabase database queryString)
+  -- If module: prefix doesn't work well, fall back to plain search
+  let finalResults = if null results
+        then take 30 (searchDatabase database (Text.unpack modName))
+        else results
+  pure (formatTargets finalResults)
+handleTool databaseRef (RegenerateDatabase (RegenerateDatabaseParams path)) = do
+  let pathStr = Text.unpack path
+  hoogle ["generate", "--local", "--database=" <> pathStr]
+  withDatabase pathStr $ \newDb -> do
+    writeIORef databaseRef newDb
+    pure "Database regenerated and reloaded successfully."
diff --git a/test/Test.hs b/test/Test.hs
new file mode 100644
--- /dev/null
+++ b/test/Test.hs
@@ -0,0 +1,129 @@
+module Main where
+
+import Test.Tasty
+import Test.Tasty.HUnit
+
+import Control.Exception (try, SomeException)
+import Data.IORef (IORef, newIORef)
+import Data.Text qualified as Text
+import Hoogle (Database, Target(..), defaultDatabaseLocation, withDatabase, hoogle)
+import McpHoogle.Format (formatTarget, formatTargets, stripHtmlTags)
+import McpHoogle.Tools (HoogleTool(..), SearchParams(..), SearchTypeParams(..), LookupModuleParams(..), handleTool)
+import System.Directory (doesFileExist, createDirectoryIfMissing, getTemporaryDirectory)
+
+main :: IO ()
+main = defaultMain tests
+
+tests :: TestTree
+tests = testGroup "McpHoogle"
+  [ formatTests
+  , stripHtmlTests
+  , withResource acquireDb releaseDb hoogleSearchTests
+  ]
+
+-- | Acquire a hoogle database for testing.
+-- Uses the default location if it exists, otherwise generates a fresh one.
+-- Returns Nothing if generation fails (e.g. no haddock docs in CI).
+acquireDb :: IO (Maybe FilePath)
+acquireDb = do
+  defaultPath <- defaultDatabaseLocation
+  dbExists <- doesFileExist defaultPath
+  if dbExists
+    then pure (Just defaultPath)
+    else do
+      tmpDir <- getTemporaryDirectory
+      let testDbDir = tmpDir <> "/mcp-hoogle-test"
+          testDbPath = testDbDir <> "/test.hoo"
+      createDirectoryIfMissing True testDbDir
+      result <- try (hoogle ["generate", "--local", "--database=" <> testDbPath]) :: IO (Either SomeException ())
+      case result of
+        Right () -> do
+          generated <- doesFileExist testDbPath
+          pure (if generated then Just testDbPath else Nothing)
+        Left _ -> pure Nothing
+
+-- | No-op cleanup (temp files are fine to leave)
+releaseDb :: Maybe FilePath -> IO ()
+releaseDb _ = pure ()
+
+formatTests :: TestTree
+formatTests = testGroup "Format"
+  [ testCase "formatTargets empty returns no results message" $
+      formatTargets [] @?= "No results found."
+  , testCase "formatTarget includes package name" $ do
+      let target = mkTarget "map" (Just ("containers", "")) (Just ("Data.Map", ""))
+          result = formatTarget target
+      assertBool "should contain package name" (Text.isInfixOf "containers" result)
+  , testCase "formatTarget includes module name" $ do
+      let target = mkTarget "map" (Just ("containers", "")) (Just ("Data.Map", ""))
+          result = formatTarget target
+      assertBool "should contain module name" (Text.isInfixOf "Data.Map" result)
+  , testCase "formatTarget includes function name" $ do
+      let target = mkTarget "<s0>map</s0> :: (a -> b) -> [a] -> [b]" Nothing Nothing
+          result = formatTarget target
+      assertBool "should contain function name after stripping html" (Text.isInfixOf "map" result)
+  ]
+
+stripHtmlTests :: TestTree
+stripHtmlTests = testGroup "stripHtmlTags"
+  [ testCase "strips simple tags" $
+      stripHtmlTags "<b>hello</b>" @?= "hello"
+  , testCase "strips span tags from hoogle output" $
+      stripHtmlTags "<span class=name><s0>map</s0></span> :: (a -> b) -> [a] -> [b]"
+        @?= "map :: (a -> b) -> [a] -> [b]"
+  , testCase "preserves plain text" $
+      stripHtmlTags "no tags here" @?= "no tags here"
+  , testCase "handles nested tags" $
+      stripHtmlTags "<a><b>deep</b></a>" @?= "deep"
+  ]
+
+-- | Integration tests that exercise hoogle search through our handler.
+-- The database path is shared across all tests via withResource.
+-- Tests pass trivially when no database is available (e.g. cabal CI without haddocks).
+hoogleSearchTests :: IO (Maybe FilePath) -> TestTree
+hoogleSearchTests getDbPath = testGroup "Hoogle search integration"
+  [ testCase "search for 'map' returns results" $ do
+      mDbPath <- getDbPath
+      case mDbPath of
+        Nothing -> pure () -- no DB available, skip
+        Just dbPath -> withHoogleDb dbPath $ \databaseRef -> do
+          result <- handleTool databaseRef (Search (SearchParams "map"))
+          assertBool "search should return results, not 'No results found'"
+            (result /= "No results found.")
+          assertBool "search for map should mention 'map' in results"
+            (Text.isInfixOf "map" result)
+  , testCase "type search '[a] -> Int' returns results" $ do
+      mDbPath <- getDbPath
+      case mDbPath of
+        Nothing -> pure ()
+        Just dbPath -> withHoogleDb dbPath $ \databaseRef -> do
+          result <- handleTool databaseRef (SearchType (SearchTypeParams "[a] -> Int"))
+          assertBool "type search should return results"
+            (result /= "No results found.")
+  , testCase "module lookup 'Data.Map' returns results" $ do
+      mDbPath <- getDbPath
+      case mDbPath of
+        Nothing -> pure ()
+        Just dbPath -> withHoogleDb dbPath $ \databaseRef -> do
+          result <- handleTool databaseRef (LookupModule (LookupModuleParams "Data.Map"))
+          assertBool "module lookup should return results"
+            (result /= "No results found.")
+  ]
+
+-- | Load a hoogle database and run an action with it
+withHoogleDb :: FilePath -> (IORef Database -> IO ()) -> IO ()
+withHoogleDb path action =
+  withDatabase path $ \database -> do
+    databaseRef <- newIORef database
+    action databaseRef
+
+-- | Helper to create a Target for testing
+mkTarget :: String -> Maybe (String, String) -> Maybe (String, String) -> Target
+mkTarget item package moduleDef = Target
+  { targetURL = "https://hackage.haskell.org/example"
+  , targetPackage = package
+  , targetModule = moduleDef
+  , targetType = ""
+  , targetItem = item
+  , targetDocs = ""
+  }
