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 _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits