diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for futhark-server
+
+## 0.1.0.0 -- 2021-06-17
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,17 @@
+ISC License
+
+Copyright (c) 2013-2021. DIKU, University of Copenhagen
+
+Permission to use, copy, modify, and/or distribute this software for
+any purpose with or without fee is hereby granted, provided that the
+above copyright notice and this permission notice appear in all
+copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
+WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
+AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
+PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/futhark-server.cabal b/futhark-server.cabal
new file mode 100644
--- /dev/null
+++ b/futhark-server.cabal
@@ -0,0 +1,37 @@
+cabal-version:      2.4
+name:               futhark-server
+version:            1.0.0.0
+synopsis: Client implementation of the Futhark server protocol.
+
+description: Provides an easy way to interact with a running Futhark
+             server-mode program from a Haskell program.  Provides
+             both direct support of the protocol, as well as
+             convenience functions for loading and extracting data.
+
+category:           Futhark
+author:             Troels Henriksen
+maintainer:         athas@sigkill.dk
+bug-reports:        https://github.com/diku-dk/futhark-server-haskell/issues
+license:            ISC
+license-file:       LICENSE
+extra-source-files: CHANGELOG.md
+
+source-repository head
+  type: git
+  location: https://github.com/diku-dk/futhark-server-haskell
+
+library
+    exposed-modules:  Futhark.Server
+                      Futhark.Server.Values
+    build-depends:    base >=4 && < 5
+                    , binary
+                    , bytestring
+                    , directory >=1.3.0.0
+                    , futhark-data
+                    , text >=1.2.2.2
+                    , temporary
+                    , process >=1.4.3.0
+                    , mtl >=2.2.1
+    hs-source-dirs:   src
+    ghc-options: -Wall -Wcompat -Wredundant-constraints -Wincomplete-record-updates -Wmissing-export-lists
+    default-language: Haskell2010
diff --git a/src/Futhark/Server.hs b/src/Futhark/Server.hs
new file mode 100644
--- /dev/null
+++ b/src/Futhark/Server.hs
@@ -0,0 +1,272 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Haskell code for interacting with a Futhark server program.  This
+-- module presents a low-level interface.  See
+-- <https://futhark.readthedocs.io/en/latest/server-protocol.html the
+-- documentation of the server protocol> for the meaning of the
+-- commands.  See also "Futhark.Server.Values" for higher-level
+-- functions for loading data into a server.
+--
+-- Error messages produced by the server will be returned as a
+-- 'CmdFailure'.  However, certain errors (such as if the server
+-- process terminates unexpectedly, or temporary files cannot be
+-- created) will result in an IO exception.
+module Futhark.Server
+  ( -- * Server creation
+    Server,
+    ServerCfg (..),
+    newServerCfg,
+    withServer,
+
+    -- * Commands
+    CmdFailure (..),
+    VarName,
+    TypeName,
+    EntryName,
+    cmdRestore,
+    cmdStore,
+    cmdCall,
+    cmdFree,
+    cmdRename,
+    cmdInputs,
+    cmdOutputs,
+    cmdClear,
+    cmdReport,
+
+    -- * Utility
+    cmdMaybe,
+    cmdEither,
+
+    -- * Raw
+    startServer,
+    stopServer,
+    sendCommand,
+  )
+where
+
+import Control.Exception
+import Control.Monad
+import Control.Monad.Except
+import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
+import System.Directory (removeFile)
+import System.Exit
+import System.IO hiding (stdin, stdout)
+import System.IO.Temp (getCanonicalTemporaryDirectory)
+import qualified System.Process as P
+
+-- | A handle to a running server.
+data Server = Server
+  { serverStdin :: Handle,
+    serverStdout :: Handle,
+    serverErrLog :: FilePath,
+    serverProc :: P.ProcessHandle,
+    serverDebug :: Bool
+  }
+
+-- | Configuration of the server.  Use 'newServerCfg' to conveniently
+-- create a sensible default configuration.
+data ServerCfg = ServerCfg
+  { -- | Path to the server executable.
+    cfgProg :: FilePath,
+    -- | Command line options to pass to the
+    -- server executable.
+    cfgProgOpts :: [String],
+    -- | If true, print a running log of server communication to stderr.
+    cfgDebug :: Bool
+  }
+  deriving (Eq, Ord, Show)
+
+-- | Create a server config with the given 'cfgProg' and 'cfgProgOpts'.
+newServerCfg :: FilePath -> [String] -> ServerCfg
+newServerCfg prog opts =
+  ServerCfg
+    { cfgProg = prog,
+      cfgProgOpts = opts,
+      cfgDebug = False
+    }
+
+-- | Start up a server.  Make sure that 'stopServer' is eventually
+-- called on the server.  If this does not happen, then temporary
+-- files may be left on the file system.  You almost certainly wish to
+-- use 'bracket' or similar to avoid this.
+startServer :: ServerCfg -> IO Server
+startServer (ServerCfg prog options debug) = do
+  tmpdir <- getCanonicalTemporaryDirectory
+  (err_log_f, err_log_h) <- openTempFile tmpdir "futhark-server-stderr.log"
+  (Just stdin, Just stdout, Nothing, phandle) <-
+    P.createProcess
+      ( (P.proc prog options)
+          { P.std_err = P.UseHandle err_log_h,
+            P.std_in = P.CreatePipe,
+            P.std_out = P.CreatePipe
+          }
+      )
+
+  code <- P.getProcessExitCode phandle
+  case code of
+    Just (ExitFailure e) ->
+      error $ "Cannot start " ++ prog ++ ": error " ++ show e
+    _ -> do
+      let server =
+            Server
+              { serverStdin = stdin,
+                serverStdout = stdout,
+                serverProc = phandle,
+                serverDebug = debug,
+                serverErrLog = err_log_f
+              }
+      void (responseLines server) `catch` onStartupError server
+      pure server
+  where
+    onStartupError :: Server -> IOError -> IO a
+    onStartupError s _ = do
+      code <- P.waitForProcess $ serverProc s
+      stderr_s <- readFile $ serverErrLog s
+      removeFile $ serverErrLog s
+      error $
+        "Command failed with " ++ show code ++ ":\n"
+          ++ unwords (prog : options)
+          ++ "\nStderr:\n"
+          ++ stderr_s
+
+-- | Shut down a server.  It may not be used again.
+stopServer :: Server -> IO ()
+stopServer s = do
+  hClose $ serverStdin s
+  void $ P.waitForProcess $ serverProc s
+  removeFile $ serverErrLog s
+
+-- | Start a server, execute an action, then shut down the server.
+-- The 'Server' may not be returned from the action.
+withServer :: ServerCfg -> (Server -> IO a) -> IO a
+withServer cfg = bracket (startServer cfg) stopServer
+
+-- Read lines of response until the next %%% OK (which is what
+-- indicates that the server is ready for new instructions).
+responseLines :: Server -> IO [Text]
+responseLines s = do
+  l <- T.hGetLine $ serverStdout s
+  when (serverDebug s) $
+    T.hPutStrLn stderr $ "<<< " <> l
+  case l of
+    "%%% OK" -> pure []
+    _ -> (l :) <$> responseLines s
+
+-- | The command failed, and this is why.  The first 'Text' is any
+-- output before the failure indincator, and the second Text is the
+-- output after the indicator.
+data CmdFailure = CmdFailure {failureLog :: [Text], failureMsg :: [Text]}
+  deriving (Eq, Ord, Show)
+
+-- Figure out whether the response is a failure, and if so, return the
+-- failure message.
+checkForFailure :: [Text] -> Either CmdFailure [Text]
+checkForFailure [] = Right []
+checkForFailure ("%%% FAILURE" : ls) = Left $ CmdFailure mempty ls
+checkForFailure (l : ls) =
+  case checkForFailure ls of
+    Left (CmdFailure xs ys) -> Left $ CmdFailure (l : xs) ys
+    Right ls' -> Right $ l : ls'
+
+-- Words with spaces in them must be quoted.
+quoteWord :: Text -> Text
+quoteWord t
+  | Just _ <- T.find (== ' ') t =
+    "\"" <> t <> "\""
+  | otherwise = t
+
+-- | Send an arbitrary command to the server.  This is only useful
+-- when the server protocol has been extended without this module
+-- having been similarly extended.  Be careful not to send invalid
+-- commands.
+sendCommand :: Server -> [Text] -> IO (Either CmdFailure [Text])
+sendCommand s command = do
+  let command' = T.unwords $ map quoteWord command
+
+  when (serverDebug s) $
+    T.hPutStrLn stderr $ ">>> " <> command'
+
+  T.hPutStrLn (serverStdin s) command'
+  hFlush $ serverStdin s
+  checkForFailure <$> responseLines s `catch` onError
+  where
+    onError :: IOError -> IO a
+    onError e = do
+      code <- P.getProcessExitCode $ serverProc s
+      let code_msg =
+            case code of
+              Just (ExitFailure x) ->
+                "\nServer process exited unexpectedly with exit code: " ++ show x
+              _ -> mempty
+      stderr_s <- readFile $ serverErrLog s
+      error $
+        "After sending command " ++ show command ++ " to server process:"
+          ++ show e
+          ++ code_msg
+          ++ "\nServer stderr:\n"
+          ++ stderr_s
+
+-- | The name of a server-side variable.
+type VarName = Text
+
+-- | The name of a server-side type.
+type TypeName = Text
+
+-- | The name of an entry point.
+type EntryName = Text
+
+helpCmd :: Server -> [Text] -> IO (Maybe CmdFailure)
+helpCmd s cmd =
+  either Just (const Nothing) <$> sendCommand s cmd
+
+-- | @restore filename var0 type0 var1 type1...@.
+cmdRestore :: Server -> FilePath -> [(VarName, TypeName)] -> IO (Maybe CmdFailure)
+cmdRestore s fname vars = helpCmd s $ "restore" : T.pack fname : concatMap f vars
+  where
+    f (v, t) = [v, t]
+
+-- | @store filename vars...@.
+cmdStore :: Server -> FilePath -> [VarName] -> IO (Maybe CmdFailure)
+cmdStore s fname vars = helpCmd s $ "store" : T.pack fname : vars
+
+-- | @call entrypoint outs... ins...@.
+cmdCall :: Server -> EntryName -> [VarName] -> [VarName] -> IO (Either CmdFailure [T.Text])
+cmdCall s entry outs ins =
+  sendCommand s $ "call" : entry : outs ++ ins
+
+-- | @free vars...@.
+cmdFree :: Server -> [VarName] -> IO (Maybe CmdFailure)
+cmdFree s vs = helpCmd s $ "free" : vs
+
+-- | @rename oldname newname@.
+cmdRename :: Server -> VarName -> VarName -> IO (Maybe CmdFailure)
+cmdRename s oldname newname = helpCmd s ["rename", oldname, newname]
+
+-- | @inputs entryname@
+cmdInputs :: Server -> EntryName -> IO (Either CmdFailure [TypeName])
+cmdInputs s entry =
+  sendCommand s ["inputs", entry]
+
+-- | @outputs entryname@
+cmdOutputs :: Server -> EntryName -> IO (Either CmdFailure [TypeName])
+cmdOutputs s entry =
+  sendCommand s ["outputs", entry]
+
+-- | @clear@
+cmdClear :: Server -> IO (Maybe CmdFailure)
+cmdClear s = helpCmd s ["clear"]
+
+-- | @report@
+cmdReport :: Server -> IO (Either CmdFailure [T.Text])
+cmdReport s = sendCommand s ["report"]
+
+-- | Turn a 'Maybe'-producing command into a monadic action.
+cmdMaybe :: (MonadError T.Text m, MonadIO m) => IO (Maybe CmdFailure) -> m ()
+cmdMaybe = maybe (pure ()) (throwError . T.unlines . failureMsg) <=< liftIO
+
+-- | Turn an 'Either'-producing command into a monadic action.
+cmdEither :: (MonadError T.Text m, MonadIO m) => IO (Either CmdFailure a) -> m a
+cmdEither = either (throwError . T.unlines . failureMsg) pure <=< liftIO
diff --git a/src/Futhark/Server/Values.hs b/src/Futhark/Server/Values.hs
new file mode 100644
--- /dev/null
+++ b/src/Futhark/Server/Values.hs
@@ -0,0 +1,43 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Convenience functions builds on top of "Futhark.Data" and
+-- "Futhark.Server" for passing non-opaque values in and out of a
+-- server instance.
+module Futhark.Server.Values (getValue, putValue) where
+
+import qualified Data.Binary as Bin
+import qualified Data.ByteString.Lazy as LBS
+import qualified Data.Text as T
+import Futhark.Data (Value, valueType, valueTypeTextNoDims)
+import Futhark.Server
+import System.IO (hClose)
+import System.IO.Temp (withSystemTempFile)
+
+-- | Retrieve a non-opaque value from the server.
+getValue :: Server -> VarName -> IO (Either T.Text Value)
+getValue server vname =
+  withSystemTempFile "futhark-server-get" $ \tmpf tmpf_h -> do
+    hClose tmpf_h
+    store_res <- cmdStore server tmpf [vname]
+    case store_res of
+      Just (CmdFailure _ err) ->
+        pure $ Left $ T.unlines err
+      Nothing -> do
+        bytes <- LBS.readFile tmpf
+        case Bin.decodeOrFail bytes of
+          Left (_, _, e) ->
+            pure $ Left $ "Cannot load value from generated byte stream:\n" <> T.pack e
+          Right (_, _, val) ->
+            pure $ Right val
+
+-- | Store a non-opaque value in the server.  A variable with the
+-- given name must not already exist (use 'cmdFree' to free it first
+-- if necessary).
+putValue :: Server -> VarName -> Value -> IO (Maybe CmdFailure)
+putValue server v val =
+  withSystemTempFile "futhark-server-put" $ \tmpf tmpf_h -> do
+    LBS.hPutStr tmpf_h $ Bin.encode val
+    hClose tmpf_h
+    cmdRestore server tmpf [(v, t)]
+  where
+    t = valueTypeTextNoDims $ valueType val
