Skip to content

[lldb] Add Model Context Protocol (MCP) support to LLDB #143628

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

JDevlieghere
Copy link
Member

@JDevlieghere JDevlieghere commented Jun 10, 2025

This PR adds an MCP (Model Context Protocol ) server to LLDB. For motivation and background, please refer to the corresponding RFC: https://discourse.llvm.org/t/rfc-adding-mcp-support-to-lldb/86798

I implemented this as a new kind of plugin. The idea is that we could support multiple protocol servers (e.g. if we want to support DAP from within LLDB). This also introduces a corresponding top-level command (protocol-server) with two subcommands to start and stop the server.

(lldb) protocol-server start MCP tcp://localhost:1234
MCP server started with connection listeners: connection://[::1]:1234, connection://[127.0.0.1]:1234

The MCP sever supports one tool (lldb_command) which executes a command, but can easily be extended with more commands.

@JDevlieghere JDevlieghere force-pushed the MCP branch 8 times, most recently from 24e3803 to 3c1cb49 Compare June 13, 2025 21:05
@JDevlieghere JDevlieghere changed the title [lldb] Add MCP support to LLDB (PoC) [lldb] Add Model Context Protocol (MCP) support to LLDB Jun 13, 2025
@JDevlieghere JDevlieghere marked this pull request as ready for review June 13, 2025 21:52
@llvmbot llvmbot added the lldb label Jun 13, 2025
@llvmbot
Copy link
Member

llvmbot commented Jun 13, 2025

@llvm/pr-subscribers-lldb

Author: Jonas Devlieghere (JDevlieghere)

Changes

This PR adds an MCP (Model Context Protocol ) server to LLDB. For motivation and background, please refer to the corresponding RFC: https://discourse.llvm.org/t/rfc-adding-mcp-support-to-lldb/86798

I implemented this as a new kind of plugin. The idea is that we could support multiple protocol servers (e.g. if we want to support DAP from within LLDB). This also introduces a corresponding top-level command (protocol-server) with two subcommands to start and stop the server.

(lldb) protocol-server start MCP tcp://localhost:1234
MCP server started with connection listeners: connection://[::1]:1234, connection://[127.0.0.1]:1234

The MCP sever supports one tool (lldb_command) which executes a command, but can easily be extended with more commands.


Patch is 71.14 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/143628.diff

32 Files Affected:

  • (modified) lldb/include/lldb/Core/Debugger.h (+6)
  • (modified) lldb/include/lldb/Core/PluginManager.h (+11)
  • (added) lldb/include/lldb/Core/ProtocolServer.h (+39)
  • (modified) lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h (+1)
  • (modified) lldb/include/lldb/lldb-enumerations.h (+1)
  • (modified) lldb/include/lldb/lldb-forward.h (+2-1)
  • (modified) lldb/include/lldb/lldb-private-interfaces.h (+2)
  • (modified) lldb/source/Commands/CMakeLists.txt (+1)
  • (added) lldb/source/Commands/CommandObjectProtocolServer.cpp (+184)
  • (added) lldb/source/Commands/CommandObjectProtocolServer.h (+25)
  • (modified) lldb/source/Core/CMakeLists.txt (+1)
  • (modified) lldb/source/Core/Debugger.cpp (+24)
  • (modified) lldb/source/Core/PluginManager.cpp (+32)
  • (added) lldb/source/Core/ProtocolServer.cpp (+21)
  • (modified) lldb/source/Interpreter/CommandInterpreter.cpp (+2)
  • (modified) lldb/source/Plugins/CMakeLists.txt (+1)
  • (added) lldb/source/Plugins/Protocol/CMakeLists.txt (+1)
  • (added) lldb/source/Plugins/Protocol/MCP/CMakeLists.txt (+13)
  • (added) lldb/source/Plugins/Protocol/MCP/MCPError.cpp (+31)
  • (added) lldb/source/Plugins/Protocol/MCP/MCPError.h (+33)
  • (added) lldb/source/Plugins/Protocol/MCP/Protocol.cpp (+183)
  • (added) lldb/source/Plugins/Protocol/MCP/Protocol.h (+131)
  • (added) lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp (+280)
  • (added) lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.h (+80)
  • (added) lldb/source/Plugins/Protocol/MCP/Tool.cpp (+72)
  • (added) lldb/source/Plugins/Protocol/MCP/Tool.h (+61)
  • (modified) lldb/unittests/CMakeLists.txt (+1)
  • (modified) lldb/unittests/DAP/ProtocolTypesTest.cpp (+14-20)
  • (added) lldb/unittests/Protocol/CMakeLists.txt (+12)
  • (added) lldb/unittests/Protocol/ProtocolMCPServerTest.cpp (+195)
  • (added) lldb/unittests/Protocol/ProtocolMCPTest.cpp (+176)
  • (modified) lldb/unittests/TestingSupport/TestUtilities.h (+9)
diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h
index d73aba1e3ce58..0f6659d1a0bf7 100644
--- a/lldb/include/lldb/Core/Debugger.h
+++ b/lldb/include/lldb/Core/Debugger.h
@@ -598,6 +598,10 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
   void FlushProcessOutput(Process &process, bool flush_stdout,
                           bool flush_stderr);
 
+  void AddProtocolServer(lldb::ProtocolServerSP protocol_server_sp);
+  void RemoveProtocolServer(lldb::ProtocolServerSP protocol_server_sp);
+  lldb::ProtocolServerSP GetProtocolServer(llvm::StringRef protocol) const;
+
   SourceManager::SourceFileCache &GetSourceFileCache() {
     return m_source_file_cache;
   }
@@ -768,6 +772,8 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
   mutable std::mutex m_progress_reports_mutex;
   /// @}
 
+  llvm::SmallVector<lldb::ProtocolServerSP> m_protocol_servers;
+
   std::mutex m_destroy_callback_mutex;
   lldb::callback_token_t m_destroy_callback_next_token = 0;
   struct DestroyCallbackInfo {
diff --git a/lldb/include/lldb/Core/PluginManager.h b/lldb/include/lldb/Core/PluginManager.h
index e7b1691031111..e50bf97189cfc 100644
--- a/lldb/include/lldb/Core/PluginManager.h
+++ b/lldb/include/lldb/Core/PluginManager.h
@@ -327,6 +327,17 @@ class PluginManager {
   static void AutoCompleteProcessName(llvm::StringRef partial_name,
                                       CompletionRequest &request);
 
+  // Protocol
+  static bool RegisterPlugin(llvm::StringRef name, llvm::StringRef description,
+                             ProtocolServerCreateInstance create_callback);
+
+  static bool UnregisterPlugin(ProtocolServerCreateInstance create_callback);
+
+  static llvm::StringRef GetProtocolServerPluginNameAtIndex(uint32_t idx);
+
+  static ProtocolServerCreateInstance
+  GetProtocolCreateCallbackForPluginName(llvm::StringRef name);
+
   // Register Type Provider
   static bool RegisterPlugin(llvm::StringRef name, llvm::StringRef description,
                              RegisterTypeBuilderCreateInstance create_callback);
diff --git a/lldb/include/lldb/Core/ProtocolServer.h b/lldb/include/lldb/Core/ProtocolServer.h
new file mode 100644
index 0000000000000..fafe460904323
--- /dev/null
+++ b/lldb/include/lldb/Core/ProtocolServer.h
@@ -0,0 +1,39 @@
+//===-- ProtocolServer.h --------------------------------------------------===//
+//
+// 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 LLDB_CORE_PROTOCOLSERVER_H
+#define LLDB_CORE_PROTOCOLSERVER_H
+
+#include "lldb/Core/PluginInterface.h"
+#include "lldb/Host/Socket.h"
+#include "lldb/lldb-private-interfaces.h"
+
+namespace lldb_private {
+
+class ProtocolServer : public PluginInterface {
+public:
+  ProtocolServer() = default;
+  virtual ~ProtocolServer() = default;
+
+  static lldb::ProtocolServerSP Create(llvm::StringRef name,
+                                       Debugger &debugger);
+
+  struct Connection {
+    Socket::SocketProtocol protocol;
+    std::string name;
+  };
+
+  virtual llvm::Error Start(Connection connection) = 0;
+  virtual llvm::Error Stop() = 0;
+
+  virtual Socket *GetSocket() const = 0;
+};
+
+} // namespace lldb_private
+
+#endif
diff --git a/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h b/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h
index 8535dfcf46da5..4face717531b1 100644
--- a/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h
+++ b/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h
@@ -315,6 +315,7 @@ static constexpr CommandObject::ArgumentTableEntry g_argument_table[] = {
     { lldb::eArgTypeCPUName, "cpu-name", lldb::CompletionType::eNoCompletion, {}, { nullptr, false }, "The name of a CPU." },
     { lldb::eArgTypeCPUFeatures, "cpu-features", lldb::CompletionType::eNoCompletion, {}, { nullptr, false }, "The CPU feature string." },
     { lldb::eArgTypeManagedPlugin, "managed-plugin", lldb::CompletionType::eNoCompletion, {}, { nullptr, false }, "Plugins managed by the PluginManager" },
+    { lldb::eArgTypeProtocol, "protocol", lldb::CompletionType::eNoCompletion, {}, { nullptr, false }, "The name of the protocol." },
     // clang-format on
 };
 
diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h
index eeb7299a354e1..69e8671b6e21b 100644
--- a/lldb/include/lldb/lldb-enumerations.h
+++ b/lldb/include/lldb/lldb-enumerations.h
@@ -664,6 +664,7 @@ enum CommandArgumentType {
   eArgTypeCPUName,
   eArgTypeCPUFeatures,
   eArgTypeManagedPlugin,
+  eArgTypeProtocol,
   eArgTypeLastArg // Always keep this entry as the last entry in this
                   // enumeration!!
 };
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index c664d1398f74d..558818e8e2309 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -164,13 +164,13 @@ class PersistentExpressionState;
 class Platform;
 class Process;
 class ProcessAttachInfo;
-class ProcessLaunchInfo;
 class ProcessInfo;
 class ProcessInstanceInfo;
 class ProcessInstanceInfoMatch;
 class ProcessLaunchInfo;
 class ProcessModID;
 class Property;
+class ProtocolServer;
 class Queue;
 class QueueImpl;
 class QueueItem;
@@ -391,6 +391,7 @@ typedef std::shared_ptr<lldb_private::Platform> PlatformSP;
 typedef std::shared_ptr<lldb_private::Process> ProcessSP;
 typedef std::shared_ptr<lldb_private::ProcessAttachInfo> ProcessAttachInfoSP;
 typedef std::shared_ptr<lldb_private::ProcessLaunchInfo> ProcessLaunchInfoSP;
+typedef std::shared_ptr<lldb_private::ProtocolServer> ProtocolServerSP;
 typedef std::weak_ptr<lldb_private::Process> ProcessWP;
 typedef std::shared_ptr<lldb_private::RegisterCheckpoint> RegisterCheckpointSP;
 typedef std::shared_ptr<lldb_private::RegisterContext> RegisterContextSP;
diff --git a/lldb/include/lldb/lldb-private-interfaces.h b/lldb/include/lldb/lldb-private-interfaces.h
index d366dbd1d7832..34eaaa8e581e9 100644
--- a/lldb/include/lldb/lldb-private-interfaces.h
+++ b/lldb/include/lldb/lldb-private-interfaces.h
@@ -81,6 +81,8 @@ typedef lldb::PlatformSP (*PlatformCreateInstance)(bool force,
 typedef lldb::ProcessSP (*ProcessCreateInstance)(
     lldb::TargetSP target_sp, lldb::ListenerSP listener_sp,
     const FileSpec *crash_file_path, bool can_connect);
+typedef lldb::ProtocolServerSP (*ProtocolServerCreateInstance)(
+    Debugger &debugger);
 typedef lldb::RegisterTypeBuilderSP (*RegisterTypeBuilderCreateInstance)(
     Target &target);
 typedef lldb::ScriptInterpreterSP (*ScriptInterpreterCreateInstance)(
diff --git a/lldb/source/Commands/CMakeLists.txt b/lldb/source/Commands/CMakeLists.txt
index 1ea51acec5f15..69e4c45f0b8e5 100644
--- a/lldb/source/Commands/CMakeLists.txt
+++ b/lldb/source/Commands/CMakeLists.txt
@@ -23,6 +23,7 @@ add_lldb_library(lldbCommands NO_PLUGIN_DEPENDENCIES
   CommandObjectPlatform.cpp
   CommandObjectPlugin.cpp
   CommandObjectProcess.cpp
+  CommandObjectProtocolServer.cpp
   CommandObjectQuit.cpp
   CommandObjectRegexCommand.cpp
   CommandObjectRegister.cpp
diff --git a/lldb/source/Commands/CommandObjectProtocolServer.cpp b/lldb/source/Commands/CommandObjectProtocolServer.cpp
new file mode 100644
index 0000000000000..3ee1a0ccc4ffa
--- /dev/null
+++ b/lldb/source/Commands/CommandObjectProtocolServer.cpp
@@ -0,0 +1,184 @@
+//===-- CommandObjectProtocolServer.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 "CommandObjectProtocolServer.h"
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Core/ProtocolServer.h"
+#include "lldb/Host/Socket.h"
+#include "lldb/Interpreter/CommandInterpreter.h"
+#include "lldb/Interpreter/CommandReturnObject.h"
+#include "lldb/Utility/UriParser.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/Support/FormatAdapters.h"
+
+using namespace llvm;
+using namespace lldb;
+using namespace lldb_private;
+
+#define LLDB_OPTIONS_mcp
+#include "CommandOptions.inc"
+
+static std::vector<llvm::StringRef> GetSupportedProtocols() {
+  std::vector<llvm::StringRef> supported_protocols;
+  size_t i = 0;
+
+  for (llvm::StringRef protocol_name =
+           PluginManager::GetProtocolServerPluginNameAtIndex(i++);
+       !protocol_name.empty();
+       protocol_name = PluginManager::GetProtocolServerPluginNameAtIndex(i++)) {
+    supported_protocols.push_back(protocol_name);
+  }
+
+  return supported_protocols;
+}
+
+static llvm::Expected<std::pair<Socket::SocketProtocol, std::string>>
+validateConnection(llvm::StringRef conn) {
+  auto uri = lldb_private::URI::Parse(conn);
+
+  if (uri && (uri->scheme == "tcp" || uri->scheme == "connect" ||
+              !uri->hostname.empty() || uri->port)) {
+    return std::make_pair(
+        Socket::ProtocolTcp,
+        formatv("[{0}]:{1}", uri->hostname.empty() ? "0.0.0.0" : uri->hostname,
+                uri->port.value_or(0)));
+  }
+
+  if (uri && (uri->scheme == "unix" || uri->scheme == "unix-connect" ||
+              uri->path != "/")) {
+    return std::make_pair(Socket::ProtocolUnixDomain, uri->path.str());
+  }
+
+  return llvm::createStringError(
+      "Unsupported connection specifier, expected 'unix-connect:///path' or "
+      "'connect://[host]:port', got '%s'.",
+      conn.str().c_str());
+}
+
+class CommandObjectProtocolServerStart : public CommandObjectParsed {
+public:
+  CommandObjectProtocolServerStart(CommandInterpreter &interpreter)
+      : CommandObjectParsed(interpreter, "mcp start", "start MCP server",
+                            "mcp start <connection>") {
+    AddSimpleArgumentList(lldb::eArgTypeProtocol, eArgRepeatPlain);
+    AddSimpleArgumentList(lldb::eArgTypeConnectURL, eArgRepeatPlain);
+  }
+
+  ~CommandObjectProtocolServerStart() override = default;
+
+protected:
+  void DoExecute(Args &args, CommandReturnObject &result) override {
+    if (args.GetArgumentCount() < 1) {
+      result.AppendError("no protocol specified");
+      return;
+    }
+
+    llvm::StringRef protocol = args.GetArgumentAtIndex(0);
+    std::vector<llvm::StringRef> supported_protocols = GetSupportedProtocols();
+    if (llvm::find(supported_protocols, protocol) ==
+        supported_protocols.end()) {
+      result.AppendErrorWithFormatv(
+          "unsupported protocol: {0}. Supported protocols are: {1}", protocol,
+          llvm::join(GetSupportedProtocols(), ", "));
+      return;
+    }
+
+    if (args.GetArgumentCount() < 2) {
+      result.AppendError("no connection specified");
+      return;
+    }
+    llvm::StringRef connection_uri = args.GetArgumentAtIndex(1);
+
+    ProtocolServerSP server_sp = GetDebugger().GetProtocolServer(protocol);
+    if (!server_sp)
+      server_sp = ProtocolServer::Create(protocol, GetDebugger());
+
+    auto maybeProtoclAndName = validateConnection(connection_uri);
+    if (auto error = maybeProtoclAndName.takeError()) {
+      result.AppendErrorWithFormatv("{0}", llvm::fmt_consume(std::move(error)));
+      return;
+    }
+
+    ProtocolServer::Connection connection;
+    std::tie(connection.protocol, connection.name) = *maybeProtoclAndName;
+
+    if (llvm::Error error = server_sp->Start(connection)) {
+      result.AppendErrorWithFormatv("{0}", llvm::fmt_consume(std::move(error)));
+      return;
+    }
+
+    GetDebugger().AddProtocolServer(server_sp);
+
+    if (Socket *socket = server_sp->GetSocket()) {
+      std::string address =
+          llvm::join(socket->GetListeningConnectionURI(), ", ");
+      result.AppendMessageWithFormatv(
+          "{0} server started with connection listeners: {1}", protocol,
+          address);
+    }
+  }
+};
+
+class CommandObjectProtocolServerStop : public CommandObjectParsed {
+public:
+  CommandObjectProtocolServerStop(CommandInterpreter &interpreter)
+      : CommandObjectParsed(interpreter, "protocol-server stop",
+                            "stop protocol server", "protocol-server stop") {
+    AddSimpleArgumentList(lldb::eArgTypeProtocol, eArgRepeatPlain);
+  }
+
+  ~CommandObjectProtocolServerStop() override = default;
+
+protected:
+  void DoExecute(Args &args, CommandReturnObject &result) override {
+    if (args.GetArgumentCount() < 1) {
+      result.AppendError("no protocol specified");
+      return;
+    }
+
+    llvm::StringRef protocol = args.GetArgumentAtIndex(0);
+    std::vector<llvm::StringRef> supported_protocols = GetSupportedProtocols();
+    if (llvm::find(supported_protocols, protocol) ==
+        supported_protocols.end()) {
+      result.AppendErrorWithFormatv(
+          "unsupported protocol: {0}. Supported protocols are: {1}", protocol,
+          llvm::join(GetSupportedProtocols(), ", "));
+      return;
+    }
+
+    Debugger &debugger = GetDebugger();
+
+    ProtocolServerSP server_sp = debugger.GetProtocolServer(protocol);
+    if (!server_sp) {
+      result.AppendError(
+          llvm::formatv("no {0} protocol server running", protocol).str());
+      return;
+    }
+
+    if (llvm::Error error = server_sp->Stop()) {
+      result.AppendErrorWithFormatv("{0}", llvm::fmt_consume(std::move(error)));
+      return;
+    }
+
+    debugger.RemoveProtocolServer(server_sp);
+  }
+};
+
+CommandObjectProtocolServer::CommandObjectProtocolServer(
+    CommandInterpreter &interpreter)
+    : CommandObjectMultiword(interpreter, "protocol-server",
+                             "Start and stop a protocol server.",
+                             "protocol-server") {
+  LoadSubCommand("start", CommandObjectSP(new CommandObjectProtocolServerStart(
+                              interpreter)));
+  LoadSubCommand("stop", CommandObjectSP(
+                             new CommandObjectProtocolServerStop(interpreter)));
+}
+
+CommandObjectProtocolServer::~CommandObjectProtocolServer() = default;
diff --git a/lldb/source/Commands/CommandObjectProtocolServer.h b/lldb/source/Commands/CommandObjectProtocolServer.h
new file mode 100644
index 0000000000000..3591216b014cb
--- /dev/null
+++ b/lldb/source/Commands/CommandObjectProtocolServer.h
@@ -0,0 +1,25 @@
+//===-- CommandObjectProtocolServer.h
+//------------------------------------------------===//
+//
+// 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 LLDB_SOURCE_COMMANDS_COMMANDOBJECTPROTOCOLSERVER_H
+#define LLDB_SOURCE_COMMANDS_COMMANDOBJECTPROTOCOLSERVER_H
+
+#include "lldb/Interpreter/CommandObjectMultiword.h"
+
+namespace lldb_private {
+
+class CommandObjectProtocolServer : public CommandObjectMultiword {
+public:
+  CommandObjectProtocolServer(CommandInterpreter &interpreter);
+  ~CommandObjectProtocolServer() override;
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_COMMANDS_COMMANDOBJECTMCP_H
diff --git a/lldb/source/Core/CMakeLists.txt b/lldb/source/Core/CMakeLists.txt
index d6b75bca7f2d6..df35bd5c025f3 100644
--- a/lldb/source/Core/CMakeLists.txt
+++ b/lldb/source/Core/CMakeLists.txt
@@ -46,6 +46,7 @@ add_lldb_library(lldbCore NO_PLUGIN_DEPENDENCIES
   Opcode.cpp
   PluginManager.cpp
   Progress.cpp
+  ProtocolServer.cpp
   Statusline.cpp
   RichManglingContext.cpp
   SearchFilter.cpp
diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index 81037d3def811..2bc9c7ead79d3 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -16,6 +16,7 @@
 #include "lldb/Core/ModuleSpec.h"
 #include "lldb/Core/PluginManager.h"
 #include "lldb/Core/Progress.h"
+#include "lldb/Core/ProtocolServer.h"
 #include "lldb/Core/StreamAsynchronousIO.h"
 #include "lldb/Core/Telemetry.h"
 #include "lldb/DataFormatters/DataVisualization.h"
@@ -2363,3 +2364,26 @@ llvm::ThreadPoolInterface &Debugger::GetThreadPool() {
          "Debugger::GetThreadPool called before Debugger::Initialize");
   return *g_thread_pool;
 }
+
+void Debugger::AddProtocolServer(lldb::ProtocolServerSP protocol_server_sp) {
+  assert(protocol_server_sp &&
+         GetProtocolServer(protocol_server_sp->GetPluginName()) == nullptr);
+  m_protocol_servers.push_back(protocol_server_sp);
+}
+
+void Debugger::RemoveProtocolServer(lldb::ProtocolServerSP protocol_server_sp) {
+  auto it = llvm::find(m_protocol_servers, protocol_server_sp);
+  if (it != m_protocol_servers.end())
+    m_protocol_servers.erase(it);
+}
+
+lldb::ProtocolServerSP
+Debugger::GetProtocolServer(llvm::StringRef protocol) const {
+  for (ProtocolServerSP protocol_server_sp : m_protocol_servers) {
+    if (!protocol_server_sp)
+      continue;
+    if (protocol_server_sp->GetPluginName() == protocol)
+      return protocol_server_sp;
+  }
+  return nullptr;
+}
diff --git a/lldb/source/Core/PluginManager.cpp b/lldb/source/Core/PluginManager.cpp
index 5d44434033c55..a59a390e40bb6 100644
--- a/lldb/source/Core/PluginManager.cpp
+++ b/lldb/source/Core/PluginManager.cpp
@@ -1006,6 +1006,38 @@ void PluginManager::AutoCompleteProcessName(llvm::StringRef name,
   }
 }
 
+#pragma mark ProtocolServer
+
+typedef PluginInstance<ProtocolServerCreateInstance> ProtocolServerInstance;
+typedef PluginInstances<ProtocolServerInstance> ProtocolServerInstances;
+
+static ProtocolServerInstances &GetProtocolServerInstances() {
+  static ProtocolServerInstances g_instances;
+  return g_instances;
+}
+
+bool PluginManager::RegisterPlugin(
+    llvm::StringRef name, llvm::StringRef description,
+    ProtocolServerCreateInstance create_callback) {
+  return GetProtocolServerInstances().RegisterPlugin(name, description,
+                                                     create_callback);
+}
+
+bool PluginManager::UnregisterPlugin(
+    ProtocolServerCreateInstance create_callback) {
+  return GetProtocolServerInstances().UnregisterPlugin(create_callback);
+}
+
+llvm::StringRef
+PluginManager::GetProtocolServerPluginNameAtIndex(uint32_t idx) {
+  return GetProtocolServerInstances().GetNameAtIndex(idx);
+}
+
+ProtocolServerCreateInstance
+PluginManager::GetProtocolCreateCallbackForPluginName(llvm::StringRef name) {
+  return GetProtocolServerInstances().GetCallbackForName(name);
+}
+
 #pragma mark RegisterTypeBuilder
 
 struct RegisterTypeBuilderInstance
diff --git a/lldb/source/Core/ProtocolServer.cpp b/lldb/source/Core/ProtocolServer.cpp
new file mode 100644
index 0000000000000..d57a047afa7b2
--- /dev/null
+++ b/lldb/source/Core/ProtocolServer.cpp
@@ -0,0 +1,21 @@
+//===-- ProtocolServer.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 "lldb/Core/ProtocolServer.h"
+#include "lldb/Core/PluginManager.h"
+
+using namespace lldb_private;
+using namespace lldb;
+
+ProtocolServerSP ProtocolServer::Create(llvm::StringRef name,
+                                        Debugger &debugger) {
+  if (ProtocolServerCreateInstance create_callback =
+          PluginManager::GetProtocolCreateCallbackForPluginName(name))
+    return create_callback(debugger);
+  return nullptr;
+}
diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp
index 4f9ae104dedea..00c3472444d2e 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -30,6 +30,7 @@
 #include "Commands/CommandObjectPlatform.h"
 #include "Commands/CommandObjectPlugin.h"
 #include "Commands/CommandObjectProcess.h"
+#include "Commands/CommandObjectProtocolServer.h"
 #include "Commands/CommandObjectQuit.h"
 #include "Commands/CommandObjectRegexCommand.h"
 #include "Commands/CommandObjectRegister.h"
@@ -574,6 +575,7 @@ void CommandInterpreter::LoadCommandDictionary() {
   REGISTER_COMMAND_OBJECT("...
[truncated]

@JDevlieghere
Copy link
Member Author

I'm adding the folks that chimed in on the RFC as reviewers.

Copy link
Contributor

@ashgti ashgti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth it to include a file in lldb/docs/resources about running this? For folks wanting to get started trying this out?

Maybe in a follow up PR.

Also, thinking about the overall flow of the MCP server and the debugger instance.

I am curious how this would work with lldb-dap. In the DAP, we make a debugger instance for each debug session and it goes away at the end of the debug session. So, I may not be able to run both lldb-dap and an MCP server in the same debug session, or at least the DAP session would stop the MCP server once the debugger session is over.

Should the MCP server mode have some way of creating a debugger instance itself? Like, should each client have its own debugger instance? If so, that sort of inverts how this should be arranged such that the MCP server should own the debugger not the other way around.


m_read_handles = std::move(*handles);
m_loop_thread = std::thread([=] {
llvm::set_thread_name("mcp-runloop");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For accounting, should this prefix the debugger name in case there are multiple debugger instances in the same process? Maybe {debugger.name}.mcp.listener (or runloop)?

Comment on lines +88 to +89
const std::string client_name =
llvm::formatv("client-{0}", m_clients.size() + 1).str();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For accounting, should this prefix the debugger name in case there are multiple debugger instances in the same process? Maybe {debugger.name}.mcp.client-{i}?

Comment on lines +94 to +98
m_clients.emplace_back(io, [=]() {
llvm::set_thread_name(client_name + "-runloop");
if (auto Err = Run(std::make_unique<JSONRPCTransport>(io, io)))
LLDB_LOG_ERROR(GetLog(LLDBLog::Host), std::move(Err), "MCP Error: {0}");
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it looks like we're only supporting connecting via sockets.

Instead of spawning a thread per-client, should we have the clients register their handles to the RunLoop and handle the requests that way? I think it would result in fewer threads. I think all the clients are sharing a connection to the same debugger instance and this may help prevent races between clients.

}

CommandReturnObject result(/*colors=*/false);
m_debugger.GetCommandInterpreter().HandleCommand(arguments.c_str(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we prevent some commands from being executed here?

For example, quit?

Comment on lines +63 to +65
struct ToolCapability {
bool listChanged = false;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No changes needed, but reading up on the schema for this, I wonder if we could also support completion (https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/completion#user-interaction-model). Maybe in the future that would be reasonable to implement.

// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we include a top level comment with a link to

https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.json

For reference?

@@ -0,0 +1,13 @@
add_lldb_library(lldbPluginProtocolServerMCP PLUGIN
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be behind a build flag?

I could see some distributions disabling this feature for security reasons.

Once an MCP server is running, it can run arbitrary commands in the debugger and may even be able to spawn arbitrary processes.

@ashgti
Copy link
Contributor

ashgti commented Jun 14, 2025

Reading more about this in the VSCode docs and on the MCP website I think I understand the flow of logic a bit more.

I think this is definitely helpful for allowing an agent to help control a debug session.

However, I'm still not sure how we should handle the MCP client and debugger instance association.

The main limitation I see is being able to launch a debug session (either by starting a binary or attaching).

I suppose that if you were to just use lldb in the command line and then start the server it could always use process launch ... to start a debug session. In the lldb-dap VSCode extension, we could use the VSCode APIs to make a different tool for launching a debug session with lldb-dap and dynamically register the MCP server once the debug session starts (https://code.visualstudio.com/api/extension-guides/mcp#register-an-mcp-server allows us to update the list of servers dynamically from our extension).

@JDevlieghere
Copy link
Member Author

I've been thinking about this too. I think you're right to distinguish two use cases:

  1. Interacting with an existing debug session. For example you've started debugging in VS Code with DAP and you want your AI assistant to interact with the debug session.
  2. Starting a new debug session. For example, you're working on a unit test and you want to run it under the debugger to see it crash so you ask your AI assistant to set a breakpoint and run the test under the debugger.

The current implementation focuses on (1). I think (2) is a lot a harder, because depending on where I'm using this, I might want it to do something different. For example, from Claude desktop, I probably just want to have the model interact with the debugger behind the scenes. But in VS Code, I want to see the debug session in the IDE, and it should use DAP and from another IDE that doesn't support DAP, it might be something different. I think it's a useful use case, but currently not one I'm motivated to solve. That said, it does impact the design, so I think it should be considered so that we don't come up with something that's fundamentally incompatible.

The second problem is that Claude assumes you have a single MCP server instance. VS Code works in a very similar way, though you can specify different servers per project. But you can have multiple concurrent active debug sessions per project, so we need a way to support that. Right now you need to use netcat with a fixed address and port and that's obviously a pretty awful user experience.

What I had in mind for that (and hinted at in the RFC) is to create a binary, lldb-mcp that acts as a multiplexer and we use a known location (e.g. ~/.lldb-mcp) where the sockets are created. The session ID then becomes an argument to every tool invocation. The tricky part is how to do the association. My hope is that with enough context (e.g. the currently active file in VS Code), the model will be smart enough to figure out the association between the MCP session, but I haven't tried anything like that.

If we go with the multiplexing approach, then I think it's fine for every debugger to have its own server instance. If we do something different, it might make more sense to have one server per lldb instance and do the "multiplexing" there by just using the debugger ID. That would work for lldb-dap in server mode, but not when they're all separate processes. Another, even simpler, solution could be that we only support a single debug session (e.g. the last one) and ignore this problem for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants