Skip to content

[lldb] Add SBProtocolServer to start protocol servers programmatically#209923

Merged
JDevlieghere merged 1 commit into
llvm:mainfrom
JDevlieghere:lldb-sbprotocolserver
Jul 17, 2026
Merged

[lldb] Add SBProtocolServer to start protocol servers programmatically#209923
JDevlieghere merged 1 commit into
llvm:mainfrom
JDevlieghere:lldb-sbprotocolserver

Conversation

@JDevlieghere

Copy link
Copy Markdown
Member

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

Starting a protocol server (MCP, 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
@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-lldb

Author: Jonas Devlieghere (JDevlieghere)

Changes

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


Full diff: https://github.com/llvm/llvm-project/pull/209923.diff

8 Files Affected:

  • (modified) lldb/bindings/interfaces.swig (+1)
  • (modified) lldb/include/lldb/API/LLDB.h (+1)
  • (modified) lldb/include/lldb/API/SBDefines.h (+2-1)
  • (added) lldb/include/lldb/API/SBProtocolServer.h (+54)
  • (modified) lldb/source/API/CMakeLists.txt (+1)
  • (added) lldb/source/API/SBProtocolServer.cpp (+160)
  • (modified) lldb/unittests/API/CMakeLists.txt (+1)
  • (added) lldb/unittests/API/SBProtocolServerTest.cpp (+128)
diff --git a/lldb/bindings/interfaces.swig b/lldb/bindings/interfaces.swig
index 5cdddd5136ac2..6f09c48fba3ab 100644
--- a/lldb/bindings/interfaces.swig
+++ b/lldb/bindings/interfaces.swig
@@ -140,6 +140,7 @@
 %include "lldb/API/SBProcessInfo.h"
 %include "lldb/API/SBProcessInfoList.h"
 %include "lldb/API/SBProgress.h"
+%include "lldb/API/SBProtocolServer.h"
 %include "lldb/API/SBQueue.h"
 %include "lldb/API/SBQueueItem.h"
 %include "lldb/API/SBReproducer.h"
diff --git a/lldb/include/lldb/API/LLDB.h b/lldb/include/lldb/API/LLDB.h
index 6ac35bb4a364b..af47d12dbc404 100644
--- a/lldb/include/lldb/API/LLDB.h
+++ b/lldb/include/lldb/API/LLDB.h
@@ -57,6 +57,7 @@
 #include "lldb/API/SBProcessInfo.h"
 #include "lldb/API/SBProcessInfoList.h"
 #include "lldb/API/SBProgress.h"
+#include "lldb/API/SBProtocolServer.h"
 #include "lldb/API/SBQueue.h"
 #include "lldb/API/SBQueueItem.h"
 #include "lldb/API/SBReproducer.h"
diff --git a/lldb/include/lldb/API/SBDefines.h b/lldb/include/lldb/API/SBDefines.h
index 5fcc685050c0b..7ec8e56067aa6 100644
--- a/lldb/include/lldb/API/SBDefines.h
+++ b/lldb/include/lldb/API/SBDefines.h
@@ -98,6 +98,7 @@ class LLDB_API SBProcess;
 class LLDB_API SBProcessInfo;
 class LLDB_API SBProcessInfoList;
 class LLDB_API SBProgress;
+class LLDB_API SBProtocolServer;
 class LLDB_API SBQueue;
 class LLDB_API SBQueueItem;
 class LLDB_API SBReplayOptions;
@@ -152,6 +153,6 @@ typedef lldb::CommandReturnObjectCallbackResult (*SBCommandPrintCallback)(
 typedef lldb::SBError (*SBPlatformLocateModuleCallback)(
     void *baton, const lldb::SBModuleSpec &module_spec,
     lldb::SBFileSpec &module_file_spec, lldb::SBFileSpec &symbol_file_spec);
-}
+} // namespace lldb
 
 #endif // LLDB_API_SBDEFINES_H
diff --git a/lldb/include/lldb/API/SBProtocolServer.h b/lldb/include/lldb/API/SBProtocolServer.h
new file mode 100644
index 0000000000000..625a71856aa9d
--- /dev/null
+++ b/lldb/include/lldb/API/SBProtocolServer.h
@@ -0,0 +1,54 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_API_SBPROTOCOLSERVER_H
+#define LLDB_API_SBPROTOCOLSERVER_H
+
+#include "lldb/API/SBDefines.h"
+
+namespace lldb_private {
+class SBProtocolServerImpl;
+}
+
+namespace lldb {
+
+/// A server that speaks a debugging protocol.
+///
+/// Protocol servers are shared across debuggers. Creating a server for a
+/// protocol that already has one hands back the existing server.
+class LLDB_API SBProtocolServer {
+public:
+  SBProtocolServer();
+  SBProtocolServer(const SBProtocolServer &rhs);
+  SBProtocolServer &operator=(const SBProtocolServer &rhs);
+  ~SBProtocolServer();
+
+  /// Returns the protocol server for \p protocol, creating it if this process
+  /// does not already have one. On failure \p error is set.
+  static SBProtocolServer Create(const char *protocol, lldb::SBError &error);
+
+  explicit operator bool() const;
+  bool IsValid() const;
+
+  /// Starts listening for clients on \p connection_uri, which must be an
+  /// accepting URI such as "listen://[host]:port" or "accept:///path".
+  lldb::SBError Start(const char *connection_uri);
+
+  lldb::SBError Stop();
+
+  /// Returns the URI clients can connect to, valid after a successful Start,
+  /// or nullptr if the server is not listening.
+  const char *GetConnectionURI();
+
+private:
+  std::unique_ptr<lldb_private::SBProtocolServerImpl> m_opaque_up;
+};
+
+} // namespace lldb
+
+#endif
diff --git a/lldb/source/API/CMakeLists.txt b/lldb/source/API/CMakeLists.txt
index 15c52aab41368..83ecb428d8ea4 100644
--- a/lldb/source/API/CMakeLists.txt
+++ b/lldb/source/API/CMakeLists.txt
@@ -81,6 +81,7 @@ add_lldb_library(liblldb SHARED ${option_framework}
   SBProcessInfo.cpp
   SBProcessInfoList.cpp
   SBProgress.cpp
+  SBProtocolServer.cpp
   SBQueue.cpp
   SBQueueItem.cpp
   SBReproducer.cpp
diff --git a/lldb/source/API/SBProtocolServer.cpp b/lldb/source/API/SBProtocolServer.cpp
new file mode 100644
index 0000000000000..7b3796599c082
--- /dev/null
+++ b/lldb/source/API/SBProtocolServer.cpp
@@ -0,0 +1,160 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/API/SBProtocolServer.h"
+#include "lldb/API/SBError.h"
+#include "lldb/Core/ProtocolServer.h"
+#include "lldb/Host/Socket.h"
+#include "lldb/Utility/ConstString.h"
+#include "lldb/Utility/Instrumentation.h"
+#include "lldb/Utility/UriParser.h"
+#include "llvm/ADT/StringExtras.h"
+#include "llvm/Support/FormatVariadic.h"
+
+#include <memory>
+
+using namespace lldb;
+using namespace lldb_private;
+
+/// PIMPL backing SBProtocolServer.
+class lldb_private::SBProtocolServerImpl {
+public:
+  ProtocolServer *server = nullptr;
+};
+
+SBProtocolServer::SBProtocolServer()
+    : m_opaque_up(std::make_unique<SBProtocolServerImpl>()) {
+  LLDB_INSTRUMENT_VA(this);
+}
+
+SBProtocolServer::SBProtocolServer(const SBProtocolServer &rhs)
+    : m_opaque_up(std::make_unique<SBProtocolServerImpl>(*rhs.m_opaque_up)) {
+  LLDB_INSTRUMENT_VA(this, rhs);
+}
+
+SBProtocolServer &SBProtocolServer::operator=(const SBProtocolServer &rhs) {
+  LLDB_INSTRUMENT_VA(this, rhs);
+
+  if (this != &rhs)
+    *m_opaque_up = *rhs.m_opaque_up;
+  return *this;
+}
+
+SBProtocolServer::~SBProtocolServer() = default;
+
+SBProtocolServer SBProtocolServer::Create(const char *protocol,
+                                          SBError &error) {
+  LLDB_INSTRUMENT_VA(protocol, error);
+
+  SBProtocolServer server;
+  error.Clear();
+
+  if (!protocol || !protocol[0]) {
+    error.SetErrorString("no protocol specified");
+    return server;
+  }
+
+  ProtocolServer *protocol_server = ProtocolServer::GetOrCreate(protocol);
+  if (!protocol_server) {
+    error.SetErrorStringWithFormat(
+        "unsupported protocol: %s. Supported protocols are: %s", protocol,
+        llvm::join(ProtocolServer::GetSupportedProtocols(), ", ").c_str());
+    return server;
+  }
+
+  server.m_opaque_up->server = protocol_server;
+  return server;
+}
+
+SBProtocolServer::operator bool() const {
+  LLDB_INSTRUMENT_VA(this);
+
+  return m_opaque_up->server != nullptr;
+}
+
+bool SBProtocolServer::IsValid() const {
+  LLDB_INSTRUMENT_VA(this);
+
+  return m_opaque_up->server != nullptr;
+}
+
+SBError SBProtocolServer::Start(const char *connection_uri) {
+  LLDB_INSTRUMENT_VA(this, connection_uri);
+
+  SBError error;
+  if (!m_opaque_up->server) {
+    error.SetErrorString("invalid protocol server");
+    return error;
+  }
+
+  const char *connection_error =
+      "unsupported connection specifier, expected 'accept:///path' or "
+      "'listen://[host]:port'";
+  std::optional<URI> uri = URI::Parse(connection_uri ? connection_uri : "");
+  if (!uri) {
+    error.SetErrorString(connection_error);
+    return error;
+  }
+
+  std::optional<Socket::ProtocolModePair> protocol_and_mode =
+      Socket::GetProtocolAndMode(uri->scheme);
+  if (!protocol_and_mode || protocol_and_mode->second != Socket::ModeAccept) {
+    error.SetErrorString(connection_error);
+    return error;
+  }
+
+  ProtocolServer::Connection connection;
+  connection.protocol = protocol_and_mode->first;
+  if (connection.protocol == Socket::SocketProtocol::ProtocolUnixDomain)
+    connection.name = uri->path;
+  else
+    connection.name =
+        llvm::formatv("[{0}]:{1}",
+                      uri->hostname.empty() ? "0.0.0.0" : uri->hostname,
+                      uri->port.value_or(0))
+            .str();
+
+  if (llvm::Error err = m_opaque_up->server->Start(connection))
+    error.SetErrorString(llvm::toString(std::move(err)).c_str());
+
+  return error;
+}
+
+SBError SBProtocolServer::Stop() {
+  LLDB_INSTRUMENT_VA(this);
+
+  SBError error;
+  if (!m_opaque_up->server) {
+    error.SetErrorString("invalid protocol server");
+    return error;
+  }
+
+  if (llvm::Error err = m_opaque_up->server->Stop())
+    error.SetErrorString(llvm::toString(std::move(err)).c_str());
+
+  return error;
+}
+
+const char *SBProtocolServer::GetConnectionURI() {
+  LLDB_INSTRUMENT_VA(this);
+
+  if (!m_opaque_up->server)
+    return nullptr;
+
+  Socket *socket = m_opaque_up->server->GetSocket();
+  if (!socket)
+    return nullptr;
+
+  std::vector<std::string> uris = socket->GetListeningConnectionURI();
+  if (uris.empty())
+    return nullptr;
+
+  // A listening socket may report several equivalent URIs (e.g. IPv6 and
+  // IPv4 loopback). Return the first, interned so the pointer stays valid.
+  return ConstString(uris.front()).GetCString();
+}
diff --git a/lldb/unittests/API/CMakeLists.txt b/lldb/unittests/API/CMakeLists.txt
index b86054fb353f7..a7db51aea4994 100644
--- a/lldb/unittests/API/CMakeLists.txt
+++ b/lldb/unittests/API/CMakeLists.txt
@@ -3,6 +3,7 @@ add_lldb_unittest(APITests
   SBLineEntryTest.cpp
   SBMutexTest.cpp
   SBBreakpointClearConditionTest.cpp
+  SBProtocolServerTest.cpp
 
   SBAPITEST
 
diff --git a/lldb/unittests/API/SBProtocolServerTest.cpp b/lldb/unittests/API/SBProtocolServerTest.cpp
new file mode 100644
index 0000000000000..1191c6761e549
--- /dev/null
+++ b/lldb/unittests/API/SBProtocolServerTest.cpp
@@ -0,0 +1,128 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// Use the umbrella header for -Wdocumentation.
+#include "lldb/API/LLDB.h"
+
+#include "TestingSupport/SubsystemRAII.h"
+#include "lldb/API/SBDebugger.h"
+#include "lldb/API/SBError.h"
+#include "lldb/API/SBProtocolServer.h"
+#include "gtest/gtest.h"
+
+using namespace lldb;
+using namespace lldb_private;
+
+class SBProtocolServerTest : public testing::Test {
+protected:
+  void SetUp() override {
+    debugger = SBDebugger::Create(/*source_init_files=*/false);
+  }
+
+  void TearDown() override { SBDebugger::Destroy(debugger); }
+
+  SubsystemRAII<lldb::SBDebugger> subsystems;
+  SBDebugger debugger;
+};
+
+TEST_F(SBProtocolServerTest, DefaultConstructedIsInvalid) {
+  SBProtocolServer server;
+  EXPECT_FALSE(server.IsValid());
+  EXPECT_FALSE(static_cast<bool>(server));
+}
+
+TEST_F(SBProtocolServerTest, CreateMCP) {
+  SBError error;
+  SBProtocolServer server = SBProtocolServer::Create("MCP", error);
+  ASSERT_TRUE(error.Success()) << error.GetCString();
+  EXPECT_TRUE(server.IsValid());
+  EXPECT_TRUE(static_cast<bool>(server));
+  // No socket is bound until Start, so there is no connection URI yet.
+  EXPECT_EQ(server.GetConnectionURI(), nullptr);
+}
+
+TEST_F(SBProtocolServerTest, CreateUnsupportedProtocol) {
+  SBError error;
+  SBProtocolServer server = SBProtocolServer::Create("bogus", error);
+  EXPECT_TRUE(error.Fail());
+  EXPECT_FALSE(server.IsValid());
+}
+
+TEST_F(SBProtocolServerTest, CreateNullProtocol) {
+  SBError error;
+  SBProtocolServer server = SBProtocolServer::Create(nullptr, error);
+  EXPECT_TRUE(error.Fail());
+  EXPECT_FALSE(server.IsValid());
+}
+
+TEST_F(SBProtocolServerTest, CreateEmptyProtocol) {
+  SBError error;
+  SBProtocolServer server = SBProtocolServer::Create("", error);
+  EXPECT_TRUE(error.Fail());
+  EXPECT_FALSE(server.IsValid());
+}
+
+TEST_F(SBProtocolServerTest, StartAndStop) {
+  SBError error;
+  SBProtocolServer server = SBProtocolServer::Create("MCP", error);
+  ASSERT_TRUE(server.IsValid());
+
+  // Bind an ephemeral port on the loopback interface so the test never touches
+  // an external network.
+  SBError start_error = server.Start("listen://[localhost]:0");
+  ASSERT_TRUE(start_error.Success()) << start_error.GetCString();
+
+  // Once listening, the server reports the concrete URI a client can connect
+  // to (the ephemeral port resolved above).
+  EXPECT_NE(server.GetConnectionURI(), nullptr);
+
+  EXPECT_TRUE(server.Stop().Success());
+}
+
+TEST_F(SBProtocolServerTest, StartInvalidServer) {
+  // A default-constructed server has no backing protocol server, so Start fails
+  // before the connection URI is ever examined.
+  SBProtocolServer server;
+  EXPECT_TRUE(server.Start("").Fail());
+}
+
+TEST_F(SBProtocolServerTest, StopNotRunning) {
+  SBError error;
+  SBProtocolServer server = SBProtocolServer::Create("MCP", error);
+  ASSERT_TRUE(server.IsValid());
+  // Stopping a server that was never started is an error.
+  EXPECT_TRUE(server.Stop().Fail());
+}
+
+TEST_F(SBProtocolServerTest, StartRejectsNonAcceptingURI) {
+  SBError error;
+  SBProtocolServer server = SBProtocolServer::Create("MCP", error);
+  ASSERT_TRUE(server.IsValid());
+  // A connecting URI (not an accepting one) is rejected before any socket work.
+  EXPECT_TRUE(server.Start("connect://localhost:1234").Fail());
+  EXPECT_TRUE(server.Start("not a uri").Fail());
+}
+
+TEST_F(SBProtocolServerTest, CopyReferencesSameServer) {
+  SBError error;
+  SBProtocolServer server = SBProtocolServer::Create("MCP", error);
+  ASSERT_TRUE(server.IsValid());
+
+  // The protocol server is owned by the engine, not by SBProtocolServer, so a
+  // copy just references the same server and the source stays valid.
+  SBProtocolServer copy(server);
+  EXPECT_TRUE(copy.IsValid());
+  EXPECT_TRUE(server.IsValid());
+
+  // Copy assignment behaves the same way.
+  SBProtocolServer assigned;
+  EXPECT_FALSE(assigned.IsValid());
+  assigned = server;
+  EXPECT_TRUE(assigned.IsValid());
+  EXPECT_TRUE(server.IsValid());
+}

@bulbazord bulbazord left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@JDevlieghere
JDevlieghere merged commit 9b096bb into llvm:main Jul 17, 2026
13 checks passed
@JDevlieghere
JDevlieghere deleted the lldb-sbprotocolserver branch July 17, 2026 18:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants