Skip to content

Conversation

ashgti
Copy link
Contributor

@ashgti ashgti commented Aug 25, 2025

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.

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.
@ashgti ashgti requested a review from JDevlieghere as a code owner August 25, 2025 21:50
@llvmbot llvmbot added the lldb label Aug 25, 2025
@llvmbot
Copy link
Member

llvmbot commented Aug 25, 2025

@llvm/pr-subscribers-lldb

Author: John Harrison (ashgti)

Changes

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 --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 &params) {
+  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 &note) {
+          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 &note) {
+          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 &params, 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 &params) {
+      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 &note) 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 &note);
+    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]

Copy link
Member

@JDevlieghere JDevlieghere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's split out the changes to the protocol, we can land and test those independently and that would simplify this patch a lot. I also think all the socket code is (not yet) necessary.

You say the purpose of the binder is to bind handlers to requests and notifications. Currently, that's part of the server, and this is splitting that off.

  • Can you explain why that's necessary going forward?
  • Can we avoid having to call reply directly from the handlers, but instead return the result and have the layer above handle the response?
  • Can we avoid having all the async stuff and keep using the MainLoop with one server instance per connection?

@ashgti
Copy link
Contributor Author

ashgti commented Aug 26, 2025

Let's split out the changes to the protocol, we can land and test those independently and that would simplify this patch a lot. I also think all the socket code is (not yet) necessary.

Split the protocol changes into #155460 and I'll also add another to move the MCPTransport into its own file instead of being in the Server.h file.

You say the purpose of the binder is to bind handlers to requests and notifications. Currently, that's part of the server, and this is splitting that off.

  • Can you explain why that's necessary going forward?

The Binder is for creating bindings between functions and the transport. This lets you create a server or a client by choosing the directions of the bindings.

I was using this to create bindings for an lldb-mcp server that would acts a coordinator between lldb instances and mcp clients.

However, I think I'll take another look at this approach and see if we can do this without a full server binary.

I'll mark this as a draft until I have a chance to look at this first.

@ashgti ashgti marked this pull request as draft August 26, 2025 18:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants