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 ¶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..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 ¶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..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
