Skip to content

Commit a49df8e

Browse files
authored
[lldb] Adopt JSONTransport in the MCP Server (#155034)
This PR adopts JSONTransport in the MCP server implementation. It required a slight change in design in the relationship between the two server classes. Previously, these two had an "is-a" connection, while now they have a "has-a" connection. The "generic" protocol server in Protocol/MCP now operates using a single connection (Transport). This matches the design in DAP where each DAP instance has its own connection. The protocol server in Plugins still supports multiple clients and creates a new server instance for each connection. I believe the new design makes sense in the long term (as proved by DAP) and allows us to make the server stateful if we choose to do so. There's no reason that multiple client support can't live in the generic protocol library, but for now I kept it in ProtocolServerMCP to avoid creating unnecessary abstractions.
1 parent f492eb9 commit a49df8e

File tree

5 files changed

+190
-127
lines changed

5 files changed

+190
-127
lines changed

lldb/include/lldb/Protocol/MCP/Server.h

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
#ifndef LLDB_PROTOCOL_MCP_SERVER_H
1010
#define LLDB_PROTOCOL_MCP_SERVER_H
1111

12+
#include "lldb/Host/JSONTransport.h"
13+
#include "lldb/Host/MainLoop.h"
1214
#include "lldb/Protocol/MCP/Protocol.h"
1315
#include "lldb/Protocol/MCP/Resource.h"
1416
#include "lldb/Protocol/MCP/Tool.h"
@@ -18,26 +20,52 @@
1820

1921
namespace lldb_protocol::mcp {
2022

21-
class Server {
23+
class MCPTransport final
24+
: public lldb_private::JSONRPCTransport<Request, Response, Notification> {
2225
public:
23-
Server(std::string name, std::string version);
24-
virtual ~Server() = default;
26+
using LogCallback = std::function<void(llvm::StringRef message)>;
27+
28+
MCPTransport(lldb::IOObjectSP in, lldb::IOObjectSP out,
29+
std::string client_name, LogCallback log_callback = {})
30+
: JSONRPCTransport(in, out), m_client_name(std::move(client_name)),
31+
m_log_callback(log_callback) {}
32+
virtual ~MCPTransport() = default;
33+
34+
void Log(llvm::StringRef message) override {
35+
if (m_log_callback)
36+
m_log_callback(llvm::formatv("{0}: {1}", m_client_name, message).str());
37+
}
38+
39+
private:
40+
std::string m_client_name;
41+
LogCallback m_log_callback;
42+
};
43+
44+
class Server : public MCPTransport::MessageHandler {
45+
public:
46+
Server(std::string name, std::string version,
47+
std::unique_ptr<MCPTransport> transport_up,
48+
lldb_private::MainLoop &loop);
49+
~Server() = default;
50+
51+
using NotificationHandler = std::function<void(const Notification &)>;
2552

2653
void AddTool(std::unique_ptr<Tool> tool);
2754
void AddResourceProvider(std::unique_ptr<ResourceProvider> resource_provider);
55+
void AddNotificationHandler(llvm::StringRef method,
56+
NotificationHandler handler);
57+
58+
llvm::Error Run();
2859

2960
protected:
30-
virtual Capabilities GetCapabilities() = 0;
61+
Capabilities GetCapabilities();
3162

3263
using RequestHandler =
3364
std::function<llvm::Expected<Response>(const Request &)>;
34-
using NotificationHandler = std::function<void(const Notification &)>;
3565

3666
void AddRequestHandlers();
3767

3868
void AddRequestHandler(llvm::StringRef method, RequestHandler handler);
39-
void AddNotificationHandler(llvm::StringRef method,
40-
NotificationHandler handler);
4169

4270
llvm::Expected<std::optional<Message>> HandleData(llvm::StringRef data);
4371

@@ -52,12 +80,23 @@ class Server {
5280
llvm::Expected<Response> ResourcesListHandler(const Request &);
5381
llvm::Expected<Response> ResourcesReadHandler(const Request &);
5482

83+
void Received(const Request &) override;
84+
void Received(const Response &) override;
85+
void Received(const Notification &) override;
86+
void OnError(llvm::Error) override;
87+
void OnClosed() override;
88+
89+
void TerminateLoop();
90+
5591
std::mutex m_mutex;
5692

5793
private:
5894
const std::string m_name;
5995
const std::string m_version;
6096

97+
std::unique_ptr<MCPTransport> m_transport_up;
98+
lldb_private::MainLoop &m_loop;
99+
61100
llvm::StringMap<std::unique_ptr<Tool>> m_tools;
62101
std::vector<std::unique_ptr<ResourceProvider>> m_resource_providers;
63102

lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp

Lines changed: 28 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,10 @@ using namespace llvm;
2626

2727
LLDB_PLUGIN_DEFINE(ProtocolServerMCP)
2828

29-
static constexpr size_t kChunkSize = 1024;
3029
static constexpr llvm::StringLiteral kName = "lldb-mcp";
3130
static constexpr llvm::StringLiteral kVersion = "0.1.0";
3231

33-
ProtocolServerMCP::ProtocolServerMCP()
34-
: ProtocolServer(),
35-
lldb_protocol::mcp::Server(std::string(kName), std::string(kVersion)) {
36-
AddNotificationHandler("notifications/initialized",
37-
[](const lldb_protocol::mcp::Notification &) {
38-
LLDB_LOG(GetLog(LLDBLog::Host),
39-
"MCP initialization complete");
40-
});
41-
42-
AddTool(
43-
std::make_unique<CommandTool>("lldb_command", "Run an lldb command."));
44-
45-
AddResourceProvider(std::make_unique<DebuggerResourceProvider>());
46-
}
32+
ProtocolServerMCP::ProtocolServerMCP() : ProtocolServer() {}
4733

4834
ProtocolServerMCP::~ProtocolServerMCP() { llvm::consumeError(Stop()); }
4935

@@ -64,57 +50,37 @@ llvm::StringRef ProtocolServerMCP::GetPluginDescriptionStatic() {
6450
return "MCP Server.";
6551
}
6652

53+
void ProtocolServerMCP::Extend(lldb_protocol::mcp::Server &server) const {
54+
server.AddNotificationHandler("notifications/initialized",
55+
[](const lldb_protocol::mcp::Notification &) {
56+
LLDB_LOG(GetLog(LLDBLog::Host),
57+
"MCP initialization complete");
58+
});
59+
server.AddTool(
60+
std::make_unique<CommandTool>("lldb_command", "Run an lldb command."));
61+
server.AddResourceProvider(std::make_unique<DebuggerResourceProvider>());
62+
}
63+
6764
void ProtocolServerMCP::AcceptCallback(std::unique_ptr<Socket> socket) {
68-
LLDB_LOG(GetLog(LLDBLog::Host), "New MCP client ({0}) connected",
69-
m_clients.size() + 1);
65+
Log *log = GetLog(LLDBLog::Host);
66+
std::string client_name = llvm::formatv("client_{0}", m_instances.size() + 1);
67+
LLDB_LOG(log, "New MCP client connected: {0}", client_name);
7068

7169
lldb::IOObjectSP io_sp = std::move(socket);
72-
auto client_up = std::make_unique<Client>();
73-
client_up->io_sp = io_sp;
74-
Client *client = client_up.get();
75-
76-
Status status;
77-
auto read_handle_up = m_loop.RegisterReadObject(
78-
io_sp,
79-
[this, client](MainLoopBase &loop) {
80-
if (llvm::Error error = ReadCallback(*client)) {
81-
LLDB_LOG_ERROR(GetLog(LLDBLog::Host), std::move(error), "{0}");
82-
client->read_handle_up.reset();
83-
}
84-
},
85-
status);
86-
if (status.Fail())
70+
auto transport_up = std::make_unique<lldb_protocol::mcp::MCPTransport>(
71+
io_sp, io_sp, std::move(client_name), [&](llvm::StringRef message) {
72+
LLDB_LOG(GetLog(LLDBLog::Host), "{0}", message);
73+
});
74+
auto instance_up = std::make_unique<lldb_protocol::mcp::Server>(
75+
std::string(kName), std::string(kVersion), std::move(transport_up),
76+
m_loop);
77+
Extend(*instance_up);
78+
llvm::Error error = instance_up->Run();
79+
if (error) {
80+
LLDB_LOG_ERROR(log, std::move(error), "Failed to run MCP server: {0}");
8781
return;
88-
89-
client_up->read_handle_up = std::move(read_handle_up);
90-
m_clients.emplace_back(std::move(client_up));
91-
}
92-
93-
llvm::Error ProtocolServerMCP::ReadCallback(Client &client) {
94-
char chunk[kChunkSize];
95-
size_t bytes_read = sizeof(chunk);
96-
if (Status status = client.io_sp->Read(chunk, bytes_read); status.Fail())
97-
return status.takeError();
98-
client.buffer.append(chunk, bytes_read);
99-
100-
for (std::string::size_type pos;
101-
(pos = client.buffer.find('\n')) != std::string::npos;) {
102-
llvm::Expected<std::optional<lldb_protocol::mcp::Message>> message =
103-
HandleData(StringRef(client.buffer.data(), pos));
104-
client.buffer = client.buffer.erase(0, pos + 1);
105-
if (!message)
106-
return message.takeError();
107-
108-
if (*message) {
109-
std::string Output;
110-
llvm::raw_string_ostream OS(Output);
111-
OS << llvm::formatv("{0}", toJSON(**message)) << '\n';
112-
size_t num_bytes = Output.size();
113-
return client.io_sp->Write(Output.data(), num_bytes).takeError();
114-
}
11582
}
116-
117-
return llvm::Error::success();
83+
m_instances.push_back(std::move(instance_up));
11884
}
11985

12086
llvm::Error ProtocolServerMCP::Start(ProtocolServer::Connection connection) {
@@ -158,27 +124,11 @@ llvm::Error ProtocolServerMCP::Stop() {
158124

159125
// Stop the main loop.
160126
m_loop.AddPendingCallback(
161-
[](MainLoopBase &loop) { loop.RequestTermination(); });
127+
[](lldb_private::MainLoopBase &loop) { loop.RequestTermination(); });
162128

163129
// Wait for the main loop to exit.
164130
if (m_loop_thread.joinable())
165131
m_loop_thread.join();
166132

167-
{
168-
std::lock_guard<std::mutex> guard(m_mutex);
169-
m_listener.reset();
170-
m_listen_handlers.clear();
171-
m_clients.clear();
172-
}
173-
174133
return llvm::Error::success();
175134
}
176-
177-
lldb_protocol::mcp::Capabilities ProtocolServerMCP::GetCapabilities() {
178-
lldb_protocol::mcp::Capabilities capabilities;
179-
capabilities.tools.listChanged = true;
180-
// FIXME: Support sending notifications when a debugger/target are
181-
// added/removed.
182-
capabilities.resources.listChanged = false;
183-
return capabilities;
184-
}

lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.h

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818

1919
namespace lldb_private::mcp {
2020

21-
class ProtocolServerMCP : public ProtocolServer,
22-
public lldb_protocol::mcp::Server {
21+
class ProtocolServerMCP : public ProtocolServer {
2322
public:
2423
ProtocolServerMCP();
2524
virtual ~ProtocolServerMCP() override;
@@ -39,26 +38,24 @@ class ProtocolServerMCP : public ProtocolServer,
3938

4039
Socket *GetSocket() const override { return m_listener.get(); }
4140

41+
protected:
42+
// This adds tools and resource providers that
43+
// are specific to this server. Overridable by the unit tests.
44+
virtual void Extend(lldb_protocol::mcp::Server &server) const;
45+
4246
private:
4347
void AcceptCallback(std::unique_ptr<Socket> socket);
4448

45-
lldb_protocol::mcp::Capabilities GetCapabilities() override;
46-
4749
bool m_running = false;
4850

49-
MainLoop m_loop;
51+
lldb_private::MainLoop m_loop;
5052
std::thread m_loop_thread;
53+
std::mutex m_mutex;
5154

5255
std::unique_ptr<Socket> m_listener;
53-
std::vector<MainLoopBase::ReadHandleUP> m_listen_handlers;
5456

55-
struct Client {
56-
lldb::IOObjectSP io_sp;
57-
MainLoopBase::ReadHandleUP read_handle_up;
58-
std::string buffer;
59-
};
60-
llvm::Error ReadCallback(Client &client);
61-
std::vector<std::unique_ptr<Client>> m_clients;
57+
std::vector<MainLoopBase::ReadHandleUP> m_listen_handlers;
58+
std::vector<std::unique_ptr<lldb_protocol::mcp::Server>> m_instances;
6259
};
6360
} // namespace lldb_private::mcp
6461

lldb/source/Protocol/MCP/Server.cpp

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
using namespace lldb_protocol::mcp;
1313
using namespace llvm;
1414

15-
Server::Server(std::string name, std::string version)
16-
: m_name(std::move(name)), m_version(std::move(version)) {
15+
Server::Server(std::string name, std::string version,
16+
std::unique_ptr<MCPTransport> transport_up,
17+
lldb_private::MainLoop &loop)
18+
: m_name(std::move(name)), m_version(std::move(version)),
19+
m_transport_up(std::move(transport_up)), m_loop(loop) {
1720
AddRequestHandlers();
1821
}
1922

@@ -232,3 +235,71 @@ llvm::Expected<Response> Server::ResourcesReadHandler(const Request &request) {
232235
llvm::formatv("no resource handler for uri: {0}", uri_str).str(),
233236
MCPError::kResourceNotFound);
234237
}
238+
239+
Capabilities Server::GetCapabilities() {
240+
lldb_protocol::mcp::Capabilities capabilities;
241+
capabilities.tools.listChanged = true;
242+
// FIXME: Support sending notifications when a debugger/target are
243+
// added/removed.
244+
capabilities.resources.listChanged = false;
245+
return capabilities;
246+
}
247+
248+
llvm::Error Server::Run() {
249+
auto handle = m_transport_up->RegisterMessageHandler(m_loop, *this);
250+
if (!handle)
251+
return handle.takeError();
252+
253+
lldb_private::Status status = m_loop.Run();
254+
if (status.Fail())
255+
return status.takeError();
256+
257+
return llvm::Error::success();
258+
}
259+
260+
void Server::Received(const Request &request) {
261+
auto SendResponse = [this](const Response &response) {
262+
if (llvm::Error error = m_transport_up->Send(response))
263+
m_transport_up->Log(llvm::toString(std::move(error)));
264+
};
265+
266+
llvm::Expected<Response> response = Handle(request);
267+
if (response)
268+
return SendResponse(*response);
269+
270+
lldb_protocol::mcp::Error protocol_error;
271+
llvm::handleAllErrors(
272+
response.takeError(),
273+
[&](const MCPError &err) { protocol_error = err.toProtocolError(); },
274+
[&](const llvm::ErrorInfoBase &err) {
275+
protocol_error.code = MCPError::kInternalError;
276+
protocol_error.message = err.message();
277+
});
278+
Response error_response;
279+
error_response.id = request.id;
280+
error_response.result = std::move(protocol_error);
281+
SendResponse(error_response);
282+
}
283+
284+
void Server::Received(const Response &response) {
285+
m_transport_up->Log("unexpected MCP message: response");
286+
}
287+
288+
void Server::Received(const Notification &notification) {
289+
Handle(notification);
290+
}
291+
292+
void Server::OnError(llvm::Error error) {
293+
m_transport_up->Log(llvm::toString(std::move(error)));
294+
TerminateLoop();
295+
}
296+
297+
void Server::OnClosed() {
298+
m_transport_up->Log("EOF");
299+
TerminateLoop();
300+
}
301+
302+
void Server::TerminateLoop() {
303+
m_loop.AddPendingCallback(
304+
[](lldb_private::MainLoopBase &loop) { loop.RequestTermination(); });
305+
}

0 commit comments

Comments
 (0)