diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h
--- a/clang-tools-extra/clangd/ClangdLSPServer.h
+++ b/clang-tools-extra/clangd/ClangdLSPServer.h
@@ -14,6 +14,7 @@
 #include "Features.inc"
 #include "FindSymbols.h"
 #include "GlobalCompilationDatabase.h"
+#include "LSPBinder.h"
 #include "Protocol.h"
 #include "Transport.h"
 #include "support/Context.h"
@@ -90,8 +91,8 @@
   // Calls have signature void(const Params&, Callback<Response>).
   void onInitialize(const InitializeParams &, Callback<llvm::json::Value>);
   void onInitialized(const InitializedParams &);
-  void onShutdown(Callback<std::nullptr_t>);
-  void onSync(Callback<std::nullptr_t>);
+  void onShutdown(const NoParams &, Callback<std::nullptr_t>);
+  void onSync(const NoParams &, Callback<std::nullptr_t>);
   void onDocumentDidOpen(const DidOpenTextDocumentParams &);
   void onDocumentDidChange(const DidChangeTextDocumentParams &);
   void onDocumentDidClose(const DidCloseTextDocumentParams &);
@@ -157,11 +158,7 @@
                              Callback<SemanticTokensOrDelta>);
   /// This is a clangd extension. Provides a json tree representing memory usage
   /// hierarchy.
-  void onMemoryUsage(Callback<MemoryTree>);
-
-  llvm::StringMap<llvm::unique_function<void(const llvm::json::Value &,
-                                             Callback<llvm::json::Value>)>>
-      CommandHandlers;
+  void onMemoryUsage(const NoParams &, Callback<MemoryTree>);
   void onCommand(const ExecuteCommandParams &, Callback<llvm::json::Value>);
 
   /// Implement commands.
@@ -229,29 +226,6 @@
   std::unique_ptr<MessageHandler> MsgHandler;
   std::mutex TranspWriter;
 
-  template <typename T>
-  static Expected<T> parse(const llvm::json::Value &Raw,
-                           llvm::StringRef PayloadName,
-                           llvm::StringRef PayloadKind) {
-    T Result;
-    llvm::json::Path::Root Root;
-    if (!fromJSON(Raw, Result, Root)) {
-      elog("Failed to decode {0} {1}: {2}", PayloadName, PayloadKind,
-           Root.getError());
-      // Dump the relevant parts of the broken message.
-      std::string Context;
-      llvm::raw_string_ostream OS(Context);
-      Root.printErrorContext(Raw, OS);
-      vlog("{0}", OS.str());
-      // Report the error (e.g. to the client).
-      return llvm::make_error<LSPError>(
-          llvm::formatv("failed to decode {0} {1}: {2}", PayloadName,
-                        PayloadKind, fmt_consume(Root.getError())),
-          ErrorCode::InvalidParams);
-    }
-    return std::move(Result);
-  }
-
   template <typename Response>
   void call(StringRef Method, llvm::json::Value Params, Callback<Response> CB) {
     // Wrap the callback with LSP conversion and error-handling.
@@ -261,7 +235,7 @@
             llvm::Expected<llvm::json::Value> RawResponse) mutable {
           if (!RawResponse)
             return CB(RawResponse.takeError());
-          CB(parse<Response>(*RawResponse, Method, "response"));
+          CB(LSPBinder::parse<Response>(*RawResponse, Method, "response"));
         };
     callRaw(Method, std::move(Params), std::move(HandleReply));
   }
@@ -274,19 +248,8 @@
     Params.value = std::move(Value);
     notify("$/progress", Params);
   }
-  template <typename Param, typename Result>
-  void bindCommand(llvm::StringLiteral Method,
-                   void (ClangdLSPServer::*Handler)(const Param &,
-                                                    Callback<Result>)) {
-    CommandHandlers[Method] = [Method, Handler,
-                               this](llvm::json::Value RawParams,
-                                     Callback<Result> Reply) {
-      auto P = parse<Param>(RawParams, Method, "command");
-      if (!P)
-        return Reply(P.takeError());
-      (this->*Handler)(*P, std::move(Reply));
-    };
-  }
+
+  LSPBinder::RawHandlers Handlers;
 
   const ThreadsafeFS &TFS;
   /// Options used for diagnostics.
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -13,6 +13,7 @@
 #include "DraftStore.h"
 #include "DumpAST.h"
 #include "GlobalCompilationDatabase.h"
+#include "LSPBinder.h"
 #include "Protocol.h"
 #include "SemanticHighlighting.h"
 #include "SourceCode.h"
@@ -158,6 +159,8 @@
   MessageHandler(ClangdLSPServer &Server) : Server(Server) {}
 
   bool onNotify(llvm::StringRef Method, llvm::json::Value Params) override {
+    trace::Span Tracer(Method, LSPLatency);
+    SPAN_ATTACH(Tracer, "Params", Params);
     WithContext HandlerContext(handlerContext());
     log("<-- {0}", Method);
     if (Method == "exit")
@@ -166,12 +169,15 @@
       elog("Notification {0} before initialization", Method);
     } else if (Method == "$/cancelRequest") {
       onCancel(std::move(Params));
-    } else if (auto Handler = Notifications.lookup(Method)) {
-      Handler(std::move(Params));
-      Server.maybeExportMemoryProfile();
-      Server.maybeCleanupMemory();
     } else {
-      log("unhandled notification {0}", Method);
+      auto Handler = Server.Handlers.NotificationHandlers.find(Method);
+      if (Handler != Server.Handlers.NotificationHandlers.end()) {
+        Handler->second(std::move(Params));
+        Server.maybeExportMemoryProfile();
+        Server.maybeCleanupMemory();
+      } else {
+        log("unhandled notification {0}", Method);
+      }
     }
     return true;
   }
@@ -189,11 +195,15 @@
       elog("Call {0} before initialization.", Method);
       Reply(llvm::make_error<LSPError>("server not initialized",
                                        ErrorCode::ServerNotInitialized));
-    } else if (auto Handler = Calls.lookup(Method))
-      Handler(std::move(Params), std::move(Reply));
-    else
-      Reply(llvm::make_error<LSPError>("method not found",
-                                       ErrorCode::MethodNotFound));
+    } else {
+      auto Handler = Server.Handlers.MethodHandlers.find(Method);
+      if (Handler != Server.Handlers.MethodHandlers.end()) {
+        Handler->second(std::move(Params), std::move(Reply));
+      } else {
+        Reply(llvm::make_error<LSPError>("method not found",
+                                         ErrorCode::MethodNotFound));
+      }
+    }
     return true;
   }
 
@@ -236,28 +246,6 @@
     return true;
   }
 
-  // Bind an LSP method name to a call.
-  template <typename Param, typename Result>
-  void bind(const char *Method,
-            void (ClangdLSPServer::*Handler)(const Param &, Callback<Result>)) {
-    Calls[Method] = [Method, Handler, this](llvm::json::Value RawParams,
-                                            ReplyOnce Reply) {
-      auto P = parse<Param>(RawParams, Method, "request");
-      if (!P)
-        return Reply(P.takeError());
-      (Server.*Handler)(*P, std::move(Reply));
-    };
-  }
-
-  template <typename Result>
-  void bind(const char *Method,
-            void (ClangdLSPServer::*Handler)(Callback<Result>)) {
-    Calls[Method] = [Handler, this](llvm::json::Value RawParams,
-                                    ReplyOnce Reply) {
-      (Server.*Handler)(std::move(Reply));
-    };
-  }
-
   // Bind a reply callback to a request. The callback will be invoked when
   // clangd receives the reply from the LSP client.
   // Return a call id of the request.
@@ -286,27 +274,6 @@
     return ID;
   }
 
-  // Bind an LSP method name to a notification.
-  template <typename Param>
-  void bind(const char *Method,
-            void (ClangdLSPServer::*Handler)(const Param &)) {
-    Notifications[Method] = [Method, Handler,
-                             this](llvm::json::Value RawParams) {
-      llvm::Expected<Param> P = parse<Param>(RawParams, Method, "request");
-      if (!P)
-        return llvm::consumeError(P.takeError());
-      trace::Span Tracer(Method, LSPLatency);
-      SPAN_ATTACH(Tracer, "Params", RawParams);
-      (Server.*Handler)(*P);
-    };
-  }
-
-  void bind(const char *Method, void (ClangdLSPServer::*Handler)()) {
-    Notifications[Method] = [Handler, this](llvm::json::Value RawParams) {
-      (Server.*Handler)();
-    };
-  }
-
 private:
   // Function object to reply to an LSP call.
   // Each instance must be called exactly once, otherwise:
@@ -378,9 +345,6 @@
     }
   };
 
-  llvm::StringMap<std::function<void(llvm::json::Value)>> Notifications;
-  llvm::StringMap<std::function<void(llvm::json::Value, ReplyOnce)>> Calls;
-
   // Method calls may be cancelled by ID, so keep track of their state.
   // This needs a mutex: handlers may finish on a different thread, and that's
   // when we clean up entries in the map.
@@ -449,14 +413,6 @@
 };
 constexpr int ClangdLSPServer::MessageHandler::MaxReplayCallbacks;
 
-template <>
-void ClangdLSPServer::MessageHandler::bind<NoParams>(
-    const char *Method, void (ClangdLSPServer::*Handler)(const NoParams &)) {
-  Notifications[Method] = [Handler, this](llvm::json::Value RawParams) {
-    (Server.*Handler)(NoParams{});
-  };
-}
-
 // call(), notify(), and reply() wrap the Transport, adding logging and locking.
 void ClangdLSPServer::callRaw(StringRef Method, llvm::json::Value Params,
                               Callback<llvm::json::Value> CB) {
@@ -568,104 +524,113 @@
     BackgroundIndexProgressState = BackgroundIndexProgress::Empty;
   BackgroundIndexSkipCreate = Params.capabilities.ImplicitProgressCreation;
 
+  llvm::json::Object ServerCaps{
+      {"textDocumentSync",
+       llvm::json::Object{
+           {"openClose", true},
+           {"change", (int)TextDocumentSyncKind::Incremental},
+           {"save", true},
+       }},
+      {"documentFormattingProvider", true},
+      {"documentRangeFormattingProvider", true},
+      {"documentOnTypeFormattingProvider",
+       llvm::json::Object{
+           {"firstTriggerCharacter", "\n"},
+           {"moreTriggerCharacter", {}},
+       }},
+      {"completionProvider",
+       llvm::json::Object{
+           {"allCommitCharacters",
+            {" ", "\t", "(", ")", "[", "]", "{",  "}", "<",
+             ">", ":",  ";", ",", "+", "-", "/",  "*", "%",
+             "^", "&",  "#", "?", ".", "=", "\"", "'", "|"}},
+           {"resolveProvider", false},
+           // We do extra checks, e.g. that > is part of ->.
+           {"triggerCharacters", {".", "<", ">", ":", "\"", "/"}},
+       }},
+      {"semanticTokensProvider",
+       llvm::json::Object{
+           {"full", llvm::json::Object{{"delta", true}}},
+           {"range", false},
+           {"legend",
+            llvm::json::Object{{"tokenTypes", semanticTokenTypes()},
+                               {"tokenModifiers", semanticTokenModifiers()}}},
+       }},
+      {"signatureHelpProvider",
+       llvm::json::Object{
+           {"triggerCharacters", {"(", ","}},
+       }},
+      {"declarationProvider", true},
+      {"definitionProvider", true},
+      {"implementationProvider", true},
+      {"documentHighlightProvider", true},
+      {"documentLinkProvider",
+       llvm::json::Object{
+           {"resolveProvider", false},
+       }},
+      {"hoverProvider", true},
+      {"selectionRangeProvider", true},
+      {"documentSymbolProvider", true},
+      {"workspaceSymbolProvider", true},
+      {"referencesProvider", true},
+      {"astProvider", true}, // clangd extension
+      {"typeHierarchyProvider", true},
+      {"memoryUsageProvider", true}, // clangd extension
+      {"compilationDatabase",        // clangd extension
+       llvm::json::Object{{"automaticReload", true}}},
+      {"callHierarchyProvider", true},
+  };
+
+  {
+    LSPBinder Binder(Handlers);
+    if (Opts.Modules)
+      for (auto &Mod : *Opts.Modules)
+        Mod.initializeLSP(Binder, Params.capabilities, ServerCaps);
+  }
+
   // Per LSP, renameProvider can be either boolean or RenameOptions.
   // RenameOptions will be specified if the client states it supports prepare.
-  llvm::json::Value RenameProvider =
-      llvm::json::Object{{"prepareProvider", true}};
-  if (!Params.capabilities.RenamePrepareSupport) // Only boolean allowed per LSP
-    RenameProvider = true;
+  ServerCaps["renameProvider"] =
+      Params.capabilities.RenamePrepareSupport
+          ? llvm::json::Object{{"prepareProvider", true}}
+          : llvm::json::Value(true);
 
-  // Per LSP, codeActionProvide can be either boolean or CodeActionOptions.
+  // Per LSP, codeActionProvider can be either boolean or CodeActionOptions.
   // CodeActionOptions is only valid if the client supports action literal
   // via textDocument.codeAction.codeActionLiteralSupport.
   llvm::json::Value CodeActionProvider = true;
-  if (Params.capabilities.CodeActionStructure)
-    CodeActionProvider = llvm::json::Object{
-        {"codeActionKinds",
-         {CodeAction::QUICKFIX_KIND, CodeAction::REFACTOR_KIND,
-          CodeAction::INFO_KIND}}};
+  ServerCaps["codeActionProvider"] =
+      Params.capabilities.CodeActionStructure
+          ? llvm::json::Object{{"codeActionKinds",
+                                {CodeAction::QUICKFIX_KIND,
+                                 CodeAction::REFACTOR_KIND,
+                                 CodeAction::INFO_KIND}}}
+          : llvm::json::Value(true);
+
+  if (Opts.FoldingRanges)
+    ServerCaps["foldingRangeProvider"] = true;
 
   std::vector<llvm::StringRef> Commands;
-  for (llvm::StringRef Command : CommandHandlers.keys())
+  for (llvm::StringRef Command : Handlers.CommandHandlers.keys())
     Commands.push_back(Command);
   llvm::sort(Commands);
+  ServerCaps["executeCommandProvider"] =
+      llvm::json::Object{{"commands", Commands}};
 
   llvm::json::Object Result{
       {{"serverInfo",
         llvm::json::Object{{"name", "clangd"},
                            {"version", getClangToolFullVersion("clangd")}}},
-       {"capabilities",
-        llvm::json::Object{
-            {"textDocumentSync",
-             llvm::json::Object{
-                 {"openClose", true},
-                 {"change", (int)TextDocumentSyncKind::Incremental},
-                 {"save", true},
-             }},
-            {"documentFormattingProvider", true},
-            {"documentRangeFormattingProvider", true},
-            {"documentOnTypeFormattingProvider",
-             llvm::json::Object{
-                 {"firstTriggerCharacter", "\n"},
-                 {"moreTriggerCharacter", {}},
-             }},
-            {"codeActionProvider", std::move(CodeActionProvider)},
-            {"completionProvider",
-             llvm::json::Object{
-                 {"allCommitCharacters",
-                  {" ", "\t", "(", ")", "[", "]", "{",  "}", "<",
-                   ">", ":",  ";", ",", "+", "-", "/",  "*", "%",
-                   "^", "&",  "#", "?", ".", "=", "\"", "'", "|"}},
-                 {"resolveProvider", false},
-                 // We do extra checks, e.g. that > is part of ->.
-                 {"triggerCharacters", {".", "<", ">", ":", "\"", "/"}},
-             }},
-            {"semanticTokensProvider",
-             llvm::json::Object{
-                 {"full", llvm::json::Object{{"delta", true}}},
-                 {"range", false},
-                 {"legend",
-                  llvm::json::Object{
-                      {"tokenTypes", semanticTokenTypes()},
-                      {"tokenModifiers", semanticTokenModifiers()}}},
-             }},
-            {"signatureHelpProvider",
-             llvm::json::Object{
-                 {"triggerCharacters", {"(", ","}},
-             }},
-            {"declarationProvider", true},
-            {"definitionProvider", true},
-            {"implementationProvider", true},
-            {"documentHighlightProvider", true},
-            {"documentLinkProvider",
-             llvm::json::Object{
-                 {"resolveProvider", false},
-             }},
-            {"hoverProvider", true},
-            {"renameProvider", std::move(RenameProvider)},
-            {"selectionRangeProvider", true},
-            {"documentSymbolProvider", true},
-            {"workspaceSymbolProvider", true},
-            {"referencesProvider", true},
-            {"astProvider", true}, // clangd extension
-            {"executeCommandProvider",
-             llvm::json::Object{{"commands", Commands}}},
-            {"typeHierarchyProvider", true},
-            {"memoryUsageProvider", true}, // clangd extension
-            {"compilationDatabase",        // clangd extension
-             llvm::json::Object{{"automaticReload", true}}},
-            {"callHierarchyProvider", true},
-        }}}};
+       {"capabilities", std::move(ServerCaps)}}};
   if (Opts.Encoding)
     Result["offsetEncoding"] = *Opts.Encoding;
-  if (Opts.FoldingRanges)
-    Result.getObject("capabilities")->insert({"foldingRangeProvider", true});
   Reply(std::move(Result));
 }
 
 void ClangdLSPServer::onInitialized(const InitializedParams &Params) {}
 
-void ClangdLSPServer::onShutdown(Callback<std::nullptr_t> Reply) {
+void ClangdLSPServer::onShutdown(const NoParams &,
+                                 Callback<std::nullptr_t> Reply) {
   // Do essentially nothing, just say we're ready to exit.
   ShutdownRequestReceived = true;
   Reply(nullptr);
@@ -673,7 +638,7 @@
 
 // sync is a clangd extension: it blocks until all background work completes.
 // It blocks the calling thread, so no messages are processed until it returns!
-void ClangdLSPServer::onSync(Callback<std::nullptr_t> Reply) {
+void ClangdLSPServer::onSync(const NoParams &, Callback<std::nullptr_t> Reply) {
   if (Server->blockUntilIdleForTest(/*TimeoutSeconds=*/60))
     Reply(nullptr);
   else
@@ -734,8 +699,8 @@
 
 void ClangdLSPServer::onCommand(const ExecuteCommandParams &Params,
                                 Callback<llvm::json::Value> Reply) {
-  auto It = CommandHandlers.find(Params.command);
-  if (It == CommandHandlers.end()) {
+  auto It = Handlers.CommandHandlers.find(Params.command);
+  if (It == Handlers.CommandHandlers.end()) {
     return Reply(llvm::make_error<LSPError>(
         llvm::formatv("Unsupported command \"{0}\".", Params.command).str(),
         ErrorCode::InvalidParams));
@@ -1462,7 +1427,8 @@
       });
 }
 
-void ClangdLSPServer::onMemoryUsage(Callback<MemoryTree> Reply) {
+void ClangdLSPServer::onMemoryUsage(const NoParams &,
+                                    Callback<MemoryTree> Reply) {
   llvm::BumpPtrAllocator DetailAlloc;
   MemoryTree MT(&DetailAlloc);
   profile(MT);
@@ -1493,50 +1459,51 @@
   }
 
   // clang-format off
-  MsgHandler->bind("initialize", &ClangdLSPServer::onInitialize);
-  MsgHandler->bind("initialized", &ClangdLSPServer::onInitialized);
-  MsgHandler->bind("shutdown", &ClangdLSPServer::onShutdown);
-  MsgHandler->bind("sync", &ClangdLSPServer::onSync);
-  MsgHandler->bind("textDocument/rangeFormatting", &ClangdLSPServer::onDocumentRangeFormatting);
-  MsgHandler->bind("textDocument/onTypeFormatting", &ClangdLSPServer::onDocumentOnTypeFormatting);
-  MsgHandler->bind("textDocument/formatting", &ClangdLSPServer::onDocumentFormatting);
-  MsgHandler->bind("textDocument/codeAction", &ClangdLSPServer::onCodeAction);
-  MsgHandler->bind("textDocument/completion", &ClangdLSPServer::onCompletion);
-  MsgHandler->bind("textDocument/signatureHelp", &ClangdLSPServer::onSignatureHelp);
-  MsgHandler->bind("textDocument/definition", &ClangdLSPServer::onGoToDefinition);
-  MsgHandler->bind("textDocument/declaration", &ClangdLSPServer::onGoToDeclaration);
-  MsgHandler->bind("textDocument/implementation", &ClangdLSPServer::onGoToImplementation);
-  MsgHandler->bind("textDocument/references", &ClangdLSPServer::onReference);
-  MsgHandler->bind("textDocument/switchSourceHeader", &ClangdLSPServer::onSwitchSourceHeader);
-  MsgHandler->bind("textDocument/prepareRename", &ClangdLSPServer::onPrepareRename);
-  MsgHandler->bind("textDocument/rename", &ClangdLSPServer::onRename);
-  MsgHandler->bind("textDocument/hover", &ClangdLSPServer::onHover);
-  MsgHandler->bind("textDocument/documentSymbol", &ClangdLSPServer::onDocumentSymbol);
-  MsgHandler->bind("workspace/executeCommand", &ClangdLSPServer::onCommand);
-  MsgHandler->bind("textDocument/documentHighlight", &ClangdLSPServer::onDocumentHighlight);
-  MsgHandler->bind("workspace/symbol", &ClangdLSPServer::onWorkspaceSymbol);
-  MsgHandler->bind("textDocument/ast", &ClangdLSPServer::onAST);
-  MsgHandler->bind("textDocument/didOpen", &ClangdLSPServer::onDocumentDidOpen);
-  MsgHandler->bind("textDocument/didClose", &ClangdLSPServer::onDocumentDidClose);
-  MsgHandler->bind("textDocument/didChange", &ClangdLSPServer::onDocumentDidChange);
-  MsgHandler->bind("textDocument/didSave", &ClangdLSPServer::onDocumentDidSave);
-  MsgHandler->bind("workspace/didChangeWatchedFiles", &ClangdLSPServer::onFileEvent);
-  MsgHandler->bind("workspace/didChangeConfiguration", &ClangdLSPServer::onChangeConfiguration);
-  MsgHandler->bind("textDocument/symbolInfo", &ClangdLSPServer::onSymbolInfo);
-  MsgHandler->bind("textDocument/typeHierarchy", &ClangdLSPServer::onTypeHierarchy);
-  MsgHandler->bind("typeHierarchy/resolve", &ClangdLSPServer::onResolveTypeHierarchy);
-  MsgHandler->bind("textDocument/prepareCallHierarchy", &ClangdLSPServer::onPrepareCallHierarchy);
-  MsgHandler->bind("callHierarchy/incomingCalls", &ClangdLSPServer::onCallHierarchyIncomingCalls);
-  MsgHandler->bind("callHierarchy/outgoingCalls", &ClangdLSPServer::onCallHierarchyOutgoingCalls);
-  MsgHandler->bind("textDocument/selectionRange", &ClangdLSPServer::onSelectionRange);
-  MsgHandler->bind("textDocument/documentLink", &ClangdLSPServer::onDocumentLink);
-  MsgHandler->bind("textDocument/semanticTokens/full", &ClangdLSPServer::onSemanticTokens);
-  MsgHandler->bind("textDocument/semanticTokens/full/delta", &ClangdLSPServer::onSemanticTokensDelta);
-  MsgHandler->bind("$/memoryUsage", &ClangdLSPServer::onMemoryUsage);
+  LSPBinder Bind(this->Handlers);
+  Bind.method("initialize", this, &ClangdLSPServer::onInitialize);
+  Bind.notification("initialized", this, &ClangdLSPServer::onInitialized);
+  Bind.method("shutdown", this, &ClangdLSPServer::onShutdown);
+  Bind.method("sync", this, &ClangdLSPServer::onSync);
+  Bind.method("textDocument/rangeFormatting", this, &ClangdLSPServer::onDocumentRangeFormatting);
+  Bind.method("textDocument/onTypeFormatting", this, &ClangdLSPServer::onDocumentOnTypeFormatting);
+  Bind.method("textDocument/formatting", this, &ClangdLSPServer::onDocumentFormatting);
+  Bind.method("textDocument/codeAction", this, &ClangdLSPServer::onCodeAction);
+  Bind.method("textDocument/completion", this, &ClangdLSPServer::onCompletion);
+  Bind.method("textDocument/signatureHelp", this, &ClangdLSPServer::onSignatureHelp);
+  Bind.method("textDocument/definition", this, &ClangdLSPServer::onGoToDefinition);
+  Bind.method("textDocument/declaration", this, &ClangdLSPServer::onGoToDeclaration);
+  Bind.method("textDocument/implementation", this, &ClangdLSPServer::onGoToImplementation);
+  Bind.method("textDocument/references", this, &ClangdLSPServer::onReference);
+  Bind.method("textDocument/switchSourceHeader", this, &ClangdLSPServer::onSwitchSourceHeader);
+  Bind.method("textDocument/prepareRename", this, &ClangdLSPServer::onPrepareRename);
+  Bind.method("textDocument/rename", this, &ClangdLSPServer::onRename);
+  Bind.method("textDocument/hover", this, &ClangdLSPServer::onHover);
+  Bind.method("textDocument/documentSymbol", this, &ClangdLSPServer::onDocumentSymbol);
+  Bind.method("workspace/executeCommand", this, &ClangdLSPServer::onCommand);
+  Bind.method("textDocument/documentHighlight", this, &ClangdLSPServer::onDocumentHighlight);
+  Bind.method("workspace/symbol", this, &ClangdLSPServer::onWorkspaceSymbol);
+  Bind.method("textDocument/ast", this, &ClangdLSPServer::onAST);
+  Bind.notification("textDocument/didOpen", this, &ClangdLSPServer::onDocumentDidOpen);
+  Bind.notification("textDocument/didClose", this, &ClangdLSPServer::onDocumentDidClose);
+  Bind.notification("textDocument/didChange", this, &ClangdLSPServer::onDocumentDidChange);
+  Bind.notification("textDocument/didSave", this, &ClangdLSPServer::onDocumentDidSave);
+  Bind.notification("workspace/didChangeWatchedFiles", this, &ClangdLSPServer::onFileEvent);
+  Bind.notification("workspace/didChangeConfiguration", this, &ClangdLSPServer::onChangeConfiguration);
+  Bind.method("textDocument/symbolInfo", this, &ClangdLSPServer::onSymbolInfo);
+  Bind.method("textDocument/typeHierarchy", this, &ClangdLSPServer::onTypeHierarchy);
+  Bind.method("typeHierarchy/resolve", this, &ClangdLSPServer::onResolveTypeHierarchy);
+  Bind.method("textDocument/prepareCallHierarchy", this, &ClangdLSPServer::onPrepareCallHierarchy);
+  Bind.method("callHierarchy/incomingCalls", this, &ClangdLSPServer::onCallHierarchyIncomingCalls);
+  Bind.method("callHierarchy/outgoingCalls", this, &ClangdLSPServer::onCallHierarchyOutgoingCalls);
+  Bind.method("textDocument/selectionRange", this, &ClangdLSPServer::onSelectionRange);
+  Bind.method("textDocument/documentLink", this, &ClangdLSPServer::onDocumentLink);
+  Bind.method("textDocument/semanticTokens/full", this, &ClangdLSPServer::onSemanticTokens);
+  Bind.method("textDocument/semanticTokens/full/delta", this, &ClangdLSPServer::onSemanticTokensDelta);
+  Bind.method("$/memoryUsage", this, &ClangdLSPServer::onMemoryUsage);
   if (Opts.FoldingRanges)
-    MsgHandler->bind("textDocument/foldingRange", &ClangdLSPServer::onFoldingRange);
-  bindCommand(APPLY_FIX_COMMAND, &ClangdLSPServer::onCommandApplyEdit);
-  bindCommand(APPLY_TWEAK_COMMAND, &ClangdLSPServer::onCommandApplyTweak);
+    Bind.method("textDocument/foldingRange", this, &ClangdLSPServer::onFoldingRange);
+  Bind.command(APPLY_FIX_COMMAND, this, &ClangdLSPServer::onCommandApplyEdit);
+  Bind.command(APPLY_TWEAK_COMMAND, this, &ClangdLSPServer::onCommandApplyTweak);
   // clang-format on
 }
 
diff --git a/clang-tools-extra/clangd/LSPBinder.h b/clang-tools-extra/clangd/LSPBinder.h
new file mode 100644
--- /dev/null
+++ b/clang-tools-extra/clangd/LSPBinder.h
@@ -0,0 +1,138 @@
+//===--- LSPBinder.h - Tables of LSP handlers --------------------*- C++-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_LSPBINDER_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_LSPBINDER_H
+
+#include "Protocol.h"
+#include "support/Function.h"
+#include "support/Logger.h"
+#include "llvm/ADT/FunctionExtras.h"
+#include "llvm/ADT/StringMap.h"
+#include "llvm/Support/JSON.h"
+
+namespace clang {
+namespace clangd {
+
+/// LSPBinder collects a table of functions that handle LSP calls.
+///
+/// It translates a handler method's signature, e.g.
+///    void codeComplete(CompletionParams, Callback<CompletionList>)
+/// into a wrapper with a generic signature:
+///    void(json::Value, Callback<json::Value>)
+/// The wrapper takes care of parsing/serializing responses.
+///
+/// Incoming calls can be methods, notifications, or commands - all are similar.
+///
+/// FIXME: this should also take responsibility for wrapping *outgoing* calls,
+/// replacing the typed ClangdLSPServer::call<> etc.
+class LSPBinder {
+public:
+  using JSON = llvm::json::Value;
+
+  struct RawHandlers {
+    template <typename HandlerT>
+    using HandlerMap = llvm::StringMap<llvm::unique_function<HandlerT>>;
+
+    HandlerMap<void(JSON)> NotificationHandlers;
+    HandlerMap<void(JSON, Callback<JSON>)> MethodHandlers;
+    HandlerMap<void(JSON, Callback<JSON>)> CommandHandlers;
+  };
+
+  LSPBinder(RawHandlers &Raw) : Raw(Raw) {}
+
+  /// Bind a handler for an LSP method.
+  template <typename Param, typename Result, typename ThisT>
+  void method(llvm::StringLiteral Method, ThisT *This,
+              void (ThisT::*Handler)(const Param &, Callback<Result>));
+
+  /// Bind a handler for an LSP notification.
+  template <typename Param, typename ThisT>
+  void notification(llvm::StringLiteral Method, ThisT *This,
+                    void (ThisT::*Handler)(const Param &));
+
+  /// Bind a handler for an LSP command.
+  template <typename Param, typename Result, typename ThisT>
+  void command(llvm::StringLiteral Command, ThisT *This,
+               void (ThisT::*Handler)(const Param &, Callback<Result>));
+
+  // FIXME: remove usage from ClangdLSPServer and make this private.
+  template <typename T>
+  static llvm::Expected<T> parse(const llvm::json::Value &Raw,
+                                 llvm::StringRef PayloadName,
+                                 llvm::StringRef PayloadKind);
+
+private:
+  RawHandlers &Raw;
+};
+
+template <typename T>
+llvm::Expected<T> LSPBinder::parse(const llvm::json::Value &Raw,
+                                   llvm::StringRef PayloadName,
+                                   llvm::StringRef PayloadKind) {
+  T Result;
+  llvm::json::Path::Root Root;
+  if (!fromJSON(Raw, Result, Root)) {
+    elog("Failed to decode {0} {1}: {2}", PayloadName, PayloadKind,
+         Root.getError());
+    // Dump the relevant parts of the broken message.
+    std::string Context;
+    llvm::raw_string_ostream OS(Context);
+    Root.printErrorContext(Raw, OS);
+    vlog("{0}", OS.str());
+    // Report the error (e.g. to the client).
+    return llvm::make_error<LSPError>(
+        llvm::formatv("failed to decode {0} {1}: {2}", PayloadName, PayloadKind,
+                      fmt_consume(Root.getError())),
+        ErrorCode::InvalidParams);
+  }
+  return std::move(Result);
+}
+
+template <typename Param, typename Result, typename ThisT>
+void LSPBinder::method(llvm::StringLiteral Method, ThisT *This,
+                       void (ThisT::*Handler)(const Param &,
+                                              Callback<Result>)) {
+  Raw.MethodHandlers[Method] = [Method, Handler, This](JSON RawParams,
+                                                       Callback<JSON> Reply) {
+    auto P = LSPBinder::parse<Param>(RawParams, Method, "request");
+    if (!P)
+      return Reply(P.takeError());
+    (This->*Handler)(*P, std::move(Reply));
+  };
+}
+
+template <typename Param, typename ThisT>
+void LSPBinder::notification(llvm::StringLiteral Method, ThisT *This,
+                             void (ThisT::*Handler)(const Param &)) {
+  Raw.NotificationHandlers[Method] = [Method, Handler, This](JSON RawParams) {
+    llvm::Expected<Param> P =
+        LSPBinder::parse<Param>(RawParams, Method, "request");
+    if (!P)
+      return llvm::consumeError(P.takeError());
+    (This->*Handler)(*P);
+  };
+}
+
+template <typename Param, typename Result, typename ThisT>
+void LSPBinder::command(llvm::StringLiteral Method, ThisT *This,
+                        void (ThisT::*Handler)(const Param &,
+                                               Callback<Result>)) {
+  Raw.CommandHandlers[Method] = [Method, Handler, This](JSON RawParams,
+                                                        Callback<JSON> Reply) {
+    auto P = LSPBinder::parse<Param>(RawParams, Method, "command");
+    if (!P)
+      return Reply(P.takeError());
+    (This->*Handler)(*P, std::move(Reply));
+  };
+}
+
+} // namespace clangd
+} // namespace clang
+
+#endif
diff --git a/clang-tools-extra/clangd/Module.h b/clang-tools-extra/clangd/Module.h
--- a/clang-tools-extra/clangd/Module.h
+++ b/clang-tools-extra/clangd/Module.h
@@ -1,7 +1,10 @@
 #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_MODULE_H
 #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_MODULE_H
 
+#include "LSPBinder.h"
+#include "Protocol.h"
 #include "llvm/ADT/StringRef.h"
+#include "llvm/Support/JSON.h"
 #include <memory>
 #include <vector>
 
@@ -10,14 +13,11 @@
 
 /// A Module contributes a vertical feature to clangd.
 ///
-/// FIXME: Extend this with LSP bindings to support reading/updating
-/// capabilities and implementing LSP endpoints.
+/// FIXME: Extend this to support outgoing LSP calls.
 ///
 /// The lifetime of a module is roughly:
 ///  - modules are created before the LSP server, in ClangdMain.cpp
 ///  - these modules are then passed to ClangdLSPServer and ClangdServer
-///    FIXME: LSP bindings should be registered at ClangdLSPServer
-///    initialization.
 ///  - module hooks can be called at this point.
 ///    FIXME: We should make some server facilities like TUScheduler and index
 ///    available to those modules after ClangdServer is initalized.
@@ -30,6 +30,17 @@
 class Module {
 public:
   virtual ~Module() = default;
+
+  /// Called by the server to connect this module to LSP.
+  /// The module should register the methods/notifications/commands it handles,
+  /// and update the server capabilities to advertise them.
+  ///
+  /// This is only called if the module is running in ClangdLSPServer!
+  /// Modules with a public interface should satisfy it without LSP bindings.
+  // FIXME: ClientCaps should be a raw json::Object here.
+  virtual void initializeLSP(LSPBinder &Bind,
+                             const ClientCapabilities &ClientCaps,
+                             llvm::json::Object &ServerCaps) {}
 };
 
 class ModuleSet {
diff --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt
--- a/clang-tools-extra/clangd/unittests/CMakeLists.txt
+++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt
@@ -70,6 +70,7 @@
   IndexTests.cpp
   JSONTransportTests.cpp
   LoggerTests.cpp
+  LSPBinderTests.cpp
   LSPClient.cpp
   ModulesTests.cpp
   ParsedASTTests.cpp
diff --git a/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp b/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
--- a/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
+++ b/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
@@ -221,6 +221,29 @@
                   DiagMessage("Use of undeclared identifier 'BAR'"))));
 }
 
+TEST_F(LSPTest, ModulesTest) {
+  class MathModule : public Module {
+    void initializeLSP(LSPBinder &Bind, const ClientCapabilities &ClientCaps,
+                       llvm::json::Object &ServerCaps) override {
+      Bind.notification("add", this, &MathModule::add);
+      Bind.method("get", this, &MathModule::get);
+    }
+
+    void add(const int &X) { Value += X; }
+    void get(const std::nullptr_t &, Callback<int> Reply) { Reply(Value); }
+    int Value = 0;
+  };
+  std::vector<std::unique_ptr<Module>> Mods;
+  Mods.push_back(std::make_unique<MathModule>());
+  ModuleSet ModSet(std::move(Mods));
+  Opts.Modules = &ModSet;
+
+  auto &Client = start();
+  Client.notify("add", 2);
+  Client.notify("add", 8);
+  EXPECT_EQ(10, Client.call("get", nullptr).takeValue());
+}
+
 } // namespace
 } // namespace clangd
 } // namespace clang
diff --git a/clang-tools-extra/clangd/unittests/LSPBinderTests.cpp b/clang-tools-extra/clangd/unittests/LSPBinderTests.cpp
new file mode 100644
--- /dev/null
+++ b/clang-tools-extra/clangd/unittests/LSPBinderTests.cpp
@@ -0,0 +1,92 @@
+//===-- LSPBinderTests.cpp ------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "LSPBinder.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace clang {
+namespace clangd {
+namespace {
+
+using testing::_;
+using testing::HasSubstr;
+using testing::Pair;
+using testing::UnorderedElementsAre;
+
+// JSON-serializable type for testing.
+struct Foo {
+  int x;
+};
+bool fromJSON(const llvm::json::Value &V, Foo &F, llvm::json::Path P) {
+  return fromJSON(V, F.x, P);
+}
+llvm::json::Value toJSON(const Foo &F) { return F.x; }
+
+// Creates a Callback that writes its received value into an Optional<Expected>.
+template <typename T>
+llvm::unique_function<void(llvm::Expected<T>)>
+capture(llvm::Optional<llvm::Expected<T>> &Out) {
+  Out.reset();
+  return [&Out](llvm::Expected<T> V) { Out.emplace(std::move(V)); };
+}
+
+TEST(LSPBinderTest, IncomingCalls) {
+  LSPBinder::RawHandlers RawHandlers;
+  LSPBinder Binder{RawHandlers};
+  struct Handler {
+    void plusOne(const Foo &Params, Callback<Foo> Reply) {
+      Reply(Foo{Params.x + 1});
+    }
+    void fail(const Foo &Params, Callback<Foo> Reply) {
+      Reply(error("x={0}", Params.x));
+    }
+    void notify(const Foo &Params) { lastNotify = Params.x; }
+    int lastNotify = -1;
+  };
+
+  Handler H;
+  Binder.method("plusOne", &H, &Handler::plusOne);
+  Binder.method("fail", &H, &Handler::fail);
+  Binder.notification("notify", &H, &Handler::notify);
+  Binder.command("cmdPlusOne", &H, &Handler::plusOne);
+  ASSERT_THAT(RawHandlers.MethodHandlers.keys(),
+              UnorderedElementsAre("plusOne", "fail"));
+  ASSERT_THAT(RawHandlers.NotificationHandlers.keys(),
+              UnorderedElementsAre("notify"));
+  ASSERT_THAT(RawHandlers.CommandHandlers.keys(),
+              UnorderedElementsAre("cmdPlusOne"));
+  llvm::Optional<llvm::Expected<llvm::json::Value>> Reply;
+
+  auto &RawPlusOne = RawHandlers.MethodHandlers["plusOne"];
+  RawPlusOne(1, capture(Reply));
+  EXPECT_THAT_EXPECTED(Reply.getValue(), llvm::HasValue(2));
+  RawPlusOne("foo", capture(Reply));
+  EXPECT_THAT_EXPECTED(
+      Reply.getValue(),
+      llvm::FailedWithMessage(
+          HasSubstr("failed to decode plusOne request: expected integer")));
+
+  auto &RawFail = RawHandlers.MethodHandlers["fail"];
+  RawFail(2, capture(Reply));
+  EXPECT_THAT_EXPECTED(Reply.getValue(), llvm::FailedWithMessage("x=2"));
+
+  auto &RawNotify = RawHandlers.NotificationHandlers["notify"];
+  RawNotify(42);
+  EXPECT_EQ(H.lastNotify, 42);
+  RawNotify("hi"); // invalid, will be logged
+  EXPECT_EQ(H.lastNotify, 42);
+
+  auto &RawCmdPlusOne = RawHandlers.CommandHandlers["cmdPlusOne"];
+  RawCmdPlusOne(1, capture(Reply));
+  EXPECT_THAT_EXPECTED(Reply.getValue(), llvm::HasValue(2));
+}
+} // namespace
+} // namespace clangd
+} // namespace clang