https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/208506
>From 6a79f9746717b6f0a6b2f27980d1ad0e320daa6f Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <[email protected]> Date: Thu, 9 Jul 2026 09:27:15 -0700 Subject: [PATCH 1/2] [lldb-mcp] Replace byte forwarding with a protocol-aware multiplexer 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 #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 --- lldb/tools/lldb-mcp/CMakeLists.txt | 1 + lldb/tools/lldb-mcp/Multiplexer.cpp | 170 ++++++++++++++++++++++++++++ lldb/tools/lldb-mcp/Multiplexer.h | 86 ++++++++++++++ lldb/tools/lldb-mcp/lldb-mcp.cpp | 73 ++++++------ 4 files changed, 294 insertions(+), 36 deletions(-) create mode 100644 lldb/tools/lldb-mcp/Multiplexer.cpp create mode 100644 lldb/tools/lldb-mcp/Multiplexer.h diff --git a/lldb/tools/lldb-mcp/CMakeLists.txt b/lldb/tools/lldb-mcp/CMakeLists.txt index ab68f853a0f49..3fbc95aeed24a 100644 --- a/lldb/tools/lldb-mcp/CMakeLists.txt +++ b/lldb/tools/lldb-mcp/CMakeLists.txt @@ -1,5 +1,6 @@ add_lldb_tool(lldb-mcp lldb-mcp.cpp + Multiplexer.cpp LINK_COMPONENTS Option diff --git a/lldb/tools/lldb-mcp/Multiplexer.cpp b/lldb/tools/lldb-mcp/Multiplexer.cpp new file mode 100644 index 0000000000000..6c13136d3e3e5 --- /dev/null +++ b/lldb/tools/lldb-mcp/Multiplexer.cpp @@ -0,0 +1,170 @@ +//===----------------------------------------------------------------------===// +// +// 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 "Multiplexer.h" +#include "llvm/ADT/StringSwitch.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/JSON.h" + +using namespace llvm; +using namespace lldb_protocol::mcp; +using namespace lldb_mcp; + +namespace { + +/// Server identity reported to the client during initialization. +/// @{ +constexpr llvm::StringLiteral kServerName = "lldb-mcp"; +constexpr llvm::StringLiteral kServerVersion = "0.1.0"; +/// @} + +/// Client-facing tool names. +/// @{ +constexpr llvm::StringLiteral kToolCommand = "command"; +constexpr llvm::StringLiteral kToolSessionsList = "sessions_list"; +/// @} + +/// Backend tool names, as exposed by the LLDB MCP server. +/// @{ +constexpr llvm::StringLiteral kBackendToolCommand = "command"; +constexpr llvm::StringLiteral kBackendToolDebuggerList = "debugger_list"; +/// @} + +} // namespace + +Multiplexer::Multiplexer(std::unique_ptr<MCPTransport> client_transport, + LogCallback log_callback) + : m_client_transport(std::move(client_transport)), + m_client_binder(std::make_unique<MCPBinder>(*m_client_transport)), + m_log_callback(std::move(log_callback)) { + m_client_binder->Bind<InitializeResult, InitializeParams>( + "initialize", &Multiplexer::HandleInitialize, this); + m_client_binder->Bind<ListToolsResult, void>( + "tools/list", &Multiplexer::HandleToolsList, this); + m_client_binder->BindAsync<CallToolResult, CallToolParams>( + "tools/call", &Multiplexer::HandleToolsCall, this); + m_client_binder->BindAsync<ListResourcesResult, void>( + "resources/list", &Multiplexer::HandleResourcesList, this); + m_client_binder->BindAsync<ReadResourceResult, ReadResourceParams>( + "resources/read", &Multiplexer::HandleResourcesRead, this); + m_client_binder->Bind<void>("notifications/initialized", [this]() { + Log("client initialization complete"); + }); +} + +void Multiplexer::AddBackend(std::unique_ptr<Client> backend) { + m_backends.push_back(std::move(backend)); +} + +llvm::Error Multiplexer::Run() { + m_client_binder->OnDisconnect(&Multiplexer::HandleDisconnect, this); + m_client_binder->OnError([this](llvm::Error err) { + Log(formatv("client transport error: {0}", toString(std::move(err))).str()); + }); + return m_client_transport->RegisterMessageHandler(*m_client_binder); +} + +void Multiplexer::SetDisconnectHandler(llvm::unique_function<void()> handler) { + m_disconnect_handler = std::move(handler); +} + +Client *Multiplexer::Route() { + if (m_backends.empty()) + return nullptr; + return m_backends.front().get(); +} + +Expected<InitializeResult> +Multiplexer::HandleInitialize(const InitializeParams &) { + InitializeResult result; + result.protocolVersion = kProtocolVersion; + result.capabilities.supportsToolsList = true; + result.capabilities.supportsResourcesList = true; + result.serverInfo.name = kServerName; + result.serverInfo.version = kServerVersion; + return result; +} + +Expected<ListToolsResult> Multiplexer::HandleToolsList() { + ListToolsResult result; + + ToolDefinition command; + command.name = kToolCommand; + command.description = "Run an LLDB command in a debug session."; + command.inputSchema = json::Object{ + {"type", "object"}, + {"properties", + json::Object{ + {"command", + json::Object{{"type", "string"}, + {"description", "The LLDB command to run."}}}, + {"debugger", + json::Object{{"type", "string"}, + {"description", + "The debugger id or URI selecting the debug " + "session. Defaults to the first."}}}, + }}, + {"required", json::Array{"command"}}, + }; + result.tools.push_back(std::move(command)); + + ToolDefinition sessions_list; + sessions_list.name = kToolSessionsList; + sessions_list.description = "List the active debug sessions."; + sessions_list.inputSchema = json::Object{{"type", "object"}}; + result.tools.push_back(std::move(sessions_list)); + + return result; +} + +void Multiplexer::HandleToolsCall(const CallToolParams ¶ms, + Reply<CallToolResult> reply) { + Client *backend = Route(); + if (!backend) + return reply(createStringError("no debug session available")); + + std::optional<StringRef> backend_tool = + StringSwitch<std::optional<StringRef>>(params.name) + .Case(kToolCommand, kBackendToolCommand) + .Case(kToolSessionsList, kBackendToolDebuggerList) + .Default(std::nullopt); + if (!backend_tool) + return reply(createStringError(formatv("no tool \"{0}\"", params.name))); + + CallToolParams backend_params; + backend_params.arguments = params.arguments; + backend_params.name = *backend_tool; + + backend->ToolsCall(backend_params, std::move(reply)); +} + +void Multiplexer::HandleResourcesList(Reply<ListResourcesResult> reply) { + Client *backend = Route(); + if (!backend) + return reply(ListResourcesResult{}); + backend->ResourcesList(std::move(reply)); +} + +void Multiplexer::HandleResourcesRead(const ReadResourceParams ¶ms, + Reply<ReadResourceResult> reply) { + Client *backend = Route(); + if (!backend) + return reply(createStringError("no debug session available")); + backend->ResourcesRead(params, std::move(reply)); +} + +void Multiplexer::HandleDisconnect() { + if (m_disconnect_handler) + m_disconnect_handler(); +} + +void Multiplexer::Log(llvm::StringRef message) { + if (m_log_callback) + m_log_callback(message); +} diff --git a/lldb/tools/lldb-mcp/Multiplexer.h b/lldb/tools/lldb-mcp/Multiplexer.h new file mode 100644 index 0000000000000..c6a09684cfdb5 --- /dev/null +++ b/lldb/tools/lldb-mcp/Multiplexer.h @@ -0,0 +1,86 @@ +//===----------------------------------------------------------------------===// +// +// 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_TOOLS_LLDB_MCP_MULTIPLEXER_H +#define LLDB_TOOLS_LLDB_MCP_MULTIPLEXER_H + +#include "lldb/Protocol/MCP/Client.h" +#include "lldb/Protocol/MCP/Protocol.h" +#include "lldb/Protocol/MCP/Transport.h" +#include "llvm/ADT/FunctionExtras.h" +#include "llvm/Support/Error.h" +#include <memory> +#include <vector> + +namespace lldb_mcp { + +/// The multiplexer is the single MCP server a client talks to. It presents a +/// unified tool and resource surface and fans out to one or more backend LLDB +/// MCP servers, each reached through an mcp::Client. +/// +/// Requests that can be answered locally (initialize, tools/list) are handled +/// directly. Requests that target a backend (tools/call, resources/list, +/// resources/read) are forwarded asynchronously, and the backend's answer is +/// relayed back to the client. +class Multiplexer { +public: + template <typename T> using Reply = lldb_private::transport::Reply<T>; + + Multiplexer( + std::unique_ptr<lldb_protocol::mcp::MCPTransport> client_transport, + lldb_protocol::mcp::LogCallback log_callback = {}); + + /// Adds a backend. The client must already be running (see Client::Run). + /// Takes ownership. + void AddBackend(std::unique_ptr<lldb_protocol::mcp::Client> backend); + + /// Registers the client-facing handlers. Backends should be added first. + llvm::Error Run(); + + /// Sets a handler invoked when the client disconnects (EOF). + void SetDisconnectHandler(llvm::unique_function<void()> handler); + +private: + /// Locally-answered requests. + /// @{ + llvm::Expected<lldb_protocol::mcp::InitializeResult> + HandleInitialize(const lldb_protocol::mcp::InitializeParams ¶ms); + llvm::Expected<lldb_protocol::mcp::ListToolsResult> HandleToolsList(); + /// @} + + /// Requests forwarded to a backend. + /// @{ + void HandleToolsCall(const lldb_protocol::mcp::CallToolParams ¶ms, + Reply<lldb_protocol::mcp::CallToolResult> reply); + void + HandleResourcesList(Reply<lldb_protocol::mcp::ListResourcesResult> reply); + void HandleResourcesRead(const lldb_protocol::mcp::ReadResourceParams ¶ms, + Reply<lldb_protocol::mcp::ReadResourceResult> reply); + /// @} + + /// Trampoline forwarding to the move-only disconnect handler. + void HandleDisconnect(); + + void Log(llvm::StringRef message); + + /// Returns the backend a request routes to, or null if none is available. + /// All requests currently route to the single backend. + lldb_protocol::mcp::Client *Route(); + + std::unique_ptr<lldb_protocol::mcp::MCPTransport> m_client_transport; + lldb_protocol::mcp::MCPBinderUP m_client_binder; + lldb_protocol::mcp::LogCallback m_log_callback; + + std::vector<std::unique_ptr<lldb_protocol::mcp::Client>> m_backends; + + llvm::unique_function<void()> m_disconnect_handler; +}; + +} // namespace lldb_mcp + +#endif diff --git a/lldb/tools/lldb-mcp/lldb-mcp.cpp b/lldb/tools/lldb-mcp/lldb-mcp.cpp index 0d17500648049..7a21cca743de8 100644 --- a/lldb/tools/lldb-mcp/lldb-mcp.cpp +++ b/lldb/tools/lldb-mcp/lldb-mcp.cpp @@ -6,6 +6,7 @@ // //===----------------------------------------------------------------------===// +#include "Multiplexer.h" #include "lldb/Host/Config.h" #include "lldb/Host/File.h" #include "lldb/Host/FileSystem.h" @@ -15,7 +16,9 @@ #include "lldb/Host/MainLoopBase.h" #include "lldb/Host/ProcessLaunchInfo.h" #include "lldb/Host/Socket.h" +#include "lldb/Protocol/MCP/Client.h" #include "lldb/Protocol/MCP/Server.h" +#include "lldb/Protocol/MCP/Transport.h" #include "lldb/Utility/FileSpec.h" #include "lldb/Utility/Status.h" #include "lldb/Utility/UriParser.h" @@ -56,8 +59,6 @@ constexpr StringLiteral kDriverName = "lldb.exe"; constexpr StringLiteral kDriverName = "lldb"; #endif -constexpr size_t kForwardIOBufferSize = 1024; - inline void exitWithError(llvm::Error Err, StringRef Prefix = "") { handleAllErrors(std::move(Err), [&](ErrorInfoBase &Info) { WithColor::error(errs(), Prefix) << Info.message() << '\n'; @@ -127,37 +128,21 @@ Expected<ServerInfo> loadOrStart( return createStringError("timed out waiting for MCP server to start"); } -void forwardIO(MainLoopBase &loop, IOObjectSP &from, IOObjectSP &to) { - char buf[kForwardIOBufferSize]; - size_t num_bytes = sizeof(buf); - - if (llvm::Error err = from->Read(buf, num_bytes).takeError()) - exitWithError(std::move(err)); - - // EOF reached. - if (num_bytes == 0) - return loop.RequestTermination(); - - if (llvm::Error err = to->Write(buf, num_bytes).takeError()) - exitWithError(std::move(err)); -} - -llvm::Error connectAndForwardIO(lldb_private::MainLoop &loop, ServerInfo &info, - IOObjectSP &input_sp, IOObjectSP &output_sp) { +/// Connects to the MCP server described by \p info and returns the connected +/// socket. +Expected<IOObjectSP> connectToServer(const ServerInfo &info) { auto uri = lldb_private::URI::Parse(info.connection_uri); if (!uri) return createStringError("invalid connection_uri"); std::optional<lldb_private::Socket::ProtocolModePair> protocol_and_mode = lldb_private::Socket::GetProtocolAndMode(uri->scheme); - if (!protocol_and_mode) return createStringError("unknown protocol scheme"); lldb_private::Status status; std::unique_ptr<lldb_private::Socket> sock = lldb_private::Socket::Create(protocol_and_mode->first, status); - if (status.Fail()) return status.takeError(); @@ -169,20 +154,15 @@ llvm::Error connectAndForwardIO(lldb_private::MainLoop &loop, ServerInfo &info, if (status.Fail()) return status.takeError(); - IOObjectSP sock_sp = std::move(sock); - auto input_handle = loop.RegisterReadObject( - input_sp, std::bind(forwardIO, std::placeholders::_1, input_sp, sock_sp), - status); - if (status.Fail()) - return status.takeError(); - - auto socket_handle = loop.RegisterReadObject( - sock_sp, std::bind(forwardIO, std::placeholders::_1, sock_sp, output_sp), - status); - if (status.Fail()) - return status.takeError(); + return IOObjectSP(std::move(sock)); +} - return loop.Run().takeError(); +/// Returns a log callback that traces to stderr when LLDB_MCP_LOG is set in the +/// environment, since stdout is reserved for the MCP protocol. +lldb_protocol::mcp::LogCallback makeLogCallback() { + if (!std::getenv("LLDB_MCP_LOG")) + return {}; + return [](StringRef message) { errs() << message << '\n'; }; } } // namespace @@ -236,8 +216,29 @@ int main(int argc, char *argv[]) { [](MainLoopBase &loop) { loop.RequestTermination(); }); }); - if (llvm::Error error = - connectAndForwardIO(loop, *server_info, input_sp, output_sp)) + // Connect to the backend LLDB MCP server and drive it as an MCP client. + Expected<IOObjectSP> backend_io = connectToServer(*server_info); + if (!backend_io) + exitWithError(backend_io.takeError()); + + auto backend_transport = std::make_unique<lldb_protocol::mcp::Transport>( + loop, *backend_io, *backend_io, makeLogCallback()); + auto backend = std::make_unique<lldb_protocol::mcp::Client>( + std::move(backend_transport), makeLogCallback()); + if (llvm::Error error = backend->Run()) + exitWithError(std::move(error)); + + // Present a unified MCP server to the client over stdio. + auto client_transport = std::make_unique<lldb_protocol::mcp::Transport>( + loop, input_sp, output_sp, makeLogCallback()); + lldb_mcp::Multiplexer multiplexer(std::move(client_transport), + makeLogCallback()); + multiplexer.AddBackend(std::move(backend)); + multiplexer.SetDisconnectHandler([]() { loop.RequestTermination(); }); + if (llvm::Error error = multiplexer.Run()) + exitWithError(std::move(error)); + + if (llvm::Error error = loop.Run().takeError()) exitWithError(std::move(error)); return EXIT_SUCCESS; >From 690484bdff48c65b6514d0f24f1b271079acf30d Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <[email protected]> Date: Thu, 9 Jul 2026 20:09:09 -0700 Subject: [PATCH 2/2] Address review feedback --- lldb/include/lldb/Protocol/MCP/Protocol.h | 4 ++++ .../Plugins/Protocol/MCP/ProtocolServerMCP.cpp | 4 ++-- lldb/source/Protocol/MCP/Protocol.cpp | 3 +++ lldb/tools/lldb-mcp/Multiplexer.cpp | 13 ++++++++----- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lldb/include/lldb/Protocol/MCP/Protocol.h b/lldb/include/lldb/Protocol/MCP/Protocol.h index a0ba8659ffe24..1836afabc56f1 100644 --- a/lldb/include/lldb/Protocol/MCP/Protocol.h +++ b/lldb/include/lldb/Protocol/MCP/Protocol.h @@ -25,6 +25,10 @@ namespace lldb_protocol::mcp { static llvm::StringLiteral kProtocolVersion = "2024-11-05"; +/// Version reported by LLDB's MCP server. The function avoids having to include +/// the version header. +llvm::StringLiteral GetServerVersion(); + /// A Request or Response 'id'. /// /// NOTE: This differs from the JSON-RPC 2.0 spec. The MCP spec says this must diff --git a/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp b/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp index 96d6910ce5ce5..7264ab00fd7f0 100644 --- a/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp +++ b/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp @@ -26,7 +26,6 @@ using namespace llvm; LLDB_PLUGIN_DEFINE(ProtocolServerMCP) static constexpr llvm::StringLiteral kName = "lldb-mcp"; -static constexpr llvm::StringLiteral kVersion = "0.1.0"; ProtocolServerMCP::ProtocolServerMCP() : ProtocolServer() {} @@ -106,7 +105,8 @@ llvm::Error ProtocolServerMCP::Start(ProtocolServer::Connection connection) { m_client_count = 0; m_server = std::make_unique<lldb_protocol::mcp::Server>( - std::string(kName), std::string(kVersion), [](StringRef message) { + std::string(kName), std::string(lldb_protocol::mcp::GetServerVersion()), + [](StringRef message) { LLDB_LOG(GetLog(LLDBLog::Host), "MCP Server: {0}", message); }); Extend(*m_server); diff --git a/lldb/source/Protocol/MCP/Protocol.cpp b/lldb/source/Protocol/MCP/Protocol.cpp index 13069ef28b175..e4354346f7927 100644 --- a/lldb/source/Protocol/MCP/Protocol.cpp +++ b/lldb/source/Protocol/MCP/Protocol.cpp @@ -7,6 +7,7 @@ //===----------------------------------------------------------------------===// #include "lldb/Protocol/MCP/Protocol.h" +#include "lldb/Version/Version.inc" #include "llvm/Support/ErrorHandling.h" #include "llvm/Support/JSON.h" @@ -14,6 +15,8 @@ using namespace llvm; namespace lldb_protocol::mcp { +llvm::StringLiteral GetServerVersion() { return LLDB_VERSION_STRING; } + static bool mapRaw(const json::Value &Params, StringLiteral Prop, std::optional<json::Value> &V, json::Path P) { const auto *O = Params.getAsObject(); diff --git a/lldb/tools/lldb-mcp/Multiplexer.cpp b/lldb/tools/lldb-mcp/Multiplexer.cpp index 6c13136d3e3e5..02bb40477c30a 100644 --- a/lldb/tools/lldb-mcp/Multiplexer.cpp +++ b/lldb/tools/lldb-mcp/Multiplexer.cpp @@ -18,11 +18,8 @@ using namespace lldb_mcp; namespace { -/// Server identity reported to the client during initialization. -/// @{ +/// Server name reported to the client during initialization. constexpr llvm::StringLiteral kServerName = "lldb-mcp"; -constexpr llvm::StringLiteral kServerVersion = "0.1.0"; -/// @} /// Client-facing tool names. /// @{ @@ -63,6 +60,12 @@ void Multiplexer::AddBackend(std::unique_ptr<Client> backend) { } llvm::Error Multiplexer::Run() { + // Run is called only after a backend has been added. Starting with none is a + // setup bug, not a usable server. + if (m_backends.empty()) + return llvm::createStringError( + "no backends registered before Multiplexer::Run"); + m_client_binder->OnDisconnect(&Multiplexer::HandleDisconnect, this); m_client_binder->OnError([this](llvm::Error err) { Log(formatv("client transport error: {0}", toString(std::move(err))).str()); @@ -87,7 +90,7 @@ Multiplexer::HandleInitialize(const InitializeParams &) { result.capabilities.supportsToolsList = true; result.capabilities.supportsResourcesList = true; result.serverInfo.name = kServerName; - result.serverInfo.version = kServerVersion; + result.serverInfo.version = GetServerVersion(); return result; } _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
