[Lldb-commits] [lldb] [lldb] Creating a new Binder helper for JSONRPC transport. (PR #155315)
via lldb-commits
lldb-commits at lists.llvm.org
Mon Aug 25 14:50:50 PDT 2025
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-lldb
Author: John Harrison (ashgti)
<details>
<summary>Changes</summary>
The `lldb_protocol::mcp::Binder` class is used to craft bindings between requests and notifications to specific handlers.
This supports both incoming and outgoing handlers that bind these functions to a MessageHandler and generates encoding/decoding helpers for each call.
For example, see the `lldb_protocol::mcp::Server` class that has been greatly simplified.
---
Patch is 75.70 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/155315.diff
19 Files Affected:
- (added) lldb/include/lldb/Protocol/MCP/Binder.h (+351)
- (modified) lldb/include/lldb/Protocol/MCP/Protocol.h (+169-4)
- (modified) lldb/include/lldb/Protocol/MCP/Resource.h (+1-1)
- (modified) lldb/include/lldb/Protocol/MCP/Server.h (+21-53)
- (modified) lldb/include/lldb/Protocol/MCP/Tool.h (+7-2)
- (added) lldb/include/lldb/Protocol/MCP/Transport.h (+50)
- (modified) lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp (+9-11)
- (modified) lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.h (+3-1)
- (modified) lldb/source/Plugins/Protocol/MCP/Resource.cpp (+5-5)
- (modified) lldb/source/Plugins/Protocol/MCP/Resource.h (+3-3)
- (modified) lldb/source/Plugins/Protocol/MCP/Tool.cpp (+15-11)
- (modified) lldb/source/Plugins/Protocol/MCP/Tool.h (+4-3)
- (added) lldb/source/Protocol/MCP/Binder.cpp (+139)
- (modified) lldb/source/Protocol/MCP/CMakeLists.txt (+3)
- (modified) lldb/source/Protocol/MCP/Protocol.cpp (+157-2)
- (modified) lldb/source/Protocol/MCP/Server.cpp (+55-200)
- (added) lldb/source/Protocol/MCP/Transport.cpp (+113)
- (modified) lldb/unittests/Protocol/ProtocolMCPTest.cpp (+6-4)
- (modified) lldb/unittests/ProtocolServer/ProtocolMCPServerTest.cpp (+49-29)
``````````diff
diff --git a/lldb/include/lldb/Protocol/MCP/Binder.h b/lldb/include/lldb/Protocol/MCP/Binder.h
new file mode 100644
index 0000000000000..f9cebd940bfcb
--- /dev/null
+++ b/lldb/include/lldb/Protocol/MCP/Binder.h
@@ -0,0 +1,351 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_PROTOCOL_MCP_BINDER_H
+#define LLDB_PROTOCOL_MCP_BINDER_H
+
+#include "lldb/Protocol/MCP/MCPError.h"
+#include "lldb/Protocol/MCP/Protocol.h"
+#include "lldb/Protocol/MCP/Transport.h"
+#include "lldb/Utility/Status.h"
+#include "llvm/ADT/FunctionExtras.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/JSON.h"
+#include <functional>
+#include <future>
+#include <memory>
+#include <mutex>
+#include <optional>
+
+namespace lldb_protocol::mcp {
+
+template <typename T> using Callback = llvm::unique_function<T>;
+
+template <typename T>
+using Reply = llvm::unique_function<void(llvm::Expected<T>)>;
+template <typename Params, typename Result>
+using OutgoingRequest =
+ llvm::unique_function<void(const Params &, Reply<Result>)>;
+template <typename Params>
+using OutgoingNotification = llvm::unique_function<void(const Params &)>;
+
+template <typename Params, typename Result>
+llvm::Expected<Result> AsyncInvoke(lldb_private::MainLoop &loop,
+ OutgoingRequest<Params, Result> &fn,
+ const Params ¶ms) {
+ std::promise<llvm::Expected<Result>> result_promise;
+ std::future<llvm::Expected<Result>> result_future =
+ result_promise.get_future();
+ std::thread thr([&loop, &fn, params,
+ result_promise = std::move(result_promise)]() mutable {
+ fn(params, [&loop, &result_promise](llvm::Expected<Result> result) mutable {
+ result_promise.set_value(std::move(result));
+ loop.AddPendingCallback(
+ [](lldb_private::MainLoopBase &loop) { loop.RequestTermination(); });
+ });
+ if (llvm::Error error = loop.Run().takeError())
+ result_promise.set_value(std::move(error));
+ });
+ thr.join();
+ return result_future.get();
+}
+
+/// Binder collects a table of functions that handle calls.
+///
+/// The wrapper takes care of parsing/serializing responses.
+class Binder {
+public:
+ explicit Binder(MCPTransport *transport) : m_handlers(transport) {}
+
+ Binder(const Binder &) = delete;
+ Binder &operator=(const Binder &) = delete;
+
+ /// Bind a handler on transport disconnect.
+ template <typename ThisT, typename... ExtraArgs>
+ void disconnected(void (ThisT::*handler)(MCPTransport *), ThisT *_this,
+ ExtraArgs... extra_args) {
+ m_handlers.m_disconnect_handler =
+ std::bind(handler, _this, std::placeholders::_1,
+ std::forward<ExtraArgs>(extra_args)...);
+ }
+
+ /// Bind a handler on error when communicating with the transport.
+ template <typename ThisT, typename... ExtraArgs>
+ void error(void (ThisT::*handler)(MCPTransport *, llvm::Error), ThisT *_this,
+ ExtraArgs... extra_args) {
+ m_handlers.m_error_handler =
+ std::bind(handler, _this, std::placeholders::_1, std::placeholders::_2,
+ std::forward<ExtraArgs>(extra_args)...);
+ }
+
+ /// Bind a handler for a request.
+ /// e.g. Bind.request("peek", this, &ThisModule::peek);
+ /// Handler should be e.g. Expected<PeekResult> peek(const PeekParams&);
+ /// PeekParams must be JSON parsable and PeekResult must be serializable.
+ template <typename Result, typename Params, typename ThisT,
+ typename... ExtraArgs>
+ void request(llvm::StringLiteral method,
+ llvm::Expected<Result> (ThisT::*fn)(const Params &,
+ ExtraArgs...),
+ ThisT *_this, ExtraArgs... extra_args) {
+ assert(m_handlers.m_request_handlers.find(method) ==
+ m_handlers.m_request_handlers.end() &&
+ "request already bound");
+ std::function<llvm::Expected<Result>(const Params &)> handler =
+ std::bind(fn, _this, std::placeholders::_1,
+ std::forward<ExtraArgs>(extra_args)...);
+ m_handlers.m_request_handlers[method] =
+ [method, handler](const Request &req,
+ llvm::unique_function<void(const Response &)> reply) {
+ Params params;
+ llvm::json::Path::Root root(method);
+ if (!fromJSON(req.params, params, root)) {
+ reply(Response{0, Error{eErrorCodeInvalidParams,
+ "invalid params for " + method.str() +
+ ": " + llvm::toString(root.getError()),
+ std::nullopt}});
+ return;
+ }
+ llvm::Expected<Result> result = handler(params);
+ if (llvm::Error error = result.takeError()) {
+ Error protocol_error;
+ llvm::handleAllErrors(
+ std::move(error),
+ [&](const MCPError &err) {
+ protocol_error = err.toProtocolError();
+ },
+ [&](const llvm::ErrorInfoBase &err) {
+ protocol_error.code = MCPError::kInternalError;
+ protocol_error.message = err.message();
+ });
+ reply(Response{0, protocol_error});
+ return;
+ }
+
+ reply(Response{0, *result});
+ };
+ }
+
+ /// Bind a handler for an async request.
+ /// e.g. Bind.asyncRequest("peek", this, &ThisModule::peek);
+ /// Handler should be e.g. `void peek(const PeekParams&,
+ /// Reply<Expected<PeekResult>>);` PeekParams must be JSON parsable and
+ /// PeekResult must be serializable.
+ template <typename Result, typename Params, typename... ExtraArgs>
+ void asyncRequest(
+ llvm::StringLiteral method,
+ std::function<void(const Params &, ExtraArgs..., Reply<Result>)> fn,
+ ExtraArgs... extra_args) {
+ assert(m_handlers.m_request_handlers.find(method) ==
+ m_handlers.m_request_handlers.end() &&
+ "request already bound");
+ std::function<void(const Params &, Reply<Result>)> handler = std::bind(
+ fn, std::placeholders::_1, std::forward<ExtraArgs>(extra_args)...,
+ std::placeholders::_2);
+ m_handlers.m_request_handlers[method] =
+ [method, handler](const Request &req,
+ Callback<void(const Response &)> reply) {
+ Params params;
+ llvm::json::Path::Root root(method);
+ if (!fromJSON(req.params, params, root)) {
+ reply(Response{0, Error{eErrorCodeInvalidParams,
+ "invalid params for " + method.str() +
+ ": " + llvm::toString(root.getError()),
+ std::nullopt}});
+ return;
+ }
+
+ handler(params, [reply = std::move(reply)](
+ llvm::Expected<Result> result) mutable {
+ if (llvm::Error error = result.takeError()) {
+ Error protocol_error;
+ llvm::handleAllErrors(
+ std::move(error),
+ [&](const MCPError &err) {
+ protocol_error = err.toProtocolError();
+ },
+ [&](const llvm::ErrorInfoBase &err) {
+ protocol_error.code = MCPError::kInternalError;
+ protocol_error.message = err.message();
+ });
+ reply(Response{0, protocol_error});
+ return;
+ }
+
+ reply(Response{0, toJSON(*result)});
+ });
+ };
+ }
+ template <typename Result, typename Params, typename ThisT,
+ typename... ExtraArgs>
+ void asyncRequest(llvm::StringLiteral method,
+ void (ThisT::*fn)(const Params &, ExtraArgs...,
+ Reply<Result>),
+ ThisT *_this, ExtraArgs... extra_args) {
+ assert(m_handlers.m_request_handlers.find(method) ==
+ m_handlers.m_request_handlers.end() &&
+ "request already bound");
+ std::function<void(const Params &, Reply<Result>)> handler = std::bind(
+ fn, _this, std::placeholders::_1,
+ std::forward<ExtraArgs>(extra_args)..., std::placeholders::_2);
+ m_handlers.m_request_handlers[method] =
+ [method, handler](const Request &req,
+ Callback<void(const Response &)> reply) {
+ Params params;
+ llvm::json::Path::Root root;
+ if (!fromJSON(req.params, params, root)) {
+ reply(Response{0, Error{eErrorCodeInvalidParams,
+ "invalid params for " + method.str(),
+ std::nullopt}});
+ return;
+ }
+
+ handler(params, [reply = std::move(reply)](
+ llvm::Expected<Result> result) mutable {
+ if (llvm::Error error = result.takeError()) {
+ Error protocol_error;
+ llvm::handleAllErrors(
+ std::move(error),
+ [&](const MCPError &err) {
+ protocol_error = err.toProtocolError();
+ },
+ [&](const llvm::ErrorInfoBase &err) {
+ protocol_error.code = MCPError::kInternalError;
+ protocol_error.message = err.message();
+ });
+ reply(Response{0, protocol_error});
+ return;
+ }
+
+ reply(Response{0, toJSON(*result)});
+ });
+ };
+ }
+
+ /// Bind a handler for a notification.
+ /// e.g. Bind.notification("peek", this, &ThisModule::peek);
+ /// Handler should be e.g. void peek(const PeekParams&);
+ /// PeekParams must be JSON parsable.
+ template <typename Params, typename ThisT, typename... ExtraArgs>
+ void notification(llvm::StringLiteral method,
+ void (ThisT::*fn)(const Params &, ExtraArgs...),
+ ThisT *_this, ExtraArgs... extra_args) {
+ std::function<void(const Params &)> handler =
+ std::bind(fn, _this, std::placeholders::_1,
+ std::forward<ExtraArgs>(extra_args)...);
+ m_handlers.m_notification_handlers[method] =
+ [handler](const Notification ¬e) {
+ Params params;
+ llvm::json::Path::Root root;
+ if (!fromJSON(note.params, params, root))
+ return; // FIXME: log error?
+
+ handler(params);
+ };
+ }
+ template <typename Params>
+ void notification(llvm::StringLiteral method,
+ std::function<void(const Params &)> handler) {
+ assert(m_handlers.m_notification_handlers.find(method) ==
+ m_handlers.m_notification_handlers.end() &&
+ "notification already bound");
+ m_handlers.m_notification_handlers[method] =
+ [handler = std::move(handler)](const Notification ¬e) {
+ Params params;
+ llvm::json::Path::Root root;
+ if (!fromJSON(note.params, params, root))
+ return; // FIXME: log error?
+
+ handler(params);
+ };
+ }
+
+ /// Bind a function object to be used for outgoing requests.
+ /// e.g. OutgoingRequest<Params, Result> Edit = Bind.outgoingRequest("edit");
+ /// Params must be JSON-serializable, Result must be parsable.
+ template <typename Params, typename Result>
+ OutgoingRequest<Params, Result> outgoingRequest(llvm::StringLiteral method) {
+ return [this, method](const Params ¶ms, Reply<Result> reply) {
+ Request request;
+ request.method = method;
+ request.params = toJSON(params);
+ m_handlers.Send(request, [reply = std::move(reply)](
+ const Response &resp) mutable {
+ if (const lldb_protocol::mcp::Error *err =
+ std::get_if<lldb_protocol::mcp::Error>(&resp.result)) {
+ reply(llvm::make_error<MCPError>(err->message, err->code));
+ return;
+ }
+ Result result;
+ llvm::json::Path::Root root;
+ if (!fromJSON(std::get<llvm::json::Value>(resp.result), result, root)) {
+ reply(llvm::make_error<MCPError>("parsing response failed: " +
+ llvm::toString(root.getError())));
+ return;
+ }
+ reply(result);
+ });
+ };
+ }
+
+ /// Bind a function object to be used for outgoing notifications.
+ /// e.g. OutgoingNotification<LogParams> Log = Bind.outgoingMethod("log");
+ /// LogParams must be JSON-serializable.
+ template <typename Params>
+ OutgoingNotification<Params>
+ outgoingNotification(llvm::StringLiteral method) {
+ return [this, method](const Params ¶ms) {
+ Notification note;
+ note.method = method;
+ note.params = toJSON(params);
+ m_handlers.Send(note);
+ };
+ }
+
+ operator MCPTransport::MessageHandler &() { return m_handlers; }
+
+private:
+ class RawHandler final : public MCPTransport::MessageHandler {
+ public:
+ explicit RawHandler(MCPTransport *transport);
+
+ void Received(const Notification ¬e) override;
+ void Received(const Request &req) override;
+ void Received(const Response &resp) override;
+ void OnError(llvm::Error err) override;
+ void OnClosed() override;
+
+ void Send(const Request &req,
+ Callback<void(const Response &)> response_handler);
+ void Send(const Notification ¬e);
+ void Send(const Response &resp);
+
+ friend class Binder;
+
+ private:
+ std::recursive_mutex m_mutex;
+ MCPTransport *m_transport;
+ int m_seq = 0;
+ std::map<Id, Callback<void(const Response &)>> m_pending_responses;
+ llvm::StringMap<
+ Callback<void(const Request &, Callback<void(const Response &)>)>>
+ m_request_handlers;
+ llvm::StringMap<Callback<void(const Notification &)>>
+ m_notification_handlers;
+ Callback<void(MCPTransport *)> m_disconnect_handler;
+ Callback<void(MCPTransport *, llvm::Error)> m_error_handler;
+ };
+
+ RawHandler m_handlers;
+};
+using BinderUP = std::unique_ptr<Binder>;
+
+} // namespace lldb_protocol::mcp
+
+#endif
diff --git a/lldb/include/lldb/Protocol/MCP/Protocol.h b/lldb/include/lldb/Protocol/MCP/Protocol.h
index 49f9490221755..d21a5ef85ece6 100644
--- a/lldb/include/lldb/Protocol/MCP/Protocol.h
+++ b/lldb/include/lldb/Protocol/MCP/Protocol.h
@@ -14,10 +14,12 @@
#ifndef LLDB_PROTOCOL_MCP_PROTOCOL_H
#define LLDB_PROTOCOL_MCP_PROTOCOL_H
+#include "lldb/lldb-types.h"
#include "llvm/Support/JSON.h"
#include <optional>
#include <string>
#include <variant>
+#include <vector>
namespace lldb_protocol::mcp {
@@ -43,6 +45,12 @@ llvm::json::Value toJSON(const Request &);
bool fromJSON(const llvm::json::Value &, Request &, llvm::json::Path);
bool operator==(const Request &, const Request &);
+enum ErrorCode : signed {
+ eErrorCodeMethodNotFound = -32601,
+ eErrorCodeInvalidParams = -32602,
+ eErrorCodeInternalError = -32000,
+};
+
struct Error {
/// The error type that occurred.
int64_t code = 0;
@@ -147,6 +155,14 @@ struct Resource {
llvm::json::Value toJSON(const Resource &);
bool fromJSON(const llvm::json::Value &, Resource &, llvm::json::Path);
+/// The server’s response to a resources/list request from the client.
+struct ResourcesListResult {
+ std::vector<Resource> resources;
+};
+llvm::json::Value toJSON(const ResourcesListResult &);
+bool fromJSON(const llvm::json::Value &, ResourcesListResult &,
+ llvm::json::Path);
+
/// The contents of a specific resource or sub-resource.
struct ResourceContents {
/// The URI of this resource.
@@ -163,13 +179,23 @@ struct ResourceContents {
llvm::json::Value toJSON(const ResourceContents &);
bool fromJSON(const llvm::json::Value &, ResourceContents &, llvm::json::Path);
+/// Sent from the client to the server, to read a specific resource URI.
+struct ResourcesReadParams {
+ /// The URI of the resource to read. The URI can use any protocol; it is up to
+ /// the server how to interpret it.
+ std::string URI;
+};
+llvm::json::Value toJSON(const ResourcesReadParams &);
+bool fromJSON(const llvm::json::Value &, ResourcesReadParams &,
+ llvm::json::Path);
+
/// The server's response to a resources/read request from the client.
-struct ResourceResult {
+struct ResourcesReadResult {
std::vector<ResourceContents> contents;
};
-
-llvm::json::Value toJSON(const ResourceResult &);
-bool fromJSON(const llvm::json::Value &, ResourceResult &, llvm::json::Path);
+llvm::json::Value toJSON(const ResourcesReadResult &);
+bool fromJSON(const llvm::json::Value &, ResourcesReadResult &,
+ llvm::json::Path);
/// Text provided to or from an LLM.
struct TextContent {
@@ -204,6 +230,145 @@ bool fromJSON(const llvm::json::Value &, ToolDefinition &, llvm::json::Path);
using ToolArguments = std::variant<std::monostate, llvm::json::Value>;
+/// Describes the name and version of an MCP implementation, with an optional
+/// title for UI representation.
+///
+/// see
+/// https://modelcontextprotocol.io/specification/2025-06-18/schema#implementation
+struct Implementation {
+ /// Intended for programmatic or logical use, but used as a display name in
+ /// past specs or fallback (if title isn’t present).
+ std::string name;
+
+ /// Intended for UI and end-user contexts — optimized to be human-readable and
+ /// easily understood, even by those unfamiliar with domain-specific
+ /// terminology.
+ ///
+ /// If not provided, the name should be used for display (except for Tool,
+ /// where annotations.title should be given precedence over using name, if
+ /// present).
+ std::string title;
+
+ std::string version;
+};
+llvm::json::Value toJSON(const Implementation &);
+bool fromJSON(const llvm::json::Value &, Implementation &, llvm::json::Path);
+
+/// Capabilities a client may support. Known capabilities are defined here, in
+/// this schema, but this is not a closed set: any client can define its own,
+/// additional capabilities.
+struct ClientCapabilities {};
+llvm::json::Value toJSON(const ClientCapabilities &);
+bool fromJSON(const llvm::json::Value &, ClientCapabilities &,
+ llvm::json::Path);
+
+/// Capabilities that a server may support. Known capabilities are defined here,
+/// in this schema, but this is not a closed set: any server can define its own,
+/// additional capabilities.
+struct ServerCapabilities {
+ bool supportsToolsList = false;
+ bool supportsResourcesList = false;
+ bool supportsResourcesSubscribe = false;
+
+ /// Utilities.
+ bool supportsCompletions = false;
+ bool supportsLogging = false;
+};
+llvm::json::Value toJSON(const ServerCapabilities &);
+bool fromJSON(const llvm::json::Value &, ServerCapabilities &,
+ llvm::json::Path);
+
+/// Initialization
+
+/// This request is sent from the client to the server when it first connects,
+/// asking it to begin initialization.
+///
+/// @category initialize
+struct InitializeParams {
+ /// The latest version of the Model Context Protocol that the client supports.
+ /// The client MAY decide to support older versions as well.
+ std::string protocolVersion;
+
+ ClientCapabilities capabilities;
+
+ Implementation clientInfo;
+};
+llvm::json::Value toJSON(const InitializeParams &);
+bool fromJSON(const llvm::json::Value &, InitializeParams &, llvm::json::Path);
+
+/// After receiving an initialize request from the client, the server sends this
+/// response.
+///
+/// @category initialize
+struct InitializeResult {
+ /// The version of the Model Context Protocol that the server wants to use.
+ /// This may not match the version that the client requested. If the client
+ /// cannot support this version, it MUST disconnect.
+ std::string protocolVersion;
+
+ ServerCapabilities capabilities;
+ Implementation serverInfo;
+
+ /// Instructions describing how to use the server and its features.
+ ///
+ /// This can be used by cl...
[truncated]
``````````
</details>
https://github.com/llvm/llvm-project/pull/155315
More information about the lldb-commits
mailing list