[Lldb-commits] [lldb] [lldb-mcp] Replace byte forwarding with a protocol-aware multiplexer (PR #208506)
Jonas Devlieghere via lldb-commits
lldb-commits at lists.llvm.org
Thu Jul 9 20:09:27 PDT 2026
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 <jonas at devlieghere.com>
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 <jonas at devlieghere.com>
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;
}
More information about the lldb-commits
mailing list