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("lldb_command", "Run an lldb command.")); + std::make_unique("command", "Run an lldb command.")); + server.AddTool(std::make_unique( + "debugger_list", "List debugger instances with their debugger_id.")); server.AddResourceProvider(std::make_unique()); } @@ -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..cb134b965c2e2 100644 --- a/lldb/source/Plugins/Protocol/MCP/Tool.cpp +++ b/lldb/source/Plugins/Protocol/MCP/Tool.cpp @@ -7,26 +7,36 @@ //===----------------------------------------------------------------------===// #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 +#include using namespace lldb_private; using namespace lldb_protocol; using namespace lldb_private::mcp; +using namespace lldb; using namespace llvm; namespace { + +static constexpr StringLiteral kSchemeAndHost = "lldb-mcp://debugger/"; + struct CommandToolArguments { - uint64_t debugger_id; - std::string arguments; + /// Either an id like '1' or a uri like 'lldb-mcp://debugger/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 +49,13 @@ createTextResult(std::string output, bool is_error = false) { return text_result; } +std::string to_uri(DebuggerSP debugger) { + return (kSchemeAndHost + std::to_string(debugger->GetID())).str(); +} + } // namespace -llvm::Expected +Expected CommandTool::Call(const lldb_protocol::mcp::ToolArguments &args) { if (!std::holds_alternative(args)) return createStringError("CommandTool requires arguments"); @@ -52,19 +66,35 @@ CommandTool::Call(const lldb_protocol::mcp::ToolArguments &args) { if (!fromJSON(std::get(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 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 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 +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/1 + // - lldb-mcp://debugger/2 + // + // 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 GetSchema() const override; }; +class DebuggerListTool : public lldb_protocol::mcp::Tool { +public: + using lldb_protocol::mcp::Tool::Tool; + ~DebuggerListTool() = default; + + llvm::Expected + 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}}; }