Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lldb/include/lldb/Protocol/MCP/Server.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -121,6 +120,9 @@ class ServerInfoHandle {
ServerInfoHandle &operator=(const ServerInfoHandle &) = delete;
/// @}

/// Remove the file.
void Remove();

private:
llvm::SmallString<128> m_filename;
};
Expand Down
8 changes: 4 additions & 4 deletions lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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>());
}

Expand Down Expand Up @@ -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();
Expand Down
106 changes: 82 additions & 24 deletions lldb/source/Plugins/Protocol/MCP/Tool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <cstdint>
#include <optional>

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.
Expand All @@ -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<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");
Expand All @@ -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();

Expand All @@ -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/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);
}
9 changes: 9 additions & 0 deletions lldb/source/Plugins/Protocol/MCP/Tool.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
26 changes: 12 additions & 14 deletions lldb/source/Protocol/MCP/Server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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}};
}
Expand Down
Loading