Skip to content

Conversation

ashgti
Copy link
Contributor

@ashgti ashgti commented Sep 12, 2025

This brings back the tool for listing debuggers.

This is helpful when an LLM doesn't support resources, like gemini-cli.

I also fixed an issue with ServerInfoHandle not always cleaning up the ~/lldb/lldb-mcp-<pid>.json file correctly.

@llvmbot
Copy link
Member

llvmbot commented Sep 12, 2025

@llvm/pr-subscribers-lldb

Author: John Harrison (ashgti)

Changes

This brings back the tool for listing debuggers.

This is helpful when an LLM doesn't support resources, like gemini-cli.

I also fixed an issue with ServerInfoHandle not always cleaning up the ~/lldb/lldb-mcp-&lt;pid&gt;.json file correctly.


Full diff: https://github.com/llvm/llvm-project/pull/158340.diff

5 Files Affected:

  • (modified) lldb/include/lldb/Protocol/MCP/Server.h (+4-2)
  • (modified) lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp (+4-4)
  • (modified) lldb/source/Plugins/Protocol/MCP/Tool.cpp (+82-24)
  • (modified) lldb/source/Plugins/Protocol/MCP/Tool.h (+9)
  • (modified) lldb/source/Protocol/MCP/Server.cpp (+12-14)
diff --git a/lldb/include/lldb/Protocol/MCP/Server.h b/lldb/include/lldb/Protocol/MCP/Server.h
index b674d58159550..1f916ae525b5c 100644
--- a/lldb/include/lldb/Protocol/MCP/Server.h
+++ b/lldb/include/lldb/Protocol/MCP/Server.h
@@ -108,8 +108,7 @@ bool fromJSON(const llvm::json::Value &, ServerInfo &, llvm::json::Path);
 /// once it is no longer referenced.
 class ServerInfoHandle {
 public:
-  ServerInfoHandle();
-  explicit ServerInfoHandle(llvm::StringRef filename);
+  explicit ServerInfoHandle(llvm::StringRef filename = "");
   ~ServerInfoHandle();
 
   ServerInfoHandle(ServerInfoHandle &&other);
@@ -121,6 +120,9 @@ class ServerInfoHandle {
   ServerInfoHandle &operator=(const ServerInfoHandle &) = delete;
   /// @}
 
+  /// Remove the file.
+  void Remove();
+
 private:
   llvm::SmallString<128> m_filename;
 };
diff --git a/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp b/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp
index dc18c8e06803a..d3af3cf25c4a1 100644
--- a/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp
+++ b/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp
@@ -10,8 +10,6 @@
 #include "Resource.h"
 #include "Tool.h"
 #include "lldb/Core/PluginManager.h"
-#include "lldb/Host/FileSystem.h"
-#include "lldb/Host/HostInfo.h"
 #include "lldb/Protocol/MCP/Server.h"
 #include "lldb/Utility/LLDBLog.h"
 #include "lldb/Utility/Log.h"
@@ -60,7 +58,9 @@ void ProtocolServerMCP::Extend(lldb_protocol::mcp::Server &server) const {
                                            "MCP initialization complete");
                                 });
   server.AddTool(
-      std::make_unique<CommandTool>("lldb_command", "Run an lldb command."));
+      std::make_unique<CommandTool>("command", "Run an lldb command."));
+  server.AddTool(std::make_unique<DebuggerListTool>(
+      "debugger_list", "List debugger instances with their debugger_id."));
   server.AddResourceProvider(std::make_unique<DebuggerResourceProvider>());
 }
 
@@ -145,8 +145,8 @@ llvm::Error ProtocolServerMCP::Stop() {
   if (m_loop_thread.joinable())
     m_loop_thread.join();
 
+  m_server_info_handle.Remove();
   m_listen_handlers.clear();
-  m_server_info_handle = ServerInfoHandle();
   m_instances.clear();
 
   return llvm::Error::success();
diff --git a/lldb/source/Plugins/Protocol/MCP/Tool.cpp b/lldb/source/Plugins/Protocol/MCP/Tool.cpp
index 2f451bf76e81d..7250ed3a5518d 100644
--- a/lldb/source/Plugins/Protocol/MCP/Tool.cpp
+++ b/lldb/source/Plugins/Protocol/MCP/Tool.cpp
@@ -7,26 +7,34 @@
 //===----------------------------------------------------------------------===//
 
 #include "Tool.h"
+#include "lldb/Core/Debugger.h"
 #include "lldb/Interpreter/CommandInterpreter.h"
 #include "lldb/Interpreter/CommandReturnObject.h"
 #include "lldb/Protocol/MCP/Protocol.h"
+#include "lldb/Utility/UriParser.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Error.h"
+#include <cstdint>
+#include <optional>
 
 using namespace lldb_private;
 using namespace lldb_protocol;
 using namespace lldb_private::mcp;
+using namespace lldb;
 using namespace llvm;
 
 namespace {
+
 struct CommandToolArguments {
-  uint64_t debugger_id;
-  std::string arguments;
+  /// Either an id like '1' or a uri like 'lldb://sessions/1'.
+  std::string debugger;
+  std::string command;
 };
 
-bool fromJSON(const llvm::json::Value &V, CommandToolArguments &A,
-              llvm::json::Path P) {
-  llvm::json::ObjectMapper O(V, P);
-  return O && O.map("debugger_id", A.debugger_id) &&
-         O.mapOptional("arguments", A.arguments);
+bool fromJSON(const json::Value &V, CommandToolArguments &A, json::Path P) {
+  json::ObjectMapper O(V, P);
+  return O && O.mapOptional("debugger", A.debugger) &&
+         O.mapOptional("command", A.command);
 }
 
 /// Helper function to create a CallToolResult from a string output.
@@ -39,9 +47,15 @@ createTextResult(std::string output, bool is_error = false) {
   return text_result;
 }
 
+static constexpr StringLiteral kSchemeAndHost = "lldb-mcp://debugger/";
+
+std::string to_uri(DebuggerSP debugger) {
+  return (kSchemeAndHost + std::to_string(debugger->GetID())).str();
+}
+
 } // namespace
 
-llvm::Expected<lldb_protocol::mcp::CallToolResult>
+Expected<lldb_protocol::mcp::CallToolResult>
 CommandTool::Call(const lldb_protocol::mcp::ToolArguments &args) {
   if (!std::holds_alternative<json::Value>(args))
     return createStringError("CommandTool requires arguments");
@@ -52,19 +66,35 @@ CommandTool::Call(const lldb_protocol::mcp::ToolArguments &args) {
   if (!fromJSON(std::get<json::Value>(args), arguments, root))
     return root.getError();
 
-  lldb::DebuggerSP debugger_sp =
-      Debugger::FindDebuggerWithID(arguments.debugger_id);
+  lldb::DebuggerSP debugger_sp;
+
+  if (!arguments.debugger.empty()) {
+    llvm::StringRef debugger_specifier = arguments.debugger;
+    debugger_specifier.consume_front(kSchemeAndHost);
+    uint32_t debugger_id = 0;
+    if (debugger_specifier.consumeInteger(10, debugger_id))
+      return createStringError(
+          formatv("malformed debugger specifier {0}", arguments.debugger));
+
+    debugger_sp = Debugger::FindDebuggerWithID(debugger_id);
+  } else {
+    for (size_t i = 0; i < Debugger::GetNumDebuggers(); i++) {
+      debugger_sp = Debugger::GetDebuggerAtIndex(i);
+      if (debugger_sp)
+        break;
+    }
+  }
+
   if (!debugger_sp)
-    return createStringError(
-        llvm::formatv("no debugger with id {0}", arguments.debugger_id));
+    return createStringError("no debugger found");
 
   // FIXME: Disallow certain commands and their aliases.
   CommandReturnObject result(/*colors=*/false);
-  debugger_sp->GetCommandInterpreter().HandleCommand(
-      arguments.arguments.c_str(), eLazyBoolYes, result);
+  debugger_sp->GetCommandInterpreter().HandleCommand(arguments.command.c_str(),
+                                                     eLazyBoolYes, result);
 
   std::string output;
-  llvm::StringRef output_str = result.GetOutputString();
+  StringRef output_str = result.GetOutputString();
   if (!output_str.empty())
     output += output_str.str();
 
@@ -78,14 +108,42 @@ CommandTool::Call(const lldb_protocol::mcp::ToolArguments &args) {
   return createTextResult(output, !result.Succeeded());
 }
 
-std::optional<llvm::json::Value> CommandTool::GetSchema() const {
-  llvm::json::Object id_type{{"type", "number"}};
-  llvm::json::Object str_type{{"type", "string"}};
-  llvm::json::Object properties{{"debugger_id", std::move(id_type)},
-                                {"arguments", std::move(str_type)}};
-  llvm::json::Array required{"debugger_id"};
-  llvm::json::Object schema{{"type", "object"},
-                            {"properties", std::move(properties)},
-                            {"required", std::move(required)}};
+std::optional<json::Value> CommandTool::GetSchema() const {
+  using namespace llvm::json;
+  Object properties{
+      {"debugger",
+       Object{{"type", "string"},
+              {"description",
+               "The debugger ID or URI to a specific debug session. If not "
+               "specified, the first debugger will be used."}}},
+      {"command",
+       Object{{"type", "string"}, {"description", "An lldb command to run."}}}};
+  Object schema{{"type", "object"}, {"properties", std::move(properties)}};
   return schema;
 }
+
+Expected<lldb_protocol::mcp::CallToolResult>
+DebuggerListTool::Call(const lldb_protocol::mcp::ToolArguments &args) {
+  llvm::json::Path::Root root;
+
+  // Return a nested Markdown list with debuggers and target.
+  // Example output:
+  //
+  // - lldb-mcp://debugger/0
+  // - lldb-mcp://debugger/1
+  //
+  // FIXME: Use Structured Content when we adopt protocol version 2025-06-18.
+  std::string output;
+  llvm::raw_string_ostream os(output);
+
+  const size_t num_debuggers = Debugger::GetNumDebuggers();
+  for (size_t i = 0; i < num_debuggers; ++i) {
+    lldb::DebuggerSP debugger_sp = Debugger::GetDebuggerAtIndex(i);
+    if (!debugger_sp)
+      continue;
+
+    os << "- " << to_uri(debugger_sp) << '\n';
+  }
+
+  return createTextResult(output);
+}
diff --git a/lldb/source/Plugins/Protocol/MCP/Tool.h b/lldb/source/Plugins/Protocol/MCP/Tool.h
index 1886525b9168f..8450ce3d6c2dd 100644
--- a/lldb/source/Plugins/Protocol/MCP/Tool.h
+++ b/lldb/source/Plugins/Protocol/MCP/Tool.h
@@ -28,6 +28,15 @@ class CommandTool : public lldb_protocol::mcp::Tool {
   std::optional<llvm::json::Value> GetSchema() const override;
 };
 
+class DebuggerListTool : public lldb_protocol::mcp::Tool {
+public:
+  using lldb_protocol::mcp::Tool::Tool;
+  ~DebuggerListTool() = default;
+
+  llvm::Expected<lldb_protocol::mcp::CallToolResult>
+  Call(const lldb_protocol::mcp::ToolArguments &args) override;
+};
+
 } // namespace lldb_private::mcp
 
 #endif
diff --git a/lldb/source/Protocol/MCP/Server.cpp b/lldb/source/Protocol/MCP/Server.cpp
index f3489c620832f..a08874e7321af 100644
--- a/lldb/source/Protocol/MCP/Server.cpp
+++ b/lldb/source/Protocol/MCP/Server.cpp
@@ -22,34 +22,32 @@ using namespace llvm;
 using namespace lldb_private;
 using namespace lldb_protocol::mcp;
 
-ServerInfoHandle::ServerInfoHandle() : ServerInfoHandle("") {}
-
 ServerInfoHandle::ServerInfoHandle(StringRef filename) : m_filename(filename) {
   if (!m_filename.empty())
     sys::RemoveFileOnSignal(m_filename);
 }
 
-ServerInfoHandle::~ServerInfoHandle() {
-  if (m_filename.empty())
-    return;
-
-  sys::fs::remove(m_filename);
-  sys::DontRemoveFileOnSignal(m_filename);
-  m_filename.clear();
-}
+ServerInfoHandle::~ServerInfoHandle() { Remove(); }
 
-ServerInfoHandle::ServerInfoHandle(ServerInfoHandle &&other)
-    : m_filename(other.m_filename) {
+ServerInfoHandle::ServerInfoHandle(ServerInfoHandle &&other) {
   *this = std::move(other);
 }
 
 ServerInfoHandle &
 ServerInfoHandle::operator=(ServerInfoHandle &&other) noexcept {
-  m_filename = other.m_filename;
-  other.m_filename.clear();
+  m_filename = std::move(other.m_filename);
   return *this;
 }
 
+void ServerInfoHandle::Remove() {
+  if (m_filename.empty())
+    return;
+
+  sys::fs::remove(m_filename);
+  sys::DontRemoveFileOnSignal(m_filename);
+  m_filename.clear();
+}
+
 json::Value lldb_protocol::mcp::toJSON(const ServerInfo &SM) {
   return json::Object{{"connection_uri", SM.connection_uri}};
 }

This brings back the tool for listing debuggers.

This is helpful when an LLM doesn't support resources, like gemini-cli.
Copy link
Member

@JDevlieghere JDevlieghere left a comment

Choose a reason for hiding this comment

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

LGTM.

Not directly related to this PR, but something I'm planning to address that will affect this. I've gotten some reports that nearly all models struggle with the debugger IDs being 1-based. Some of them are smart enough to try 1 if 0 fails, but others just give up. I'm planning to make them zero based at the MCP layer.

@ashgti
Copy link
Contributor Author

ashgti commented Sep 15, 2025

I filed #158676 for the index issue so we don't loose track of it.

@ashgti ashgti merged commit ec8819f into llvm:main Sep 15, 2025
9 checks passed
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