1//===-- ProtocolServerMCPTest.cpp -----------------------------------------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
9#include "Plugins/Platform/MacOSX/PlatformRemoteMacOSX.h"
10#include "Plugins/Protocol/MCP/MCPError.h"
11#include "Plugins/Protocol/MCP/ProtocolServerMCP.h"
12#include "TestingSupport/Host/SocketTestUtilities.h"
13#include "TestingSupport/SubsystemRAII.h"
14#include "lldb/Core/ProtocolServer.h"
15#include "lldb/Host/FileSystem.h"
16#include "lldb/Host/HostInfo.h"
17#include "lldb/Host/JSONTransport.h"
18#include "lldb/Host/Socket.h"
19#include "llvm/Testing/Support/Error.h"
20#include "gtest/gtest.h"
21
22using namespace llvm;
23using namespace lldb;
24using namespace lldb_private;
25using namespace lldb_private::mcp::protocol;
26
27namespace {
28class TestProtocolServerMCP : public lldb_private::mcp::ProtocolServerMCP {
29public:
30 using ProtocolServerMCP::AddNotificationHandler;
31 using ProtocolServerMCP::AddRequestHandler;
32 using ProtocolServerMCP::AddResourceProvider;
33 using ProtocolServerMCP::AddTool;
34 using ProtocolServerMCP::GetSocket;
35 using ProtocolServerMCP::ProtocolServerMCP;
36};
37
38class TestJSONTransport : public lldb_private::JSONRPCTransport {
39public:
40 using JSONRPCTransport::JSONRPCTransport;
41 using JSONRPCTransport::ReadImpl;
42 using JSONRPCTransport::WriteImpl;
43};
44
45/// Test tool that returns it argument as text.
46class TestTool : public mcp::Tool {
47public:
48 using mcp::Tool::Tool;
49
50 virtual llvm::Expected<mcp::protocol::TextResult>
51 Call(const ToolArguments &args) override {
52 std::string argument;
53 if (const json::Object *args_obj =
54 std::get<json::Value>(v: args).getAsObject()) {
55 if (const json::Value *s = args_obj->get(K: "arguments")) {
56 argument = s->getAsString().value_or(u: "");
57 }
58 }
59
60 mcp::protocol::TextResult text_result;
61 text_result.content.emplace_back(args: mcp::protocol::TextContent{.text: {argument}});
62 return text_result;
63 }
64};
65
66class TestResourceProvider : public mcp::ResourceProvider {
67 using mcp::ResourceProvider::ResourceProvider;
68
69 virtual std::vector<Resource> GetResources() const override {
70 std::vector<Resource> resources;
71
72 Resource resource;
73 resource.uri = "lldb://foo/bar";
74 resource.name = "name";
75 resource.description = "description";
76 resource.mimeType = "application/json";
77
78 resources.push_back(x: resource);
79 return resources;
80 }
81
82 virtual llvm::Expected<ResourceResult>
83 ReadResource(llvm::StringRef uri) const override {
84 if (uri != "lldb://foo/bar")
85 return llvm::make_error<mcp::UnsupportedURI>(Args: uri.str());
86
87 ResourceContents contents;
88 contents.uri = "lldb://foo/bar";
89 contents.mimeType = "application/json";
90 contents.text = "foobar";
91
92 ResourceResult result;
93 result.contents.push_back(x: contents);
94 return result;
95 }
96};
97
98/// Test tool that returns an error.
99class ErrorTool : public mcp::Tool {
100public:
101 using mcp::Tool::Tool;
102
103 virtual llvm::Expected<mcp::protocol::TextResult>
104 Call(const ToolArguments &args) override {
105 return llvm::createStringError(Fmt: "error");
106 }
107};
108
109/// Test tool that fails but doesn't return an error.
110class FailTool : public mcp::Tool {
111public:
112 using mcp::Tool::Tool;
113
114 virtual llvm::Expected<mcp::protocol::TextResult>
115 Call(const ToolArguments &args) override {
116 mcp::protocol::TextResult text_result;
117 text_result.content.emplace_back(args: mcp::protocol::TextContent{.text: {"failed"}});
118 text_result.isError = true;
119 return text_result;
120 }
121};
122
123class ProtocolServerMCPTest : public ::testing::Test {
124public:
125 SubsystemRAII<FileSystem, HostInfo, PlatformRemoteMacOSX, Socket> subsystems;
126 DebuggerSP m_debugger_sp;
127
128 lldb::IOObjectSP m_io_sp;
129 std::unique_ptr<TestJSONTransport> m_transport_up;
130 std::unique_ptr<TestProtocolServerMCP> m_server_up;
131
132 static constexpr llvm::StringLiteral k_localhost = "localhost";
133
134 llvm::Error Write(llvm::StringRef message) {
135 return m_transport_up->WriteImpl(message: llvm::formatv(Fmt: "{0}\n", Vals&: message).str());
136 }
137
138 llvm::Expected<std::string> Read() {
139 return m_transport_up->ReadImpl(timeout: std::chrono::milliseconds(100));
140 }
141
142 void SetUp() {
143 // Create a debugger.
144 ArchSpec arch("arm64-apple-macosx-");
145 Platform::SetHostPlatform(
146 PlatformRemoteMacOSX::CreateInstance(force: true, arch: &arch));
147 m_debugger_sp = Debugger::CreateInstance();
148
149 // Create & start the server.
150 ProtocolServer::Connection connection;
151 connection.protocol = Socket::SocketProtocol::ProtocolTcp;
152 connection.name = llvm::formatv(Fmt: "{0}:0", Vals: k_localhost).str();
153 m_server_up = std::make_unique<TestProtocolServerMCP>();
154 m_server_up->AddTool(tool: std::make_unique<TestTool>(args: "test", args: "test tool"));
155 m_server_up->AddResourceProvider(resource_provider: std::make_unique<TestResourceProvider>());
156 ASSERT_THAT_ERROR(m_server_up->Start(connection), llvm::Succeeded());
157
158 // Connect to the server over a TCP socket.
159 auto connect_socket_up = std::make_unique<TCPSocket>(args: true);
160 ASSERT_THAT_ERROR(connect_socket_up
161 ->Connect(llvm::formatv("{0}:{1}", k_localhost,
162 static_cast<TCPSocket *>(
163 m_server_up->GetSocket())
164 ->GetLocalPortNumber())
165 .str())
166 .ToError(),
167 llvm::Succeeded());
168
169 // Set up JSON transport for the client.
170 m_io_sp = std::move(connect_socket_up);
171 m_transport_up = std::make_unique<TestJSONTransport>(args&: m_io_sp, args&: m_io_sp);
172 }
173
174 void TearDown() {
175 // Stop the server.
176 ASSERT_THAT_ERROR(m_server_up->Stop(), llvm::Succeeded());
177 }
178};
179
180} // namespace
181
182TEST_F(ProtocolServerMCPTest, Intialization) {
183 llvm::StringLiteral request =
184 R"json({"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"lldb-unit","version":"0.1.0"}},"jsonrpc":"2.0","id":0})json";
185 llvm::StringLiteral response =
186 R"json( {"id":0,"jsonrpc":"2.0","result":{"capabilities":{"resources":{"listChanged":false,"subscribe":false},"tools":{"listChanged":true}},"protocolVersion":"2024-11-05","serverInfo":{"name":"lldb-mcp","version":"0.1.0"}}})json";
187
188 ASSERT_THAT_ERROR(Write(request), llvm::Succeeded());
189
190 llvm::Expected<std::string> response_str = Read();
191 ASSERT_THAT_EXPECTED(response_str, llvm::Succeeded());
192
193 llvm::Expected<json::Value> response_json = json::parse(JSON: *response_str);
194 ASSERT_THAT_EXPECTED(response_json, llvm::Succeeded());
195
196 llvm::Expected<json::Value> expected_json = json::parse(JSON: response);
197 ASSERT_THAT_EXPECTED(expected_json, llvm::Succeeded());
198
199 EXPECT_EQ(*response_json, *expected_json);
200}
201
202TEST_F(ProtocolServerMCPTest, ToolsList) {
203 llvm::StringLiteral request =
204 R"json({"method":"tools/list","params":{},"jsonrpc":"2.0","id":1})json";
205 llvm::StringLiteral response =
206 R"json({"id":1,"jsonrpc":"2.0","result":{"tools":[{"description":"test tool","inputSchema":{"type":"object"},"name":"test"},{"description":"Run an lldb command.","inputSchema":{"properties":{"arguments":{"type":"string"},"debugger_id":{"type":"number"}},"required":["debugger_id"],"type":"object"},"name":"lldb_command"}]}})json";
207
208 ASSERT_THAT_ERROR(Write(request), llvm::Succeeded());
209
210 llvm::Expected<std::string> response_str = Read();
211 ASSERT_THAT_EXPECTED(response_str, llvm::Succeeded());
212
213 llvm::Expected<json::Value> response_json = json::parse(JSON: *response_str);
214 ASSERT_THAT_EXPECTED(response_json, llvm::Succeeded());
215
216 llvm::Expected<json::Value> expected_json = json::parse(JSON: response);
217 ASSERT_THAT_EXPECTED(expected_json, llvm::Succeeded());
218
219 EXPECT_EQ(*response_json, *expected_json);
220}
221
222TEST_F(ProtocolServerMCPTest, ResourcesList) {
223 llvm::StringLiteral request =
224 R"json({"method":"resources/list","params":{},"jsonrpc":"2.0","id":2})json";
225 llvm::StringLiteral response =
226 R"json({"id":2,"jsonrpc":"2.0","result":{"resources":[{"description":"description","mimeType":"application/json","name":"name","uri":"lldb://foo/bar"}]}})json";
227
228 ASSERT_THAT_ERROR(Write(request), llvm::Succeeded());
229
230 llvm::Expected<std::string> response_str = Read();
231 ASSERT_THAT_EXPECTED(response_str, llvm::Succeeded());
232
233 llvm::Expected<json::Value> response_json = json::parse(JSON: *response_str);
234 ASSERT_THAT_EXPECTED(response_json, llvm::Succeeded());
235
236 llvm::Expected<json::Value> expected_json = json::parse(JSON: response);
237 ASSERT_THAT_EXPECTED(expected_json, llvm::Succeeded());
238
239 EXPECT_EQ(*response_json, *expected_json);
240}
241
242TEST_F(ProtocolServerMCPTest, ToolsCall) {
243 llvm::StringLiteral request =
244 R"json({"method":"tools/call","params":{"name":"test","arguments":{"arguments":"foo","debugger_id":0}},"jsonrpc":"2.0","id":11})json";
245 llvm::StringLiteral response =
246 R"json({"id":11,"jsonrpc":"2.0","result":{"content":[{"text":"foo","type":"text"}],"isError":false}})json";
247
248 ASSERT_THAT_ERROR(Write(request), llvm::Succeeded());
249
250 llvm::Expected<std::string> response_str = Read();
251 ASSERT_THAT_EXPECTED(response_str, llvm::Succeeded());
252
253 llvm::Expected<json::Value> response_json = json::parse(JSON: *response_str);
254 ASSERT_THAT_EXPECTED(response_json, llvm::Succeeded());
255
256 llvm::Expected<json::Value> expected_json = json::parse(JSON: response);
257 ASSERT_THAT_EXPECTED(expected_json, llvm::Succeeded());
258
259 EXPECT_EQ(*response_json, *expected_json);
260}
261
262TEST_F(ProtocolServerMCPTest, ToolsCallError) {
263 m_server_up->AddTool(tool: std::make_unique<ErrorTool>(args: "error", args: "error tool"));
264
265 llvm::StringLiteral request =
266 R"json({"method":"tools/call","params":{"name":"error","arguments":{"arguments":"foo","debugger_id":0}},"jsonrpc":"2.0","id":11})json";
267 llvm::StringLiteral response =
268 R"json({"error":{"code":-32603,"message":"error"},"id":11,"jsonrpc":"2.0"})json";
269
270 ASSERT_THAT_ERROR(Write(request), llvm::Succeeded());
271
272 llvm::Expected<std::string> response_str = Read();
273 ASSERT_THAT_EXPECTED(response_str, llvm::Succeeded());
274
275 llvm::Expected<json::Value> response_json = json::parse(JSON: *response_str);
276 ASSERT_THAT_EXPECTED(response_json, llvm::Succeeded());
277
278 llvm::Expected<json::Value> expected_json = json::parse(JSON: response);
279 ASSERT_THAT_EXPECTED(expected_json, llvm::Succeeded());
280
281 EXPECT_EQ(*response_json, *expected_json);
282}
283
284TEST_F(ProtocolServerMCPTest, ToolsCallFail) {
285 m_server_up->AddTool(tool: std::make_unique<FailTool>(args: "fail", args: "fail tool"));
286
287 llvm::StringLiteral request =
288 R"json({"method":"tools/call","params":{"name":"fail","arguments":{"arguments":"foo","debugger_id":0}},"jsonrpc":"2.0","id":11})json";
289 llvm::StringLiteral response =
290 R"json({"id":11,"jsonrpc":"2.0","result":{"content":[{"text":"failed","type":"text"}],"isError":true}})json";
291
292 ASSERT_THAT_ERROR(Write(request), llvm::Succeeded());
293
294 llvm::Expected<std::string> response_str = Read();
295 ASSERT_THAT_EXPECTED(response_str, llvm::Succeeded());
296
297 llvm::Expected<json::Value> response_json = json::parse(JSON: *response_str);
298 ASSERT_THAT_EXPECTED(response_json, llvm::Succeeded());
299
300 llvm::Expected<json::Value> expected_json = json::parse(JSON: response);
301 ASSERT_THAT_EXPECTED(expected_json, llvm::Succeeded());
302
303 EXPECT_EQ(*response_json, *expected_json);
304}
305
306TEST_F(ProtocolServerMCPTest, NotificationInitialized) {
307 bool handler_called = false;
308 std::condition_variable cv;
309 std::mutex mutex;
310
311 m_server_up->AddNotificationHandler(
312 method: "notifications/initialized",
313 handler: [&](const mcp::protocol::Notification &notification) {
314 {
315 std::lock_guard<std::mutex> lock(mutex);
316 handler_called = true;
317 }
318 cv.notify_all();
319 });
320 llvm::StringLiteral request =
321 R"json({"method":"notifications/initialized","jsonrpc":"2.0"})json";
322
323 ASSERT_THAT_ERROR(Write(request), llvm::Succeeded());
324
325 std::unique_lock<std::mutex> lock(mutex);
326 cv.wait(lock&: lock, p: [&] { return handler_called; });
327}
328

source code of lldb/unittests/Protocol/ProtocolMCPServerTest.cpp