[lldb] Add Model Context Protocol (MCP) support to LLDB - #143628
Conversation
3c1cb49 to
4156ce2
Compare
|
@llvm/pr-subscribers-lldb Author: Jonas Devlieghere (JDevlieghere) ChangesThis 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 ( The MCP sever supports one tool ( 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:
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]
|
|
I'm adding the folks that chimed in on the RFC as reviewers. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Should we prevent some commands from being executed here?
For example, quit?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I've added a FIXME for now.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
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 |
|
I've been thinking about this too. I think you're right to distinguish two use cases:
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, 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 |
labath
left a comment
There was a problem hiding this comment.
A very lightweight review. I'm focusing on the low-level details. I have no clue about this AI stuff.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
With #144610 I can drop the timeout here. I already treat timeouts as non-fatal.
There was a problem hiding this comment.
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.
|
There was a problem hiding this comment.
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.
|
Rebased and addressed @ashgti's comments:
|
ashgti
left a comment
There was a problem hiding this comment.
LGTM!
I look forward to trying this out and iterating on this.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
I copied this from DAP where I think @ashgti wrote it originally. Happy to settle on something different and share that across the two.
There was a problem hiding this comment.
I'd rather them be consistent if we need to change lldb-dap as well.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I'll put up a PR to change lldb-dap to match lldb.
There was a problem hiding this comment.
typo? (I think saving one char isn't worth it)
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.
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)
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)
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)
|
I haven't looked at everything, but I believe my concerns are addressed now. |
|
@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. |
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)
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)
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)
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)
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)
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.
#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
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
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)
* [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>
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 tostartandstopthe server.The MCP sever supports one tool (
lldb_command) which executes a command, but can easily be extended with more commands.