Skip to content

Commit ec8819f

Browse files
authored
[lldb-mcp] Adding a tool to list debuggers again. (#158340)
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.
1 parent 4957c47 commit ec8819f

File tree

5 files changed

+111
-44
lines changed

5 files changed

+111
-44
lines changed

lldb/include/lldb/Protocol/MCP/Server.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,7 @@ bool fromJSON(const llvm::json::Value &, ServerInfo &, llvm::json::Path);
108108
/// once it is no longer referenced.
109109
class ServerInfoHandle {
110110
public:
111-
ServerInfoHandle();
112-
explicit ServerInfoHandle(llvm::StringRef filename);
111+
explicit ServerInfoHandle(llvm::StringRef filename = "");
113112
~ServerInfoHandle();
114113

115114
ServerInfoHandle(ServerInfoHandle &&other);
@@ -121,6 +120,9 @@ class ServerInfoHandle {
121120
ServerInfoHandle &operator=(const ServerInfoHandle &) = delete;
122121
/// @}
123122

123+
/// Remove the file.
124+
void Remove();
125+
124126
private:
125127
llvm::SmallString<128> m_filename;
126128
};

lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
#include "Resource.h"
1111
#include "Tool.h"
1212
#include "lldb/Core/PluginManager.h"
13-
#include "lldb/Host/FileSystem.h"
14-
#include "lldb/Host/HostInfo.h"
1513
#include "lldb/Protocol/MCP/Server.h"
1614
#include "lldb/Utility/LLDBLog.h"
1715
#include "lldb/Utility/Log.h"
@@ -60,7 +58,9 @@ void ProtocolServerMCP::Extend(lldb_protocol::mcp::Server &server) const {
6058
"MCP initialization complete");
6159
});
6260
server.AddTool(
63-
std::make_unique<CommandTool>("lldb_command", "Run an lldb command."));
61+
std::make_unique<CommandTool>("command", "Run an lldb command."));
62+
server.AddTool(std::make_unique<DebuggerListTool>(
63+
"debugger_list", "List debugger instances with their debugger_id."));
6464
server.AddResourceProvider(std::make_unique<DebuggerResourceProvider>());
6565
}
6666

@@ -145,8 +145,8 @@ llvm::Error ProtocolServerMCP::Stop() {
145145
if (m_loop_thread.joinable())
146146
m_loop_thread.join();
147147

148+
m_server_info_handle.Remove();
148149
m_listen_handlers.clear();
149-
m_server_info_handle = ServerInfoHandle();
150150
m_instances.clear();
151151

152152
return llvm::Error::success();

lldb/source/Plugins/Protocol/MCP/Tool.cpp

Lines changed: 82 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,36 @@
77
//===----------------------------------------------------------------------===//
88

99
#include "Tool.h"
10+
#include "lldb/Core/Debugger.h"
1011
#include "lldb/Interpreter/CommandInterpreter.h"
1112
#include "lldb/Interpreter/CommandReturnObject.h"
1213
#include "lldb/Protocol/MCP/Protocol.h"
14+
#include "lldb/Utility/UriParser.h"
15+
#include "llvm/ADT/StringRef.h"
16+
#include "llvm/Support/Error.h"
17+
#include <cstdint>
18+
#include <optional>
1319

1420
using namespace lldb_private;
1521
using namespace lldb_protocol;
1622
using namespace lldb_private::mcp;
23+
using namespace lldb;
1724
using namespace llvm;
1825

1926
namespace {
27+
28+
static constexpr StringLiteral kSchemeAndHost = "lldb-mcp://debugger/";
29+
2030
struct CommandToolArguments {
21-
uint64_t debugger_id;
22-
std::string arguments;
31+
/// Either an id like '1' or a uri like 'lldb-mcp://debugger/1'.
32+
std::string debugger;
33+
std::string command;
2334
};
2435

25-
bool fromJSON(const llvm::json::Value &V, CommandToolArguments &A,
26-
llvm::json::Path P) {
27-
llvm::json::ObjectMapper O(V, P);
28-
return O && O.map("debugger_id", A.debugger_id) &&
29-
O.mapOptional("arguments", A.arguments);
36+
bool fromJSON(const json::Value &V, CommandToolArguments &A, json::Path P) {
37+
json::ObjectMapper O(V, P);
38+
return O && O.mapOptional("debugger", A.debugger) &&
39+
O.mapOptional("command", A.command);
3040
}
3141

3242
/// Helper function to create a CallToolResult from a string output.
@@ -39,9 +49,13 @@ createTextResult(std::string output, bool is_error = false) {
3949
return text_result;
4050
}
4151

52+
std::string to_uri(DebuggerSP debugger) {
53+
return (kSchemeAndHost + std::to_string(debugger->GetID())).str();
54+
}
55+
4256
} // namespace
4357

44-
llvm::Expected<lldb_protocol::mcp::CallToolResult>
58+
Expected<lldb_protocol::mcp::CallToolResult>
4559
CommandTool::Call(const lldb_protocol::mcp::ToolArguments &args) {
4660
if (!std::holds_alternative<json::Value>(args))
4761
return createStringError("CommandTool requires arguments");
@@ -52,19 +66,35 @@ CommandTool::Call(const lldb_protocol::mcp::ToolArguments &args) {
5266
if (!fromJSON(std::get<json::Value>(args), arguments, root))
5367
return root.getError();
5468

55-
lldb::DebuggerSP debugger_sp =
56-
Debugger::FindDebuggerWithID(arguments.debugger_id);
69+
lldb::DebuggerSP debugger_sp;
70+
71+
if (!arguments.debugger.empty()) {
72+
llvm::StringRef debugger_specifier = arguments.debugger;
73+
debugger_specifier.consume_front(kSchemeAndHost);
74+
uint32_t debugger_id = 0;
75+
if (debugger_specifier.consumeInteger(10, debugger_id))
76+
return createStringError(
77+
formatv("malformed debugger specifier {0}", arguments.debugger));
78+
79+
debugger_sp = Debugger::FindDebuggerWithID(debugger_id);
80+
} else {
81+
for (size_t i = 0; i < Debugger::GetNumDebuggers(); i++) {
82+
debugger_sp = Debugger::GetDebuggerAtIndex(i);
83+
if (debugger_sp)
84+
break;
85+
}
86+
}
87+
5788
if (!debugger_sp)
58-
return createStringError(
59-
llvm::formatv("no debugger with id {0}", arguments.debugger_id));
89+
return createStringError("no debugger found");
6090

6191
// FIXME: Disallow certain commands and their aliases.
6292
CommandReturnObject result(/*colors=*/false);
63-
debugger_sp->GetCommandInterpreter().HandleCommand(
64-
arguments.arguments.c_str(), eLazyBoolYes, result);
93+
debugger_sp->GetCommandInterpreter().HandleCommand(arguments.command.c_str(),
94+
eLazyBoolYes, result);
6595

6696
std::string output;
67-
llvm::StringRef output_str = result.GetOutputString();
97+
StringRef output_str = result.GetOutputString();
6898
if (!output_str.empty())
6999
output += output_str.str();
70100

@@ -78,14 +108,42 @@ CommandTool::Call(const lldb_protocol::mcp::ToolArguments &args) {
78108
return createTextResult(output, !result.Succeeded());
79109
}
80110

81-
std::optional<llvm::json::Value> CommandTool::GetSchema() const {
82-
llvm::json::Object id_type{{"type", "number"}};
83-
llvm::json::Object str_type{{"type", "string"}};
84-
llvm::json::Object properties{{"debugger_id", std::move(id_type)},
85-
{"arguments", std::move(str_type)}};
86-
llvm::json::Array required{"debugger_id"};
87-
llvm::json::Object schema{{"type", "object"},
88-
{"properties", std::move(properties)},
89-
{"required", std::move(required)}};
111+
std::optional<json::Value> CommandTool::GetSchema() const {
112+
using namespace llvm::json;
113+
Object properties{
114+
{"debugger",
115+
Object{{"type", "string"},
116+
{"description",
117+
"The debugger ID or URI to a specific debug session. If not "
118+
"specified, the first debugger will be used."}}},
119+
{"command",
120+
Object{{"type", "string"}, {"description", "An lldb command to run."}}}};
121+
Object schema{{"type", "object"}, {"properties", std::move(properties)}};
90122
return schema;
91123
}
124+
125+
Expected<lldb_protocol::mcp::CallToolResult>
126+
DebuggerListTool::Call(const lldb_protocol::mcp::ToolArguments &args) {
127+
llvm::json::Path::Root root;
128+
129+
// Return a nested Markdown list with debuggers and target.
130+
// Example output:
131+
//
132+
// - lldb-mcp://debugger/1
133+
// - lldb-mcp://debugger/2
134+
//
135+
// FIXME: Use Structured Content when we adopt protocol version 2025-06-18.
136+
std::string output;
137+
llvm::raw_string_ostream os(output);
138+
139+
const size_t num_debuggers = Debugger::GetNumDebuggers();
140+
for (size_t i = 0; i < num_debuggers; ++i) {
141+
lldb::DebuggerSP debugger_sp = Debugger::GetDebuggerAtIndex(i);
142+
if (!debugger_sp)
143+
continue;
144+
145+
os << "- " << to_uri(debugger_sp) << '\n';
146+
}
147+
148+
return createTextResult(output);
149+
}

lldb/source/Plugins/Protocol/MCP/Tool.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ class CommandTool : public lldb_protocol::mcp::Tool {
2828
std::optional<llvm::json::Value> GetSchema() const override;
2929
};
3030

31+
class DebuggerListTool : public lldb_protocol::mcp::Tool {
32+
public:
33+
using lldb_protocol::mcp::Tool::Tool;
34+
~DebuggerListTool() = default;
35+
36+
llvm::Expected<lldb_protocol::mcp::CallToolResult>
37+
Call(const lldb_protocol::mcp::ToolArguments &args) override;
38+
};
39+
3140
} // namespace lldb_private::mcp
3241

3342
#endif

lldb/source/Protocol/MCP/Server.cpp

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,34 +22,32 @@ using namespace llvm;
2222
using namespace lldb_private;
2323
using namespace lldb_protocol::mcp;
2424

25-
ServerInfoHandle::ServerInfoHandle() : ServerInfoHandle("") {}
26-
2725
ServerInfoHandle::ServerInfoHandle(StringRef filename) : m_filename(filename) {
2826
if (!m_filename.empty())
2927
sys::RemoveFileOnSignal(m_filename);
3028
}
3129

32-
ServerInfoHandle::~ServerInfoHandle() {
33-
if (m_filename.empty())
34-
return;
35-
36-
sys::fs::remove(m_filename);
37-
sys::DontRemoveFileOnSignal(m_filename);
38-
m_filename.clear();
39-
}
30+
ServerInfoHandle::~ServerInfoHandle() { Remove(); }
4031

41-
ServerInfoHandle::ServerInfoHandle(ServerInfoHandle &&other)
42-
: m_filename(other.m_filename) {
32+
ServerInfoHandle::ServerInfoHandle(ServerInfoHandle &&other) {
4333
*this = std::move(other);
4434
}
4535

4636
ServerInfoHandle &
4737
ServerInfoHandle::operator=(ServerInfoHandle &&other) noexcept {
48-
m_filename = other.m_filename;
49-
other.m_filename.clear();
38+
m_filename = std::move(other.m_filename);
5039
return *this;
5140
}
5241

42+
void ServerInfoHandle::Remove() {
43+
if (m_filename.empty())
44+
return;
45+
46+
sys::fs::remove(m_filename);
47+
sys::DontRemoveFileOnSignal(m_filename);
48+
m_filename.clear();
49+
}
50+
5351
json::Value lldb_protocol::mcp::toJSON(const ServerInfo &SM) {
5452
return json::Object{{"connection_uri", SM.connection_uri}};
5553
}

0 commit comments

Comments
 (0)