[Lldb-commits] [lldb] [lldb] Add unit tests for the MCP server (PR #202752)
Jonas Devlieghere via lldb-commits
lldb-commits at lists.llvm.org
Tue Jun 9 12:47:58 PDT 2026
https://github.com/JDevlieghere created https://github.com/llvm/llvm-project/pull/202752
Add unit-test coverage for the MCP protocol types and server under source/Protocol/MCP and the MCP plugin under
source/Plugins/Protocol/MCP.
The Server handlers run over the in-memory TestTransport, which gains SimulateError/SimulateClosed/SetRegisterMessageHandlerShouldFail helpers to drive the handler lifecycle without a real socket.
Code that touches the filesystem or otherwise requires mucking with the test environment are deliberately left uncovered until those layers can be mocked.
Assisted-by: Claude
>From e40ae165f9c8668fb154eaf7feea47a8e5d10316 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Tue, 9 Jun 2026 12:37:58 -0700
Subject: [PATCH] [lldb] Add unit tests for the MCP server
Add unit-test coverage for the MCP protocol types and server under
source/Protocol/MCP and the MCP plugin under
source/Plugins/Protocol/MCP.
The Server handlers run over the in-memory TestTransport, which gains
SimulateError/SimulateClosed/SetRegisterMessageHandlerShouldFail helpers
to drive the handler lifecycle without a real socket.
Code that touches the filesystem or otherwise requires mucking with the
test environment are deliberately left uncovered until those layers can
be mocked.
Assisted-by: Claude
---
lldb/unittests/Protocol/CMakeLists.txt | 16 +
lldb/unittests/Protocol/MCPErrorTest.cpp | 73 +++
lldb/unittests/Protocol/MCPPluginTest.cpp | 414 ++++++++++++++++++
lldb/unittests/Protocol/MCPServerInfoTest.cpp | 40 ++
lldb/unittests/Protocol/MCPTransportTest.cpp | 40 ++
.../Protocol/ProtocolMCPServerTest.cpp | 161 +++++++
lldb/unittests/Protocol/ProtocolMCPTest.cpp | 392 +++++++++++++++++
.../Host/JSONTransportTestUtilities.h | 25 ++
8 files changed, 1161 insertions(+)
create mode 100644 lldb/unittests/Protocol/MCPErrorTest.cpp
create mode 100644 lldb/unittests/Protocol/MCPPluginTest.cpp
create mode 100644 lldb/unittests/Protocol/MCPServerInfoTest.cpp
create mode 100644 lldb/unittests/Protocol/MCPTransportTest.cpp
diff --git a/lldb/unittests/Protocol/CMakeLists.txt b/lldb/unittests/Protocol/CMakeLists.txt
index f877517ea233d..78953b1b95ae8 100644
--- a/lldb/unittests/Protocol/CMakeLists.txt
+++ b/lldb/unittests/Protocol/CMakeLists.txt
@@ -1,10 +1,26 @@
add_lldb_unittest(ProtocolTests
+ MCPErrorTest.cpp
+ MCPPluginTest.cpp
+ MCPServerInfoTest.cpp
+ MCPTransportTest.cpp
ProtocolMCPTest.cpp
ProtocolMCPServerTest.cpp
+ LINK_COMPONENTS
+ Support
LINK_LIBS
+ lldbCore
+ lldbCommands
lldbHost
+ lldbInterpreter
lldbProtocolMCP
+ lldbPluginPlatformMacOSX
+ lldbPluginProtocolServerMCP
+ lldbPluginScriptInterpreterNone
+ lldbSymbol
+ lldbTarget
lldbUtility
+ lldbUtilityHelpers
+ lldbHostHelpers
LLVMTestingSupport
)
diff --git a/lldb/unittests/Protocol/MCPErrorTest.cpp b/lldb/unittests/Protocol/MCPErrorTest.cpp
new file mode 100644
index 0000000000000..7bf01f1042913
--- /dev/null
+++ b/lldb/unittests/Protocol/MCPErrorTest.cpp
@@ -0,0 +1,73 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/MCPError.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/raw_ostream.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gtest/gtest.h"
+#include <system_error>
+
+using namespace llvm;
+using namespace lldb_protocol::mcp;
+
+static std::string Log(const ErrorInfoBase &info) {
+ std::string message;
+ raw_string_ostream os(message);
+ info.log(os);
+ return message;
+}
+
+TEST(MCPErrorTest, DefaultErrorCode) {
+ MCPError error("something went wrong");
+ EXPECT_EQ(error.getMessage(), "something went wrong");
+ EXPECT_EQ(Log(error), "something went wrong");
+
+ std::error_code ec = error.convertToErrorCode();
+ EXPECT_EQ(ec.value(), MCPError::kInternalError);
+ EXPECT_EQ(ec.category(), std::generic_category());
+}
+
+TEST(MCPErrorTest, CustomErrorCode) {
+ MCPError error("not found", MCPError::kResourceNotFound);
+ EXPECT_EQ(error.getMessage(), "not found");
+
+ std::error_code ec = error.convertToErrorCode();
+ EXPECT_EQ(ec.value(), MCPError::kResourceNotFound);
+ EXPECT_EQ(ec.category(), std::generic_category());
+}
+
+TEST(MCPErrorTest, AsLLVMError) {
+ Error error = make_error<MCPError>("boom", 42);
+ EXPECT_TRUE(error.isA<MCPError>());
+
+ std::error_code ec = errorToErrorCode(std::move(error));
+ EXPECT_EQ(ec.value(), 42);
+}
+
+TEST(MCPErrorTest, FailedWithMessage) {
+ EXPECT_THAT_ERROR(make_error<MCPError>("explosion"),
+ FailedWithMessage("explosion"));
+}
+
+TEST(MCPErrorTest, UnsupportedURILog) {
+ UnsupportedURI error("lldb://debugger/0");
+ EXPECT_EQ(Log(error), "unsupported uri: lldb://debugger/0");
+}
+
+TEST(MCPErrorTest, UnsupportedURIErrorCode) {
+ UnsupportedURI error("lldb://debugger/0");
+ EXPECT_EQ(error.convertToErrorCode(), inconvertibleErrorCode());
+}
+
+TEST(MCPErrorTest, UnsupportedURIAsLLVMError) {
+ Error error = make_error<UnsupportedURI>("lldb://foo");
+ EXPECT_TRUE(error.isA<UnsupportedURI>());
+ EXPECT_FALSE(error.isA<MCPError>());
+ consumeError(std::move(error));
+}
diff --git a/lldb/unittests/Protocol/MCPPluginTest.cpp b/lldb/unittests/Protocol/MCPPluginTest.cpp
new file mode 100644
index 0000000000000..97850d173719c
--- /dev/null
+++ b/lldb/unittests/Protocol/MCPPluginTest.cpp
@@ -0,0 +1,414 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "Plugins/Platform/MacOSX/PlatformMacOSX.h"
+#include "Plugins/Platform/MacOSX/PlatformRemoteMacOSX.h"
+#include "Plugins/Protocol/MCP/ProtocolServerMCP.h"
+#include "Plugins/Protocol/MCP/Resource.h"
+#include "Plugins/Protocol/MCP/Tool.h"
+#include "Plugins/ScriptInterpreter/None/ScriptInterpreterNone.h"
+#include "TestingSupport/SubsystemRAII.h"
+#include "TestingSupport/TestUtilities.h"
+#include "lldb/Core/Debugger.h"
+#include "lldb/Core/Module.h"
+#include "lldb/Core/ProtocolServer.h"
+#include "lldb/Host/FileSystem.h"
+#include "lldb/Host/HostInfo.h"
+#include "lldb/Host/Socket.h"
+#include "lldb/Protocol/MCP/MCPError.h"
+#include "lldb/Target/Platform.h"
+#include "lldb/Target/Target.h"
+#include "lldb/Utility/ArchSpec.h"
+#include "lldb/Utility/FileSpec.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/JSON.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <string>
+#include <vector>
+
+using namespace llvm;
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::mcp;
+using namespace lldb_protocol::mcp;
+
+#ifndef _WIN32
+
+namespace {
+class MCPPluginTest : public testing::Test {
+public:
+ SubsystemRAII<FileSystem, HostInfo, PlatformMacOSX, ScriptInterpreterNone,
+ Socket>
+ subsystems;
+ DebuggerSP m_debugger_sp;
+
+ void SetUp() override {
+ std::call_once(TestUtilities::g_debugger_initialize_flag,
+ []() { Debugger::Initialize(nullptr); });
+ ArchSpec arch("x86_64-apple-macosx-");
+ Platform::SetHostPlatform(
+ PlatformRemoteMacOSX::CreateInstance(true, &arch));
+ m_debugger_sp = Debugger::CreateInstance();
+ }
+
+ void TearDown() override {
+ Debugger::Destroy(m_debugger_sp);
+ m_debugger_sp.reset();
+ }
+
+ TargetSP CreateTarget() {
+ ArchSpec arch("x86_64-apple-macosx-");
+ PlatformSP platform_sp;
+ TargetSP target_sp;
+ m_debugger_sp->GetTargetList().CreateTarget(
+ *m_debugger_sp, "", arch, eLoadDependentsNo, platform_sp, target_sp);
+ return target_sp;
+ }
+
+ TargetSP CreateTargetWithExecutable(StringRef path) {
+ TargetSP target_sp = CreateTarget();
+ ArchSpec arch("x86_64-apple-macosx-");
+ // Use the FileSpec/ArchSpec constructor so the module keeps its file spec
+ // even though the path doesn't point at a real object file.
+ ModuleSP module_sp = std::make_shared<Module>(FileSpec(path), arch);
+ target_sp->SetExecutableModule(module_sp, eLoadDependentsNo);
+ return target_sp;
+ }
+};
+
+void ExpectUnsupportedURI(Expected<ReadResourceResult> result) {
+ ASSERT_FALSE(static_cast<bool>(result));
+ llvm::Error err = result.takeError();
+ EXPECT_TRUE(err.isA<UnsupportedURI>());
+ consumeError(std::move(err));
+}
+} // namespace
+
+//===----------------------------------------------------------------------===//
+// DebuggerResourceProvider
+//===----------------------------------------------------------------------===//
+
+TEST_F(MCPPluginTest, GetResources) {
+ CreateTarget();
+ CreateTargetWithExecutable("/tmp/lldb-mcp-test/my_executable");
+
+ DebuggerResourceProvider provider;
+ std::vector<Resource> resources = provider.GetResources();
+
+ std::string debugger_uri =
+ formatv("lldb://debugger/{0}", m_debugger_sp->GetID()).str();
+ std::string target0_uri =
+ formatv("lldb://debugger/{0}/target/0", m_debugger_sp->GetID()).str();
+ std::string target1_uri =
+ formatv("lldb://debugger/{0}/target/1", m_debugger_sp->GetID()).str();
+
+ std::vector<std::string> uris;
+ std::string exe_target_name;
+ for (const Resource &resource : resources) {
+ uris.push_back(resource.uri);
+ if (resource.uri == target1_uri)
+ exe_target_name = resource.name;
+ }
+
+ EXPECT_THAT(uris, testing::Contains(debugger_uri));
+ EXPECT_THAT(uris, testing::Contains(target0_uri));
+ EXPECT_THAT(uris, testing::Contains(target1_uri));
+ // The target with an executable module is named after the executable.
+ EXPECT_EQ(exe_target_name, "my_executable");
+}
+
+TEST_F(MCPPluginTest, ReadResourceUnsupportedScheme) {
+ DebuggerResourceProvider provider;
+ ExpectUnsupportedURI(provider.ReadResource("http://example.com"));
+}
+
+TEST_F(MCPPluginTest, ReadResourceTooFewComponents) {
+ DebuggerResourceProvider provider;
+ ExpectUnsupportedURI(provider.ReadResource("lldb://x"));
+}
+
+TEST_F(MCPPluginTest, ReadResourceNotDebugger) {
+ DebuggerResourceProvider provider;
+ ExpectUnsupportedURI(provider.ReadResource("lldb://session/0"));
+}
+
+TEST_F(MCPPluginTest, ReadResourceInvalidDebuggerId) {
+ DebuggerResourceProvider provider;
+ EXPECT_THAT_EXPECTED(
+ provider.ReadResource("lldb://debugger/abc"),
+ FailedWithMessage("invalid debugger id 'abc': debugger/abc"));
+}
+
+TEST_F(MCPPluginTest, ReadResourceUnknownDebugger) {
+ DebuggerResourceProvider provider;
+ EXPECT_THAT_EXPECTED(provider.ReadResource("lldb://debugger/999999"),
+ FailedWithMessage("invalid debugger id: 999999"));
+}
+
+TEST_F(MCPPluginTest, ReadResourceDebugger) {
+ CreateTarget();
+
+ DebuggerResourceProvider provider;
+ std::string uri =
+ formatv("lldb://debugger/{0}", m_debugger_sp->GetID()).str();
+ Expected<ReadResourceResult> result = provider.ReadResource(uri);
+ ASSERT_THAT_EXPECTED(result, Succeeded());
+ ASSERT_EQ(result->contents.size(), 1u);
+ EXPECT_EQ(result->contents[0].uri, uri);
+ EXPECT_EQ(result->contents[0].mimeType, "application/json");
+
+ Expected<json::Value> json = json::parse(result->contents[0].text);
+ ASSERT_THAT_EXPECTED(json, Succeeded());
+ const json::Object *obj = json->getAsObject();
+ ASSERT_NE(obj, nullptr);
+ EXPECT_EQ(obj->getInteger("debugger_id"),
+ static_cast<int64_t>(m_debugger_sp->GetID()));
+ EXPECT_EQ(obj->getInteger("num_targets"), 1);
+}
+
+TEST_F(MCPPluginTest, ReadResourceTargetNotTarget) {
+ DebuggerResourceProvider provider;
+ std::string uri =
+ formatv("lldb://debugger/{0}/session/0", m_debugger_sp->GetID()).str();
+ ExpectUnsupportedURI(provider.ReadResource(uri));
+}
+
+TEST_F(MCPPluginTest, ReadResourceInvalidTargetId) {
+ DebuggerResourceProvider provider;
+ std::string uri =
+ formatv("lldb://debugger/{0}/target/abc", m_debugger_sp->GetID()).str();
+ std::string path =
+ formatv("debugger/{0}/target/abc", m_debugger_sp->GetID()).str();
+ EXPECT_THAT_EXPECTED(
+ provider.ReadResource(uri),
+ FailedWithMessage(formatv("invalid target id 'abc': {0}", path).str()));
+}
+
+TEST_F(MCPPluginTest, ReadResourceTargetUnknownDebugger) {
+ DebuggerResourceProvider provider;
+ EXPECT_THAT_EXPECTED(provider.ReadResource("lldb://debugger/999999/target/0"),
+ FailedWithMessage("invalid debugger id: 999999"));
+}
+
+TEST_F(MCPPluginTest, ReadResourceUnknownTarget) {
+ DebuggerResourceProvider provider;
+ std::string uri =
+ formatv("lldb://debugger/{0}/target/999", m_debugger_sp->GetID()).str();
+ EXPECT_THAT_EXPECTED(provider.ReadResource(uri),
+ FailedWithMessage("invalid target idx: 999"));
+}
+
+TEST_F(MCPPluginTest, ReadResourceTarget) {
+ CreateTarget();
+
+ DebuggerResourceProvider provider;
+ std::string uri =
+ formatv("lldb://debugger/{0}/target/0", m_debugger_sp->GetID()).str();
+ Expected<ReadResourceResult> result = provider.ReadResource(uri);
+ ASSERT_THAT_EXPECTED(result, Succeeded());
+ ASSERT_EQ(result->contents.size(), 1u);
+
+ Expected<json::Value> json = json::parse(result->contents[0].text);
+ ASSERT_THAT_EXPECTED(json, Succeeded());
+ const json::Object *obj = json->getAsObject();
+ ASSERT_NE(obj, nullptr);
+ EXPECT_EQ(obj->getInteger("debugger_id"),
+ static_cast<int64_t>(m_debugger_sp->GetID()));
+ EXPECT_EQ(obj->getInteger("target_idx"), 0);
+ // No executable module: there is no "path" entry.
+ EXPECT_EQ(obj->get("path"), nullptr);
+}
+
+TEST_F(MCPPluginTest, ReadResourceTargetWithExecutable) {
+ CreateTargetWithExecutable("/tmp/lldb-mcp-test/my_executable");
+
+ DebuggerResourceProvider provider;
+ std::string uri =
+ formatv("lldb://debugger/{0}/target/0", m_debugger_sp->GetID()).str();
+ Expected<ReadResourceResult> result = provider.ReadResource(uri);
+ ASSERT_THAT_EXPECTED(result, Succeeded());
+ ASSERT_EQ(result->contents.size(), 1u);
+
+ Expected<json::Value> json = json::parse(result->contents[0].text);
+ ASSERT_THAT_EXPECTED(json, Succeeded());
+ const json::Object *obj = json->getAsObject();
+ ASSERT_NE(obj, nullptr);
+ EXPECT_EQ(obj->getString("path"), "/tmp/lldb-mcp-test/my_executable");
+}
+
+//===----------------------------------------------------------------------===//
+// CommandTool
+//===----------------------------------------------------------------------===//
+
+TEST_F(MCPPluginTest, CommandToolRequiresArguments) {
+ CommandTool tool("command", "Run an lldb command.");
+ ToolArguments args; // std::monostate
+ EXPECT_THAT_EXPECTED(tool.Call(args),
+ FailedWithMessage("CommandTool requires arguments"));
+}
+
+TEST_F(MCPPluginTest, CommandToolInvalidArguments) {
+ CommandTool tool("command", "Run an lldb command.");
+ ToolArguments args = json::Value(42);
+ EXPECT_THAT_EXPECTED(tool.Call(args), Failed());
+}
+
+TEST_F(MCPPluginTest, CommandToolMalformedDebugger) {
+ CommandTool tool("command", "Run an lldb command.");
+ ToolArguments args = json::Value(
+ json::Object{{"debugger", "notanumber"}, {"command", "version"}});
+ EXPECT_THAT_EXPECTED(
+ tool.Call(args),
+ FailedWithMessage("malformed debugger specifier notanumber"));
+}
+
+TEST_F(MCPPluginTest, CommandToolNoDebuggerFound) {
+ CommandTool tool("command", "Run an lldb command.");
+ ToolArguments args =
+ json::Value(json::Object{{"debugger", "999999"}, {"command", "version"}});
+ EXPECT_THAT_EXPECTED(tool.Call(args), FailedWithMessage("no debugger found"));
+}
+
+TEST_F(MCPPluginTest, CommandToolRunsCommandForDebuggerId) {
+ CommandTool tool("command", "Run an lldb command.");
+ ToolArguments args = json::Value(
+ json::Object{{"debugger", std::to_string(m_debugger_sp->GetID())},
+ {"command", "version"}});
+
+ Expected<CallToolResult> result = tool.Call(args);
+ ASSERT_THAT_EXPECTED(result, Succeeded());
+ EXPECT_FALSE(result->isError);
+ ASSERT_EQ(result->content.size(), 1u);
+ EXPECT_THAT(result->content[0].text, testing::HasSubstr("lldb"));
+}
+
+TEST_F(MCPPluginTest, CommandToolRunsCommandForDebuggerURI) {
+ CommandTool tool("command", "Run an lldb command.");
+ ToolArguments args = json::Value(json::Object{
+ {"debugger",
+ formatv("lldb-mcp://debugger/{0}", m_debugger_sp->GetID()).str()},
+ {"command", "version"}});
+
+ Expected<CallToolResult> result = tool.Call(args);
+ ASSERT_THAT_EXPECTED(result, Succeeded());
+ EXPECT_FALSE(result->isError);
+}
+
+TEST_F(MCPPluginTest, CommandToolUsesFirstDebuggerWhenUnspecified) {
+ CommandTool tool("command", "Run an lldb command.");
+ ToolArguments args = json::Value(json::Object{{"command", "version"}});
+
+ Expected<CallToolResult> result = tool.Call(args);
+ ASSERT_THAT_EXPECTED(result, Succeeded());
+ EXPECT_FALSE(result->isError);
+}
+
+TEST_F(MCPPluginTest, CommandToolReportsCommandError) {
+ CommandTool tool("command", "Run an lldb command.");
+ ToolArguments args = json::Value(
+ json::Object{{"debugger", std::to_string(m_debugger_sp->GetID())},
+ {"command", "this-is-not-a-real-command"}});
+
+ Expected<CallToolResult> result = tool.Call(args);
+ ASSERT_THAT_EXPECTED(result, Succeeded());
+ EXPECT_TRUE(result->isError);
+ ASSERT_EQ(result->content.size(), 1u);
+ EXPECT_FALSE(result->content[0].text.empty());
+}
+
+TEST_F(MCPPluginTest, CommandToolSchema) {
+ CommandTool tool("command", "Run an lldb command.");
+ std::optional<json::Value> schema = tool.GetSchema();
+ ASSERT_TRUE(schema.has_value());
+ const json::Object *obj = schema->getAsObject();
+ ASSERT_NE(obj, nullptr);
+ EXPECT_EQ(obj->getString("type"), "object");
+ EXPECT_NE(obj->getObject("properties"), nullptr);
+}
+
+//===----------------------------------------------------------------------===//
+// DebuggerListTool
+//===----------------------------------------------------------------------===//
+
+TEST_F(MCPPluginTest, DebuggerListTool) {
+ DebuggerListTool tool("debugger_list", "List debugger instances.");
+ ToolArguments args;
+ Expected<CallToolResult> result = tool.Call(args);
+ ASSERT_THAT_EXPECTED(result, Succeeded());
+ EXPECT_FALSE(result->isError);
+ ASSERT_EQ(result->content.size(), 1u);
+ EXPECT_THAT(
+ result->content[0].text,
+ testing::HasSubstr(
+ formatv("lldb-mcp://debugger/{0}", m_debugger_sp->GetID()).str()));
+}
+
+//===----------------------------------------------------------------------===//
+// ProtocolServerMCP
+//===----------------------------------------------------------------------===//
+
+namespace {
+/// Exposes the protected Extend hook for testing.
+class ExtendableProtocolServerMCP : public ProtocolServerMCP {
+public:
+ using ProtocolServerMCP::Extend;
+};
+} // namespace
+
+TEST_F(MCPPluginTest, ProtocolServerExtend) {
+ ExtendableProtocolServerMCP server;
+ lldb_protocol::mcp::Server mcp_server("lldb-mcp", "0.1.0");
+ // Extend registers the "command" and "debugger_list" tools and the debugger
+ // resource provider on the server.
+ server.Extend(mcp_server);
+}
+
+TEST_F(MCPPluginTest, ProtocolServerStaticPluginInfo) {
+ EXPECT_EQ(ProtocolServerMCP::GetPluginNameStatic(), "MCP");
+ EXPECT_EQ(ProtocolServerMCP::GetPluginDescriptionStatic(), "MCP Server.");
+}
+
+TEST_F(MCPPluginTest, ProtocolServerCreateInstance) {
+ ProtocolServerUP server = ProtocolServerMCP::CreateInstance();
+ ASSERT_TRUE(server);
+ EXPECT_EQ(server->GetPluginName(), "MCP");
+ EXPECT_EQ(server->GetSocket(), nullptr);
+}
+
+TEST_F(MCPPluginTest, ProtocolServerInitializeAndTerminate) {
+ ProtocolServerMCP::Initialize();
+
+ ProtocolServer *server = ProtocolServer::GetOrCreate("MCP");
+ EXPECT_NE(server, nullptr);
+
+ std::vector<StringRef> protocols = ProtocolServer::GetSupportedProtocols();
+ EXPECT_THAT(protocols, testing::Contains(StringRef("MCP")));
+
+ ProtocolServerMCP::Terminate();
+}
+
+TEST_F(MCPPluginTest, ProtocolServerStopNotRunning) {
+ ProtocolServerUP server = ProtocolServerMCP::CreateInstance();
+ ASSERT_TRUE(server);
+ // Stopping a server that was never started is an error.
+ EXPECT_THAT_ERROR(server->Stop(), Failed());
+}
+
+// NOTE: ProtocolServerMCP::Start (and therefore AcceptCallback) binds a
+// listening socket and writes the server info into the real `~/.lldb`
+// directory, so it can't be exercised hermetically yet. It is covered by the
+// API test (TestMCPUnixSocket.py) and should get unit coverage once the
+// socket and filesystem layers can be mocked.
+
+#endif
diff --git a/lldb/unittests/Protocol/MCPServerInfoTest.cpp b/lldb/unittests/Protocol/MCPServerInfoTest.cpp
new file mode 100644
index 0000000000000..d78c158e3ca7d
--- /dev/null
+++ b/lldb/unittests/Protocol/MCPServerInfoTest.cpp
@@ -0,0 +1,40 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "TestingSupport/TestUtilities.h"
+#include "lldb/Protocol/MCP/Server.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gtest/gtest.h"
+
+using namespace llvm;
+using namespace lldb_private;
+using namespace lldb_protocol::mcp;
+
+// NOTE: ServerInfo::Write/Load and the non-empty ServerInfoHandle paths read
+// and write the real `~/.lldb` directory (via HostInfo::GetUserLLDBDir) and so
+// can't be exercised hermetically yet. They should be covered once the
+// filesystem can be mocked.
+
+TEST(MCPServerInfoTest, JSONRoundtrip) {
+ ServerInfo info;
+ info.connection_uri = "unix:///tmp/test.sock";
+
+ Expected<ServerInfo> deserialized = roundtripJSON(info);
+ ASSERT_THAT_EXPECTED(deserialized, Succeeded());
+ EXPECT_EQ(deserialized->connection_uri, info.connection_uri);
+}
+
+TEST(MCPServerInfoTest, EmptyHandleRemoveIsNoOp) {
+ // A default-constructed handle tracks no file, so Remove is a no-op and does
+ // not touch the filesystem.
+ ServerInfoHandle handle;
+ handle.Remove();
+ // Calling Remove again must be safe (idempotent).
+ handle.Remove();
+}
diff --git a/lldb/unittests/Protocol/MCPTransportTest.cpp b/lldb/unittests/Protocol/MCPTransportTest.cpp
new file mode 100644
index 0000000000000..570dbee3fbc3f
--- /dev/null
+++ b/lldb/unittests/Protocol/MCPTransportTest.cpp
@@ -0,0 +1,40 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/Host/MainLoop.h"
+#include "lldb/Protocol/MCP/Transport.h"
+#include "llvm/ADT/StringRef.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include <string>
+#include <vector>
+
+using namespace llvm;
+using namespace lldb_private;
+using namespace lldb_protocol::mcp;
+
+TEST(MCPTransportTest, LogWithCallback) {
+ MainLoop loop;
+ std::vector<std::string> messages;
+ Transport transport(
+ loop, /*in=*/nullptr, /*out=*/nullptr,
+ [&](StringRef message) { messages.push_back(message.str()); });
+
+ transport.Log("hello");
+ transport.Log("world");
+
+ EXPECT_THAT(messages, testing::ElementsAre("hello", "world"));
+}
+
+TEST(MCPTransportTest, LogWithoutCallback) {
+ MainLoop loop;
+ Transport transport(loop, /*in=*/nullptr, /*out=*/nullptr);
+
+ // Without a log callback, logging is a no-op and must not crash.
+ transport.Log("ignored");
+}
diff --git a/lldb/unittests/Protocol/ProtocolMCPServerTest.cpp b/lldb/unittests/Protocol/ProtocolMCPServerTest.cpp
index 9a5b75edeeb9d..d83cbfd7c036d 100644
--- a/lldb/unittests/Protocol/ProtocolMCPServerTest.cpp
+++ b/lldb/unittests/Protocol/ProtocolMCPServerTest.cpp
@@ -124,6 +124,33 @@ class FailTool : public Tool {
}
};
+/// Test resource provider that fails with a non-UnsupportedURI error.
+class ErrorResourceProvider : public ResourceProvider {
+public:
+ using ResourceProvider::ResourceProvider;
+
+ std::vector<Resource> GetResources() const override { return {}; }
+
+ llvm::Expected<ReadResourceResult>
+ ReadResource(llvm::StringRef uri) const override {
+ return llvm::createStringError("resource boom");
+ }
+};
+
+/// Test tool that omits its input schema.
+class NoSchemaTool : public Tool {
+public:
+ using Tool::Tool;
+
+ llvm::Expected<CallToolResult> Call(const ToolArguments &args) override {
+ return CallToolResult{};
+ }
+
+ std::optional<llvm::json::Value> GetSchema() const override {
+ return std::nullopt;
+ }
+};
+
class TestServer : public Server {
public:
using Server::Bind;
@@ -326,4 +353,138 @@ TEST_F(ProtocolServerMCPTest, NotificationInitialized) {
testing::Contains("MCP initialization complete"));
}
+TEST_F(ProtocolServerMCPTest, ToolsCallNoName) {
+ EXPECT_THAT_EXPECTED(
+ (Call<CallToolResult, CallToolParams>(
+ "tools/call",
+ CallToolParams{/*name=*/"", /*arguments=*/std::nullopt})),
+ HasValue(make_response(
+ lldb_protocol::mcp::Error{eErrorCodeInternalError, "no tool name"})));
+}
+
+TEST_F(ProtocolServerMCPTest, ToolsCallUnknownTool) {
+ EXPECT_THAT_EXPECTED(
+ (Call<CallToolResult, CallToolParams>(
+ "tools/call",
+ CallToolParams{/*name=*/"missing", /*arguments=*/std::nullopt})),
+ HasValue(make_response(lldb_protocol::mcp::Error{
+ eErrorCodeInternalError, "no tool \"missing\""})));
+}
+
+TEST_F(ProtocolServerMCPTest, ToolsListWithoutSchema) {
+ server_up->AddTool(
+ std::make_unique<NoSchemaTool>("noschema", "no schema tool"));
+
+ ToolDefinition no_schema_tool;
+ no_schema_tool.name = "noschema";
+ no_schema_tool.description = "no schema tool";
+
+ EXPECT_THAT_EXPECTED(
+ Call<ListToolsResult>("tools/list", Void{}),
+ HasValue(make_response(ListToolsResult{{no_schema_tool}})));
+}
+
+TEST_F(ProtocolServerMCPTest, ResourcesRead) {
+ server_up->AddResourceProvider(std::make_unique<TestResourceProvider>());
+
+ EXPECT_THAT_EXPECTED(
+ (Call<ReadResourceResult, ReadResourceParams>(
+ "resources/read", ReadResourceParams{/*uri=*/"lldb://foo/bar"})),
+ HasValue(make_response(ReadResourceResult{{
+ {
+ /*uri=*/"lldb://foo/bar",
+ /*text=*/"foobar",
+ /*mimeType=*/"application/json",
+ },
+ }})));
+}
+
+TEST_F(ProtocolServerMCPTest, ResourcesReadNoURI) {
+ server_up->AddResourceProvider(std::make_unique<TestResourceProvider>());
+
+ EXPECT_THAT_EXPECTED((Call<ReadResourceResult, ReadResourceParams>(
+ "resources/read", ReadResourceParams{/*uri=*/""})),
+ HasValue(make_response(lldb_protocol::mcp::Error{
+ eErrorCodeInternalError, "no resource uri"})));
+}
+
+TEST_F(ProtocolServerMCPTest, ResourcesReadNotFound) {
+ server_up->AddResourceProvider(std::make_unique<TestResourceProvider>());
+
+ EXPECT_THAT_EXPECTED(
+ (Call<ReadResourceResult, ReadResourceParams>(
+ "resources/read", ReadResourceParams{/*uri=*/"lldb://unknown"})),
+ HasValue(make_response(lldb_protocol::mcp::Error{
+ MCPError::kResourceNotFound,
+ "no resource handler for uri: lldb://unknown"})));
+}
+
+TEST(MCPToolTest, GetDefinitionWithSchema) {
+ TestTool tool("test", "test tool");
+ EXPECT_EQ(tool.GetName(), "test");
+
+ ToolDefinition definition = tool.GetDefinition();
+ EXPECT_EQ(definition.name, "test");
+ EXPECT_EQ(definition.description, "test tool");
+ ASSERT_TRUE(definition.inputSchema.has_value());
+}
+
+TEST(MCPToolTest, GetDefinitionWithoutSchema) {
+ NoSchemaTool tool("noschema", "no schema tool");
+
+ ToolDefinition definition = tool.GetDefinition();
+ EXPECT_EQ(definition.name, "noschema");
+ EXPECT_EQ(definition.description, "no schema tool");
+ EXPECT_FALSE(definition.inputSchema.has_value());
+}
+
+TEST_F(ProtocolServerMCPTest, AddNullToolAndResourceProvider) {
+ // Null tools and resource providers are silently ignored.
+ server_up->AddTool(nullptr);
+ server_up->AddResourceProvider(nullptr);
+
+ EXPECT_THAT_EXPECTED(Call<ListToolsResult>("tools/list", Void{}),
+ HasValue(make_response(ListToolsResult{})));
+ EXPECT_THAT_EXPECTED(Call<ListResourcesResult>("resources/list", Void{}),
+ HasValue(make_response(ListResourcesResult{})));
+}
+
+TEST_F(ProtocolServerMCPTest, ResourcesReadError) {
+ server_up->AddResourceProvider(std::make_unique<ErrorResourceProvider>());
+
+ EXPECT_THAT_EXPECTED(
+ (Call<ReadResourceResult, ReadResourceParams>(
+ "resources/read", ReadResourceParams{/*uri=*/"lldb://x"})),
+ HasValue(make_response(lldb_protocol::mcp::Error{eErrorCodeInternalError,
+ "resource boom"})));
+}
+
+TEST_F(ProtocolServerMCPTest, Accept) {
+ auto transports = TestTransport<ProtocolDescriptor>::createPair(loop);
+ EXPECT_THAT_ERROR(server_up->Accept(std::move(transports.second)),
+ Succeeded());
+}
+
+TEST_F(ProtocolServerMCPTest, AcceptErrorAndDisconnect) {
+ auto transports = TestTransport<ProtocolDescriptor>::createPair(loop);
+ TestTransport<ProtocolDescriptor> *server_transport = transports.second.get();
+ EXPECT_THAT_ERROR(server_up->Accept(std::move(transports.second)),
+ Succeeded());
+
+ // A transport error is logged by the server.
+ server_transport->SimulateError(llvm::createStringError("boom"));
+ EXPECT_THAT(logged_messages,
+ testing::Contains(testing::HasSubstr("Transport error: boom")));
+
+ // Closing the transport removes the client from the server. This destroys
+ // the transport, so it must not be used afterwards.
+ server_transport->SimulateClosed();
+}
+
+TEST_F(ProtocolServerMCPTest, AcceptRegisterFailure) {
+ auto transport = std::make_unique<TestTransport<ProtocolDescriptor>>(loop);
+ transport->SetRegisterMessageHandlerShouldFail(true);
+ EXPECT_THAT_ERROR(server_up->Accept(std::move(transport)), Failed());
+}
+
#endif
diff --git a/lldb/unittests/Protocol/ProtocolMCPTest.cpp b/lldb/unittests/Protocol/ProtocolMCPTest.cpp
index 5f7391e43fe34..efed243912c00 100644
--- a/lldb/unittests/Protocol/ProtocolMCPTest.cpp
+++ b/lldb/unittests/Protocol/ProtocolMCPTest.cpp
@@ -296,4 +296,396 @@ TEST(ProtocolMCPTest, ReadResourceResultEmpty) {
EXPECT_TRUE(deserialized_result->contents.empty());
}
+TEST(ProtocolMCPTest, RequestWithStringId) {
+ Request request;
+ request.id = "request-1";
+ request.method = "foo";
+
+ llvm::Expected<Request> deserialized = roundtripJSON(request);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ EXPECT_EQ(request, *deserialized);
+}
+
+TEST(ProtocolMCPTest, RequestWithoutParams) {
+ Request request;
+ request.id = 7;
+ request.method = "bar";
+
+ llvm::Expected<Request> deserialized = roundtripJSON(request);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ EXPECT_EQ(request, *deserialized);
+ EXPECT_FALSE(deserialized->params.has_value());
+}
+
+TEST(ProtocolMCPTest, RequestMissingId) {
+ llvm::json::Value value =
+ llvm::json::Object{{"jsonrpc", "2.0"}, {"method", "foo"}};
+ Request request;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, request, root));
+}
+
+TEST(ProtocolMCPTest, RequestInvalidId) {
+ llvm::json::Value value =
+ llvm::json::Object{{"jsonrpc", "2.0"}, {"id", true}, {"method", "foo"}};
+ Request request;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, request, root));
+}
+
+TEST(ProtocolMCPTest, ResponseWithStringId) {
+ Response response;
+ response.id = "resp-1";
+ response.result = llvm::json::Value("ok");
+
+ llvm::Expected<Response> deserialized = roundtripJSON(response);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ EXPECT_EQ(response, *deserialized);
+}
+
+TEST(ProtocolMCPTest, ResponseResultAndErrorMutuallyExclusive) {
+ llvm::json::Value value = llvm::json::Object{
+ {"jsonrpc", "2.0"},
+ {"id", 1},
+ {"result", 1},
+ {"error", llvm::json::Object{{"code", 1}, {"message", "m"}}}};
+ Response response;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, response, root));
+}
+
+TEST(ProtocolMCPTest, ResponseRequiresResultOrError) {
+ llvm::json::Value value = llvm::json::Object{{"jsonrpc", "2.0"}, {"id", 1}};
+ Response response;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, response, root));
+}
+
+TEST(ProtocolMCPTest, ResponseExpectsObject) {
+ llvm::json::Value value(42);
+ Response response;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, response, root));
+}
+
+TEST(ProtocolMCPTest, ResponseInvalidError) {
+ llvm::json::Value value = llvm::json::Object{
+ {"jsonrpc", "2.0"}, {"id", 1}, {"error", "not-an-object"}};
+ Response response;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, response, root));
+}
+
+TEST(ProtocolMCPTest, ErrorWithData) {
+ Error error;
+ error.code = -32000;
+ error.message = "boom";
+ error.data = llvm::json::Object{{"detail", "stack"}};
+
+ llvm::Expected<Error> deserialized = roundtripJSON(error);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ EXPECT_EQ(error, *deserialized);
+ EXPECT_TRUE(deserialized->data.has_value());
+}
+
+TEST(ProtocolMCPTest, NotificationWithoutParams) {
+ Notification notification;
+ notification.method = "ping";
+
+ llvm::Expected<Notification> deserialized = roundtripJSON(notification);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ EXPECT_EQ(notification, *deserialized);
+ EXPECT_FALSE(deserialized->params.has_value());
+}
+
+TEST(ProtocolMCPTest, NotificationExpectsObject) {
+ llvm::json::Value value(42);
+ Notification notification;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, notification, root));
+}
+
+TEST(ProtocolMCPTest, NotificationMissingMethod) {
+ llvm::json::Value value = llvm::json::Object{{"jsonrpc", "2.0"}};
+ Notification notification;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, notification, root));
+}
+
+TEST(ProtocolMCPTest, ToolDefinitionMinimal) {
+ ToolDefinition tool_definition;
+ tool_definition.name = "tool";
+
+ llvm::Expected<ToolDefinition> deserialized = roundtripJSON(tool_definition);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ EXPECT_EQ(tool_definition.name, deserialized->name);
+ EXPECT_TRUE(deserialized->description.empty());
+ EXPECT_FALSE(deserialized->inputSchema.has_value());
+}
+
+TEST(ProtocolMCPTest, ToolDefinitionMissingName) {
+ llvm::json::Value value = llvm::json::Object{{"description", "d"}};
+ ToolDefinition tool_definition;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, tool_definition, root));
+}
+
+TEST(ProtocolMCPTest, MessageExpectsObject) {
+ llvm::json::Value value(42);
+ Message message;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, message, root));
+}
+
+TEST(ProtocolMCPTest, MessageRequiresJSONRPC) {
+ llvm::json::Value value = llvm::json::Object{{"id", 1}, {"method", "m"}};
+ Message message;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, message, root));
+}
+
+TEST(ProtocolMCPTest, MessageUnsupportedJSONRPCVersion) {
+ llvm::json::Value value =
+ llvm::json::Object{{"jsonrpc", "1.0"}, {"id", 1}, {"method", "m"}};
+ Message message;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, message, root));
+}
+
+TEST(ProtocolMCPTest, MessageUnrecognized) {
+ llvm::json::Value value = llvm::json::Object{{"jsonrpc", "2.0"}, {"id", 1}};
+ Message message;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, message, root));
+}
+
+TEST(ProtocolMCPTest, MessageInvalidNotification) {
+ // No "id" routes to a Notification, but a missing "method" is invalid.
+ llvm::json::Value value = llvm::json::Object{{"jsonrpc", "2.0"}};
+ Message message;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, message, root));
+}
+
+TEST(ProtocolMCPTest, MessageInvalidRequest) {
+ // Routed to a Request (it has a "method"), but the id is invalid.
+ llvm::json::Value value =
+ llvm::json::Object{{"jsonrpc", "2.0"}, {"id", true}, {"method", "m"}};
+ Message message;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, message, root));
+}
+
+TEST(ProtocolMCPTest, MessageInvalidResponse) {
+ // Routed to a Response (it has "result"/"error" but no "method"), but
+ // 'result' and 'error' are mutually exclusive.
+ llvm::json::Value value = llvm::json::Object{
+ {"jsonrpc", "2.0"},
+ {"id", 1},
+ {"result", 1},
+ {"error", llvm::json::Object{{"code", 1}, {"message", "m"}}}};
+ Message message;
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, message, root));
+}
+
+TEST(ProtocolMCPTest, ImplementationWithTitle) {
+ Implementation impl;
+ impl.name = "lldb-mcp";
+ impl.version = "0.1.0";
+ impl.title = "LLDB MCP";
+
+ llvm::Expected<Implementation> deserialized = roundtripJSON(impl);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ EXPECT_EQ(impl.name, deserialized->name);
+ EXPECT_EQ(impl.version, deserialized->version);
+ EXPECT_EQ(impl.title, deserialized->title);
+}
+
+TEST(ProtocolMCPTest, ImplementationWithoutTitle) {
+ Implementation impl;
+ impl.name = "lldb-mcp";
+ impl.version = "0.1.0";
+
+ llvm::Expected<Implementation> deserialized = roundtripJSON(impl);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ EXPECT_EQ(impl.name, deserialized->name);
+ EXPECT_TRUE(deserialized->title.empty());
+}
+
+TEST(ProtocolMCPTest, ServerCapabilitiesAllFields) {
+ ServerCapabilities caps;
+ caps.supportsToolsList = true;
+ caps.supportsResourcesList = true;
+ caps.supportsResourcesSubscribe = true;
+ caps.supportsCompletions = true;
+ caps.supportsLogging = true;
+
+ llvm::json::Value value = toJSON(caps);
+ const llvm::json::Object *obj = value.getAsObject();
+ ASSERT_NE(obj, nullptr);
+ EXPECT_NE(obj->get("tools"), nullptr);
+ EXPECT_NE(obj->get("completions"), nullptr);
+ EXPECT_NE(obj->get("logging"), nullptr);
+
+ const llvm::json::Object *resources = obj->getObject("resources");
+ ASSERT_NE(resources, nullptr);
+ EXPECT_EQ(resources->getBoolean("listChanged"), true);
+ EXPECT_EQ(resources->getBoolean("subscribe"), true);
+}
+
+TEST(ProtocolMCPTest, ServerCapabilitiesSubscribeOnly) {
+ ServerCapabilities caps;
+ caps.supportsResourcesSubscribe = true;
+
+ llvm::json::Value value = toJSON(caps);
+ const llvm::json::Object *resources =
+ value.getAsObject()->getObject("resources");
+ ASSERT_NE(resources, nullptr);
+ EXPECT_EQ(resources->getBoolean("subscribe"), true);
+ EXPECT_EQ(resources->get("listChanged"), nullptr);
+}
+
+TEST(ProtocolMCPTest, ServerCapabilitiesFromJSONWithoutTools) {
+ ServerCapabilities caps;
+ llvm::json::Value value = llvm::json::Object{};
+ llvm::json::Path::Root root;
+ ASSERT_TRUE(fromJSON(value, caps, root));
+ EXPECT_FALSE(caps.supportsToolsList);
+}
+
+TEST(ProtocolMCPTest, ServerCapabilitiesFromJSONExpectsObject) {
+ ServerCapabilities caps;
+ llvm::json::Value value(42);
+ llvm::json::Path::Root root;
+ EXPECT_FALSE(fromJSON(value, caps, root));
+}
+
+TEST(ProtocolMCPTest, ClientCapabilities) {
+ ClientCapabilities caps;
+ EXPECT_EQ(toJSON(caps), llvm::json::Value(llvm::json::Object{}));
+
+ llvm::json::Value value = llvm::json::Object{};
+ llvm::json::Path::Root root;
+ EXPECT_TRUE(fromJSON(value, caps, root));
+}
+
+TEST(ProtocolMCPTest, InitializeParams) {
+ InitializeParams params;
+ params.protocolVersion = "2024-11-05";
+ params.clientInfo.name = "client";
+ params.clientInfo.version = "1.0";
+
+ llvm::Expected<InitializeParams> deserialized = roundtripJSON(params);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ EXPECT_EQ(params.protocolVersion, deserialized->protocolVersion);
+ EXPECT_EQ(params.clientInfo.name, deserialized->clientInfo.name);
+ EXPECT_EQ(params.clientInfo.version, deserialized->clientInfo.version);
+}
+
+TEST(ProtocolMCPTest, InitializeResultWithInstructions) {
+ InitializeResult result;
+ result.protocolVersion = "2024-11-05";
+ result.capabilities.supportsToolsList = true;
+ result.serverInfo.name = "lldb-mcp";
+ result.serverInfo.version = "0.1.0";
+ result.instructions = "Use the tools wisely.";
+
+ llvm::Expected<InitializeResult> deserialized = roundtripJSON(result);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ EXPECT_EQ(result.protocolVersion, deserialized->protocolVersion);
+ EXPECT_EQ(result.instructions, deserialized->instructions);
+ EXPECT_TRUE(deserialized->capabilities.supportsToolsList);
+}
+
+TEST(ProtocolMCPTest, InitializeResultWithoutInstructions) {
+ InitializeResult result;
+ result.protocolVersion = "2024-11-05";
+ result.serverInfo.name = "lldb-mcp";
+ result.serverInfo.version = "0.1.0";
+
+ llvm::Expected<InitializeResult> deserialized = roundtripJSON(result);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ EXPECT_TRUE(deserialized->instructions.empty());
+}
+
+TEST(ProtocolMCPTest, ListToolsResult) {
+ ToolDefinition tool_definition;
+ tool_definition.name = "a";
+ tool_definition.description = "d";
+
+ ListToolsResult result;
+ result.tools = {tool_definition};
+
+ llvm::Expected<ListToolsResult> deserialized = roundtripJSON(result);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ ASSERT_EQ(deserialized->tools.size(), 1u);
+ EXPECT_EQ(deserialized->tools[0].name, "a");
+}
+
+TEST(ProtocolMCPTest, CallToolResultStructuredContent) {
+ CallToolResult result;
+ result.content = {TextContent{"text"}};
+ result.structuredContent = llvm::json::Object{{"k", "v"}};
+
+ llvm::Expected<CallToolResult> deserialized = roundtripJSON(result);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ ASSERT_TRUE(deserialized->structuredContent.has_value());
+ ASSERT_EQ(deserialized->content.size(), 1u);
+ EXPECT_EQ(deserialized->content[0].text, "text");
+}
+
+TEST(ProtocolMCPTest, CallToolParamsWithArguments) {
+ CallToolParams params;
+ params.name = "tool";
+ params.arguments = llvm::json::Object{{"a", 1}};
+
+ llvm::Expected<CallToolParams> deserialized = roundtripJSON(params);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ EXPECT_EQ(deserialized->name, "tool");
+ EXPECT_TRUE(deserialized->arguments.has_value());
+}
+
+TEST(ProtocolMCPTest, CallToolParamsWithoutArguments) {
+ CallToolParams params;
+ params.name = "tool";
+
+ llvm::Expected<CallToolParams> deserialized = roundtripJSON(params);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ EXPECT_EQ(deserialized->name, "tool");
+ EXPECT_FALSE(deserialized->arguments.has_value());
+}
+
+TEST(ProtocolMCPTest, ReadResourceParams) {
+ ReadResourceParams params;
+ params.uri = "lldb://debugger/0";
+
+ llvm::Expected<ReadResourceParams> deserialized = roundtripJSON(params);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ EXPECT_EQ(deserialized->uri, params.uri);
+}
+
+TEST(ProtocolMCPTest, ListResourcesResult) {
+ Resource resource;
+ resource.uri = "lldb://x";
+ resource.name = "x";
+
+ ListResourcesResult result;
+ result.resources = {resource};
+
+ llvm::Expected<ListResourcesResult> deserialized = roundtripJSON(result);
+ ASSERT_THAT_EXPECTED(deserialized, llvm::Succeeded());
+ ASSERT_EQ(deserialized->resources.size(), 1u);
+ EXPECT_EQ(deserialized->resources[0].uri, "lldb://x");
+}
+
+TEST(ProtocolMCPTest, Void) {
+ EXPECT_EQ(toJSON(Void{}), llvm::json::Value(llvm::json::Object{}));
+
+ Void value;
+ llvm::json::Value json = llvm::json::Object{};
+ llvm::json::Path::Root root;
+ EXPECT_TRUE(fromJSON(json, value, root));
+}
+
#endif
diff --git a/lldb/unittests/TestingSupport/Host/JSONTransportTestUtilities.h b/lldb/unittests/TestingSupport/Host/JSONTransportTestUtilities.h
index 4623c365c960f..93845fc82d173 100644
--- a/lldb/unittests/TestingSupport/Host/JSONTransportTestUtilities.h
+++ b/lldb/unittests/TestingSupport/Host/JSONTransportTestUtilities.h
@@ -64,17 +64,42 @@ class TestTransport final
}
llvm::Error RegisterMessageHandler(MessageHandler &handler) override {
+ if (m_register_should_fail)
+ return llvm::createStringError("RegisterMessageHandler failed");
if (!m_handler)
m_handler = &handler;
return llvm::Error::success();
}
+ /// Makes the next RegisterMessageHandler call fail, to exercise error paths.
+ void SetRegisterMessageHandlerShouldFail(bool fail) {
+ m_register_should_fail = fail;
+ }
+
+ /// Drives the registered handler's error callback, as the real transport
+ /// would on a read or parse failure.
+ void SimulateError(llvm::Error error) {
+ EXPECT_TRUE(m_handler)
+ << "SimulateError called before RegisterMessageHandler";
+ m_handler->OnError(std::move(error));
+ }
+
+ /// Drives the registered handler's close callback, as the real transport
+ /// would on EOF. Mirrors IOTransport::OnRead: the handler may destroy this
+ /// transport, so members must not be accessed after this returns.
+ void SimulateClosed() {
+ EXPECT_TRUE(m_handler)
+ << "SimulateClosed called before RegisterMessageHandler";
+ m_handler->OnClosed();
+ }
+
protected:
void Log(llvm::StringRef message) override {};
private:
lldb_private::MainLoop &m_loop;
MessageHandler *m_handler = nullptr;
+ bool m_register_should_fail = false;
};
template <typename Proto>
More information about the lldb-commits
mailing list