packages feed

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 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 @@+[![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
+ 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 = ""+  }