[Lldb-commits] [lldb] [lldb] Add MCP support to LLDB (PoC) (PR #143628)
Jonas Devlieghere via lldb-commits
lldb-commits at lists.llvm.org
Fri Jun 13 14:05:41 PDT 2025
https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/143628
>From 3c1cb491ab014714866335f8de451c2bf7e7ca01 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Thu, 12 Jun 2025 16:33:55 -0700
Subject: [PATCH] [lldb] Add MCP support to LLDB
https://discourse.llvm.org/t/rfc-adding-mcp-support-to-lldb/86798
---
lldb/include/lldb/Core/Debugger.h | 6 +
lldb/include/lldb/Core/PluginManager.h | 9 +
lldb/include/lldb/Core/ProtocolServer.h | 37 +++
.../Interpreter/CommandOptionArgumentTable.h | 1 +
lldb/include/lldb/lldb-enumerations.h | 1 +
lldb/include/lldb/lldb-forward.h | 3 +-
lldb/include/lldb/lldb-private-interfaces.h | 2 +
lldb/source/Commands/CMakeLists.txt | 1 +
.../Commands/CommandObjectProtocolServer.cpp | 145 +++++++++
.../Commands/CommandObjectProtocolServer.h | 25 ++
lldb/source/Core/CMakeLists.txt | 1 +
lldb/source/Core/Debugger.cpp | 24 ++
lldb/source/Core/PluginManager.cpp | 27 ++
lldb/source/Core/ProtocolServer.cpp | 21 ++
.../source/Interpreter/CommandInterpreter.cpp | 2 +
lldb/source/Plugins/CMakeLists.txt | 1 +
lldb/source/Plugins/Protocol/CMakeLists.txt | 1 +
.../Plugins/Protocol/MCP/CMakeLists.txt | 13 +
lldb/source/Plugins/Protocol/MCP/MCPError.cpp | 31 ++
lldb/source/Plugins/Protocol/MCP/MCPError.h | 33 +++
lldb/source/Plugins/Protocol/MCP/Protocol.cpp | 183 ++++++++++++
lldb/source/Plugins/Protocol/MCP/Protocol.h | 131 ++++++++
.../Protocol/MCP/ProtocolServerMCP.cpp | 280 ++++++++++++++++++
.../Plugins/Protocol/MCP/ProtocolServerMCP.h | 80 +++++
lldb/source/Plugins/Protocol/MCP/Tool.cpp | 72 +++++
lldb/source/Plugins/Protocol/MCP/Tool.h | 61 ++++
lldb/unittests/CMakeLists.txt | 1 +
lldb/unittests/DAP/ProtocolTypesTest.cpp | 34 +--
lldb/unittests/Protocol/CMakeLists.txt | 12 +
.../Protocol/ProtocolMCPServerTest.cpp | 195 ++++++++++++
lldb/unittests/Protocol/ProtocolMCPTest.cpp | 176 +++++++++++
lldb/unittests/TestingSupport/TestUtilities.h | 9 +
32 files changed, 1597 insertions(+), 21 deletions(-)
create mode 100644 lldb/include/lldb/Core/ProtocolServer.h
create mode 100644 lldb/source/Commands/CommandObjectProtocolServer.cpp
create mode 100644 lldb/source/Commands/CommandObjectProtocolServer.h
create mode 100644 lldb/source/Core/ProtocolServer.cpp
create mode 100644 lldb/source/Plugins/Protocol/CMakeLists.txt
create mode 100644 lldb/source/Plugins/Protocol/MCP/CMakeLists.txt
create mode 100644 lldb/source/Plugins/Protocol/MCP/MCPError.cpp
create mode 100644 lldb/source/Plugins/Protocol/MCP/MCPError.h
create mode 100644 lldb/source/Plugins/Protocol/MCP/Protocol.cpp
create mode 100644 lldb/source/Plugins/Protocol/MCP/Protocol.h
create mode 100644 lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp
create mode 100644 lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.h
create mode 100644 lldb/source/Plugins/Protocol/MCP/Tool.cpp
create mode 100644 lldb/source/Plugins/Protocol/MCP/Tool.h
create mode 100644 lldb/unittests/Protocol/CMakeLists.txt
create mode 100644 lldb/unittests/Protocol/ProtocolMCPServerTest.cpp
create mode 100644 lldb/unittests/Protocol/ProtocolMCPTest.cpp
diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h
index d73aba1e3ce58..0f6659d1a0bf7 100644
--- a/lldb/include/lldb/Core/Debugger.h
+++ b/lldb/include/lldb/Core/Debugger.h
@@ -598,6 +598,10 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
void FlushProcessOutput(Process &process, bool flush_stdout,
bool flush_stderr);
+ void AddProtocolServer(lldb::ProtocolServerSP protocol_server_sp);
+ void RemoveProtocolServer(lldb::ProtocolServerSP protocol_server_sp);
+ lldb::ProtocolServerSP GetProtocolServer(llvm::StringRef protocol) const;
+
SourceManager::SourceFileCache &GetSourceFileCache() {
return m_source_file_cache;
}
@@ -768,6 +772,8 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
mutable std::mutex m_progress_reports_mutex;
/// @}
+ llvm::SmallVector<lldb::ProtocolServerSP> m_protocol_servers;
+
std::mutex m_destroy_callback_mutex;
lldb::callback_token_t m_destroy_callback_next_token = 0;
struct DestroyCallbackInfo {
diff --git a/lldb/include/lldb/Core/PluginManager.h b/lldb/include/lldb/Core/PluginManager.h
index e7b1691031111..967af598c40ff 100644
--- a/lldb/include/lldb/Core/PluginManager.h
+++ b/lldb/include/lldb/Core/PluginManager.h
@@ -327,6 +327,15 @@ class PluginManager {
static void AutoCompleteProcessName(llvm::StringRef partial_name,
CompletionRequest &request);
+ // Protocol
+ static bool RegisterPlugin(llvm::StringRef name, llvm::StringRef description,
+ ProtocolServerCreateInstance create_callback);
+
+ static bool UnregisterPlugin(ProtocolServerCreateInstance create_callback);
+
+ static ProtocolServerCreateInstance
+ GetProtocolCreateCallbackForPluginName(llvm::StringRef name);
+
// Register Type Provider
static bool RegisterPlugin(llvm::StringRef name, llvm::StringRef description,
RegisterTypeBuilderCreateInstance create_callback);
diff --git a/lldb/include/lldb/Core/ProtocolServer.h b/lldb/include/lldb/Core/ProtocolServer.h
new file mode 100644
index 0000000000000..ca0210a0bbe72
--- /dev/null
+++ b/lldb/include/lldb/Core/ProtocolServer.h
@@ -0,0 +1,37 @@
+//===-- ProtocolServer.h --------------------------------------------------===//
+//
+// 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_CORE_PROTOCOLSERVER_H
+#define LLDB_CORE_PROTOCOLSERVER_H
+
+#include "lldb/Core/PluginInterface.h"
+#include "lldb/Host/Socket.h"
+#include "lldb/lldb-private-interfaces.h"
+
+namespace lldb_private {
+
+class ProtocolServer : public PluginInterface {
+public:
+ ProtocolServer() = default;
+ virtual ~ProtocolServer() = default;
+
+ static lldb::ProtocolServerSP Create(llvm::StringRef name,
+ Debugger &debugger);
+
+ struct Connection {
+ Socket::SocketProtocol protocol;
+ std::string name;
+ };
+
+ virtual llvm::Error Start(Connection connection);
+ virtual llvm::Error Stop();
+};
+
+} // namespace lldb_private
+
+#endif
diff --git a/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h b/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h
index 8535dfcf46da5..4face717531b1 100644
--- a/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h
+++ b/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h
@@ -315,6 +315,7 @@ static constexpr CommandObject::ArgumentTableEntry g_argument_table[] = {
{ lldb::eArgTypeCPUName, "cpu-name", lldb::CompletionType::eNoCompletion, {}, { nullptr, false }, "The name of a CPU." },
{ lldb::eArgTypeCPUFeatures, "cpu-features", lldb::CompletionType::eNoCompletion, {}, { nullptr, false }, "The CPU feature string." },
{ lldb::eArgTypeManagedPlugin, "managed-plugin", lldb::CompletionType::eNoCompletion, {}, { nullptr, false }, "Plugins managed by the PluginManager" },
+ { lldb::eArgTypeProtocol, "protocol", lldb::CompletionType::eNoCompletion, {}, { nullptr, false }, "The name of the protocol." },
// clang-format on
};
diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h
index eeb7299a354e1..69e8671b6e21b 100644
--- a/lldb/include/lldb/lldb-enumerations.h
+++ b/lldb/include/lldb/lldb-enumerations.h
@@ -664,6 +664,7 @@ enum CommandArgumentType {
eArgTypeCPUName,
eArgTypeCPUFeatures,
eArgTypeManagedPlugin,
+ eArgTypeProtocol,
eArgTypeLastArg // Always keep this entry as the last entry in this
// enumeration!!
};
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index c664d1398f74d..558818e8e2309 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -164,13 +164,13 @@ class PersistentExpressionState;
class Platform;
class Process;
class ProcessAttachInfo;
-class ProcessLaunchInfo;
class ProcessInfo;
class ProcessInstanceInfo;
class ProcessInstanceInfoMatch;
class ProcessLaunchInfo;
class ProcessModID;
class Property;
+class ProtocolServer;
class Queue;
class QueueImpl;
class QueueItem;
@@ -391,6 +391,7 @@ typedef std::shared_ptr<lldb_private::Platform> PlatformSP;
typedef std::shared_ptr<lldb_private::Process> ProcessSP;
typedef std::shared_ptr<lldb_private::ProcessAttachInfo> ProcessAttachInfoSP;
typedef std::shared_ptr<lldb_private::ProcessLaunchInfo> ProcessLaunchInfoSP;
+typedef std::shared_ptr<lldb_private::ProtocolServer> ProtocolServerSP;
typedef std::weak_ptr<lldb_private::Process> ProcessWP;
typedef std::shared_ptr<lldb_private::RegisterCheckpoint> RegisterCheckpointSP;
typedef std::shared_ptr<lldb_private::RegisterContext> RegisterContextSP;
diff --git a/lldb/include/lldb/lldb-private-interfaces.h b/lldb/include/lldb/lldb-private-interfaces.h
index d366dbd1d7832..34eaaa8e581e9 100644
--- a/lldb/include/lldb/lldb-private-interfaces.h
+++ b/lldb/include/lldb/lldb-private-interfaces.h
@@ -81,6 +81,8 @@ typedef lldb::PlatformSP (*PlatformCreateInstance)(bool force,
typedef lldb::ProcessSP (*ProcessCreateInstance)(
lldb::TargetSP target_sp, lldb::ListenerSP listener_sp,
const FileSpec *crash_file_path, bool can_connect);
+typedef lldb::ProtocolServerSP (*ProtocolServerCreateInstance)(
+ Debugger &debugger);
typedef lldb::RegisterTypeBuilderSP (*RegisterTypeBuilderCreateInstance)(
Target &target);
typedef lldb::ScriptInterpreterSP (*ScriptInterpreterCreateInstance)(
diff --git a/lldb/source/Commands/CMakeLists.txt b/lldb/source/Commands/CMakeLists.txt
index 1ea51acec5f15..69e4c45f0b8e5 100644
--- a/lldb/source/Commands/CMakeLists.txt
+++ b/lldb/source/Commands/CMakeLists.txt
@@ -23,6 +23,7 @@ add_lldb_library(lldbCommands NO_PLUGIN_DEPENDENCIES
CommandObjectPlatform.cpp
CommandObjectPlugin.cpp
CommandObjectProcess.cpp
+ CommandObjectProtocolServer.cpp
CommandObjectQuit.cpp
CommandObjectRegexCommand.cpp
CommandObjectRegister.cpp
diff --git a/lldb/source/Commands/CommandObjectProtocolServer.cpp b/lldb/source/Commands/CommandObjectProtocolServer.cpp
new file mode 100644
index 0000000000000..bdb237cf010f4
--- /dev/null
+++ b/lldb/source/Commands/CommandObjectProtocolServer.cpp
@@ -0,0 +1,145 @@
+//===-- CommandObjectProtocolServer.cpp
+//----------------------------------------------===//
+//
+// 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 "CommandObjectProtocolServer.h"
+#include "lldb/Core/ProtocolServer.h"
+#include "lldb/Host/Socket.h"
+#include "lldb/Interpreter/CommandInterpreter.h"
+#include "lldb/Interpreter/CommandReturnObject.h"
+#include "lldb/Utility/UriParser.h"
+#include "llvm/Support/FormatAdapters.h"
+
+using namespace llvm;
+using namespace lldb;
+using namespace lldb_private;
+
+#define LLDB_OPTIONS_mcp
+#include "CommandOptions.inc"
+
+static llvm::Expected<std::pair<Socket::SocketProtocol, std::string>>
+validateConnection(llvm::StringRef conn) {
+ auto uri = lldb_private::URI::Parse(conn);
+
+ if (uri && (uri->scheme == "tcp" || uri->scheme == "connect" ||
+ !uri->hostname.empty() || uri->port)) {
+ return std::make_pair(
+ Socket::ProtocolTcp,
+ formatv("[{0}]:{1}", uri->hostname.empty() ? "0.0.0.0" : uri->hostname,
+ uri->port.value_or(0)));
+ }
+
+ if (uri && (uri->scheme == "unix" || uri->scheme == "unix-connect" ||
+ uri->path != "/")) {
+ return std::make_pair(Socket::ProtocolUnixDomain, uri->path.str());
+ }
+
+ return llvm::createStringError(
+ "Unsupported connection specifier, expected 'unix-connect:///path' or "
+ "'connect://[host]:port', got '%s'.",
+ conn.str().c_str());
+}
+
+class CommandObjectProtocolServerStart : public CommandObjectParsed {
+public:
+ CommandObjectProtocolServerStart(CommandInterpreter &interpreter)
+ : CommandObjectParsed(interpreter, "mcp start", "start MCP server",
+ "mcp start <connection>") {
+ AddSimpleArgumentList(lldb::eArgTypeProtocol, eArgRepeatPlain);
+ AddSimpleArgumentList(lldb::eArgTypeConnectURL, eArgRepeatPlain);
+ }
+
+ ~CommandObjectProtocolServerStart() override = default;
+
+protected:
+ void DoExecute(Args &args, CommandReturnObject &result) override {
+ if (args.GetArgumentCount() < 1) {
+ result.AppendError("no protocol specified");
+ return;
+ }
+
+ if (args.GetArgumentCount() < 2) {
+ result.AppendError("no connection specified");
+ return;
+ }
+
+ llvm::StringRef protocol = args.GetArgumentAtIndex(0);
+
+ ProtocolServerSP server_sp = GetDebugger().GetProtocolServer(protocol);
+ if (!server_sp)
+ server_sp = ProtocolServer::Create(protocol, GetDebugger());
+
+ llvm::StringRef connection_uri = args.GetArgumentAtIndex(1);
+
+ auto maybeProtoclAndName = validateConnection(connection_uri);
+ if (auto error = maybeProtoclAndName.takeError()) {
+ result.AppendErrorWithFormatv("{0}", llvm::fmt_consume(std::move(error)));
+ return;
+ }
+
+ ProtocolServer::Connection connection;
+ std::tie(connection.protocol, connection.name) = *maybeProtoclAndName;
+
+ if (llvm::Error error = server_sp->Start(connection)) {
+ result.AppendErrorWithFormatv("{0}", llvm::fmt_consume(std::move(error)));
+ return;
+ }
+
+ GetDebugger().AddProtocolServer(server_sp);
+ }
+};
+
+class CommandObjectProtocolServerStop : public CommandObjectParsed {
+public:
+ CommandObjectProtocolServerStop(CommandInterpreter &interpreter)
+ : CommandObjectParsed(interpreter, "protocol-server stop",
+ "stop protocol server", "protocol-server stop") {
+ AddSimpleArgumentList(lldb::eArgTypeProtocol, eArgRepeatPlain);
+ }
+
+ ~CommandObjectProtocolServerStop() override = default;
+
+protected:
+ void DoExecute(Args &args, CommandReturnObject &result) override {
+ if (args.GetArgumentCount() < 1) {
+ result.AppendError("no protocol specified");
+ return;
+ }
+
+ llvm::StringRef protocol = args.GetArgumentAtIndex(0);
+
+ Debugger &debugger = GetDebugger();
+
+ ProtocolServerSP server_sp = debugger.GetProtocolServer(protocol);
+ if (!server_sp) {
+ result.AppendError(
+ llvm::formatv("no {0} protocol server running", protocol).str());
+ return;
+ }
+
+ if (llvm::Error error = server_sp->Stop()) {
+ result.AppendErrorWithFormatv("{0}", llvm::fmt_consume(std::move(error)));
+ return;
+ }
+
+ debugger.RemoveProtocolServer(server_sp);
+ }
+};
+
+CommandObjectProtocolServer::CommandObjectProtocolServer(
+ CommandInterpreter &interpreter)
+ : CommandObjectMultiword(interpreter, "protocol-server",
+ "Start and stop a protocol server.",
+ "protocol-server") {
+ LoadSubCommand("start", CommandObjectSP(new CommandObjectProtocolServerStart(
+ interpreter)));
+ LoadSubCommand("stop", CommandObjectSP(
+ new CommandObjectProtocolServerStop(interpreter)));
+}
+
+CommandObjectProtocolServer::~CommandObjectProtocolServer() = default;
diff --git a/lldb/source/Commands/CommandObjectProtocolServer.h b/lldb/source/Commands/CommandObjectProtocolServer.h
new file mode 100644
index 0000000000000..3591216b014cb
--- /dev/null
+++ b/lldb/source/Commands/CommandObjectProtocolServer.h
@@ -0,0 +1,25 @@
+//===-- CommandObjectProtocolServer.h
+//------------------------------------------------===//
+//
+// 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_SOURCE_COMMANDS_COMMANDOBJECTPROTOCOLSERVER_H
+#define LLDB_SOURCE_COMMANDS_COMMANDOBJECTPROTOCOLSERVER_H
+
+#include "lldb/Interpreter/CommandObjectMultiword.h"
+
+namespace lldb_private {
+
+class CommandObjectProtocolServer : public CommandObjectMultiword {
+public:
+ CommandObjectProtocolServer(CommandInterpreter &interpreter);
+ ~CommandObjectProtocolServer() override;
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_COMMANDS_COMMANDOBJECTMCP_H
diff --git a/lldb/source/Core/CMakeLists.txt b/lldb/source/Core/CMakeLists.txt
index d6b75bca7f2d6..df35bd5c025f3 100644
--- a/lldb/source/Core/CMakeLists.txt
+++ b/lldb/source/Core/CMakeLists.txt
@@ -46,6 +46,7 @@ add_lldb_library(lldbCore NO_PLUGIN_DEPENDENCIES
Opcode.cpp
PluginManager.cpp
Progress.cpp
+ ProtocolServer.cpp
Statusline.cpp
RichManglingContext.cpp
SearchFilter.cpp
diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index 81037d3def811..2bc9c7ead79d3 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -16,6 +16,7 @@
#include "lldb/Core/ModuleSpec.h"
#include "lldb/Core/PluginManager.h"
#include "lldb/Core/Progress.h"
+#include "lldb/Core/ProtocolServer.h"
#include "lldb/Core/StreamAsynchronousIO.h"
#include "lldb/Core/Telemetry.h"
#include "lldb/DataFormatters/DataVisualization.h"
@@ -2363,3 +2364,26 @@ llvm::ThreadPoolInterface &Debugger::GetThreadPool() {
"Debugger::GetThreadPool called before Debugger::Initialize");
return *g_thread_pool;
}
+
+void Debugger::AddProtocolServer(lldb::ProtocolServerSP protocol_server_sp) {
+ assert(protocol_server_sp &&
+ GetProtocolServer(protocol_server_sp->GetPluginName()) == nullptr);
+ m_protocol_servers.push_back(protocol_server_sp);
+}
+
+void Debugger::RemoveProtocolServer(lldb::ProtocolServerSP protocol_server_sp) {
+ auto it = llvm::find(m_protocol_servers, protocol_server_sp);
+ if (it != m_protocol_servers.end())
+ m_protocol_servers.erase(it);
+}
+
+lldb::ProtocolServerSP
+Debugger::GetProtocolServer(llvm::StringRef protocol) const {
+ for (ProtocolServerSP protocol_server_sp : m_protocol_servers) {
+ if (!protocol_server_sp)
+ continue;
+ if (protocol_server_sp->GetPluginName() == protocol)
+ return protocol_server_sp;
+ }
+ return nullptr;
+}
diff --git a/lldb/source/Core/PluginManager.cpp b/lldb/source/Core/PluginManager.cpp
index 5d44434033c55..11085a8463803 100644
--- a/lldb/source/Core/PluginManager.cpp
+++ b/lldb/source/Core/PluginManager.cpp
@@ -1006,6 +1006,33 @@ void PluginManager::AutoCompleteProcessName(llvm::StringRef name,
}
}
+#pragma mark ProtocolServer
+
+typedef PluginInstance<ProtocolServerCreateInstance> ProtocolServerInstance;
+typedef PluginInstances<ProtocolServerInstance> ProtocolServerInstances;
+
+static ProtocolServerInstances &GetProtocolServerInstances() {
+ static ProtocolServerInstances g_instances;
+ return g_instances;
+}
+
+bool PluginManager::RegisterPlugin(
+ llvm::StringRef name, llvm::StringRef description,
+ ProtocolServerCreateInstance create_callback) {
+ return GetProtocolServerInstances().RegisterPlugin(name, description,
+ create_callback);
+}
+
+bool PluginManager::UnregisterPlugin(
+ ProtocolServerCreateInstance create_callback) {
+ return GetProtocolServerInstances().UnregisterPlugin(create_callback);
+}
+
+ProtocolServerCreateInstance
+PluginManager::GetProtocolCreateCallbackForPluginName(llvm::StringRef name) {
+ return GetProtocolServerInstances().GetCallbackForName(name);
+}
+
#pragma mark RegisterTypeBuilder
struct RegisterTypeBuilderInstance
diff --git a/lldb/source/Core/ProtocolServer.cpp b/lldb/source/Core/ProtocolServer.cpp
new file mode 100644
index 0000000000000..d57a047afa7b2
--- /dev/null
+++ b/lldb/source/Core/ProtocolServer.cpp
@@ -0,0 +1,21 @@
+//===-- ProtocolServer.cpp ------------------------------------------------===//
+//
+// 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/Core/ProtocolServer.h"
+#include "lldb/Core/PluginManager.h"
+
+using namespace lldb_private;
+using namespace lldb;
+
+ProtocolServerSP ProtocolServer::Create(llvm::StringRef name,
+ Debugger &debugger) {
+ if (ProtocolServerCreateInstance create_callback =
+ PluginManager::GetProtocolCreateCallbackForPluginName(name))
+ return create_callback(debugger);
+ return nullptr;
+}
diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp
index 4f9ae104dedea..00c3472444d2e 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -30,6 +30,7 @@
#include "Commands/CommandObjectPlatform.h"
#include "Commands/CommandObjectPlugin.h"
#include "Commands/CommandObjectProcess.h"
+#include "Commands/CommandObjectProtocolServer.h"
#include "Commands/CommandObjectQuit.h"
#include "Commands/CommandObjectRegexCommand.h"
#include "Commands/CommandObjectRegister.h"
@@ -574,6 +575,7 @@ void CommandInterpreter::LoadCommandDictionary() {
REGISTER_COMMAND_OBJECT("platform", CommandObjectPlatform);
REGISTER_COMMAND_OBJECT("plugin", CommandObjectPlugin);
REGISTER_COMMAND_OBJECT("process", CommandObjectMultiwordProcess);
+ REGISTER_COMMAND_OBJECT("protocol-server", CommandObjectProtocolServer);
REGISTER_COMMAND_OBJECT("quit", CommandObjectQuit);
REGISTER_COMMAND_OBJECT("register", CommandObjectRegister);
REGISTER_COMMAND_OBJECT("scripting", CommandObjectMultiwordScripting);
diff --git a/lldb/source/Plugins/CMakeLists.txt b/lldb/source/Plugins/CMakeLists.txt
index 854f589f45ae0..cf2da73e38931 100644
--- a/lldb/source/Plugins/CMakeLists.txt
+++ b/lldb/source/Plugins/CMakeLists.txt
@@ -14,6 +14,7 @@ add_subdirectory(ObjectFile)
add_subdirectory(OperatingSystem)
add_subdirectory(Platform)
add_subdirectory(Process)
+add_subdirectory(Protocol)
add_subdirectory(REPL)
add_subdirectory(RegisterTypeBuilder)
add_subdirectory(ScriptInterpreter)
diff --git a/lldb/source/Plugins/Protocol/CMakeLists.txt b/lldb/source/Plugins/Protocol/CMakeLists.txt
new file mode 100644
index 0000000000000..93b347d4cc9d8
--- /dev/null
+++ b/lldb/source/Plugins/Protocol/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(MCP)
diff --git a/lldb/source/Plugins/Protocol/MCP/CMakeLists.txt b/lldb/source/Plugins/Protocol/MCP/CMakeLists.txt
new file mode 100644
index 0000000000000..db31a7a69cb33
--- /dev/null
+++ b/lldb/source/Plugins/Protocol/MCP/CMakeLists.txt
@@ -0,0 +1,13 @@
+add_lldb_library(lldbPluginProtocolServerMCP PLUGIN
+ MCPError.cpp
+ Protocol.cpp
+ ProtocolServerMCP.cpp
+ Tool.cpp
+
+ LINK_COMPONENTS
+ Support
+
+ LINK_LIBS
+ lldbHost
+ lldbUtility
+)
diff --git a/lldb/source/Plugins/Protocol/MCP/MCPError.cpp b/lldb/source/Plugins/Protocol/MCP/MCPError.cpp
new file mode 100644
index 0000000000000..69e1b5371af6f
--- /dev/null
+++ b/lldb/source/Plugins/Protocol/MCP/MCPError.cpp
@@ -0,0 +1,31 @@
+//===-- MCPError.cpp ------------------------------------------------------===//
+//
+// 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 "MCPError.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/raw_ostream.h"
+#include <system_error>
+
+namespace lldb_private::mcp {
+
+char MCPError::ID;
+
+MCPError::MCPError(std::string message, int64_t error_code)
+ : m_message(message), m_error_code(error_code) {}
+
+void MCPError::log(llvm::raw_ostream &OS) const { OS << m_message; }
+
+std::error_code MCPError::convertToErrorCode() const {
+ return llvm::inconvertibleErrorCode();
+}
+
+protocol::Error MCPError::toProtcolError() const {
+ return protocol::Error{m_error_code, m_message, std::nullopt};
+}
+
+} // namespace lldb_private::mcp
diff --git a/lldb/source/Plugins/Protocol/MCP/MCPError.h b/lldb/source/Plugins/Protocol/MCP/MCPError.h
new file mode 100644
index 0000000000000..2a76a7b087e20
--- /dev/null
+++ b/lldb/source/Plugins/Protocol/MCP/MCPError.h
@@ -0,0 +1,33 @@
+//===-- MCPError.h --------------------------------------------------------===//
+//
+// 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 "Protocol.h"
+#include "llvm/Support/Error.h"
+#include <string>
+
+namespace lldb_private::mcp {
+
+class MCPError : public llvm::ErrorInfo<MCPError> {
+public:
+ static char ID;
+
+ MCPError(std::string message, int64_t error_code);
+
+ void log(llvm::raw_ostream &OS) const override;
+ std::error_code convertToErrorCode() const override;
+
+ const std::string &getMessage() const { return m_message; }
+
+ protocol::Error toProtcolError() const;
+
+private:
+ std::string m_message;
+ int64_t m_error_code;
+};
+
+} // namespace lldb_private::mcp
diff --git a/lldb/source/Plugins/Protocol/MCP/Protocol.cpp b/lldb/source/Plugins/Protocol/MCP/Protocol.cpp
new file mode 100644
index 0000000000000..38b4842e0a98d
--- /dev/null
+++ b/lldb/source/Plugins/Protocol/MCP/Protocol.cpp
@@ -0,0 +1,183 @@
+//===- Protocol.cpp -------------------------------------------------------===//
+//
+// 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 "Protocol.h"
+#include "llvm/Support/JSON.h"
+
+using namespace llvm;
+
+namespace lldb_private::mcp::protocol {
+
+static bool mapRaw(const json::Value &Params, StringLiteral Prop,
+ std::optional<json::Value> &V, json::Path P) {
+ const auto *O = Params.getAsObject();
+ if (!O) {
+ P.report("expected object");
+ return false;
+ }
+ const json::Value *E = O->get(Prop);
+ if (E)
+ V = std::move(*E);
+ return true;
+}
+
+llvm::json::Value toJSON(const Request &R) {
+ json::Object Result{{"jsonrpc", "2.0"}, {"id", R.id}, {"method", R.method}};
+ if (R.params)
+ Result.insert({"params", R.params});
+ return Result;
+}
+
+bool fromJSON(const llvm::json::Value &V, Request &R, llvm::json::Path P) {
+ llvm::json::ObjectMapper O(V, P);
+ if (!O || !O.map("id", R.id) || !O.map("method", R.method))
+ return false;
+ return mapRaw(V, "params", R.params, P);
+}
+
+llvm::json::Value toJSON(const Error &E) {
+ return llvm::json::Object{
+ {"code", E.code}, {"message", E.message}, {"data", E.data}};
+}
+
+bool fromJSON(const llvm::json::Value &V, Error &E, llvm::json::Path P) {
+ llvm::json::ObjectMapper O(V, P);
+ return O && O.map("code", E.code) && O.map("message", E.message) &&
+ O.map("data", E.data);
+}
+
+llvm::json::Value toJSON(const ProtocolError &PE) {
+ return llvm::json::Object{{"id", PE.id}, {"error", toJSON(PE.error)}};
+}
+
+bool fromJSON(const llvm::json::Value &V, ProtocolError &PE,
+ llvm::json::Path P) {
+ llvm::json::ObjectMapper O(V, P);
+ return O && O.map("id", PE.id) && O.map("error", PE.error);
+}
+
+llvm::json::Value toJSON(const Response &R) {
+ llvm::json::Object Result{{"jsonrpc", "2.0"}, {"id", R.id}};
+ if (R.result)
+ Result.insert({"result", R.result});
+ if (R.error)
+ Result.insert({"error", R.error});
+ return Result;
+}
+
+bool fromJSON(const llvm::json::Value &V, Response &R, llvm::json::Path P) {
+ llvm::json::ObjectMapper O(V, P);
+ if (!O || !O.map("id", R.id) || !O.map("error", R.error))
+ return false;
+ return mapRaw(V, "result", R.result, P);
+}
+
+llvm::json::Value toJSON(const Notification &N) {
+ llvm::json::Object Result{{"jsonrpc", "2.0"}, {"method", N.method}};
+ if (N.params)
+ Result.insert({"params", N.params});
+ return Result;
+}
+
+bool fromJSON(const llvm::json::Value &V, Notification &N, llvm::json::Path P) {
+ llvm::json::ObjectMapper O(V, P);
+ if (!O || !O.map("method", N.method))
+ return false;
+ auto *Obj = V.getAsObject();
+ if (!Obj)
+ return false;
+ if (auto *Params = Obj->get("params"))
+ N.params = *Params;
+ return true;
+}
+
+llvm::json::Value toJSON(const ToolCapability &TC) {
+ return llvm::json::Object{{"listChanged", TC.listChanged}};
+}
+
+bool fromJSON(const llvm::json::Value &V, ToolCapability &TC,
+ llvm::json::Path P) {
+ llvm::json::ObjectMapper O(V, P);
+ return O && O.map("listChanged", TC.listChanged);
+}
+
+llvm::json::Value toJSON(const Capabilities &C) {
+ return llvm::json::Object{{"tools", C.tools}};
+}
+
+bool fromJSON(const llvm::json::Value &V, Capabilities &C, llvm::json::Path P) {
+ llvm::json::ObjectMapper O(V, P);
+ return O && O.map("tools", C.tools);
+}
+
+llvm::json::Value toJSON(const TextContent &TC) {
+ return llvm::json::Object{{"type", "text"}, {"text", TC.text}};
+}
+
+bool fromJSON(const llvm::json::Value &V, TextContent &TC, llvm::json::Path P) {
+ llvm::json::ObjectMapper O(V, P);
+ return O && O.map("text", TC.text);
+}
+
+llvm::json::Value toJSON(const TextResult &TR) {
+ return llvm::json::Object{{"content", TR.content}, {"isError", TR.isError}};
+}
+
+bool fromJSON(const llvm::json::Value &V, TextResult &TR, llvm::json::Path P) {
+ llvm::json::ObjectMapper O(V, P);
+ return O && O.map("content", TR.content) && O.map("isError", TR.isError);
+}
+
+llvm::json::Value toJSON(const ToolAnnotations &TA) {
+ llvm::json::Object Result;
+ if (TA.title)
+ Result.insert({"title", TA.title});
+ if (TA.readOnlyHint)
+ Result.insert({"readOnlyHint", TA.readOnlyHint});
+ if (TA.destructiveHint)
+ Result.insert({"destructiveHint", TA.destructiveHint});
+ if (TA.idempotentHint)
+ Result.insert({"idempotentHint", TA.idempotentHint});
+ if (TA.openWorldHint)
+ Result.insert({"openWorldHint", TA.openWorldHint});
+ return Result;
+}
+
+bool fromJSON(const llvm::json::Value &V, ToolAnnotations &TA,
+ llvm::json::Path P) {
+ llvm::json::ObjectMapper O(V, P);
+ return O && O.mapOptional("title", TA.title) &&
+ O.mapOptional("readOnlyHint", TA.readOnlyHint) &&
+ O.mapOptional("destructiveHint", TA.destructiveHint) &&
+ O.mapOptional("idempotentHint", TA.idempotentHint) &&
+ O.mapOptional("openWorldHint", TA.openWorldHint);
+}
+
+llvm::json::Value toJSON(const ToolDefinition &TD) {
+ llvm::json::Object Result{{"name", TD.name}};
+ if (TD.description)
+ Result.insert({"description", TD.description});
+ if (TD.inputSchema)
+ Result.insert({"inputSchema", TD.inputSchema});
+ if (TD.annotations)
+ Result.insert({"annotations", TD.annotations});
+ return Result;
+}
+
+bool fromJSON(const llvm::json::Value &V, ToolDefinition &TD,
+ llvm::json::Path P) {
+
+ llvm::json::ObjectMapper O(V, P);
+ if (!O || !O.map("name", TD.name) ||
+ !O.mapOptional("description", TD.description) ||
+ !O.mapOptional("annotations", TD.annotations))
+ return false;
+ return mapRaw(V, "inputSchema", TD.inputSchema, P);
+}
+
+} // namespace lldb_private::mcp::protocol
diff --git a/lldb/source/Plugins/Protocol/MCP/Protocol.h b/lldb/source/Plugins/Protocol/MCP/Protocol.h
new file mode 100644
index 0000000000000..9e52a47957b85
--- /dev/null
+++ b/lldb/source/Plugins/Protocol/MCP/Protocol.h
@@ -0,0 +1,131 @@
+//===- Protocol.h ---------------------------------------------------------===//
+//
+// 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_PLUGINS_PROTOCOL_MCP_PROTOCOL_H
+#define LLDB_PLUGINS_PROTOCOL_MCP_PROTOCOL_H
+
+#include "llvm/Support/JSON.h"
+#include <optional>
+#include <string>
+
+namespace lldb_private::mcp::protocol {
+
+static llvm::StringLiteral kProtocolVersion = "2025-03-26";
+
+struct Request {
+ uint64_t id = 0;
+ std::string method;
+ std::optional<llvm::json::Value> params;
+};
+
+llvm::json::Value toJSON(const Request &);
+bool fromJSON(const llvm::json::Value &, Request &, llvm::json::Path);
+
+struct Error {
+ int64_t code = 0;
+ std::string message;
+ std::optional<std::string> data;
+};
+
+llvm::json::Value toJSON(const Error &);
+bool fromJSON(const llvm::json::Value &, Error &, llvm::json::Path);
+
+struct ProtocolError {
+ uint64_t id = 0;
+ Error error;
+};
+
+llvm::json::Value toJSON(const ProtocolError &);
+bool fromJSON(const llvm::json::Value &, ProtocolError &, llvm::json::Path);
+
+struct Response {
+ uint64_t id = 0;
+ std::optional<llvm::json::Value> result;
+ std::optional<Error> error;
+};
+
+llvm::json::Value toJSON(const Response &);
+bool fromJSON(const llvm::json::Value &, Response &, llvm::json::Path);
+
+struct Notification {
+ std::string method;
+ std::optional<llvm::json::Value> params;
+};
+
+llvm::json::Value toJSON(const Notification &);
+bool fromJSON(const llvm::json::Value &, Notification &, llvm::json::Path);
+
+struct ToolCapability {
+ bool listChanged = false;
+};
+
+llvm::json::Value toJSON(const ToolCapability &);
+bool fromJSON(const llvm::json::Value &, ToolCapability &, llvm::json::Path);
+
+struct Capabilities {
+ ToolCapability tools;
+};
+
+llvm::json::Value toJSON(const Capabilities &);
+bool fromJSON(const llvm::json::Value &, Capabilities &, llvm::json::Path);
+
+struct TextContent {
+ std::string text;
+};
+
+llvm::json::Value toJSON(const TextContent &);
+bool fromJSON(const llvm::json::Value &, TextContent &, llvm::json::Path);
+
+struct TextResult {
+ std::vector<TextContent> content;
+ bool isError = false;
+};
+
+llvm::json::Value toJSON(const TextResult &);
+bool fromJSON(const llvm::json::Value &, TextResult &, llvm::json::Path);
+
+struct ToolAnnotations {
+ // Human-readable title for the tool.
+ std::optional<std::string> title;
+
+ /// If true, the tool does not modify its environment.
+ std::optional<bool> readOnlyHint;
+
+ /// If true, the tool may perform destructive updates.
+ std::optional<bool> destructiveHint;
+
+ /// If true, repeated calls with same args have no additional effect.
+ std::optional<bool> idempotentHint;
+
+ /// If true, tool interacts with external entities.
+ std::optional<bool> openWorldHint;
+};
+
+llvm::json::Value toJSON(const ToolAnnotations &);
+bool fromJSON(const llvm::json::Value &, ToolAnnotations &, llvm::json::Path);
+
+struct ToolDefinition {
+ /// Unique identifier for the tool.
+ std::string name;
+
+ /// Human-readable description.
+ std::optional<std::string> description;
+
+ // JSON Schema for the tool's parameters.
+ std::optional<llvm::json::Value> inputSchema;
+
+ // Optional hints about tool behavior.
+ std::optional<ToolAnnotations> annotations;
+};
+
+llvm::json::Value toJSON(const ToolDefinition &);
+bool fromJSON(const llvm::json::Value &, ToolDefinition &, llvm::json::Path);
+
+} // namespace lldb_private::mcp::protocol
+
+#endif
diff --git a/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp b/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp
new file mode 100644
index 0000000000000..042c7ac8b76fa
--- /dev/null
+++ b/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp
@@ -0,0 +1,280 @@
+//===- ProtocolServerMCP.cpp ----------------------------------------------===//
+//
+// 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 "ProtocolServerMCP.h"
+#include "MCPError.h"
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Utility/LLDBLog.h"
+#include "lldb/Utility/Log.h"
+#include "llvm/ADT/StringExtras.h"
+#include "llvm/Support/Threading.h"
+#include <thread>
+
+using namespace lldb_private;
+using namespace lldb_private::mcp;
+using namespace llvm;
+
+LLDB_PLUGIN_DEFINE(ProtocolServerMCP)
+
+ProtocolServerMCP::ProtocolServerMCP(Debugger &debugger)
+ : ProtocolServer(), m_debugger(debugger) {
+ AddHandler("initialize", std::bind(&ProtocolServerMCP::InitializeHandler,
+ this, std::placeholders::_1));
+ AddHandler("tools/list", std::bind(&ProtocolServerMCP::ToolsListHandler, this,
+ std::placeholders::_1));
+ AddHandler("tools/call", std::bind(&ProtocolServerMCP::ToolsCallHandler, this,
+ std::placeholders::_1));
+ AddTool(std::make_unique<LLDBCommandTool>(
+ "lldb_command", "Run an lldb command.", m_debugger));
+}
+
+ProtocolServerMCP::~ProtocolServerMCP() { llvm::consumeError(Stop()); }
+
+void ProtocolServerMCP::Initialize() {
+ PluginManager::RegisterPlugin(GetPluginNameStatic(),
+ GetPluginDescriptionStatic(), CreateInstance);
+}
+
+void ProtocolServerMCP::Terminate() {
+ PluginManager::UnregisterPlugin(CreateInstance);
+}
+
+lldb::ProtocolServerSP ProtocolServerMCP::CreateInstance(Debugger &debugger) {
+ return std::make_shared<ProtocolServerMCP>(debugger);
+}
+
+llvm::StringRef ProtocolServerMCP::GetPluginDescriptionStatic() {
+ return "MCP Server.";
+}
+
+llvm::Expected<protocol::Response>
+ProtocolServerMCP::Handle(protocol::Request request) {
+ auto it = m_handlers.find(request.method);
+ if (it != m_handlers.end())
+ return it->second(request);
+
+ return make_error<MCPError>(
+ llvm::formatv("no handler for request: {0}", request.method).str(), 1);
+}
+
+llvm::Error ProtocolServerMCP::Start(ProtocolServer::Connection connection) {
+ std::lock_guard<std::mutex> guard(m_server_mutex);
+
+ if (m_running)
+ return llvm::createStringError("server already running");
+
+ Status status;
+ m_listener = Socket::Create(connection.protocol, status);
+ if (status.Fail())
+ return status.takeError();
+
+ status = m_listener->Listen(connection.name, /*backlog=*/5);
+ if (status.Fail())
+ return status.takeError();
+
+ std::string address =
+ llvm::join(m_listener->GetListeningConnectionURI(), ", ");
+ Log *log = GetLog(LLDBLog::Host);
+ LLDB_LOG(log, "MCP server started with connection listeners: {0}", address);
+
+ auto handles = m_listener->Accept(m_loop, [=](std::unique_ptr<Socket> sock) {
+ std::lock_guard<std::mutex> guard(m_server_mutex);
+
+ const std::string client_name =
+ llvm::formatv("client-{0}", m_clients.size() + 1).str();
+ LLDB_LOG(log, "client {0} connected", client_name);
+
+ lldb::IOObjectSP io(std::move(sock));
+
+ m_clients.emplace_back(io, [=]() {
+ llvm::set_thread_name(client_name + "-runloop");
+ if (auto Err = Run(std::make_unique<JSONRPCTransport>(io, io)))
+ LLDB_LOG_ERROR(GetLog(LLDBLog::Host), std::move(Err), "MCP Error: {0}");
+ });
+ });
+ if (llvm::Error error = handles.takeError())
+ return error;
+
+ m_read_handles = std::move(*handles);
+ m_loop_thread = std::thread([=] {
+ llvm::set_thread_name("mcp-runloop");
+ m_loop.Run();
+ });
+
+ return llvm::Error::success();
+}
+
+llvm::Error ProtocolServerMCP::Stop() {
+ {
+ std::lock_guard<std::mutex> guard(m_server_mutex);
+ m_running = false;
+ }
+
+ // Stop accepting new connections.
+ m_loop.AddPendingCallback(
+ [](MainLoopBase &loop) { loop.RequestTermination(); });
+
+ // Wait for the main loop to exit.
+ if (m_loop_thread.joinable())
+ m_loop_thread.join();
+
+ // Wait for all our clients to exit.
+ for (auto &client : m_clients) {
+ client.first->Close();
+ if (client.second.joinable())
+ client.second.join();
+ }
+
+ {
+ std::lock_guard<std::mutex> guard(m_server_mutex);
+ m_listener.reset();
+ m_read_handles.clear();
+ m_clients.clear();
+ }
+
+ return llvm::Error::success();
+}
+
+llvm::Error ProtocolServerMCP::Run(std::unique_ptr<JSONTransport> transport) {
+ Log *log = GetLog(LLDBLog::Host);
+
+ while (true) {
+ llvm::Expected<protocol::Request> request =
+ transport->Read<protocol::Request>(std::chrono::seconds(1));
+ if (request.errorIsA<TransportEOFError>() ||
+ request.errorIsA<TransportInvalidError>()) {
+ consumeError(request.takeError());
+ break;
+ }
+
+ if (request.errorIsA<TransportTimeoutError>()) {
+ consumeError(request.takeError());
+ continue;
+ }
+
+ if (llvm::Error err = request.takeError()) {
+ LLDB_LOG_ERROR(log, std::move(err), "{0}");
+ continue;
+ }
+
+ protocol::Response response;
+ llvm::Expected<protocol::Response> maybe_response = Handle(*request);
+ if (!maybe_response) {
+ llvm::handleAllErrors(
+ maybe_response.takeError(),
+ [&](const MCPError &err) {
+ response.error.emplace(err.toProtcolError());
+ },
+ [&](const llvm::ErrorInfoBase &err) {
+ response.error.emplace();
+ response.error->code = -1;
+ response.error->message = err.message();
+ });
+ } else {
+ response = *maybe_response;
+ }
+
+ response.id = request->id;
+
+ if (llvm::Error err = transport->Write(response))
+ return err;
+ }
+
+ return llvm::Error::success();
+}
+
+protocol::Capabilities ProtocolServerMCP::GetCapabilities() {
+ protocol::Capabilities capabilities;
+ capabilities.tools.listChanged = true;
+ return capabilities;
+}
+
+void ProtocolServerMCP::AddTool(std::unique_ptr<Tool> tool) {
+ std::lock_guard<std::mutex> guard(m_server_mutex);
+
+ if (!tool)
+ return;
+ m_tools[tool->GetName()] = std::move(tool);
+}
+
+void ProtocolServerMCP::AddHandler(
+ llvm::StringRef method,
+ std::function<protocol::Response(const protocol::Request &)> handler) {
+ std::lock_guard<std::mutex> guard(m_server_mutex);
+
+ m_handlers[method] = std::move(handler);
+}
+
+protocol::Response
+ProtocolServerMCP::InitializeHandler(const protocol::Request &request) {
+ protocol::Response response;
+
+ std::string protocol_version = protocol::kProtocolVersion.str();
+ if (request.params) {
+ if (const json::Object *param_obj = request.params->getAsObject()) {
+ if (const json::Value *val = param_obj->get("protocolVersion")) {
+ if (auto protocol_version_str = val->getAsString()) {
+ protocol_version = *protocol_version_str;
+ }
+ }
+ }
+ }
+
+ response.result.emplace(llvm::json::Object{
+ {"protocolVersion", protocol_version},
+ {"capabilities", GetCapabilities()},
+ {"serverInfo",
+ llvm::json::Object{{"name", kName}, {"version", kVersion}}}});
+ return response;
+}
+
+protocol::Response
+ProtocolServerMCP::ToolsListHandler(const protocol::Request &request) {
+ protocol::Response response;
+
+ llvm::json::Array tools;
+ for (const auto &tool : m_tools)
+ tools.emplace_back(toJSON(tool.second->GetDefinition()));
+
+ response.result.emplace(llvm::json::Object{{"tools", std::move(tools)}});
+
+ return response;
+}
+
+protocol::Response
+ProtocolServerMCP::ToolsCallHandler(const protocol::Request &request) {
+ protocol::Response response;
+
+ if (!request.params)
+ return response;
+
+ const json::Object *param_obj = request.params->getAsObject();
+ if (!param_obj)
+ return response;
+
+ const json::Value *name = param_obj->get("name");
+ if (!name)
+ return response;
+
+ llvm::StringRef tool_name = name->getAsString().value_or("");
+ if (tool_name.empty())
+ return response;
+
+ auto it = m_tools.find(tool_name);
+ if (it == m_tools.end())
+ return response;
+
+ const json::Value *args = param_obj->get("arguments");
+ if (!args)
+ return response;
+
+ protocol::TextResult text_result = it->second->Call(*args);
+ response.result.emplace(toJSON(text_result));
+
+ return response;
+}
diff --git a/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.h b/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.h
new file mode 100644
index 0000000000000..30284d5661ca0
--- /dev/null
+++ b/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.h
@@ -0,0 +1,80 @@
+//===- ProtocolServerMCP.h ------------------------------------------------===//
+//
+// 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_PLUGINS_PROTOCOL_MCP_PROTOCOLSERVERMCP_H
+#define LLDB_PLUGINS_PROTOCOL_MCP_PROTOCOLSERVERMCP_H
+
+#include "Protocol.h"
+#include "Tool.h"
+#include "lldb/Core/ProtocolServer.h"
+#include "lldb/Host/JSONTransport.h"
+#include "lldb/Host/MainLoop.h"
+#include "lldb/Host/Socket.h"
+#include "llvm/ADT/StringMap.h"
+#include <thread>
+
+namespace lldb_private::mcp {
+
+class ProtocolServerMCP : public ProtocolServer {
+public:
+ ProtocolServerMCP(Debugger &debugger);
+ virtual ~ProtocolServerMCP() override;
+
+ virtual llvm::Error Start(ProtocolServer::Connection connection) override;
+ virtual llvm::Error Stop() override;
+
+ static void Initialize();
+ static void Terminate();
+
+ static llvm::StringRef GetPluginNameStatic() { return "MCP"; }
+ static llvm::StringRef GetPluginDescriptionStatic();
+
+ static lldb::ProtocolServerSP CreateInstance(Debugger &debugger);
+
+ llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
+
+protected:
+ void AddTool(std::unique_ptr<Tool> tool);
+ void AddHandler(
+ llvm::StringRef method,
+ std::function<protocol::Response(const protocol::Request &)> handler);
+
+ Socket *GetSocket() const { return m_listener.get(); }
+
+private:
+ llvm::Error Run(std::unique_ptr<JSONTransport> transport);
+ llvm::Expected<protocol::Response> Handle(protocol::Request request);
+
+ protocol::Response InitializeHandler(const protocol::Request &);
+ protocol::Response ToolsListHandler(const protocol::Request &);
+ protocol::Response ToolsCallHandler(const protocol::Request &);
+
+ protocol::Capabilities GetCapabilities();
+
+ llvm::StringLiteral kName = "lldb-mcp";
+ llvm::StringLiteral kVersion = "0.1.0";
+
+ Debugger &m_debugger;
+
+ bool m_running = false;
+
+ MainLoop m_loop;
+ std::thread m_loop_thread;
+
+ std::unique_ptr<Socket> m_listener;
+ std::vector<MainLoopBase::ReadHandleUP> m_read_handles;
+ std::vector<std::pair<lldb::IOObjectSP, std::thread>> m_clients;
+
+ std::mutex m_server_mutex;
+ llvm::StringMap<std::unique_ptr<Tool>> m_tools;
+ llvm::StringMap<std::function<protocol::Response(const protocol::Request &)>>
+ m_handlers;
+};
+} // namespace lldb_private::mcp
+
+#endif
diff --git a/lldb/source/Plugins/Protocol/MCP/Tool.cpp b/lldb/source/Plugins/Protocol/MCP/Tool.cpp
new file mode 100644
index 0000000000000..8a83ab907409e
--- /dev/null
+++ b/lldb/source/Plugins/Protocol/MCP/Tool.cpp
@@ -0,0 +1,72 @@
+//===- Tool.cpp -----------------------------------------------------------===//
+//
+// 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 "Tool.h"
+#include "lldb/Interpreter/CommandInterpreter.h"
+#include "lldb/Interpreter/CommandReturnObject.h"
+
+using namespace lldb_private::mcp;
+using namespace llvm;
+
+Tool::Tool(std::string name, std::string description)
+ : m_name(std::move(name)), m_description(std::move(description)) {}
+
+protocol::ToolDefinition Tool::GetDefinition() const {
+ protocol::ToolDefinition definition;
+ definition.name = m_name;
+ definition.description.emplace(m_description);
+
+ if (std::optional<llvm::json::Value> input_schema = GetSchema())
+ definition.inputSchema = *input_schema;
+
+ if (m_annotations)
+ definition.annotations = m_annotations;
+
+ return definition;
+}
+
+LLDBCommandTool::LLDBCommandTool(std::string name, std::string description,
+ Debugger &debugger)
+ : Tool(std::move(name), std::move(description)), m_debugger(debugger) {}
+
+protocol::TextResult LLDBCommandTool::Call(const llvm::json::Value &args) {
+ std::string arguments;
+ if (const json::Object *args_obj = args.getAsObject()) {
+ if (const json::Value *s = args_obj->get("arguments")) {
+ arguments = s->getAsString().value_or("");
+ }
+ }
+
+ CommandReturnObject result(/*colors=*/false);
+ m_debugger.GetCommandInterpreter().HandleCommand(arguments.c_str(),
+ eLazyBoolYes, result);
+
+ std::string output;
+ llvm::StringRef output_str = result.GetOutputString();
+ if (!output_str.empty())
+ output += output_str.str();
+
+ std::string err_str = result.GetErrorString();
+ if (!err_str.empty()) {
+ if (!output.empty())
+ output += '\n';
+ output += err_str;
+ }
+
+ mcp::protocol::TextResult text_result;
+ text_result.content.emplace_back(mcp::protocol::TextContent{{output}});
+ return text_result;
+}
+
+std::optional<llvm::json::Value> LLDBCommandTool::GetSchema() const {
+ llvm::json::Object str_type{{"type", "string"}};
+ llvm::json::Object properties{{"arguments", std::move(str_type)}};
+ llvm::json::Object schema{{"type", "object"},
+ {"properties", std::move(properties)}};
+ return schema;
+}
diff --git a/lldb/source/Plugins/Protocol/MCP/Tool.h b/lldb/source/Plugins/Protocol/MCP/Tool.h
new file mode 100644
index 0000000000000..1b233d592397b
--- /dev/null
+++ b/lldb/source/Plugins/Protocol/MCP/Tool.h
@@ -0,0 +1,61 @@
+//===- Tool.h -------------------------------------------------------------===//
+//
+// 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_PLUGINS_PROTOCOL_MCP_TOOL_H
+#define LLDB_PLUGINS_PROTOCOL_MCP_TOOL_H
+
+#include "Protocol.h"
+#include "lldb/Core/Debugger.h"
+#include "llvm/Support/JSON.h"
+#include <string>
+
+namespace lldb_private::mcp {
+
+class Tool {
+public:
+ Tool(std::string name, std::string description);
+ virtual ~Tool() = default;
+
+ virtual protocol::TextResult Call(const llvm::json::Value &args) = 0;
+
+ virtual std::optional<llvm::json::Value> GetSchema() const {
+ return std::nullopt;
+ }
+
+ protocol::ToolDefinition GetDefinition() const;
+
+ const std::string &GetName() { return m_name; }
+
+protected:
+ void SetAnnotations(protocol::ToolAnnotations annotations) {
+ m_annotations.emplace(std::move(annotations));
+ }
+
+private:
+ std::string m_name;
+ std::string m_description;
+ std::optional<protocol::ToolAnnotations> m_annotations;
+};
+
+class LLDBCommandTool : public mcp::Tool {
+public:
+ LLDBCommandTool(std::string name, std::string description,
+ Debugger &debugger);
+ ~LLDBCommandTool() = default;
+
+ virtual mcp::protocol::TextResult
+ Call(const llvm::json::Value &args) override;
+
+ virtual std::optional<llvm::json::Value> GetSchema() const override;
+
+private:
+ Debugger &m_debugger;
+};
+} // namespace lldb_private::mcp
+
+#endif
diff --git a/lldb/unittests/CMakeLists.txt b/lldb/unittests/CMakeLists.txt
index 6eaaa4f4c8c98..8708bd5f8f07b 100644
--- a/lldb/unittests/CMakeLists.txt
+++ b/lldb/unittests/CMakeLists.txt
@@ -67,6 +67,7 @@ add_subdirectory(Language)
add_subdirectory(ObjectFile)
add_subdirectory(Platform)
add_subdirectory(Process)
+add_subdirectory(Protocol)
add_subdirectory(ScriptInterpreter)
add_subdirectory(Signals)
add_subdirectory(Symbol)
diff --git a/lldb/unittests/DAP/ProtocolTypesTest.cpp b/lldb/unittests/DAP/ProtocolTypesTest.cpp
index adf43c9ac2046..152c4d9236aab 100644
--- a/lldb/unittests/DAP/ProtocolTypesTest.cpp
+++ b/lldb/unittests/DAP/ProtocolTypesTest.cpp
@@ -9,6 +9,7 @@
#include "Protocol/ProtocolTypes.h"
#include "Protocol/ProtocolEvents.h"
#include "Protocol/ProtocolRequests.h"
+#include "TestingSupport/TestUtilities.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/JSON.h"
#include "llvm/Testing/Support/Error.h"
@@ -20,6 +21,7 @@ using namespace llvm;
using namespace lldb;
using namespace lldb_dap;
using namespace lldb_dap::protocol;
+using lldb_private::roundtripJSON;
using llvm::json::parse;
using llvm::json::Value;
@@ -28,15 +30,6 @@ static std::string pp(const json::Value &E) {
return formatv("{0:2}", E).str();
}
-template <typename T> static llvm::Expected<T> roundtrip(const T &input) {
- llvm::json::Value value = toJSON(input);
- llvm::json::Path::Root root;
- T output;
- if (!fromJSON(value, output, root))
- return root.getError();
- return output;
-}
-
TEST(ProtocolTypesTest, ExceptionBreakpointsFilter) {
ExceptionBreakpointsFilter filter;
filter.filter = "testFilter";
@@ -47,7 +40,7 @@ TEST(ProtocolTypesTest, ExceptionBreakpointsFilter) {
filter.conditionDescription = "Condition for test filter";
llvm::Expected<ExceptionBreakpointsFilter> deserialized_filter =
- roundtrip(filter);
+ roundtripJSON(filter);
ASSERT_THAT_EXPECTED(deserialized_filter, llvm::Succeeded());
EXPECT_EQ(filter.filter, deserialized_filter->filter);
@@ -66,7 +59,7 @@ TEST(ProtocolTypesTest, Source) {
source.sourceReference = 12345;
source.presentationHint = Source::eSourcePresentationHintEmphasize;
- llvm::Expected<Source> deserialized_source = roundtrip(source);
+ llvm::Expected<Source> deserialized_source = roundtripJSON(source);
ASSERT_THAT_EXPECTED(deserialized_source, llvm::Succeeded());
EXPECT_EQ(source.name, deserialized_source->name);
@@ -83,7 +76,7 @@ TEST(ProtocolTypesTest, ColumnDescriptor) {
column.type = eColumnTypeString;
column.width = 20;
- llvm::Expected<ColumnDescriptor> deserialized_column = roundtrip(column);
+ llvm::Expected<ColumnDescriptor> deserialized_column = roundtripJSON(column);
ASSERT_THAT_EXPECTED(deserialized_column, llvm::Succeeded());
EXPECT_EQ(column.attributeName, deserialized_column->attributeName);
@@ -101,7 +94,7 @@ TEST(ProtocolTypesTest, BreakpointMode) {
mode.appliesTo = {eBreakpointModeApplicabilitySource,
eBreakpointModeApplicabilityException};
- llvm::Expected<BreakpointMode> deserialized_mode = roundtrip(mode);
+ llvm::Expected<BreakpointMode> deserialized_mode = roundtripJSON(mode);
ASSERT_THAT_EXPECTED(deserialized_mode, llvm::Succeeded());
EXPECT_EQ(mode.mode, deserialized_mode->mode);
@@ -125,7 +118,8 @@ TEST(ProtocolTypesTest, Breakpoint) {
breakpoint.offset = 4;
breakpoint.reason = BreakpointReason::eBreakpointReasonPending;
- llvm::Expected<Breakpoint> deserialized_breakpoint = roundtrip(breakpoint);
+ llvm::Expected<Breakpoint> deserialized_breakpoint =
+ roundtripJSON(breakpoint);
ASSERT_THAT_EXPECTED(deserialized_breakpoint, llvm::Succeeded());
EXPECT_EQ(breakpoint.id, deserialized_breakpoint->id);
@@ -157,7 +151,7 @@ TEST(ProtocolTypesTest, SourceBreakpoint) {
source_breakpoint.mode = "hardware";
llvm::Expected<SourceBreakpoint> deserialized_source_breakpoint =
- roundtrip(source_breakpoint);
+ roundtripJSON(source_breakpoint);
ASSERT_THAT_EXPECTED(deserialized_source_breakpoint, llvm::Succeeded());
EXPECT_EQ(source_breakpoint.line, deserialized_source_breakpoint->line);
@@ -178,7 +172,7 @@ TEST(ProtocolTypesTest, FunctionBreakpoint) {
function_breakpoint.hitCondition = "3";
llvm::Expected<FunctionBreakpoint> deserialized_function_breakpoint =
- roundtrip(function_breakpoint);
+ roundtripJSON(function_breakpoint);
ASSERT_THAT_EXPECTED(deserialized_function_breakpoint, llvm::Succeeded());
EXPECT_EQ(function_breakpoint.name, deserialized_function_breakpoint->name);
@@ -196,7 +190,7 @@ TEST(ProtocolTypesTest, DataBreakpoint) {
data_breakpoint_info.hitCondition = "10";
llvm::Expected<DataBreakpoint> deserialized_data_breakpoint_info =
- roundtrip(data_breakpoint_info);
+ roundtripJSON(data_breakpoint_info);
ASSERT_THAT_EXPECTED(deserialized_data_breakpoint_info, llvm::Succeeded());
EXPECT_EQ(data_breakpoint_info.dataId,
@@ -233,9 +227,9 @@ TEST(ProtocolTypesTest, Capabilities) {
{eBreakpointModeApplicabilitySource}}};
capabilities.lldbExtVersion = "1.0.0";
- // Perform roundtrip serialization and deserialization.
+ // Perform roundtripJSON serialization and deserialization.
llvm::Expected<Capabilities> deserialized_capabilities =
- roundtrip(capabilities);
+ roundtripJSON(capabilities);
ASSERT_THAT_EXPECTED(deserialized_capabilities, llvm::Succeeded());
// Verify supported features.
@@ -326,7 +320,7 @@ TEST(ProtocolTypesTest, Scope) {
source.presentationHint = Source::eSourcePresentationHintNormal;
scope.source = source;
- llvm::Expected<Scope> deserialized_scope = roundtrip(scope);
+ llvm::Expected<Scope> deserialized_scope = roundtripJSON(scope);
ASSERT_THAT_EXPECTED(deserialized_scope, llvm::Succeeded());
EXPECT_EQ(scope.name, deserialized_scope->name);
EXPECT_EQ(scope.presentationHint, deserialized_scope->presentationHint);
diff --git a/lldb/unittests/Protocol/CMakeLists.txt b/lldb/unittests/Protocol/CMakeLists.txt
new file mode 100644
index 0000000000000..801662b0544d8
--- /dev/null
+++ b/lldb/unittests/Protocol/CMakeLists.txt
@@ -0,0 +1,12 @@
+add_lldb_unittest(ProtocolTests
+ ProtocolMCPTest.cpp
+ ProtocolMCPServerTest.cpp
+
+ LINK_LIBS
+ lldbCore
+ lldbUtility
+ lldbHost
+ lldbPluginPlatformMacOSX
+ lldbPluginProtocolServerMCP
+ LLVMTestingSupport
+ )
diff --git a/lldb/unittests/Protocol/ProtocolMCPServerTest.cpp b/lldb/unittests/Protocol/ProtocolMCPServerTest.cpp
new file mode 100644
index 0000000000000..ab94c5ad5fbc5
--- /dev/null
+++ b/lldb/unittests/Protocol/ProtocolMCPServerTest.cpp
@@ -0,0 +1,195 @@
+//===-- ProtocolServerMCPTest.cpp -----------------------------------------===//
+//
+// 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/PlatformRemoteMacOSX.h"
+#include "Plugins/Protocol/MCP/ProtocolServerMCP.h"
+#include "TestingSupport/Host/SocketTestUtilities.h"
+#include "TestingSupport/SubsystemRAII.h"
+#include "lldb/Core/ProtocolServer.h"
+#include "lldb/Host/FileSystem.h"
+#include "lldb/Host/HostInfo.h"
+#include "lldb/Host/Socket.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gtest/gtest.h"
+
+using namespace llvm;
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::mcp::protocol;
+
+namespace {
+class TestProtocolServerMCP : public lldb_private::mcp::ProtocolServerMCP {
+public:
+ using ProtocolServerMCP::AddTool;
+ using ProtocolServerMCP::GetSocket;
+ using ProtocolServerMCP::ProtocolServerMCP;
+};
+
+class TestJSONTransport : public lldb_private::JSONRPCTransport {
+public:
+ using JSONRPCTransport::JSONRPCTransport;
+ using JSONRPCTransport::ReadImpl;
+ using JSONRPCTransport::WriteImpl;
+};
+
+class TestTool : public mcp::Tool {
+public:
+ using mcp::Tool::Tool;
+
+ virtual mcp::protocol::TextResult
+ Call(const llvm::json::Value &args) override {
+ std::string argument;
+ if (const json::Object *args_obj = args.getAsObject()) {
+ if (const json::Value *s = args_obj->get("arguments")) {
+ argument = s->getAsString().value_or("");
+ }
+ }
+
+ mcp::protocol::TextResult text_result;
+ text_result.content.emplace_back(mcp::protocol::TextContent{{argument}});
+ return text_result;
+ }
+};
+
+class ProtocolServerMCPTest : public ::testing::Test {
+public:
+ SubsystemRAII<FileSystem, HostInfo, PlatformRemoteMacOSX, Socket> subsystems;
+ DebuggerSP m_debugger_sp;
+
+ lldb::IOObjectSP m_io_sp;
+ std::unique_ptr<TestJSONTransport> m_transport_up;
+ std::unique_ptr<TestProtocolServerMCP> m_server_up;
+
+ static constexpr llvm::StringLiteral k_ip = "127.0.0.1";
+
+ llvm::Error Write(llvm::StringRef message) {
+ return m_transport_up->WriteImpl(llvm::formatv("{0}\n", message).str());
+ }
+
+ llvm::Expected<std::string> Read() {
+ return m_transport_up->ReadImpl(std::chrono::milliseconds(100));
+ }
+
+ void SetUp() {
+ // Create a debugger.
+ ArchSpec arch("arm64-apple-macosx-");
+ Platform::SetHostPlatform(
+ PlatformRemoteMacOSX::CreateInstance(true, &arch));
+ m_debugger_sp = Debugger::CreateInstance();
+
+ // Create & start the server.
+ ProtocolServer::Connection connection;
+ connection.protocol = Socket::SocketProtocol::ProtocolTcp;
+ connection.name = llvm::formatv("{0}:0", k_ip).str();
+ m_server_up = std::make_unique<TestProtocolServerMCP>(*m_debugger_sp);
+ m_server_up->AddTool(std::make_unique<TestTool>("test", "test tool"));
+ ASSERT_THAT_ERROR(m_server_up->Start(connection), llvm::Succeeded());
+
+ // Connect to the server over a TCP socket.
+ auto connect_socket_up = std::make_unique<TCPSocket>(true);
+ ASSERT_THAT_ERROR(connect_socket_up
+ ->Connect(llvm::formatv("{0}:{1}", k_ip,
+ static_cast<TCPSocket *>(
+ m_server_up->GetSocket())
+ ->GetLocalPortNumber())
+ .str())
+ .ToError(),
+ llvm::Succeeded());
+
+ // Set up JSON transport for the client.
+ m_io_sp = std::move(connect_socket_up);
+ m_transport_up = std::make_unique<TestJSONTransport>(m_io_sp, m_io_sp);
+ }
+
+ void TearDown() {
+ // Stop the server.
+ ASSERT_THAT_ERROR(m_server_up->Stop(), llvm::Succeeded());
+ }
+};
+
+} // namespace
+
+TEST_F(ProtocolServerMCPTest, Intialization) {
+ llvm::StringLiteral request =
+ R"json({"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"claude-ai","version":"0.1.0"}},"jsonrpc":"2.0","id":0})json";
+ llvm::StringLiteral response =
+ R"json({"jsonrpc":"2.0","id":0,"result":{"capabilities":{"tools":{"listChanged":true}},"protocolVersion":"2024-11-05","serverInfo":{"name":"lldb-mcp","version":"0.1.0"}}})json";
+
+ ASSERT_THAT_ERROR(Write(request), llvm::Succeeded());
+
+ llvm::Expected<std::string> response_str = Read();
+ ASSERT_THAT_EXPECTED(response_str, llvm::Succeeded());
+
+ llvm::Expected<json::Value> response_json = json::parse(*response_str);
+ ASSERT_THAT_EXPECTED(response_json, llvm::Succeeded());
+
+ llvm::Expected<json::Value> expected_json = json::parse(response);
+ ASSERT_THAT_EXPECTED(expected_json, llvm::Succeeded());
+
+ EXPECT_EQ(*response_json, *expected_json);
+}
+
+TEST_F(ProtocolServerMCPTest, ToolsList) {
+ llvm::StringLiteral request =
+ R"json({"method":"tools/list","params":{},"jsonrpc":"2.0","id":1})json";
+ llvm::StringLiteral response =
+ R"json({"id":1,"jsonrpc":"2.0","result":{"tools":[{"description":"test tool","name":"test"},{"description":"Run an lldb command.","inputSchema":{"properties":{"arguments":{"type":"string"}},"type":"object"},"name":"lldb_command"}]}})json";
+
+ ASSERT_THAT_ERROR(Write(request), llvm::Succeeded());
+
+ llvm::Expected<std::string> response_str = Read();
+ ASSERT_THAT_EXPECTED(response_str, llvm::Succeeded());
+
+ llvm::Expected<json::Value> response_json = json::parse(*response_str);
+ ASSERT_THAT_EXPECTED(response_json, llvm::Succeeded());
+
+ llvm::Expected<json::Value> expected_json = json::parse(response);
+ ASSERT_THAT_EXPECTED(expected_json, llvm::Succeeded());
+
+ EXPECT_EQ(*response_json, *expected_json);
+}
+
+TEST_F(ProtocolServerMCPTest, ResourcesList) {
+ llvm::StringLiteral request =
+ R"json({"method":"resources/list","params":{},"jsonrpc":"2.0","id":2})json";
+ llvm::StringLiteral response =
+ R"json({"error":{"code":1,"data":null,"message":"no handler for request: resources/list"},"id":2,"jsonrpc":"2.0"})json";
+
+ ASSERT_THAT_ERROR(Write(request), llvm::Succeeded());
+
+ llvm::Expected<std::string> response_str = Read();
+ ASSERT_THAT_EXPECTED(response_str, llvm::Succeeded());
+
+ llvm::Expected<json::Value> response_json = json::parse(*response_str);
+ ASSERT_THAT_EXPECTED(response_json, llvm::Succeeded());
+
+ llvm::Expected<json::Value> expected_json = json::parse(response);
+ ASSERT_THAT_EXPECTED(expected_json, llvm::Succeeded());
+
+ EXPECT_EQ(*response_json, *expected_json);
+}
+
+TEST_F(ProtocolServerMCPTest, ToolsCall) {
+ llvm::StringLiteral request =
+ R"json({"method":"tools/call","params":{"name":"test","arguments":{"arguments":"foo"}},"jsonrpc":"2.0","id":11})json";
+ llvm::StringLiteral response =
+ R"json({"id":11,"jsonrpc":"2.0","result":{"content":[{"text":"foo","type":"text"}],"isError":false}})json";
+
+ ASSERT_THAT_ERROR(Write(request), llvm::Succeeded());
+
+ llvm::Expected<std::string> response_str = Read();
+ ASSERT_THAT_EXPECTED(response_str, llvm::Succeeded());
+
+ llvm::Expected<json::Value> response_json = json::parse(*response_str);
+ ASSERT_THAT_EXPECTED(response_json, llvm::Succeeded());
+
+ llvm::Expected<json::Value> expected_json = json::parse(response);
+ ASSERT_THAT_EXPECTED(expected_json, llvm::Succeeded());
+
+ EXPECT_EQ(*response_json, *expected_json);
+}
diff --git a/lldb/unittests/Protocol/ProtocolMCPTest.cpp b/lldb/unittests/Protocol/ProtocolMCPTest.cpp
new file mode 100644
index 0000000000000..58263b9839657
--- /dev/null
+++ b/lldb/unittests/Protocol/ProtocolMCPTest.cpp
@@ -0,0 +1,176 @@
+//===-- ProtocolMCPTest.cpp -----------------------------------------------===//
+//
+// 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/Protocol/MCP/Protocol.h"
+#include "TestingSupport/TestUtilities.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gtest/gtest.h"
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::mcp::protocol;
+
+TEST(ProtocolMCPTest, Request) {
+ Request request;
+ request.id = 1;
+ request.method = "foo";
+ request.params = llvm::json::Object{{"key", "value"}};
+
+ llvm::Expected<Request> deserialized_request = roundtripJSON(request);
+ ASSERT_THAT_EXPECTED(deserialized_request, llvm::Succeeded());
+
+ EXPECT_EQ(request.id, deserialized_request->id);
+ EXPECT_EQ(request.method, deserialized_request->method);
+ EXPECT_EQ(request.params, deserialized_request->params);
+}
+
+TEST(ProtocolMCPTest, Response) {
+ Error error;
+ error.code = 42;
+ error.message = "error";
+ error.data = "data";
+
+ Response response;
+ response.id = 1;
+ response.result = llvm::json::Object{{"key", "value"}};
+ response.error = error;
+
+ llvm::Expected<Response> deserialized_response = roundtripJSON(response);
+ ASSERT_THAT_EXPECTED(deserialized_response, llvm::Succeeded());
+
+ EXPECT_EQ(response.id, deserialized_response->id);
+ EXPECT_EQ(response.result, deserialized_response->result);
+ EXPECT_EQ(response.error->code, deserialized_response->error->code);
+ EXPECT_EQ(response.error->message, deserialized_response->error->message);
+ EXPECT_EQ(response.error->data, deserialized_response->error->data);
+}
+
+TEST(ProtocolMCPTest, Notification) {
+ Notification notification;
+ notification.method = "notifyMethod";
+ notification.params = llvm::json::Object{{"key", "value"}};
+
+ llvm::Expected<Notification> deserialized_notification =
+ roundtripJSON(notification);
+ ASSERT_THAT_EXPECTED(deserialized_notification, llvm::Succeeded());
+
+ EXPECT_EQ(notification.method, deserialized_notification->method);
+ EXPECT_EQ(notification.params, deserialized_notification->params);
+}
+
+TEST(ProtocolMCPTest, ToolCapability) {
+ ToolCapability tool_capability;
+ tool_capability.listChanged = true;
+
+ llvm::Expected<ToolCapability> deserialized_tool_capability =
+ roundtripJSON(tool_capability);
+ ASSERT_THAT_EXPECTED(deserialized_tool_capability, llvm::Succeeded());
+
+ EXPECT_EQ(tool_capability.listChanged,
+ deserialized_tool_capability->listChanged);
+}
+
+TEST(ProtocolMCPTest, Capabilities) {
+ ToolCapability tool_capability;
+ tool_capability.listChanged = true;
+
+ Capabilities capabilities;
+ capabilities.tools = tool_capability;
+
+ llvm::Expected<Capabilities> deserialized_capabilities =
+ roundtripJSON(capabilities);
+ ASSERT_THAT_EXPECTED(deserialized_capabilities, llvm::Succeeded());
+
+ EXPECT_EQ(capabilities.tools.listChanged,
+ deserialized_capabilities->tools.listChanged);
+}
+
+TEST(ProtocolMCPTest, TextContent) {
+ TextContent text_content;
+ text_content.text = "Sample text";
+
+ llvm::Expected<TextContent> deserialized_text_content =
+ roundtripJSON(text_content);
+ ASSERT_THAT_EXPECTED(deserialized_text_content, llvm::Succeeded());
+
+ EXPECT_EQ(text_content.text, deserialized_text_content->text);
+}
+
+TEST(ProtocolMCPTest, TextResult) {
+ TextContent text_content1;
+ text_content1.text = "Text 1";
+
+ TextContent text_content2;
+ text_content2.text = "Text 2";
+
+ TextResult text_result;
+ text_result.content = {text_content1, text_content2};
+ text_result.isError = true;
+
+ llvm::Expected<TextResult> deserialized_text_result =
+ roundtripJSON(text_result);
+ ASSERT_THAT_EXPECTED(deserialized_text_result, llvm::Succeeded());
+
+ EXPECT_EQ(text_result.isError, deserialized_text_result->isError);
+ ASSERT_EQ(text_result.content.size(),
+ deserialized_text_result->content.size());
+ EXPECT_EQ(text_result.content[0].text,
+ deserialized_text_result->content[0].text);
+ EXPECT_EQ(text_result.content[1].text,
+ deserialized_text_result->content[1].text);
+}
+
+TEST(ProtocolMCPTest, ToolAnnotations) {
+ ToolAnnotations tool_annotations;
+ tool_annotations.title = "Tool Title";
+ tool_annotations.readOnlyHint = true;
+ tool_annotations.destructiveHint = false;
+ tool_annotations.idempotentHint = true;
+ tool_annotations.openWorldHint = false;
+
+ llvm::Expected<ToolAnnotations> deserialized_tool_annotations =
+ roundtripJSON(tool_annotations);
+ ASSERT_THAT_EXPECTED(deserialized_tool_annotations, llvm::Succeeded());
+
+ EXPECT_EQ(tool_annotations.title, deserialized_tool_annotations->title);
+ EXPECT_EQ(tool_annotations.readOnlyHint,
+ deserialized_tool_annotations->readOnlyHint);
+ EXPECT_EQ(tool_annotations.destructiveHint,
+ deserialized_tool_annotations->destructiveHint);
+ EXPECT_EQ(tool_annotations.idempotentHint,
+ deserialized_tool_annotations->idempotentHint);
+ EXPECT_EQ(tool_annotations.openWorldHint,
+ deserialized_tool_annotations->openWorldHint);
+}
+
+TEST(ProtocolMCPTest, ToolDefinition) {
+ ToolAnnotations tool_annotations;
+ tool_annotations.title = "Tool Title";
+ tool_annotations.readOnlyHint = true;
+
+ ToolDefinition tool_definition;
+ tool_definition.name = "ToolName";
+ tool_definition.description = "Tool Description";
+ tool_definition.inputSchema =
+ llvm::json::Object{{"schemaKey", "schemaValue"}};
+ tool_definition.annotations = tool_annotations;
+
+ llvm::Expected<ToolDefinition> deserialized_tool_definition =
+ roundtripJSON(tool_definition);
+ ASSERT_THAT_EXPECTED(deserialized_tool_definition, llvm::Succeeded());
+
+ EXPECT_EQ(tool_definition.name, deserialized_tool_definition->name);
+ EXPECT_EQ(tool_definition.description,
+ deserialized_tool_definition->description);
+ EXPECT_EQ(tool_definition.inputSchema,
+ deserialized_tool_definition->inputSchema);
+ EXPECT_EQ(tool_definition.annotations->title,
+ deserialized_tool_definition->annotations->title);
+ EXPECT_EQ(tool_definition.annotations->readOnlyHint,
+ deserialized_tool_definition->annotations->readOnlyHint);
+}
diff --git a/lldb/unittests/TestingSupport/TestUtilities.h b/lldb/unittests/TestingSupport/TestUtilities.h
index 65994384059fb..db62881872fef 100644
--- a/lldb/unittests/TestingSupport/TestUtilities.h
+++ b/lldb/unittests/TestingSupport/TestUtilities.h
@@ -59,6 +59,15 @@ class TestFile {
std::string Buffer;
};
+
+template <typename T> static llvm::Expected<T> roundtripJSON(const T &input) {
+ llvm::json::Value value = toJSON(input);
+ llvm::json::Path::Root root;
+ T output;
+ if (!fromJSON(value, output, root))
+ return root.getError();
+ return output;
+}
} // namespace lldb_private
#endif
More information about the lldb-commits
mailing list