Skip to content

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

Merged
JDevlieghere merged 1 commit into
llvm:mainfrom
JDevlieghere:MCP
Jun 20, 2025
Merged

[lldb] Add Model Context Protocol (MCP) support to LLDB#143628
JDevlieghere merged 1 commit into
llvm:mainfrom
JDevlieghere:MCP

Conversation

@JDevlieghere

@JDevlieghere JDevlieghere commented Jun 10, 2025

Copy link
Copy Markdown
Member

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 9 times, most recently from 3c1cb49 to 4156ce2 Compare June 13, 2025 21:40
@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

llvmbot commented Jun 13, 2025

Copy link
Copy Markdown
Member

@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
Copy Markdown
Member Author

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

@ashgti ashgti left a comment

Copy link
Copy Markdown
Contributor

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.

Comment thread lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp Outdated
Comment thread lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp Outdated
Comment thread lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp Outdated

Copy link
Copy Markdown
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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I know a team which tries to do that, but I'm not sure they realize how easily workaroundable that is (command alias print_help quit; print_help). Given what I've read about AI, I'm fairly certain it could come up with this sequence if it sets its mind to it.

To make this reliable, I think we'd at least need to resolve all aliases down to core commands.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've added a FIXME for now.

Comment on lines 63 to 74

Copy link
Copy Markdown
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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yup, I think that's something we could support. Completion was added in the latest revision of the spec and to support older tools I've settled on the previous revision for now, but this might be a good motivator to move to the next version.

Comment thread lldb/source/Plugins/Protocol/MCP/Protocol.h Outdated
Comment thread lldb/source/Plugins/Protocol/MCP/CMakeLists.txt Outdated
@ashgti

ashgti commented Jun 14, 2025

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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.

@labath labath left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A very lightweight review. I'm focusing on the low-level details. I have no clue about this AI stuff.

Comment thread lldb/source/Commands/CommandObjectProtocolServer.cpp Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a very wrong way to shut down a connection. The problem is that closed fd can get recycled and then the reading thread will end up reading from a random file. And since there's no way to guarantee this does not happen, linux doesn't even bother to support the case it theoretically could support: closing an fd while a thread is already blocked reading from it (as opposed to "a thread that is about to read from it) -- the thread will just end up blocked there ~forever.

You really need a way to have the thread wait for data on the connection and the termination signal. Currently, I think the only portable way we have is with MainLoop::AddPendingCallback, but since you already have a main loop for accepting the connections, maybe you could structure the code such that the loop also handles reading from those connections (and submits them to the worker thread via a (mutex-protected) std::queue or whatever)? Then it would be easy to make that wait for termination as well.

This is going to make reading serialized, but I doubt that's going to be the bottleneck here. And maybe then we don't need one thread for every connection, and we could have some sort of a thread pool of threads waiting to process requests?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks Pavel, I knew about all those things in isolation but I didn't put the pieces together. There's no reason we can't do this fully in the MainLoop.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks. This may sort of work, but I feel I have to point out one issue with the current implementation. Using a blocking (I think) read with a 1 second timeout, means that it will be very easy for the connection to go out of sync if the data is not coming fast enough.

The canonical implementation for multiplexed reader uses a non-blocking (zero timeout) wait and caches the incoming data until it contains a complete packet. We have one in in the gdb remote implementation -- it's not really multiplexed, but it does use caching. Here, timeout is not an error -- it just means you need to go back to waiting for more data.

A blocking read is a lazy solution which sort of works but may cause flaky failures under heavy load (or whatever). These can be mitigated by increasing the timeout, at the cost of letting one slow connection essentially DoS'ing the others by sending an incomplete packet.

@JDevlieghere JDevlieghere Jun 17, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

With #144610 I can drop the timeout here. I already treat timeouts as non-fatal.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I know a team which tries to do that, but I'm not sure they realize how easily workaroundable that is (command alias print_help quit; print_help). Given what I've read about AI, I'm fairly certain it could come up with this sequence if it sets its mind to it.

To make this reliable, I think we'd at least need to resolve all aliases down to core commands.

Comment thread lldb/unittests/Protocol/ProtocolMCPServerTest.cpp Outdated
@JDevlieghere

Copy link
Copy Markdown
Member Author
  • Added a CMake flag to disable building protocol servers at configuration time.
  • Added a link to the spec, settled on 2024-11-05 as that's what Claude Desktop supports right now and we weren't using anything from 2025-03-26.
  • Eliminated all threads except the one that runs the MCP MainLoop, which now serves all the clients.
  • Correctly handle notifications instead of treating them like requests.

Comment thread lldb/source/Plugins/Protocol/MCP/Protocol.cpp Outdated
Comment thread lldb/source/Plugins/Protocol/MCP/Protocol.cpp Outdated
Comment thread lldb/unittests/Protocol/ProtocolMCPServerTest.cpp Outdated
Comment thread lldb/source/Plugins/Protocol/MCP/Tool.h Outdated
Comment on lines 36 to 40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we add a helper for decoding the arguments the LLDB command expects? We have the schema specified, so I think we could define this type at least internally to this file.

@JDevlieghere

Copy link
Copy Markdown
Member Author

Rebased and addressed @ashgti's comments:

  • Support decoding Response messages
  • Parse the JSON RPC version
  • Test notification handlers
  • Support returning errors from tool calls
  • Decode LLDBTool arguments into a POD type

@ashgti ashgti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

I look forward to trying this out and iterating on this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm troubled by the inconsistency of this function w.r.t rest of lldb (actually, I'm also somewhat troubled by its existence).
Currently if you do something like platform connect connect://localhost:1234, lldb will connect to the remote server at that address, while platform connect listen://localhost:1234 will cause lldb to wait for a connection from the remote side. Similarly for unix-connect and unix-accept.

Here you seem to be defining "connect" to mean "wait for a connection". I suppose that active (outgoing) connections aren't going to be particularly useful, but in principle I don't see why they couldn't work (for a protocol/server that only serves a single connection), and this seems like it could cause confusion down the line. Could we stick to the protocols as defined in ConnectionFileDescriptor::Connect (ideally, we would reuse that function, but I suspect that won't be very easy)?

I think I'd be fine with also accepting host:port as a shorthand for listen://host:port

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I copied this from DAP where I think @ashgti wrote it originally. Happy to settle on something different and share that across the two.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd rather them be consistent if we need to change lldb-dap as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would much rather change lldb-dap (which has this syntax for how many months?) that live with an inconsistent url specification forever. While it would be nice if it was consistent, the inconsistency with between lldb and lldb-dap is not as troubling for me. "platform connect" and "protocol-server start" make a much more blatant inconsistency, since they are both CLI commands directly implemented by lldb.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'll put up a PR to change lldb-dap to match lldb.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

typo? (I think saving one char isn't worth it)

JDevlieghere added a commit to JDevlieghere/llvm-project that referenced this pull request Jun 17, 2025
Support non-blocking reads for JSONRPCTransport so we can implement a
multiplexed reader using the MainLoop. Pavel pointed out in llvm#143628 that
the implementation there (which was using blocking reads) can easily to
reading partial JSON RPC packets.
JDevlieghere added a commit to JDevlieghere/llvm-project that referenced this pull request Jun 18, 2025
Use the same scheme as ConnectionFileDescriptor::Connect and use
"listen" and "accept". Addresses feedback from a Pavel in a different PR
[1].

[1] llvm#143628 (comment)
JDevlieghere added a commit that referenced this pull request Jun 20, 2025
Use the same scheme as ConnectionFileDescriptor::Connect and use
"listen" and "accept". Addresses feedback from a Pavel in a different PR
[1].

[1] #143628 (comment)
llvm-sync Bot pushed a commit to arm/arm-toolchain that referenced this pull request Jun 20, 2025
Use the same scheme as ConnectionFileDescriptor::Connect and use
"listen" and "accept". Addresses feedback from a Pavel in a different PR
[1].

[1] llvm/llvm-project#143628 (comment)
@labath

labath commented Jun 20, 2025

Copy link
Copy Markdown
Contributor

I haven't looked at everything, but I believe my concerns are addressed now.

@JDevlieghere
JDevlieghere merged commit 9524bfb into llvm:main Jun 20, 2025
7 checks passed
@JDevlieghere
JDevlieghere deleted the MCP branch June 20, 2025 15:48
@DavidSpickett

Copy link
Copy Markdown
Contributor

@JDevlieghere Release note please.

Feel free to put it at the the top for maximum buzzword power :) Include a link to the documentation page once you've added that.

JDevlieghere added a commit to swiftlang/llvm-project that referenced this pull request Jul 31, 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.

(cherry picked from commit 9524bfb)
JDevlieghere added a commit to swiftlang/llvm-project that referenced this pull request Jul 31, 2025
Use the same scheme as ConnectionFileDescriptor::Connect and use
"listen" and "accept". Addresses feedback from a Pavel in a different PR
[1].

[1] llvm#143628 (comment)

(cherry picked from commit 4f991cc)
JDevlieghere added a commit to swiftlang/llvm-project that referenced this pull request Aug 19, 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.

(cherry picked from commit 9524bfb)
JDevlieghere added a commit to swiftlang/llvm-project that referenced this pull request Aug 19, 2025
Use the same scheme as ConnectionFileDescriptor::Connect and use
"listen" and "accept". Addresses feedback from a Pavel in a different PR
[1].

[1] llvm#143628 (comment)

(cherry picked from commit 4f991cc)
markrvmurray pushed a commit to markrvmurray/llvm-mc6809 that referenced this pull request Jun 14, 2026
Use the same scheme as ConnectionFileDescriptor::Connect and use
"listen" and "accept". Addresses feedback from a Pavel in a different PR
[1].

[1] llvm/llvm-project#143628 (comment)
youngd007 added a commit to youngd007/llvm-project that referenced this pull request Jul 14, 2026
llvm#143628 added the lldb-mcp tool (an MCP server multiplexer for LLDB),
built by lldb/tools/lldb-mcp/CMakeLists.txt. The Bazel overlay already
models the ProtocolMCP library and the MCP protocol-server plugin, but
not the lldb-mcp executable itself, so it is missing from the Bazel
build.

Add an lldb-mcp cc_binary modeled on the existing lldb-dap target: glob
tools/lldb-mcp/**, expand the macOS Info.plist via expand_template, and
depend on liblldb.wrapper, Host, Utility, ProtocolMCP, and the LLVM
Option/Support libraries. lldb-mcp has no Options.td, so unlike lldb-dap
no gentbl_cc_library is needed.
rupprecht pushed a commit that referenced this pull request Jul 15, 2026
#143628 added the lldb-mcp tool (an MCP server multiplexer for LLDB),
built by lldb/tools/lldb-mcp/CMakeLists.txt. The Bazel overlay already
models the ProtocolMCP library and the MCP protocol-server plugin, but
not the lldb-mcp executable itself, so it is missing from the Bazel
build.

Add an lldb-mcp cc_binary modeled on the existing lldb-dap target: glob
tools/lldb-mcp/**, expand the macOS Info.plist via expand_template, and
depend on liblldb.wrapper, Host, Utility, ProtocolMCP, and the LLVM
Option/Support libraries. lldb-mcp has no Options.td, so unlike lldb-dap
no gentbl_cc_library is needed.

This properly translates into BUCk internally at Meta and then builds
with buck2, but I cannot validate the bazel build of this new target
directly.

bazel rule generation assisted with: claude
pedroMVicente pushed a commit to pedroMVicente/llvm-project that referenced this pull request Jul 15, 2026
llvm#143628 added the lldb-mcp tool (an MCP server multiplexer for LLDB),
built by lldb/tools/lldb-mcp/CMakeLists.txt. The Bazel overlay already
models the ProtocolMCP library and the MCP protocol-server plugin, but
not the lldb-mcp executable itself, so it is missing from the Bazel
build.

Add an lldb-mcp cc_binary modeled on the existing lldb-dap target: glob
tools/lldb-mcp/**, expand the macOS Info.plist via expand_template, and
depend on liblldb.wrapper, Host, Utility, ProtocolMCP, and the LLVM
Option/Support libraries. lldb-mcp has no Options.td, so unlike lldb-dap
no gentbl_cc_library is needed.

This properly translates into BUCk internally at Meta and then builds
with buck2, but I cannot validate the bazel build of this new target
directly.

bazel rule generation assisted with: claude
OpenHarmonySCM-noreply pushed a commit to openharmony/third_party_llvm-project that referenced this pull request Jul 19, 2026
Use the same scheme as ConnectionFileDescriptor::Connect and use
"listen" and "accept". Addresses feedback from a Pavel in a different PR
[1].

[1] llvm/llvm-project#143628 (comment)
JDevlieghere added a commit to swiftlang/llvm-project that referenced this pull request Jul 21, 2026
* [lldb] Refactor JSONTransport own MainLoop read handle. (llvm#179564)

When working with a MainLoop, if the file reaches the EOF it will
immediately fire the read handle callback. We cannot readily determine
if the file is at EOF or if the file is pointing to a socket/pipe that
has consumed all the current data in the buffer but the remote end has
not yet hung up. This is causing JSONTransport to continuously fire the
OnRead callback trigging repeated calls to the
`MessageHandler::OnClose`.

Since MainLoop does not perform the actual read, we need to adjust the
behavior of JSONTransport to fully own the read handle.

This change moves the ownership of the `MainLoop::ReadHandleUP` and
additionally own a reference to the `MainLoop` itself to ensure the loop
outlives the JSONTransport object.

This allows us to remove the handle immediately when we detect an EOF /
hang up has occurred.

(cherry picked from commit 69878f9)

* Fix use-after-free in IOTransport::OnRead on client disconnect (llvm#198548)

When an MCP client disconnects (EOF), `IOTransport::OnRead` called
`handler.OnClosed()` before resetting `m_read_handle`. The MCP server's
`OnClosed` handler erases the client from `m_instances`, destroying both
  the transport (`this`) and the binder (`handler`). The subsequent
`m_read_handle.reset()` then accessed the destroyed transport's member,
  causing a use-after-free (SIGSEGV).

* thread #1, stop reason = signal SIGSEGV: address not mapped to object
(fault address=0x28)
* frame #0: 0x00007ff5d4d5afda
liblldb.so.23.2`lldb_private::transport::IOTransport<lldb_protocol::mcp::ProtocolDescriptor>::OnRead(lldb_private::MainLoopBase&,
lldb_private::transport::JSONTransport<lldb_protocol::mcp::ProtocolDescriptor>::MessageHandler&)
+ 1274
frame #1: 0x00007ff5d1140ad8
liblldb.so.23.0`lldb_private::MainLoopPosix::Run() + 408
frame #2: 0x00007ff5d1760c1c
liblldb.so.23.0`std::thread::_State_impl<std::thre

  Fix by resetting the read handle before calling `OnClosed()`, so no
  transport members are accessed after the handler potentially destroys
  the transport.

Then when the scope is left, the destructor is called for the new
read_handle local variable and it is cleaned up.

New unit tests added that fail without this change. With the change, the
custom 'ai' script (allows end user locally to communicate lldb context
to agent backend via a spun up MCP server: "protocol-server start MCP
listen://localhost:{port}") now successfully concludes without this
crash

Assisted with: claude

(cherry picked from commit 1e1f3dd)

* [lldb] Add unit tests for the MCP server (llvm#202752)

Add unit-test coverage for the MCP protocol types and server under
source/Protocol/MCP and the MCP plugin under
source/Plugins/Protocol/MCP.

The Server handlers run over the in-memory TestTransport, which gains
SimulateError/SimulateClosed/SetRegisterMessageHandlerShouldFail helpers
to drive the handler lifecycle without a real socket.

Code that touches the filesystem or otherwise requires mucking with the
test environment are deliberately left uncovered until those layers can
be mocked.

Assisted-by: Claude
(cherry picked from commit a85441c)

* [lldb] Add an MCP client and asynchronous request binding (llvm#208371)

This PR contains the groundwork to convert lldb-mcp from a naive
forwarder into a multiplexer. This requires acting both an MCP server
and MCP client.

This PR adds a new MCP Client abstraction, a thin wrapper over
MCPTransport and MCPBinder exposing the protocol's requests as typed
asynchronous calls. BindAsync hands an incoming-request handler a Reply
it may invoke later.

This PR also makes fromJSON symmetric with toJSON for
ServerCapabilities. The former only restored supportsToolsList and
silently dropped the resources, completions and logging capabilities, so
a client parsing an initialize result never saw them.

Assisted-by: Claude
(cherry picked from commit 15f7d14)

* [lldb-mcp] Replace byte forwarding with a protocol-aware multiplexer (llvm#208506)

To serve several LLDB instances behind one endpoint it has to act as a
multiplexer. This PR adds a Multiplexer that presents a unified MCP
server to the client. It answers initialize and tools/list locally, and
forwards tools/call and the resource requests to the different instances
through an mcp::Client (added in llvm#208371), relaying the answer back.

For now, this still drives a single backend. Discovering and routing
across several instances is coming next.

Assisted-by: Claude
(cherry picked from commit 5ddad72)

* [lldb-mcp] Multiplex across all discovered LLDB instances (llvm#208827)

Connect to every LLDB MCP server advertised under ~/.lldb rather than a
single one, and present them to the client as one server. A stale
registry entry from a crashed instance simply fails to connect and is
skipped.

Each instance is identified by the pid of its lldb process, now recorded
in the ServerInfo registry file. Tools and resources are addressed with
instance-qualified URIs, e.g. lldb-mcp://instance/{pid}/debugger/{id}
and lldb://instance/{pid}/debugger/{id}/target/{idx}. Listing requests
(sessions_list, resources/list) fan out to every backend and aggregate;
targeted requests (command, resources/read) are routed by the pid parsed
from the URI. Backends only know their local lldb-mcp://debugger/{id}
form, so URIs are rewritten in both directions.

Add Binder::FailPendingRequests (and Client::CancelPendingRequests) so
that when the client disconnects with a request still in flight to a
backend, the abandoned reply is satisfied with an error instead of being
destroyed unanswered, which would trip the binder's "must reply" assert.
The multiplexer cancels its backends on shutdown before unwinding.

Assisted-by: Claude
(cherry picked from commit d6d0ccc)

* [lldb] Add MCP tools to create and destroy debugger instances (llvm#209288)

Add debugger_create and debugger_delete tools to the MCP server so a
client can manage debugger instances, not just command the ones that
already exist. debugger_create detaches the new debugger's stdio from
the host process (redirecting input/output/error to the null device) so
its prompt and asynchronous output cannot corrupt an MCP stream that
shares the host's stdout. Command results flow through
CommandReturnObject and are unaffected.

Factor the tool and resource registration out of
ProtocolServerMCP::Extend into a shared PopulateServer() so an embedded
in-process server (e.g. in lldb-mcp) can install the same set.

Assisted-by: Claude

rdar://181722721
(cherry picked from commit a6d35f4)

* [lldb][bazel] Add the lldb-mcp binary to the Bazel overlay (llvm#209498)

llvm#143628 added the lldb-mcp tool (an MCP server multiplexer for LLDB),
built by lldb/tools/lldb-mcp/CMakeLists.txt. The Bazel overlay already
models the ProtocolMCP library and the MCP protocol-server plugin, but
not the lldb-mcp executable itself, so it is missing from the Bazel
build.

Add an lldb-mcp cc_binary modeled on the existing lldb-dap target: glob
tools/lldb-mcp/**, expand the macOS Info.plist via expand_template, and
depend on liblldb.wrapper, Host, Utility, ProtocolMCP, and the LLVM
Option/Support libraries. lldb-mcp has no Options.td, so unlike lldb-dap
no gentbl_cc_library is needed.

This properly translates into BUCk internally at Meta and then builds
with buck2, but I cannot validate the bazel build of this new target
directly.

bazel rule generation assisted with: claude

(cherry picked from commit 543e714)

* [lldb] Add SBProtocolServer to start protocol servers programmatically (llvm#209923)

Starting a protocol server (such as MCP, and in the future potentially
DAP) is only possible from the command line today, via `protocol-server
start`. Add an SB API so an embedder can start one in its own process
and learn where to connect.

SBProtocolServer wraps ProtocolServer::GetOrCreate/Start/Stop and
exposes the listening connection URI. This lets a tool host the engine's
protocol server in-process and talk to it as a normal client, reusing
the server's tools rather than reimplementing them.

Assisted-by: Claude
(cherry picked from commit 9b096bb)

* [lldb-mcp] Host managed debug sessions in-process (llvm#210450)

Let a client create and own debug sessions with session_create and
session_close. Rather than spawn a separate lldb per session, lldb-mcp
hosts them in its own process, communicating over a loopback socket to
keep things uniform with external lldb instances.

The benefits of this approach are:

- There is no child-process machinery, so nothing needs to be spawned
and cleaned up.
- It works without the need for an external lldb binary.
- It avoids the deadlock by reading stdin through a raw fd instead of
the FILE* stdio path that previously hung the Debugger constructor
contending on the REPL's stdin lock.
- The architecture stays uniform between in-process and external
sessions.

The trade-off is no isolation, so an LLDB crash takes down lldb-mcp
along with its proxied connections.

Assisted-by: Claude
(cherry picked from commit 9208fc3)

---------

Co-authored-by: John Harrison <harjohn@google.com>
Co-authored-by: youngd007 <davidayoung@meta.com>
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.

5 participants