diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for glean-lsp
+
+## 0.1.0.0
+
+* First version. Released on an unsuspecting world.
diff --git a/Data/ConcurrentCache.hs b/Data/ConcurrentCache.hs
new file mode 100644
--- /dev/null
+++ b/Data/ConcurrentCache.hs
@@ -0,0 +1,72 @@
+{-
+  Derived from Data.ConcurrentCache in static-ls
+
+  Copyright (c) 2023 Joseph Sumabat
+-}
+
+module Data.ConcurrentCache (
+  ConcurrentCache,
+  new,
+  flush,
+  remove,
+  insert
+) where
+
+import Control.Monad
+import Control.Monad.IO.Class
+import Control.Monad.IO.Unlift
+import Data.Hashable
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HashMap
+import UnliftIO.Exception
+import UnliftIO.IORef
+import UnliftIO.MVar
+
+newtype ConcurrentCache k v = ConcurrentCache {
+  map :: IORef (HashMap k (MVar (Maybe v)))
+    -- k -> missing => computation for k not cached
+    -- k -> MVar empty => computation for k in progress
+    -- k -> MVar (Just v) => computation for k cached with value v
+    -- k -> MVar Nothing => computation for k failed, try again
+}
+
+new :: (MonadIO m) => m (ConcurrentCache k v)
+new = do
+  map <- newIORef HashMap.empty
+  pure ConcurrentCache {map}
+
+flush :: (MonadIO m) => ConcurrentCache k v -> m ()
+flush cache = writeIORef cache.map HashMap.empty
+
+remove :: (Hashable k, MonadIO m) => k -> ConcurrentCache k v -> m ()
+remove k cache = do
+  atomicModifyIORef' cache.map $ \m -> do
+    (HashMap.delete k m, ())
+
+-- | Perform a computation and insert its result into the cache. If
+-- multiple threads try to insert a computation for the same key, one
+-- will perform its computation while the others wait. If the
+-- computation throws, another thread will try its computation.
+insert :: (Hashable k, MonadUnliftIO m) => k -> ConcurrentCache k v -> m v -> m v
+insert k cache act = mask $ \restore -> do
+  var <- newEmptyMVar
+  existed <- atomicModifyIORef' cache.map $ \m -> do
+    case HashMap.lookup k m of
+      Nothing -> (HashMap.insert k var m, Nothing)
+      Just otherVar -> (m, Just otherVar)
+  case existed of
+    Nothing -> do
+      v <-
+        (restore act) `onException` do
+          remove k cache
+          putMVar var Nothing
+      putMVar var (Just v)
+      pure v
+    Just otherVar ->
+      join $ restore $ do
+        v <- readMVar otherVar
+        case v of
+          Just v -> return $ return v
+          Nothing ->
+            -- try again; the key was removed from the cache
+            return $ insert k cache act
diff --git a/Data/Path.hs b/Data/Path.hs
new file mode 100644
--- /dev/null
+++ b/Data/Path.hs
@@ -0,0 +1,110 @@
+{-
+  Copyright (c) 2023 Joseph Sumabat
+
+  See glean/lsp/LICENSE
+-}
+
+module Data.Path (
+  Path (path, Path, UncheckedPath),
+  KnownPathKind (sPathKind),
+  AbsPath,
+  PathKind (..),
+  RelPath,
+  filePathToAbs,
+  unsafeFilePathToAbs,
+  filePathToRel,
+  toFilePath,
+  (</>),
+  makeRelative,
+  absToRel,
+  (<.>),
+  (-<.>),
+  filePathToAbsThrow,
+  uncheckedCoercePath,
+  relToAbsThrow,
+)
+where
+
+import Control.Monad.Catch
+import Control.Monad.IO.Class
+import Data.Aeson qualified as Aeson
+import Data.Coerce (coerce)
+import Data.Hashable (Hashable)
+import Data.String (IsString (..))
+import GHC.Stack (HasCallStack)
+import System.Directory qualified as Dir
+import System.FilePath qualified as FilePath
+import UnliftIO (stringException)
+
+-- | Rel means may be relative or absolute, absolute means must be absolute
+data PathKind = Rel | Abs
+
+data SPathKind p where
+  SRel :: SPathKind Rel
+  SAbs :: SPathKind Abs
+
+class KnownPathKind p where
+  sPathKind :: SPathKind p
+
+instance KnownPathKind Abs where
+  sPathKind = SAbs
+
+instance KnownPathKind Rel where
+  sPathKind = SRel
+
+newtype Path (p :: PathKind) = UncheckedPath {path :: FilePath}
+  deriving (Show, Eq, Ord, Hashable, Aeson.FromJSON, Aeson.ToJSON)
+
+uncheckedCoercePath :: Path p -> Path q
+uncheckedCoercePath = coerce
+
+instance IsString (Path Rel) where
+  fromString = UncheckedPath
+
+pattern Path :: FilePath -> Path p
+pattern Path p <- UncheckedPath p
+
+type AbsPath = Path Abs
+
+type RelPath = Path Rel
+
+toFilePath :: (HasCallStack) => Path p -> FilePath
+toFilePath = (.path)
+
+filePathToRel :: (HasCallStack) => FilePath -> RelPath
+filePathToRel = UncheckedPath
+
+filePathToAbs :: (HasCallStack, MonadIO m) => FilePath -> m AbsPath
+filePathToAbs p = do
+  absPath <- liftIO $ Dir.makeAbsolute p
+  pure $ UncheckedPath absPath
+
+unsafeFilePathToAbs :: (HasCallStack) => FilePath -> AbsPath
+unsafeFilePathToAbs p
+  | FilePath.isAbsolute p = UncheckedPath p
+  | otherwise = error "unsafeOsPathToAbs: path is not absolute"
+
+relToAbsThrow :: (MonadThrow m, HasCallStack) => RelPath -> m AbsPath
+relToAbsThrow (UncheckedPath p) = filePathToAbsThrow p
+
+filePathToAbsThrow :: (MonadThrow m, HasCallStack) => FilePath -> m AbsPath
+filePathToAbsThrow p
+  | FilePath.isAbsolute p = pure $ UncheckedPath p
+  | otherwise = throwM (stringException $ "filepath was not absolute: " ++ p)
+
+(</>) :: Path p -> Path Rel -> Path p
+(UncheckedPath p) </> (UncheckedPath p') = UncheckedPath (p FilePath.</> p')
+
+infixr 5 </>
+
+(<.>) :: Path p -> String -> Path p
+(UncheckedPath p) <.> ext = UncheckedPath (p FilePath.<.> ext)
+
+(-<.>) :: Path p -> String -> Path p
+(UncheckedPath p) -<.> ext = UncheckedPath (p FilePath.-<.> ext)
+
+absToRel :: AbsPath -> RelPath
+absToRel (UncheckedPath p) = UncheckedPath p
+
+makeRelative :: Path p -> Path q -> Path Rel
+makeRelative (UncheckedPath p) (UncheckedPath q) = UncheckedPath (FilePath.makeRelative p q)
diff --git a/Glean/LSP.hs b/Glean/LSP.hs
new file mode 100644
--- /dev/null
+++ b/Glean/LSP.hs
@@ -0,0 +1,694 @@
+{-# LANGUAGE ApplicativeDo, RecursiveDo #-}
+module Glean.LSP (main) where
+
+import Control.Monad
+import Control.Monad.Catch
+import Control.Monad.Fix
+import Control.Monad.IO.Class
+import Control.Monad.IO.Unlift
+import Control.Monad.Trans.Class
+import Control.Monad.Trans.Except
+import Control.Monad.Trans.Reader
+import Data.Aeson as Aeson
+import Data.Default
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HashMap
+import Data.Int
+import Data.List (sortBy)
+import qualified Data.Map as Map
+import Data.Maybe
+import Data.Ord (comparing)
+import Data.Text (Text)
+import qualified Data.Text as Text
+import qualified Language.LSP.Server as LSP
+import Language.LSP.Server (LspM)
+import qualified Language.LSP.Protocol.Message as LSP
+import qualified Language.LSP.Protocol.Types as LSP
+import Options.Applicative
+import Thrift.Protocol
+import UnliftIO.Async
+import UnliftIO.Exception
+import UnliftIO.IORef
+import Util.Log.Text
+import Util.Timing
+
+-- use Glass as a library directly
+import qualified Glean.Glass.Env as Glass
+import qualified Glean.Glass.Main as Glass
+import qualified Glean.Glass.Options as Glass
+import qualified Glean.Glass.Tracing as Glass
+import qualified Glean.Glass.Types as Glass
+import qualified Glean.Glass.Range as Glass
+import qualified Glean.Glass.Handler.Documents as Glass.Handler
+import qualified Glean.Glass.Handler.Symbols as Glass.Handler
+
+import Data.ConcurrentCache as ConcurrentCache
+import Data.Path as Path
+
+{- TODO / ideas
+  - go to decl / go to impl / go to type def?
+  - call hierarchy
+  - documentSymbols:
+    - maybe filter out type parameters?
+  - update index after edits
+    1. make it possible to re-index on the side and create a new DB,
+       using a separate glean-server. Need to flush the cache if we
+       detect that the data has changed.
+    2. Trigger re-indexing from the UI somehow, with a progress update
+    3. Support full re-index & in-memory incremental index for changed files only?
+  - can we show errors somehow?
+  - maybe use tree-sitter to get uptodate documentSymbols and
+    merge those with the real symbols from Glass?
+  - implement setTrace?
+-}
+
+-- -----------------------------------------------------------------------------
+-- Command-line options
+
+-- | Inherit CLI options from Glass
+data LspOptions = LspOptions {
+    glass :: Glass.Config Glass.GlassTrace
+  }
+
+options :: ParserInfo LspOptions
+options = info (helper <*> (LspOptions <$> Glass.configParser)) fullDesc
+
+-- -----------------------------------------------------------------------------
+-- Config
+
+-- | Config from the LSP client. e.g. for VS Code, @<workspace>/.vscode/settings.json@:
+--
+-- > {
+-- >   "glean-lsp": {
+-- >     "repo": "stackage"
+-- >   },
+-- >   ...
+-- > }
+--
+data LspConfig = LspConfig {
+    repo :: Glass.RepoName
+  }
+  deriving (Show)
+
+instance FromJSON LspConfig where
+  parseJSON = withObject "LspConfig" $ \v -> LspConfig
+    <$> fmap Glass.RepoName (v .: "repo")
+
+instance Default LspConfig where
+  def = LspConfig {
+    repo = Glass.RepoName "stackage"
+  }
+
+-- -----------------------------------------------------------------------------
+-- Monad
+
+-- | Environment of the running LSP server after initialisation
+data LspEnv = LspEnv {
+    options :: LspOptions,
+    wsRoot :: AbsPath,
+    glass :: Glass.Env,
+    symbolCache :: ConcurrentCache RelPath Glass.DocumentSymbolIndex,
+    requests :: IORef (HashMap LSP.SomeLspId (Async ()))
+  }
+
+newtype GleanLspM a =
+  GleanLspM { unGleanLspM :: ReaderT (IORef (Maybe LspEnv)) (LspM LspConfig) a }
+  deriving (Monad, Applicative, Functor, MonadIO, MonadThrow, MonadUnliftIO, MonadFix)
+
+deriving instance LSP.MonadLsp LspConfig GleanLspM
+
+runGleanLspM :: IORef (Maybe LspEnv) -> GleanLspM a -> LspM LspConfig a
+runGleanLspM envRef = flip runReaderT envRef . (.unGleanLspM)
+
+newtype GleanLspException = GleanLspException Text
+  deriving Show
+
+instance Exception GleanLspException
+
+getGleanLspEnv :: GleanLspM LspEnv
+getGleanLspEnv = GleanLspM $ do
+  ref <- ask
+  r <- readIORef ref
+  case r of
+    Nothing -> throwIO $ GleanLspException "not initialized"
+    Just env -> return env
+
+-- -----------------------------------------------------------------------------
+-- Executing requests
+
+-- | Perform a request asynchronously. It can subsequently be
+-- cancelled by 'cancelRequest', which will send the appropriate
+-- @RequestCancelled@ response back to the client if the request was
+-- still in progress at the time of cancellation.
+asyncRequest ::
+  forall (m :: LSP.Method LSP.ClientToServer LSP.Request) .
+  (LSP.TRequestMessage m ->
+    GleanLspM (Either (LSP.TResponseError m) (LSP.MessageResult m))) ->
+  LSP.Handler GleanLspM m
+asyncRequest act = \msg respond -> mdo
+  env <- getGleanLspEnv
+  m <- readIORef env.requests
+  liftIO $ logInfo $ "asyncRequest: " <>
+    Text.pack (show (HashMap.size m)) <> " in progress"
+  let
+    register =
+      atomicModifyIORef env.requests $ -- not strict due to mdo
+        \m -> (HashMap.insert (LSP.SomeLspId msg._id) async m, ())
+    unregister =
+      atomicModifyIORef' env.requests $
+        \m -> (HashMap.delete (LSP.SomeLspId msg._id) m, ())
+  register
+  async <- UnliftIO.Exception.mask_ $ asyncWithUnmask $ \unmask -> do
+    r <- UnliftIO.Exception.trySyncOrAsync $ unmask $ act msg
+      -- we want to catch everything, including async cancellation
+    unregister
+    case r of
+      Left (ex :: SomeException)
+        | Just AsyncCancelled <- fromException ex ->
+          respond $ Left $ responseCancelled "cancelled by client"
+        | otherwise -> respond $ Left $ responseException ex
+      Right result -> respond result
+  return ()
+
+-- | Cancel a request by LspId
+cancelRequest ::
+  forall (m :: LSP.Method LSP.ClientToServer LSP.Request) .
+  LSP.LspId m ->
+  GleanLspM ()
+cancelRequest id = do
+  env <- getGleanLspEnv
+  m <- readIORef env.requests
+  case HashMap.lookup (LSP.SomeLspId id) m of
+    Nothing -> logWarning $ "cancelRequest: not found"
+    Just async -> cancel async
+
+responseCancelled ::
+  forall (m :: LSP.Method LSP.ClientToServer LSP.Request) .
+  Text ->
+  LSP.TResponseError m
+responseCancelled msg =
+  LSP.TResponseError {
+    _code = LSP.InL LSP.LSPErrorCodes_RequestCancelled,
+    _message = msg,
+    _xdata = Nothing
+  }
+
+responseException ::
+  forall (m :: LSP.Method LSP.ClientToServer LSP.Request) .
+  SomeException ->
+  LSP.TResponseError m
+responseException ex =
+  LSP.TResponseError {
+    _code = LSP.InL LSP.LSPErrorCodes_RequestFailed,
+    _message = Text.pack (show ex),
+    _xdata = Nothing
+  }
+
+-- -----------------------------------------------------------------------------
+-- Setup & initialisation
+
+initServer ::
+  Glass.Env ->
+  LspOptions ->
+  IORef (Maybe LspEnv) ->
+  LSP.LanguageContextEnv LspConfig ->
+  LSP.TMessage 'LSP.Method_Initialize ->
+  IO (Either (LSP.TResponseError LSP.Method_Initialize) (LSP.LanguageContextEnv LspConfig))
+initServer glass options envRef serverConfig _msg = do
+  runExceptT $ do
+    wsRoot <- ExceptT $ LSP.runLspT serverConfig getWsRoot
+    wsRoot <- filePathToAbs wsRoot
+    symbolCache <- ConcurrentCache.new
+    requests <- newIORef HashMap.empty
+    writeIORef envRef (Just LspEnv { options, glass, wsRoot, symbolCache, requests })
+    liftIO $ logInfo $ "wsRoot: " <> Text.pack (Path.toFilePath wsRoot)
+    pure serverConfig
+  where
+  getWsRoot :: LSP.LspM config (Either (LSP.TResponseError LSP.Method_Initialize) FilePath)
+  getWsRoot = do
+    mRootPath <- LSP.getRootPath
+    pure $ case mRootPath of
+      Nothing -> Left $ LSP.TResponseError (LSP.InR LSP.ErrorCodes_InvalidRequest)
+        "No root workspace was found" Nothing
+      Just p -> Right p
+
+serverDef
+  :: Glass.Env
+  -> LspOptions
+  -> IO (LSP.ServerDefinition LspConfig)
+serverDef glass options = do
+  envRef <- newIORef Nothing
+  let
+    mapReq ::
+      forall (a :: LSP.Method LSP.ClientToServer LSP.Request).
+      LSP.Handler GleanLspM a ->
+      LSP.Handler (LspM LspConfig) a
+    mapReq f = \msg responseCont -> runGleanLspM envRef $
+      f msg (\resp -> GleanLspM (lift (responseCont resp)))
+
+    mapNot ::
+      forall (a :: LSP.Method LSP.ClientToServer LSP.Notification).
+      LSP.Handler GleanLspM a ->
+      LSP.Handler (LspM LspConfig) a
+    mapNot f = \msg -> runGleanLspM envRef $ f msg
+
+  pure
+    LSP.ServerDefinition
+      { onConfigChange = \_conf -> runGleanLspM envRef $ do
+          liftIO $ logInfo "config change"
+          flushSymbolCache
+      , configSection = "glean-lsp"
+      , parseConfig = \_conf value ->
+          case fromJSON value of
+            Error err -> Left (Text.pack err)
+            Aeson.Success conf -> Right conf
+      , doInitialize = initServer glass options envRef
+      , -- TODO: Do handlers need to inspect clientCapabilities?
+        staticHandlers = \_clientCapabilities ->
+          LSP.mapHandlers mapReq mapNot $
+            mconcat [
+                  handleInitialized
+                , handleChangeConfiguration
+                , handleTextDocumentHoverRequest
+                , handleDefinitionRequest
+                -- , handleTypeDefinitionRequest
+                -- , handleImplementationRequest
+                , handleReferencesRequest
+                -- , handleRenameRequest
+                -- , handlePrepareRenameRequest
+                , handleCancelNotification
+                , handleDidOpen
+                -- , handleDidChange
+                -- , handleDidSave
+                , handleDidClose
+                , handleWorkspaceSymbol
+                , handleSetTrace
+                -- , handleCodeAction
+                -- , handleResolveCodeAction
+                , handleDocumentSymbols
+                -- , handleCompletion
+                -- , handleCompletionItemResolve
+                ]
+      , interpretHandler = \env -> LSP.Iso (LSP.runLspT env) liftIO
+      , options = lspOptions
+      , defaultConfig = def
+      }
+
+lspOptions :: LSP.Options
+lspOptions =
+  LSP.defaultOptions
+    { LSP.optTextDocumentSync =
+        Just
+          LSP.TextDocumentSyncOptions
+            { LSP._openClose = Just True
+            , LSP._change = Just LSP.TextDocumentSyncKind_Incremental
+            , LSP._willSave = Just False
+            , LSP._willSaveWaitUntil = Just False
+            , LSP._save =
+                Just $
+                  LSP.InR $
+                    LSP.SaveOptions
+                      { LSP._includeText = Just False
+                      }
+            }
+    , LSP.optCompletionTriggerCharacters = Just ['.']
+    }
+
+main :: IO ()
+main = do
+  opts <- execParser options
+  Glass.withEnv opts.glass Nothing $ \glass -> do
+    server <- serverDef glass opts
+    void $ LSP.runServer server
+
+-- -----------------------------------------------------------------------------
+-- Handlers
+
+handleChangeConfiguration :: LSP.Handlers GleanLspM
+handleChangeConfiguration =
+  LSP.notificationHandler LSP.SMethod_WorkspaceDidChangeConfiguration $
+    pure $ pure ()
+
+handleInitialized :: LSP.Handlers GleanLspM
+handleInitialized =
+  LSP.notificationHandler LSP.SMethod_Initialized $
+    pure $ pure ()
+
+handleCancelNotification :: LSP.Handlers GleanLspM
+handleCancelNotification =
+  LSP.notificationHandler LSP.SMethod_CancelRequest $ \req -> do
+    let id = case req._params._id of
+               LSP.InL i -> LSP.IdInt i
+               LSP.InR t -> LSP.IdString t
+    liftIO $ logInfo $ "cancel: " <> Text.pack (show id)
+    cancelRequest id
+
+handleDidOpen :: LSP.Handlers GleanLspM
+handleDidOpen =
+  LSP.notificationHandler LSP.SMethod_TextDocumentDidOpen $ \message -> do
+    liftIO $ logInfo $ "did open: " <> message._params._textDocument._uri.getUri
+
+handleDidClose :: LSP.Handlers GleanLspM
+handleDidClose =
+  LSP.notificationHandler LSP.SMethod_TextDocumentDidClose $ \req -> do
+    let uri = req._params._textDocument._uri
+    liftIO $ logInfo $ "did close: " <> uri.getUri
+    path <- uriToAbsPath uri
+    removeCachedSymbols path
+
+handleDocumentSymbols :: LSP.Handlers GleanLspM
+handleDocumentSymbols =
+  LSP.requestHandler LSP.SMethod_TextDocumentDocumentSymbol $ asyncRequest $ \req ->
+    logTimed ("documentSymbols: " <> req._params._textDocument._uri.getUri) $ do
+      let params = req._params
+      path <- uriToAbsPath params._textDocument._uri
+      syms <- getDocumentSymbols path
+      liftIO $ logInfo $ "symbols: " <> Text.pack (show (length syms))
+      return $ Right $ LSP.InR $ LSP.InL syms
+
+handleDefinitionRequest :: LSP.Handlers GleanLspM
+handleDefinitionRequest =
+  LSP.requestHandler LSP.SMethod_TextDocumentDefinition $ asyncRequest $ \req -> do
+    let params = req._params
+    logTimed ("definition: " <> params._textDocument._uri.getUri <>
+      Text.pack (show req._params._position)) $ do
+      path <- uriToAbsPath params._textDocument._uri
+      defs <- getDefinition path params._position
+      return $ Right . LSP.InR $ LSP.InL defs
+
+handleSetTrace :: LSP.Handlers GleanLspM
+handleSetTrace = LSP.notificationHandler LSP.SMethod_SetTrace $ \_ -> pure ()
+
+handleTextDocumentHoverRequest :: LSP.Handlers GleanLspM
+handleTextDocumentHoverRequest =
+  LSP.requestHandler LSP.SMethod_TextDocumentHover $ asyncRequest $ \req -> do
+    let hoverParams = req._params
+    logTimed ("hover: " <> hoverParams._textDocument._uri.getUri <>
+      Text.pack (show hoverParams._position)) $ do
+      path <- uriToAbsPath hoverParams._textDocument._uri
+      hover <- retrieveHover path hoverParams._position
+      return $ Right $ LSP.maybeToNull hover
+
+handleReferencesRequest :: LSP.Handlers GleanLspM
+handleReferencesRequest =
+  LSP.requestHandler LSP.SMethod_TextDocumentReferences $ asyncRequest $ \req -> do
+    let params = req._params
+    logTimed ("references: " <> params._textDocument._uri.getUri <>
+      Text.pack (show params._position)) $ do
+      path <- uriToAbsPath params._textDocument._uri
+      refs <- findRefs path params._position
+      return $ Right $ LSP.InL refs
+
+handleWorkspaceSymbol :: LSP.Handlers GleanLspM
+handleWorkspaceSymbol =
+  LSP.requestHandler LSP.SMethod_WorkspaceSymbol $ asyncRequest $ \req ->
+    logTimed ("search: " <> req._params._query) $ do
+      symbols <- symbolSearch req._params._query
+      return $ Right . LSP.InL $ symbols
+
+-- -----------------------------------------------------------------------------
+-- Glean / Glass stuff
+
+getDefinition ::
+  AbsPath ->
+  LSP.Position ->
+  GleanLspM [LSP.DefinitionLink]
+getDefinition path lineCol = do
+  env <- getGleanLspEnv
+  symbols <- findSymbol lineCol <$> getSymbolsCached path
+  return $ fmap (LSP.DefinitionLink . locationToLocationLink) $
+    refTargets env.wsRoot symbols
+
+getSymbols ::
+  RelPath ->
+  Bool ->
+  GleanLspM Glass.DocumentSymbolIndex
+getSymbols path includeRefs = do
+  env <- getGleanLspEnv
+  cfg :: LspConfig <- LSP.getConfig
+  let
+    query = def {
+            Glass.documentSymbolsRequest_repository = cfg.repo
+          , Glass.documentSymbolsRequest_filepath =
+              Glass.Path (Text.pack (Path.toFilePath path))
+          , Glass.documentSymbolsRequest_include_refs = includeRefs
+    }
+    opts = def
+  liftIO $ Glass.Handler.documentSymbolIndex env.glass query opts
+
+getSymbolsCached :: AbsPath -> GleanLspM Glass.DocumentSymbolIndex
+getSymbolsCached path = do
+  env <- getGleanLspEnv
+  let relPath = Path.makeRelative env.wsRoot path
+  ConcurrentCache.insert relPath env.symbolCache $
+    getSymbols relPath True {- includeRefs -}
+
+flushSymbolCache :: GleanLspM ()
+flushSymbolCache = do
+  env <- getGleanLspEnv
+  ConcurrentCache.flush env.symbolCache
+
+removeCachedSymbols :: AbsPath -> GleanLspM ()
+removeCachedSymbols path = do
+  env <- getGleanLspEnv
+  let relPath = Path.makeRelative env.wsRoot path
+  ConcurrentCache.remove relPath env.symbolCache
+
+findSymbol ::
+  LSP.Position ->
+  Glass.DocumentSymbolIndex ->
+  [Glass.SymbolX]
+findSymbol (LSP.Position l c) ix =
+    [ sym
+    | sym <- Map.findWithDefault [] (fromIntegral (l+1)) refs
+    , inRange (fromIntegral (l+1)) (fromIntegral (c+1)) sym.symbolX_range
+    ]
+  where
+  refs = Glass.documentSymbolIndex_symbols ix
+  inRange line col (Glass.Range lb cb le ce) =
+    line >= lb && line <= le &&
+    (if line == lb then col >= cb else True) &&
+    (if line == le then col <= ce else True)
+
+retrieveHover ::
+  AbsPath ->
+  LSP.Position ->
+  GleanLspM (Maybe LSP.Hover)
+retrieveHover path position = do
+  syms <- getSymbolsCached path
+  case findSymbol position syms of
+    (sym:_) | Just ty <- attrSymbolSignature sym.symbolX_attributes -> do
+      -- TODO: pick the innermost match
+      logInfo $ "hover: " <> Text.pack (show sym)
+      return $ Just $ LSP.Hover
+        { _range = Just $ toLspRange sym.symbolX_range
+        , _contents = LSP.InL $ LSP.MarkupContent LSP.MarkupKind_PlainText ty
+        }
+    _ -> return Nothing
+
+findRefs ::
+  AbsPath ->
+  LSP.Position ->
+  GleanLspM [LSP.Location]
+findRefs path pos = do
+  logInfo $ "Glean.findRefs: " <> Text.pack (show pos)
+  env <- getGleanLspEnv
+  syms <- getSymbolsCached path
+  case findSymbol pos syms of
+    [] -> do
+      logInfo $ "not found"
+      return []
+    (defn:_) -> do
+      -- TODO: pick the innermost or tightest range if there are many
+      logInfo $ "found: " <> Text.pack (show defn)
+      ranges <- liftIO $
+        Glass.Handler.findReferenceRanges env.glass defn.symbolX_sym def
+      return (map (toLspLocation env.wsRoot) ranges)
+
+-- | Document symbols, used to generate the outline.
+--
+-- We want a hierarchical symbol structure for the outline. Glass
+-- provides a symbolParent attribute, but this only reflects the
+-- parent container of the qualified name which is usually the module
+-- or namespace, not the full containment relation.
+--
+-- We could ask Glass for the containment relation, but that would
+-- likely be expensive. So instead we reconstruct an approximate
+-- symbol hierarchy using source ranges - arguably this is what we
+-- want for the outline anyway.
+--
+getDocumentSymbols ::
+  AbsPath ->
+  GleanLspM [LSP.DocumentSymbol]
+getDocumentSymbols path = do
+  syms <- getSymbolsCached path
+  let
+    defs =
+      [ sym
+      | sym <- concat $ Map.elems syms.documentSymbolIndex_symbols
+      , isNothing sym.symbolX_target -- only definitions
+      ]
+  return $ mkSymbolTree (sortBy (comparing (.symbolX_range)) defs)
+  where
+  mkSymbolTree [] = []
+  mkSymbolTree (sym : rest)
+    | Just name <- attrSymbolName sym.symbolX_attributes =
+      parent name : mkSymbolTree others
+    | otherwise = mkSymbolTree rest
+    where
+    (children, others) = span isChild rest
+    isChild child = sym.symbolX_range `Glass.rangeContains` child.symbolX_range
+    parent name = LSP.DocumentSymbol {
+      _name = name,
+      _detail = attrSymbolSignature sym.symbolX_attributes,
+      _kind = kind,
+      _tags = Nothing, -- TODO?
+      _deprecated = Nothing,
+      _range = range,
+      _selectionRange = range,
+      _children = case mkSymbolTree children of
+        [] -> Nothing
+        some -> Just some
+    }
+    kind = fromMaybe LSP.SymbolKind_Function (attrSymbolKind sym.symbolX_attributes)
+    range = toLspRange sym.symbolX_range
+
+symbolSearch ::
+  Text ->
+  GleanLspM [LSP.SymbolInformation]
+symbolSearch query = do
+  env <- getGleanLspEnv
+  cfg :: LspConfig <- LSP.getConfig
+  let
+    req = def {
+      Glass.symbolSearchRequest_name = query,
+      Glass.symbolSearchRequest_repo_name = Just cfg.repo,
+      Glass.symbolSearchRequest_options = def {
+        Glass.symbolSearchOptions_detailedResults = True -- we need kinds
+      }
+    }
+    opts = def
+  res <- liftIO $ Glass.Handler.searchSymbol env.glass req opts
+  return [
+    LSP.SymbolInformation {
+      _name = sym.symbolDescription_name.qualifiedName_localName.unName,
+      _kind = maybe LSP.SymbolKind_Function toLspSymbolKind sym.symbolDescription_kind,
+      _tags = Nothing,
+      _deprecated = Nothing,
+      _location = toLspLocation env.wsRoot sym.symbolDescription_sym_location,
+      _containerName = Nothing
+    }
+    | sym <- res.symbolSearchResult_symbolDetails
+    ]
+
+-- -----------------------------------------------------------------------------
+-- Data conversion Glass <-> LSP
+
+refTargets :: AbsPath -> [Glass.SymbolX] -> [LSP.Location]
+refTargets wsRoot syms =
+  [ toLspLocation wsRoot rg
+  | Glass.SymbolX{symbolX_target = Just rg} <- syms
+  ]
+
+toLspLocation :: AbsPath -> Glass.LocationRange -> LSP.Location
+toLspLocation wsRoot locRange =
+  LSP.Location uri (toLspRange locRange.locationRange_range)
+  where
+  path = Glass.unPath locRange.locationRange_filepath
+  uri = absPathToUri (wsRoot Path.</> Path.filePathToRel (Text.unpack path))
+
+toLspPosition :: Int64 -> Int64 -> LSP.Position
+toLspPosition line col =
+  LSP.Position (fromIntegral (line-1)) (fromIntegral (col-1))
+
+toLspRange :: Glass.Range -> LSP.Range
+toLspRange range = LSP.Range begin end
+  where
+  begin = toLspPosition range.range_lineBegin range.range_columnBegin
+  end = toLspPosition range.range_lineEnd range.range_columnEnd
+
+toLspSymbolKind :: Glass.SymbolKind -> LSP.SymbolKind
+toLspSymbolKind = \case
+  Glass.SymbolKind_Package -> LSP.SymbolKind_Package
+  Glass.SymbolKind_Type -> LSP.SymbolKind_Class -- ?
+  Glass.SymbolKind_Value -> LSP.SymbolKind_Constant -- ?
+  Glass.SymbolKind_File -> LSP.SymbolKind_File
+  Glass.SymbolKind_Module -> LSP.SymbolKind_Module
+  Glass.SymbolKind_Namespace -> LSP.SymbolKind_Namespace
+  Glass.SymbolKind_Class_ -> LSP.SymbolKind_Class
+  Glass.SymbolKind_Method -> LSP.SymbolKind_Method
+  Glass.SymbolKind_Property -> LSP.SymbolKind_Property
+  Glass.SymbolKind_Field -> LSP.SymbolKind_Field
+  Glass.SymbolKind_Constructor -> LSP.SymbolKind_Constructor
+  Glass.SymbolKind_Enum -> LSP.SymbolKind_Enum
+  Glass.SymbolKind_Interface -> LSP.SymbolKind_Interface
+  Glass.SymbolKind_Function -> LSP.SymbolKind_Function
+  Glass.SymbolKind_Variable -> LSP.SymbolKind_Variable
+  Glass.SymbolKind_Constant -> LSP.SymbolKind_Constant
+  Glass.SymbolKind_String -> LSP.SymbolKind_String
+  Glass.SymbolKind_Number -> LSP.SymbolKind_Number
+  Glass.SymbolKind_Boolean -> LSP.SymbolKind_Boolean
+  Glass.SymbolKind_Array -> LSP.SymbolKind_Array
+  Glass.SymbolKind_Object -> LSP.SymbolKind_Object
+  Glass.SymbolKind_Key -> LSP.SymbolKind_Key
+  Glass.SymbolKind_Null -> LSP.SymbolKind_Null
+  Glass.SymbolKind_Enumerator -> LSP.SymbolKind_EnumMember
+  Glass.SymbolKind_Struct -> LSP.SymbolKind_Struct
+  Glass.SymbolKind_Event -> LSP.SymbolKind_Event
+  Glass.SymbolKind_Operator -> LSP.SymbolKind_Operator
+  Glass.SymbolKind_TypeParameter -> LSP.SymbolKind_TypeParameter
+  Glass.SymbolKind_Union -> LSP.SymbolKind_Class -- ?
+  Glass.SymbolKind_Macro -> LSP.SymbolKind_Function -- ?
+  Glass.SymbolKind_Trait -> LSP.SymbolKind_Interface -- ?
+  Glass.SymbolKind_Fragment -> LSP.SymbolKind_Struct -- ?
+  Glass.SymbolKind_Operation -> LSP.SymbolKind_Method -- ?
+  Glass.SymbolKind_Directive -> LSP.SymbolKind_Null -- ?
+  _ -> LSP.SymbolKind_Null
+
+attrSymbolSignature :: Glass.Attributes -> Maybe Text
+attrSymbolSignature attrs =
+  case Map.lookup "symbolSignature" attrs.unAttributes of
+    Just (Glass.Attribute_aString ty) -> Just ty
+    _ -> Nothing
+
+attrSymbolName :: Glass.Attributes -> Maybe Text
+attrSymbolName attrs =
+  case Map.lookup "symbolName" attrs.unAttributes of
+    Just (Glass.Attribute_aString nm) -> Just nm
+    _ -> Nothing
+
+attrSymbolKind :: Glass.Attributes -> Maybe LSP.SymbolKind
+attrSymbolKind attrs =
+  case Map.lookup "symbolKind" attrs.unAttributes of
+    Just (Glass.Attribute_aInteger kind) ->
+      Just (toLspSymbolKind (toThriftEnum (fromIntegral kind)))
+    _ -> Nothing
+
+-- -----------------------------------------------------------------------------
+-- Utils
+
+uriToAbsPath :: (MonadThrow m) => LSP.Uri -> m AbsPath
+uriToAbsPath uri =
+  case LSP.uriToFilePath uri of
+    Nothing -> throwM $ GleanLspException $ "URI is not a file: " <> LSP.getUri uri
+    Just path -> Path.filePathToAbsThrow path
+
+absPathToUri :: AbsPath -> LSP.Uri
+absPathToUri = LSP.filePathToUri . Path.toFilePath
+
+locationToLocationLink :: LSP.Location -> LSP.LocationLink
+locationToLocationLink LSP.Location {_uri, _range} =
+  LSP.LocationLink
+    { _originSelectionRange = Nothing
+    , _targetUri = _uri
+    , _targetRange = _range
+    , _targetSelectionRange = _range
+    }
+
+logTimed :: MonadIO m => Text -> m a -> m a
+logTimed msg io = do
+  (t, b, a) <- timeIt io
+  liftIO $ logInfo $ msg <> ": " <>
+    Text.pack (showTime t) <> ", " <> Text.pack (showAllocs b)
+  return a
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,70 @@
+BSD License
+
+For Glean software
+
+Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+ * Neither the name Facebook nor the names of its contributors may be used to
+   endorse or promote products derived from this software without specific
+   prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Portions of this software were taken from
+https://github.com/josephsumabat/static-ls, which is provided with the
+following license:
+
+Copyright (c) 2023 Joseph Sumabat
+
+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
+
+The following license is also included for code taken from ghcide:
+
+Copyright 2019 Digital Asset (Switzerland) GmbH and/or its affiliates
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.OFTWARE 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,179 @@
+# Generic Glean LSP Server
+
+`glean-lsp` is a generic LSP server that provides a minimal set of LSP
+features using Glean data. A Glean-based LSP is particularly suited
+for navigating very large codebases, where it wouldn't be practical to
+use a traditional language service that parses all the source files on
+the fly.
+
+The Glean LSP server works for any language supported by Glean and
+Glass, and currently provides the following LSP features:
+
+* Go to Definition
+* Go to References
+* Type on Hover
+* Outline
+* Symbol Search
+
+More features may be added in the future.
+
+The LSP server is purely static, in that it will not re-index the
+source files if they are edited. To update the Glean data, currently
+you have to manually re-run the indexer yourself. This can be done
+without restarting VS Code or the LSP server, though.
+
+## Building & installing
+
+`cabal install glean-lsp`
+
+## Using it with VS Code
+
+First index your source code into a Glean DB. This will be something like
+
+```
+cd SRC
+glean index LANGUAGE --db-root GLEANDB --db NAME/HASH .
+```
+
+where
+
+* `SRC` is the directory containing your source code
+* `LANGUAGE` is the language you want to index, see the [Glean manual](https://glean.software/docs/indexer/intro/) for available languages
+* `GLEANDB` is where you want to store your DBs
+* `NAME' is the name for the DB, and `HASH` can just be 0 or you could
+  use the git hash of the repository if you want to index multiple
+  versions of the code.
+
+See the [Glean manual](https://glean.software/docs/indexer/intro/) for any language-specific options that you might need.
+
+To use this LSP server with VS Code you need a generic LSP client such
+as [Generic LSP Client (v2)](https://github.com/zsol/vscode-glspc). Using that client,
+add the following to `.vscode/settings.json`
+
+
+```
+[
+    "glean-lsp": {
+        "repo": "NAME"
+    },
+    "glspc.server.command": "GLEAN-LSP"
+    "glspc.server.commandArguments": ["--db-root", "GLEANDB"],
+    "glspc.server.languageId": [
+      "LANGUAGE-ID"
+    ]
+}
+```
+
+where
+
+* `GLEAN-LSP` is the path to the `glean-lsp` binary, which might be somethihng like `$HOME/.cabal/bin/glean-lsp` if you installed it with `cabal install glean-lsp`.
+* `GLEANDB` is the path to your Glean DBs (same as when running the indexer above).
+* `NAME` is the name of the Glean DB you want to use. Don't add
+  `HASH` here, `glean-lsp` will always use the most recent version of
+  the DB.
+* `LANGUAGE-ID` is the language you want to enable this extension
+  for. Make sure you also disable any other extensions you have for
+  the same language, you can do that locally for this workspace in VS Code.
+
+Now if you open this workspace in VS Code and open one of the source
+files that was indexed, you should have go-to-definition and the other
+features available.
+
+## Example: browse the React codebase with Glean
+
+Clone a copy of React:
+
+```
+$ git clone --depth 1 https://github.com/facebook/react.git
+$ cd react
+```
+
+Start a Glean server:
+
+```
+$ glean-server --db-root .glean --port 1234 &
+```
+
+If you don't have `Flow` installed:
+
+```
+$ sudo npm install -g flow-bin
+```
+
+Index the code into a DB called `react/0`:
+
+```
+$ grep -v '^%' scripts/flow/config/flowconfig >.flowconfig
+$ glean index flow . --write-root "" --service localhost:1234 --db react/0
+```
+
+Put the following into `/tmp/react/.vscode/settings`:
+
+```
+{
+    "glean-lsp": {
+        "repo": "react"
+    },
+    "glspc.server.command": "glean-lsp",
+    "glspc.server.commandArguments": ["--service", "localhost:1234"],
+    "glspc.server.languageId": [
+      "typescript", "javascript"
+    ]
+}
+```
+
+Start VS Code, and open the folder `react` that you cloned. You
+probably want to disable the built-in TypeScript extensions to avoid
+conflicts: `Ctrl-Shift-X` and filter on built-in extensions (Show
+Built-in Extensions in the `...` dropdown), then disable the
+extensions for TypeScript for this workspace only.
+
+Now open a `.js` file, and you should have navigation features
+provided by `glean-lsp`: try right-click and "Go to Definition" or "Go
+to References". Check that the Outline view is populated. Try symbol
+search with `Ctrl-T`.
+
+## Troubleshooting
+
+In the "Output" pane, select the output for the LSP client in the
+dropdown on the right to see the debugging output.
+
+A common problem is the file paths not matching up, which will result
+in the LSP server starting up and appearing to be working, but
+go-to-definition just doesn't work. Make sure that in the Glean DB the
+file paths are all relative to the root of the workspace. To check
+this you can open the DB in the shell and query for `src.File _`. For
+example when I look at my DB `stackage/1` which contains all the
+Haskell packages in Stackage:
+
+```
+$ glean shell --db-root ~/gleandb --db stackage/1
+Glean Shell, built on 2025-07-14 13:39:35.711312749 UTC, from rev <unknown>
+Using local DBs from rocksdb:/home/simon/gleandb
+type :help for help.
+stackage> :limit 5
+stackage> src.File _
+{ "id": 66477, "key": "AC-Angle-1.0/Data/Angle.hs" }
+{ "id": 19929124, "key": "ANum-0.2.0.2/src/Data/ANum.hs" }
+{ "id": 7772678, "key": "Agda-2.6.4/src/full/Agda/Auto/Auto.hs" }
+{ "id": 15530734, "key": "Agda-2.6.4/src/full/Agda/Auto/CaseSplit.hs" }
+{ "id": 11442673, "key": "Agda-2.6.4/src/full/Agda/Auto/Convert.hs" }
+
+5 results, 5 facts, 151.82ms, 208003368 bytes, 584 compiled bytes
+results truncated (current limit 5, use :limit <n> to change it)
+Use :more to see more results
+stackage> 
+```
+
+Check that the paths look reasonable. If not, you may need to use
+language-specific options to get the indexer to generate the right
+paths.
+
+If the paths look OK, things should work to the extent that support
+for the language is implemented in Glean's `codemarkup` layer and the
+Glass server. Some languages might be missing certain features, but
+the basics should work for most languages. If you're still having
+trouble try raising an issue on the [Glean issue
+tracker](https://github.com/facebookincubator/Glean/issues) or asking
+on Discord, details of which are in Glean's
+[README](https://github.com/facebookincubator/Glean/blob/main/README.md).
diff --git a/glean-lsp.cabal b/glean-lsp.cabal
new file mode 100644
--- /dev/null
+++ b/glean-lsp.cabal
@@ -0,0 +1,93 @@
+cabal-version:       3.6
+name:                glean-lsp
+version:             0.1.0.0
+synopsis:            Generic Glean-based LSP Server
+homepage:            https://github.com/facebookincubator/Glean
+bug-reports:         https://github.com/facebookincubator/Glean/issues
+license:             BSD-3-Clause
+license-file:        LICENSE
+author:              Facebook, Inc.
+maintainer:          Glean-team@fb.com
+copyright:           (c) Facebook, All Rights Reserved
+build-type:          Simple
+extra-doc-files:     CHANGELOG.md, README.md
+
+common fb-haskell
+    default-language: Haskell2010
+    default-extensions:
+        BangPatterns
+        BinaryLiterals
+        DataKinds
+        DeriveDataTypeable
+        DeriveGeneric
+        EmptyCase
+        ExistentialQuantification
+        FlexibleContexts
+        FlexibleInstances
+        GADTs
+        GeneralizedNewtypeDeriving
+        LambdaCase
+        MultiParamTypeClasses
+        MultiWayIf
+        NamedFieldPuns
+        NoMonomorphismRestriction
+        OverloadedStrings
+        PatternSynonyms
+        RankNTypes
+        ScopedTypeVariables
+        StandaloneDeriving
+        TupleSections
+        TypeFamilies
+        TypeSynonymInstances
+        NondecreasingIndentation
+        TypeOperators
+        -- these are new relative to regular fb-haskell:
+        DisambiguateRecordFields
+        OverloadedRecordDot
+        NoFieldSelectors
+        DuplicateRecordFields
+        ImportQualifiedPost
+
+    ghc-options:
+        -Wall
+        -Wno-orphans
+        -Wno-name-shadowing
+        -Wno-unticked-promoted-constructors
+    if flag(opt)
+       ghc-options: -O2
+
+flag opt
+     default: False
+
+common exe
+  ghc-options: -threaded -rtsopts
+
+executable glean-lsp
+    import: fb-haskell, exe
+    main-is: Glean/LSP.hs
+    hs-source-dirs: .
+    ghc-options: -main-is Glean.LSP
+    other-modules:
+        Data.Path
+        Data.ConcurrentCache
+    build-depends:
+        aeson >= 2.0.3 && < 2.3,
+        base >=4.11.1 && <4.19,
+        containers >= 0.6 && < 0.7,
+        data-default >= 0.7.1 && < 0.9,
+        directory ^>=1.3.1.5,
+        exceptions ^>=0.10.0,
+        filepath >= 1.4.2 && < 1.5,
+        hashable >=1.2.7.0 && <1.5,
+        lsp >=2.7.0.0 && <2.8.0.0,
+        text >=1.2.3.0 && < 2.2,
+        transformers >= 0.5.6 && < 0.7,
+        unliftio ^>=0.2,
+        unliftio-core ^>=0.2,
+        unordered-containers >= 0.2.9.0 && < 0.3,
+        optparse-applicative >= 0.17 && < 0.19,
+        glean:glass-lib >= 0.2 && < 0.3,
+        glean:if-glass-hs,
+        glean:util,
+        fb-util,
+        thrift-lib
