================
@@ -100,22 +175,301 @@ template <typename Req, typename Resp, typename Evt>
class Transport {
virtual llvm::Expected<MainLoop::ReadHandleUP>
RegisterMessageHandler(MainLoop &loop, MessageHandler &handler) = 0;
- // FIXME: Refactor mcp::Server to not directly access log on the transport.
- // protected:
+protected:
template <typename... Ts> inline auto Logv(const char *Fmt, Ts &&...Vals) {
Log(llvm::formatv(Fmt, std::forward<Ts>(Vals)...).str());
}
virtual void Log(llvm::StringRef message) = 0;
+
+ /// Function object to reply to a call.
+ /// Each instance must be called exactly once, otherwise:
+ /// - the bug is logged, and (in debug mode) an assert will fire
+ /// - if there was no reply, an error reply is sent
+ /// - if there were multiple replies, only the first is sent
+ class ReplyOnce {
+ std::atomic<bool> replied = {false};
+ const Req req;
+ JSONTransport *transport; // Null when moved-from.
+ JSONTransport::MessageHandler *handler; // Null when moved-from.
+
+ public:
+ ReplyOnce(const Req req, JSONTransport *transport,
+ JSONTransport::MessageHandler *handler)
+ : req(req), transport(transport), handler(handler) {
+ assert(handler);
+ }
+ ReplyOnce(ReplyOnce &&other)
+ : replied(other.replied.load()), req(other.req),
+ transport(other.transport), handler(other.handler) {
+ other.transport = nullptr;
+ other.handler = nullptr;
+ }
+ ReplyOnce &operator=(ReplyOnce &&) = delete;
+ ReplyOnce(const ReplyOnce &) = delete;
+ ReplyOnce &operator=(const ReplyOnce &) = delete;
+
+ ~ReplyOnce() {
+ if (transport && handler && !replied) {
+ assert(false && "must reply to all calls!");
+ (*this)(MakeResponse<Req, Resp>(
+ req, llvm::createStringError("failed to reply")));
+ }
+ }
+
+ void operator()(const Resp &resp) {
+ assert(transport && handler && "moved-from!");
+ if (replied.exchange(true)) {
+ assert(false && "must reply to each call only once!");
+ return;
+ }
+
+ if (llvm::Error error = transport->Send(resp))
+ handler->OnError(std::move(error));
+ }
+ };
+
+public:
+ class Binder;
+ using BinderUP = std::unique_ptr<Binder>;
+
+ /// Binder collects a table of functions that handle calls.
----------------
ashgti wrote:
> By defining message types we can get that automatically out of the messages,
> and then have it call the right callback?
Yea, for example, from the Server.cpp
```
MCPTransport::BinderUP Server::Bind(MCPTransport &transport) {
MCPTransport::BinderUP binder =
std::make_unique<MCPTransport::Binder>(transport);
binder->bind<InitializeResult, InitializeParams>(
"initialize", &Server::InitializeHandler, this);
binder->bind<ListToolsResult, void>("tools/list", &Server::ToolsListHandler,
this);
binder->bind<CallToolResult, CallToolParams>("tools/call",
&Server::ToolsCallHandler, this);
binder->bind<ListResourcesResult, void>("resources/list",
&Server::ResourcesListHandler, this);
binder->bind<ReadResourceResult, ReadResourceParams>(
"resources/read", &Server::ResourcesReadHandler, this);
binder->bind<void>("notifications/initialized",
[this]() { Log("MCP initialization complete"); });
return binder;
}
```
The `bind` method setups the serialization of the Params and Results and wraps
them into the `Request` and `Response` types.
This means the server doesn't need to implement its own method dispatching
mechanism, that can be handled by the `Binder` and Server can be simplified a
bit. You can also see an example in JSONTransportTest.cpp that does this simple
binding of a incoming call, outgoing call, incoming event and outgoing event.
> does this need to be tied to JSONTransport? Can this be built on top of an
> arbitrary transport?
Within each `bind` call we do assume that `Params` and `Result` types are all
JSON serializable.
That could be pulled apart a bit more though if we wanted to remove the JSON
requirement. That could become a parameter to the template I suppose.
https://github.com/llvm/llvm-project/pull/159160
_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits