Author: Jonas Devlieghere
Date: 2026-07-09T17:06:22-07:00
New Revision: 15f7d143d6d61a6d3358144af611a1210eca2357

URL: 
https://github.com/llvm/llvm-project/commit/15f7d143d6d61a6d3358144af611a1210eca2357
DIFF: 
https://github.com/llvm/llvm-project/commit/15f7d143d6d61a6d3358144af611a1210eca2357.diff

LOG: [lldb] Add an MCP client and asynchronous request binding (#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

Added: 
    lldb/include/lldb/Protocol/MCP/Client.h
    lldb/source/Protocol/MCP/Client.cpp
    lldb/unittests/Protocol/MCPClientTest.cpp

Modified: 
    lldb/include/lldb/Host/JSONTransport.h
    lldb/source/Protocol/MCP/CMakeLists.txt
    lldb/source/Protocol/MCP/Protocol.cpp
    lldb/unittests/Host/JSONTransportTest.cpp
    lldb/unittests/Protocol/CMakeLists.txt
    lldb/unittests/Protocol/ProtocolMCPTest.cpp
    lldb/unittests/TestingSupport/Host/JSONTransportTestUtilities.h

Removed: 
    


################################################################################
diff  --git a/lldb/include/lldb/Host/JSONTransport.h 
b/lldb/include/lldb/Host/JSONTransport.h
index c3a90cb0c3364..0f58697e9f34d 100644
--- a/lldb/include/lldb/Host/JSONTransport.h
+++ b/lldb/include/lldb/Host/JSONTransport.h
@@ -514,6 +514,14 @@ class Binder : public JSONTransport<Proto>::MessageHandler 
{
   template <typename Result, typename Params, typename Fn, typename... Args>
   void Bind(llvm::StringLiteral method, Fn &&fn, Args &&...args);
 
+  /// Bind an asynchronous handler for an incoming request. The handler 
receives
+  /// a Reply to invoke later instead of returning a result. This lets it defer
+  /// the response, e.g. until a request it forwarded elsewhere is answered.
+  /// Handler should be e.g. `void peek(const PeekParams&, Reply<PeekResult>);`
+  /// PeekParams must be JSON parsable and PeekResult must be serializable.
+  template <typename Result, typename Params, typename Fn, typename... Args>
+  void BindAsync(llvm::StringLiteral method, Fn &&fn, Args &&...args);
+
   /// Bind a handler for an incoming event.
   /// e.g. `bind("peek", &ThisModule::peek, this);`
   /// Handler should be e.g. `void peek(const PeekParams&);`
@@ -869,6 +877,52 @@ llvm::Expected<T> Binder<Proto>::Parse(const 
llvm::json::Value &raw,
   return std::move(result);
 }
 
+#if __cplusplus >= 202002L
+template <BindingBuilder Proto>
+#else
+template <typename Proto>
+#endif
+template <typename Result, typename Params, typename Fn, typename... Args>
+void Binder<Proto>::BindAsync(llvm::StringLiteral method, Fn &&fn,
+                              Args &&...args) {
+  assert(m_request_handlers.find(method) == m_request_handlers.end() &&
+         "request already bound");
+  // The handler is captured by value and may be invoked once per incoming
+  // request, so it is invoked as an lvalue (never forwarded) to avoid moving
+  // from it between calls.
+  if constexpr (std::is_void_v<Params>) {
+    m_request_handlers[method] =
+        [fn, args...](const Req &req,
+                      Callback<void(const Resp &)> reply) mutable {
+          Reply<Result> typed_reply =
+              [req, reply = std::move(reply)](
+                  llvm::Expected<Result> result) mutable {
+                if (!result)
+                  return reply(Proto::Make(req, result.takeError()));
+                reply(Proto::Make(req, toJSON(*result)));
+              };
+          std::invoke(fn, args..., std::move(typed_reply));
+        };
+  } else {
+    m_request_handlers[method] =
+        [method, fn, args...](const Req &req,
+                              Callback<void(const Resp &)> reply) mutable {
+          Reply<Result> typed_reply =
+              [req, reply = std::move(reply)](
+                  llvm::Expected<Result> result) mutable {
+                if (!result)
+                  return reply(Proto::Make(req, result.takeError()));
+                reply(Proto::Make(req, toJSON(*result)));
+              };
+          llvm::Expected<Params> params =
+              Parse<Params>(Proto::Extract(req), method);
+          if (!params)
+            return typed_reply(params.takeError());
+          std::invoke(fn, args..., *params, std::move(typed_reply));
+        };
+  }
+}
+
 } // namespace lldb_private::transport
 
 #endif

diff  --git a/lldb/include/lldb/Protocol/MCP/Client.h 
b/lldb/include/lldb/Protocol/MCP/Client.h
new file mode 100644
index 0000000000000..cefbd136cda2a
--- /dev/null
+++ b/lldb/include/lldb/Protocol/MCP/Client.h
@@ -0,0 +1,93 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_PROTOCOL_MCP_CLIENT_H
+#define 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 <string>
+
+namespace lldb_protocol::mcp {
+
+/// An MCP client: the counterpart to Server. It speaks the MCP protocol to a
+/// remote server over a transport and exposes the protocol's requests as 
typed,
+/// asynchronous calls. Each request completes by invoking its Reply on the
+/// transport's MainLoop.
+class Client {
+public:
+  template <typename T> using Reply = lldb_private::transport::Reply<T>;
+
+  Client(std::unique_ptr<MCPTransport> transport,
+         LogCallback log_callback = {});
+  ~Client() = default;
+
+  Client(const Client &) = delete;
+  Client &operator=(const Client &) = delete;
+
+  /// Registers the client with the transport's MainLoop. Must be called before
+  /// issuing any request.
+  llvm::Error Run();
+
+  /// Sets a handler invoked when the transport disconnects (EOF).
+  void SetDisconnectHandler(llvm::unique_function<void()> handler);
+  /// Sets a handler invoked on a transport-level error.
+  void SetErrorHandler(llvm::unique_function<void(llvm::Error)> handler);
+
+  /// Outgoing MCP requests to the server. Each sends its request and invokes
+  /// its Reply asynchronously when the response arrives.
+  /// @{
+  void Initialize(const InitializeParams &params,
+                  Reply<InitializeResult> reply);
+  void ToolsList(Reply<ListToolsResult> reply);
+  void ToolsCall(const CallToolParams &params, Reply<CallToolResult> reply);
+  void ResourcesList(Reply<ListResourcesResult> reply);
+  void ResourcesRead(const ReadResourceParams &params,
+                     Reply<ReadResourceResult> reply);
+  /// @}
+
+  /// MCP notifications.
+  void NotifyInitialized();
+
+private:
+  void Log(llvm::StringRef message);
+
+  /// Trampolines registered with the binder, which forward to the move-only
+  /// handlers below. The binder requires copyable callables, so the handlers
+  /// cannot be registered with it directly.
+  /// @{
+  void HandleDisconnect();
+  void HandleError(llvm::Error error);
+  /// @}
+
+  std::unique_ptr<MCPTransport> m_transport;
+  MCPBinderUP m_binder;
+  LogCallback m_log_callback;
+
+  llvm::unique_function<void()> m_disconnect_handler;
+  llvm::unique_function<void(llvm::Error)> m_error_handler;
+
+  lldb_private::transport::OutgoingRequest<InitializeResult, InitializeParams>
+      m_initialize;
+  lldb_private::transport::OutgoingRequest<ListToolsResult, void> m_tools_list;
+  lldb_private::transport::OutgoingRequest<CallToolResult, CallToolParams>
+      m_tools_call;
+  lldb_private::transport::OutgoingRequest<ListResourcesResult, void>
+      m_resources_list;
+  lldb_private::transport::OutgoingRequest<ReadResourceResult,
+                                           ReadResourceParams>
+      m_resources_read;
+  lldb_private::transport::OutgoingEvent<void> m_notify_initialized;
+};
+
+} // namespace lldb_protocol::mcp
+
+#endif

diff  --git a/lldb/source/Protocol/MCP/CMakeLists.txt 
b/lldb/source/Protocol/MCP/CMakeLists.txt
index 5258cb61a7d10..851c54aff8eeb 100644
--- a/lldb/source/Protocol/MCP/CMakeLists.txt
+++ b/lldb/source/Protocol/MCP/CMakeLists.txt
@@ -1,4 +1,5 @@
 add_lldb_library(lldbProtocolMCP NO_PLUGIN_DEPENDENCIES
+  Client.cpp
   MCPError.cpp
   Protocol.cpp
   Server.cpp

diff  --git a/lldb/source/Protocol/MCP/Client.cpp 
b/lldb/source/Protocol/MCP/Client.cpp
new file mode 100644
index 0000000000000..edf651920ada2
--- /dev/null
+++ b/lldb/source/Protocol/MCP/Client.cpp
@@ -0,0 +1,87 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/Protocol/MCP/Client.h"
+#include "lldb/Protocol/MCP/Protocol.h"
+#include "lldb/Protocol/MCP/Transport.h"
+#include "llvm/Support/FormatVariadic.h"
+
+using namespace llvm;
+using namespace lldb_protocol::mcp;
+
+Client::Client(std::unique_ptr<MCPTransport> transport,
+               LogCallback log_callback)
+    : m_transport(std::move(transport)),
+      m_binder(std::make_unique<MCPBinder>(*m_transport)),
+      m_log_callback(std::move(log_callback)) {
+  m_initialize =
+      m_binder->Bind<InitializeResult, InitializeParams>("initialize");
+  m_tools_list = m_binder->Bind<ListToolsResult, void>("tools/list");
+  m_tools_call = m_binder->Bind<CallToolResult, CallToolParams>("tools/call");
+  m_resources_list =
+      m_binder->Bind<ListResourcesResult, void>("resources/list");
+  m_resources_read =
+      m_binder->Bind<ReadResourceResult, ReadResourceParams>("resources/read");
+  m_notify_initialized = m_binder->Bind<void>("notifications/initialized");
+}
+
+llvm::Error Client::Run() {
+  m_binder->OnDisconnect(&Client::HandleDisconnect, this);
+  m_binder->OnError(&Client::HandleError, this);
+  return m_transport->RegisterMessageHandler(*m_binder);
+}
+
+void Client::SetDisconnectHandler(llvm::unique_function<void()> handler) {
+  m_disconnect_handler = std::move(handler);
+}
+
+void Client::SetErrorHandler(llvm::unique_function<void(llvm::Error)> handler) 
{
+  m_error_handler = std::move(handler);
+}
+
+void Client::HandleDisconnect() {
+  if (m_disconnect_handler)
+    m_disconnect_handler();
+}
+
+void Client::HandleError(llvm::Error error) {
+  if (m_error_handler)
+    m_error_handler(std::move(error));
+  else
+    consumeError(std::move(error));
+}
+
+void Client::Initialize(const InitializeParams &params,
+                        Reply<InitializeResult> reply) {
+  m_initialize(params, std::move(reply));
+}
+
+void Client::ToolsList(Reply<ListToolsResult> reply) {
+  m_tools_list(std::move(reply));
+}
+
+void Client::ToolsCall(const CallToolParams &params,
+                       Reply<CallToolResult> reply) {
+  m_tools_call(params, std::move(reply));
+}
+
+void Client::ResourcesList(Reply<ListResourcesResult> reply) {
+  m_resources_list(std::move(reply));
+}
+
+void Client::ResourcesRead(const ReadResourceParams &params,
+                           Reply<ReadResourceResult> reply) {
+  m_resources_read(params, std::move(reply));
+}
+
+void Client::NotifyInitialized() { m_notify_initialized(); }
+
+void Client::Log(llvm::StringRef message) {
+  if (m_log_callback)
+    m_log_callback(message);
+}

diff  --git a/lldb/source/Protocol/MCP/Protocol.cpp 
b/lldb/source/Protocol/MCP/Protocol.cpp
index 0988f456adc26..13069ef28b175 100644
--- a/lldb/source/Protocol/MCP/Protocol.cpp
+++ b/lldb/source/Protocol/MCP/Protocol.cpp
@@ -340,6 +340,19 @@ bool fromJSON(const json::Value &V, ServerCapabilities &C, 
json::Path P) {
   if (O->find("tools") != O->end())
     C.supportsToolsList = true;
 
+  if (const json::Object *resources = O->getObject("resources")) {
+    C.supportsResourcesList =
+        resources->getBoolean("listChanged").value_or(false);
+    C.supportsResourcesSubscribe =
+        resources->getBoolean("subscribe").value_or(false);
+  }
+
+  if (O->find("completions") != O->end())
+    C.supportsCompletions = true;
+
+  if (O->find("logging") != O->end())
+    C.supportsLogging = true;
+
   return true;
 }
 

diff  --git a/lldb/unittests/Host/JSONTransportTest.cpp 
b/lldb/unittests/Host/JSONTransportTest.cpp
index 3e977a70f5d4e..afa59b81ae315 100644
--- a/lldb/unittests/Host/JSONTransportTest.cpp
+++ b/lldb/unittests/Host/JSONTransportTest.cpp
@@ -780,6 +780,57 @@ TEST_F(TransportBinderTest, 
InBoundRequestsVoidParamsAndResult) {
   EXPECT_TRUE(called);
 }
 
+// In-bound asynchronous request handler that defers its reply.
+TEST_F(TransportBinderTest, InBoundAsyncRequests) {
+  Reply<MyFnResult> saved_reply;
+  MyFnParams saved_params;
+  binder->BindAsync<MyFnResult, MyFnParams>(
+      "add", [&](const MyFnParams &params, Reply<MyFnResult> reply) {
+        saved_params = params;
+        saved_reply = std::move(reply);
+      });
+
+  EXPECT_THAT_ERROR(from_remote->Send(Request{1, "add", MyFnParams{3, 4}}),
+                    Succeeded());
+  // The handler runs but does not reply, so no response is sent yet.
+  Run();
+  ASSERT_TRUE(static_cast<bool>(saved_reply));
+  EXPECT_EQ(saved_params.a, 3);
+  EXPECT_EQ(saved_params.b, 4);
+
+  // Replying later sends the response.
+  saved_reply(MyFnResult{7});
+  EXPECT_CALL(remote, Received(Response{1, 0, MyFnResult{7}}));
+  Run();
+}
+
+TEST_F(TransportBinderTest, InBoundAsyncRequestsVoidParams) {
+  Reply<MyFnResult> saved_reply;
+  binder->BindAsync<MyFnResult, void>(
+      "ping", [&](Reply<MyFnResult> reply) { saved_reply = std::move(reply); 
});
+
+  EXPECT_THAT_ERROR(from_remote->Send(Request{2, "ping", std::nullopt}),
+                    Succeeded());
+  Run();
+  ASSERT_TRUE(static_cast<bool>(saved_reply));
+
+  saved_reply(MyFnResult{5});
+  EXPECT_CALL(remote, Received(Response{2, 0, MyFnResult{5}}));
+  Run();
+}
+
+TEST_F(TransportBinderTest, InBoundAsyncRequestsError) {
+  binder->BindAsync<MyFnResult, MyFnParams>(
+      "add", [&](const MyFnParams &, Reply<MyFnResult> reply) {
+        reply(llvm::createStringError("nope"));
+      });
+
+  EXPECT_THAT_ERROR(from_remote->Send(Request{3, "add", MyFnParams{1, 2}}),
+                    Succeeded());
+  EXPECT_CALL(remote, Received(Response{3, 1, "nope"}));
+  Run();
+}
+
 // Out-bound binding event handler.
 TEST_F(TransportBinderTest, OutBoundEvents) {
   OutgoingEvent<MyFnParams> emitEvent = binder->Bind<MyFnParams>("evt");

diff  --git a/lldb/unittests/Protocol/CMakeLists.txt 
b/lldb/unittests/Protocol/CMakeLists.txt
index 78953b1b95ae8..1e9b02d735544 100644
--- a/lldb/unittests/Protocol/CMakeLists.txt
+++ b/lldb/unittests/Protocol/CMakeLists.txt
@@ -1,4 +1,5 @@
 add_lldb_unittest(ProtocolTests
+  MCPClientTest.cpp
   MCPErrorTest.cpp
   MCPPluginTest.cpp
   MCPServerInfoTest.cpp

diff  --git a/lldb/unittests/Protocol/MCPClientTest.cpp 
b/lldb/unittests/Protocol/MCPClientTest.cpp
new file mode 100644
index 0000000000000..c7fa7bb93d9c3
--- /dev/null
+++ b/lldb/unittests/Protocol/MCPClientTest.cpp
@@ -0,0 +1,223 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "ProtocolMCPTestUtilities.h" // IWYU pragma: keep
+#include "TestingSupport/Host/JSONTransportTestUtilities.h"
+#include "TestingSupport/SubsystemRAII.h"
+#include "lldb/Host/FileSystem.h"
+#include "lldb/Host/HostInfo.h"
+#include "lldb/Host/MainLoop.h"
+#include "lldb/Host/MainLoopBase.h"
+#include "lldb/Host/Socket.h"
+#include "lldb/Protocol/MCP/Client.h"
+#include "lldb/Protocol/MCP/MCPError.h"
+#include "lldb/Protocol/MCP/Protocol.h"
+#include "lldb/Protocol/MCP/Resource.h"
+#include "lldb/Protocol/MCP/Server.h"
+#include "lldb/Protocol/MCP/Tool.h"
+#include "lldb/Protocol/MCP/Transport.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include <future>
+#include <memory>
+
+using namespace llvm;
+using namespace lldb_private;
+using namespace lldb_private::transport;
+using namespace lldb_protocol::mcp;
+
+// Flakey, see https://github.com/llvm/llvm-project/issues/152677.
+#ifndef _WIN32
+
+namespace {
+
+/// A tool that echoes a fixed string.
+class EchoTool : public Tool {
+public:
+  using Tool::Tool;
+
+  llvm::Expected<CallToolResult> Call(const ToolArguments &) override {
+    CallToolResult result;
+    result.content.emplace_back(TextContent{{"echo"}});
+    return result;
+  }
+};
+
+/// A tool that fails with an MCP error.
+class ErrorTool : public Tool {
+public:
+  using Tool::Tool;
+
+  llvm::Expected<CallToolResult> Call(const ToolArguments &) override {
+    return llvm::createStringError("boom");
+  }
+};
+
+class TestResourceProvider : public ResourceProvider {
+public:
+  using ResourceProvider::ResourceProvider;
+
+  std::vector<Resource> GetResources() const override {
+    Resource resource;
+    resource.uri = "lldb://foo/bar";
+    resource.name = "name";
+    resource.description = "description";
+    resource.mimeType = "application/json";
+    return {resource};
+  }
+
+  llvm::Expected<ReadResourceResult>
+  ReadResource(llvm::StringRef uri) const override {
+    if (uri != "lldb://foo/bar")
+      return llvm::make_error<UnsupportedURI>(uri.str());
+
+    TextResourceContents contents;
+    contents.uri = "lldb://foo/bar";
+    contents.mimeType = "application/json";
+    contents.text = "foobar";
+
+    ReadResourceResult result;
+    result.contents.push_back(contents);
+    return result;
+  }
+};
+
+class MCPClientTest : public testing::Test {
+public:
+  SubsystemRAII<FileSystem, HostInfo, Socket> subsystems;
+
+  MainLoop loop;
+  std::unique_ptr<Server> server;
+  std::unique_ptr<Client> client;
+  TestTransport<ProtocolDescriptor> *client_transport = nullptr;
+
+  void SetUp() override {
+    auto pair = TestTransport<ProtocolDescriptor>::createConnectedPair(loop);
+    client_transport = pair.first.get();
+
+    server = std::make_unique<Server>("lldb-mcp", "0.1.0");
+    server->AddTool(std::make_unique<EchoTool>("echo", "echo tool"));
+    server->AddTool(std::make_unique<ErrorTool>("error", "error tool"));
+    server->AddResourceProvider(std::make_unique<TestResourceProvider>());
+    EXPECT_THAT_ERROR(server->Accept(std::move(pair.second)), Succeeded());
+
+    client = std::make_unique<Client>(std::move(pair.first));
+    EXPECT_THAT_ERROR(client->Run(), Succeeded());
+  }
+
+  /// Runs the MainLoop, draining pending callbacks.
+  void Run() {
+    bool addition_succeeded = loop.AddPendingCallback(
+        [](MainLoopBase &loop) { loop.RequestTermination(); });
+    EXPECT_TRUE(addition_succeeded);
+    EXPECT_THAT_ERROR(loop.Run().takeError(), Succeeded());
+  }
+
+  /// Issues an asynchronous request and returns its captured result.
+  template <typename Result>
+  llvm::Expected<Result>
+  Capture(llvm::unique_function<void(Client::Reply<Result>)> invoke) {
+    std::promise<llvm::Expected<Result>> promised_result;
+    invoke([&promised_result](llvm::Expected<Result> result) {
+      promised_result.set_value(std::move(result));
+    });
+    Run();
+    return promised_result.get_future().get();
+  }
+};
+
+} // namespace
+
+TEST_F(MCPClientTest, Initialize) {
+  llvm::Expected<InitializeResult> result =
+      Capture<InitializeResult>([&](Client::Reply<InitializeResult> reply) {
+        client->Initialize(
+            InitializeParams{/*protocolVersion=*/"2024-11-05",
+                             /*capabilities=*/{},
+                             /*clientInfo=*/{"lldb-unit", "0.1.0"}},
+            std::move(reply));
+      });
+  ASSERT_THAT_EXPECTED(result, Succeeded());
+  EXPECT_EQ(result->protocolVersion, "2024-11-05");
+  EXPECT_EQ(result->serverInfo.name, "lldb-mcp");
+  EXPECT_EQ(result->serverInfo.version, "0.1.0");
+  EXPECT_TRUE(result->capabilities.supportsToolsList);
+  EXPECT_TRUE(result->capabilities.supportsResourcesList);
+}
+
+TEST_F(MCPClientTest, ToolsList) {
+  llvm::Expected<ListToolsResult> result =
+      Capture<ListToolsResult>([&](Client::Reply<ListToolsResult> reply) {
+        client->ToolsList(std::move(reply));
+      });
+  ASSERT_THAT_EXPECTED(result, Succeeded());
+  EXPECT_THAT(result->tools, testing::SizeIs(2));
+}
+
+TEST_F(MCPClientTest, ToolsCall) {
+  llvm::Expected<CallToolResult> result =
+      Capture<CallToolResult>([&](Client::Reply<CallToolResult> reply) {
+        client->ToolsCall(CallToolParams{/*name=*/"echo", /*arguments=*/{}},
+                          std::move(reply));
+      });
+  ASSERT_THAT_EXPECTED(result, Succeeded());
+  ASSERT_THAT(result->content, testing::SizeIs(1));
+  EXPECT_EQ(result->content.front().text, "echo");
+  EXPECT_FALSE(result->isError);
+}
+
+TEST_F(MCPClientTest, ToolsCallError) {
+  llvm::Expected<CallToolResult> result =
+      Capture<CallToolResult>([&](Client::Reply<CallToolResult> reply) {
+        client->ToolsCall(CallToolParams{/*name=*/"error", /*arguments=*/{}},
+                          std::move(reply));
+      });
+  EXPECT_THAT_EXPECTED(result, FailedWithMessage("boom"));
+}
+
+TEST_F(MCPClientTest, ResourcesList) {
+  llvm::Expected<ListResourcesResult> result = Capture<ListResourcesResult>(
+      [&](Client::Reply<ListResourcesResult> reply) {
+        client->ResourcesList(std::move(reply));
+      });
+  ASSERT_THAT_EXPECTED(result, Succeeded());
+  ASSERT_THAT(result->resources, testing::SizeIs(1));
+  EXPECT_EQ(result->resources.front().uri, "lldb://foo/bar");
+}
+
+TEST_F(MCPClientTest, ResourcesRead) {
+  llvm::Expected<ReadResourceResult> result =
+      Capture<ReadResourceResult>([&](Client::Reply<ReadResourceResult> reply) 
{
+        client->ResourcesRead(ReadResourceParams{/*uri=*/"lldb://foo/bar"},
+                              std::move(reply));
+      });
+  ASSERT_THAT_EXPECTED(result, Succeeded());
+  ASSERT_THAT(result->contents, testing::SizeIs(1));
+  EXPECT_EQ(result->contents.front().text, "foobar");
+}
+
+TEST_F(MCPClientTest, DisconnectHandler) {
+  bool disconnected = false;
+  client->SetDisconnectHandler([&]() { disconnected = true; });
+
+  client_transport->SimulateClosed();
+  EXPECT_TRUE(disconnected);
+}
+
+TEST_F(MCPClientTest, ErrorHandler) {
+  std::string message;
+  client->SetErrorHandler(
+      [&](llvm::Error error) { message = llvm::toString(std::move(error)); });
+
+  client_transport->SimulateError(llvm::createStringError("kaboom"));
+  EXPECT_EQ(message, "kaboom");
+}
+
+#endif // ifndef _WIN32

diff  --git a/lldb/unittests/Protocol/ProtocolMCPTest.cpp 
b/lldb/unittests/Protocol/ProtocolMCPTest.cpp
index efed243912c00..51ad8ef21033b 100644
--- a/lldb/unittests/Protocol/ProtocolMCPTest.cpp
+++ b/lldb/unittests/Protocol/ProtocolMCPTest.cpp
@@ -61,6 +61,10 @@ TEST(ProtocolMCPTest, Notification) {
 TEST(ProtocolMCPTest, ServerCapabilities) {
   ServerCapabilities capabilities;
   capabilities.supportsToolsList = true;
+  capabilities.supportsResourcesList = true;
+  capabilities.supportsResourcesSubscribe = true;
+  capabilities.supportsCompletions = true;
+  capabilities.supportsLogging = true;
 
   llvm::Expected<ServerCapabilities> deserialized_capabilities =
       roundtripJSON(capabilities);
@@ -68,6 +72,14 @@ TEST(ProtocolMCPTest, ServerCapabilities) {
 
   EXPECT_EQ(capabilities.supportsToolsList,
             deserialized_capabilities->supportsToolsList);
+  EXPECT_EQ(capabilities.supportsResourcesList,
+            deserialized_capabilities->supportsResourcesList);
+  EXPECT_EQ(capabilities.supportsResourcesSubscribe,
+            deserialized_capabilities->supportsResourcesSubscribe);
+  EXPECT_EQ(capabilities.supportsCompletions,
+            deserialized_capabilities->supportsCompletions);
+  EXPECT_EQ(capabilities.supportsLogging,
+            deserialized_capabilities->supportsLogging);
 }
 
 TEST(ProtocolMCPTest, TextContent) {

diff  --git a/lldb/unittests/TestingSupport/Host/JSONTransportTestUtilities.h 
b/lldb/unittests/TestingSupport/Host/JSONTransportTestUtilities.h
index 93845fc82d173..6fc67a2b6c8d6 100644
--- a/lldb/unittests/TestingSupport/Host/JSONTransportTestUtilities.h
+++ b/lldb/unittests/TestingSupport/Host/JSONTransportTestUtilities.h
@@ -37,28 +37,46 @@ class TestTransport final
     return std::make_pair(std::move(transports[0]), std::move(transports[1]));
   }
 
+  /// Creates a cross-wired pair, where a message sent on one transport is
+  /// delivered to the other's registered handler. This mirrors a real
+  /// connected transport pair (bytes written to one end arrive at the other),
+  /// unlike createPair() where a sent message is delivered back to the 
sender's
+  /// own handler.
+  static std::pair<std::unique_ptr<TestTransport<Proto>>,
+                   std::unique_ptr<TestTransport<Proto>>>
+  createConnectedPair(lldb_private::MainLoop &loop) {
+    auto a = std::make_unique<TestTransport<Proto>>(loop);
+    auto b = std::make_unique<TestTransport<Proto>>(loop);
+    a->m_peer = b.get();
+    b->m_peer = a.get();
+    return std::make_pair(std::move(a), std::move(b));
+  }
+
   explicit TestTransport(lldb_private::MainLoop &loop) : m_loop(loop) {}
 
   llvm::Error Send(const typename Proto::Evt &evt) override {
-    EXPECT_TRUE(m_handler) << "Send called before RegisterMessageHandler";
+    EXPECT_TRUE(Target()) << "Send called before RegisterMessageHandler";
     m_loop.AddPendingCallback([this, evt](lldb_private::MainLoopBase &) {
-      m_handler->Received(evt);
+      if (MessageHandler *target = Target())
+        target->Received(evt);
     });
     return llvm::Error::success();
   }
 
   llvm::Error Send(const typename Proto::Req &req) override {
-    EXPECT_TRUE(m_handler) << "Send called before RegisterMessageHandler";
+    EXPECT_TRUE(Target()) << "Send called before RegisterMessageHandler";
     m_loop.AddPendingCallback([this, req](lldb_private::MainLoopBase &) {
-      m_handler->Received(req);
+      if (MessageHandler *target = Target())
+        target->Received(req);
     });
     return llvm::Error::success();
   }
 
   llvm::Error Send(const typename Proto::Resp &resp) override {
-    EXPECT_TRUE(m_handler) << "Send called before RegisterMessageHandler";
+    EXPECT_TRUE(Target()) << "Send called before RegisterMessageHandler";
     m_loop.AddPendingCallback([this, resp](lldb_private::MainLoopBase &) {
-      m_handler->Received(resp);
+      if (MessageHandler *target = Target())
+        target->Received(resp);
     });
     return llvm::Error::success();
   }
@@ -97,8 +115,13 @@ class TestTransport final
   void Log(llvm::StringRef message) override {};
 
 private:
+  /// The handler a sent message is delivered to: the peer's when cross-wired
+  /// (see createConnectedPair), otherwise this transport's own handler.
+  MessageHandler *Target() { return m_peer ? m_peer->m_handler : m_handler; }
+
   lldb_private::MainLoop &m_loop;
   MessageHandler *m_handler = nullptr;
+  TestTransport<Proto> *m_peer = nullptr;
   bool m_register_should_fail = false;
 };
 


        
_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to