mcp-hoogle (empty) → 0.1.0
raw patch · 9 files changed
+658/−0 lines, 9 filesdep +basedep +directorydep +hoogle
Dependencies added: base, directory, hoogle, mcp-hoogle, mcp-server, optparse-applicative, tasty, tasty-hunit, text
Files
- Changelog.md +6/−0
- LICENSE +21/−0
- Readme.md +75/−0
- app/Main.hs +63/−0
- mcp-hoogle.cabal +112/−0
- src/McpHoogle.hs +92/−0
- src/McpHoogle/Format.hs +60/−0
- src/McpHoogle/Tools.hs +100/−0
- test/Test.hs +129/−0
+ Changelog.md view
@@ -0,0 +1,6 @@+# Change log for template project++## Version 0.0.0 ++import [template](https://github.com/jappeace/template).+
+ LICENSE view
@@ -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.
+ Readme.md view
@@ -0,0 +1,75 @@+[](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
+ app/Main.hs view
@@ -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
+ mcp-hoogle.cabal view
@@ -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
+ src/McpHoogle.hs view
@@ -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
+ src/McpHoogle/Format.hs view
@@ -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)
+ src/McpHoogle/Tools.hs view
@@ -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."
+ test/Test.hs view
@@ -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 = ""+ }