[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