[Lldb-commits] [lldb] [lldb] Add an MCP client and asynchronous request binding (PR #208371)
Jonas Devlieghere via lldb-commits
lldb-commits at lists.llvm.org
Wed Jul 8 20:19:04 PDT 2026
https://github.com/JDevlieghere created https://github.com/llvm/llvm-project/pull/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
>From 9e3bd775c04c81057194e7643d9ddfc3c92961ad Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Wed, 8 Jul 2026 13:59:18 -0700
Subject: [PATCH] [lldb] Add an MCP client and asynchronous request binding
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
---
lldb/include/lldb/Host/JSONTransport.h | 55 +++++
lldb/include/lldb/Protocol/MCP/Client.h | 93 ++++++++
lldb/source/Protocol/MCP/CMakeLists.txt | 1 +
lldb/source/Protocol/MCP/Client.cpp | 87 +++++++
lldb/source/Protocol/MCP/Protocol.cpp | 13 +
lldb/unittests/Host/JSONTransportTest.cpp | 55 ++++-
lldb/unittests/Protocol/CMakeLists.txt | 1 +
lldb/unittests/Protocol/MCPClientTest.cpp | 223 ++++++++++++++++++
lldb/unittests/Protocol/ProtocolMCPTest.cpp | 12 +
.../Host/JSONTransportTestUtilities.h | 35 ++-
10 files changed, 568 insertions(+), 7 deletions(-)
create mode 100644 lldb/include/lldb/Protocol/MCP/Client.h
create mode 100644 lldb/source/Protocol/MCP/Client.cpp
create mode 100644 lldb/unittests/Protocol/MCPClientTest.cpp
diff --git a/lldb/include/lldb/Host/JSONTransport.h b/lldb/include/lldb/Host/JSONTransport.h
index c3a90cb0c3364..4fdaf4d6f805f 100644
--- a/lldb/include/lldb/Host/JSONTransport.h
+++ b/lldb/include/lldb/Host/JSONTransport.h
@@ -521,6 +521,15 @@ class Binder : public JSONTransport<Proto>::MessageHandler {
template <typename Params, typename Fn, typename... Args>
void Bind(llvm::StringLiteral method, Fn &&fn, Args &&...args);
+ /// Bind an asynchronous handler for an incoming request. Unlike the
+ /// synchronous overload, the handler is handed a Reply it may invoke later,
+ /// which lets a handler defer its response, e.g. until after it has forwarded
+ /// the request to another transport and received the answer.
+ /// 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 function object to be used for outgoing requests.
/// e.g. `OutgoingRequest<Params, Result> Edit = bind("edit");`
/// Params must be JSON-serializable, Result must be parsable.
@@ -869,6 +878,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 ¶ms,
+ Reply<InitializeResult> reply);
+ void ToolsList(Reply<ListToolsResult> reply);
+ void ToolsCall(const CallToolParams ¶ms, Reply<CallToolResult> reply);
+ void ResourcesList(Reply<ListResourcesResult> reply);
+ void ResourcesRead(const ReadResourceParams ¶ms,
+ 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 ¶ms,
+ 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 ¶ms,
+ 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 ¶ms,
+ 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..5059fc9351c45 100644
--- a/lldb/unittests/Host/JSONTransportTest.cpp
+++ b/lldb/unittests/Host/JSONTransportTest.cpp
@@ -182,7 +182,9 @@ bool fromJSON(const json::Value &V, MyFnParams &T, json::Path P) {
struct MyFnResult {
int c = 0;
};
-json::Value toJSON(const MyFnResult &T) { return json::Object{{"c", T.c}}; }
+json::Value toJSON(const MyFnResult &T) {
+ return json::Object{{"c", T.c}};
+}
bool fromJSON(const json::Value &V, MyFnResult &T, json::Path P) {
json::ObjectMapper O(V, P);
return O && O.map("c", T.c);
@@ -780,6 +782,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 ¶ms, 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..7415279ee77bc 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;
};
More information about the lldb-commits
mailing list