From cc820bb22fd0c9c30c5bae1942e1a17c8b975d39 Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere Date: Wed, 29 Jul 2026 12:52:13 -0500 Subject: [PATCH 1/4] [lldb-mcp] Add --help and --version (#212680) lldb-mcp accepted no arguments at all, so --help and --version fell through to the protocol loop and blocked reading stdin, and a mistyped flag was silently ignored while the tool waited for MCP traffic. Handle both flags and reject anything else with a usage message. (cherry picked from commit f958a55f66a062b4c005015b691a949deb24e237) --- lldb/test/CMakeLists.txt | 4 +++ lldb/test/Shell/MCP/TestHelp.test | 7 ++++ lldb/test/Shell/MCP/TestUnknownArgument.test | 3 ++ lldb/test/Shell/MCP/TestVersion.test | 4 +++ lldb/test/Shell/helper/toolchain.py | 1 + lldb/tools/lldb-mcp/lldb-mcp.cpp | 36 ++++++++++++++++++++ 6 files changed, 55 insertions(+) create mode 100644 lldb/test/Shell/MCP/TestHelp.test create mode 100644 lldb/test/Shell/MCP/TestUnknownArgument.test create mode 100644 lldb/test/Shell/MCP/TestVersion.test diff --git a/lldb/test/CMakeLists.txt b/lldb/test/CMakeLists.txt index 691ecc68d1c78..42b3b214b658a 100644 --- a/lldb/test/CMakeLists.txt +++ b/lldb/test/CMakeLists.txt @@ -126,6 +126,10 @@ if(TARGET lldb-dap) add_lldb_test_dependency(lldb-dap) endif() +if(TARGET lldb-mcp) + add_lldb_test_dependency(lldb-mcp) +endif() + if(TARGET liblldb) add_lldb_test_dependency(liblldb) endif() diff --git a/lldb/test/Shell/MCP/TestHelp.test b/lldb/test/Shell/MCP/TestHelp.test new file mode 100644 index 0000000000000..11654876f9c31 --- /dev/null +++ b/lldb/test/Shell/MCP/TestHelp.test @@ -0,0 +1,7 @@ +# RUN: lldb-mcp --help | FileCheck %s +# RUN: lldb-mcp -h | FileCheck %s +# CHECK: OVERVIEW: LLDB MCP +# CHECK: USAGE: lldb-mcp +# CHECK: OPTIONS: +# CHECK: --help +# CHECK: --version diff --git a/lldb/test/Shell/MCP/TestUnknownArgument.test b/lldb/test/Shell/MCP/TestUnknownArgument.test new file mode 100644 index 0000000000000..9519a3b401056 --- /dev/null +++ b/lldb/test/Shell/MCP/TestUnknownArgument.test @@ -0,0 +1,3 @@ +# RUN: not lldb-mcp --bogus 2>&1 | FileCheck %s +# CHECK: error: unknown argument '--bogus' +# CHECK: USAGE: lldb-mcp diff --git a/lldb/test/Shell/MCP/TestVersion.test b/lldb/test/Shell/MCP/TestVersion.test new file mode 100644 index 0000000000000..50a387cb1d39e --- /dev/null +++ b/lldb/test/Shell/MCP/TestVersion.test @@ -0,0 +1,4 @@ +# RUN: lldb-mcp --version | FileCheck %s +# RUN: lldb-mcp -v | FileCheck %s +# CHECK: lldb-mcp{{.*}}: +# CHECK: liblldb: diff --git a/lldb/test/Shell/helper/toolchain.py b/lldb/test/Shell/helper/toolchain.py index 5dfbadd340f0a..95b6f5a802286 100644 --- a/lldb/test/Shell/helper/toolchain.py +++ b/lldb/test/Shell/helper/toolchain.py @@ -168,6 +168,7 @@ def use_lldb_substitutions(config): ), "lldb-test", "lldb-dap", + "lldb-mcp", ToolSubst( "%build", command="'" + sys.executable + "'", extra_args=build_script_args ), diff --git a/lldb/tools/lldb-mcp/lldb-mcp.cpp b/lldb/tools/lldb-mcp/lldb-mcp.cpp index 6e2181b9396ea..4b8217fdcc575 100644 --- a/lldb/tools/lldb-mcp/lldb-mcp.cpp +++ b/lldb/tools/lldb-mcp/lldb-mcp.cpp @@ -25,9 +25,11 @@ #include "lldb/lldb-forward.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/StringRef.h" +#include "llvm/Support/CommandLine.h" #include "llvm/Support/Error.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/InitLLVM.h" +#include "llvm/Support/Path.h" #include "llvm/Support/Process.h" #include "llvm/Support/Signals.h" #include "llvm/Support/WithColor.h" @@ -117,6 +119,24 @@ llvm::Error connectBackend(lldb_mcp::Multiplexer &multiplexer, MainLoop &loop, return llvm::Error::success(); } +void printHelp(StringRef tool_name) { + outs() << "OVERVIEW: LLDB MCP\n\nUSAGE: " << tool_name << " [options]\n"; + outs() << R"___( +Multiplexes the Model Context Protocol over stdio across the running LLDB +instances, and hosts its own debug sessions. + +OPTIONS: + -h, --help Display this help message + -v, --version Display the version +)___"; +} + +void printVersion(StringRef tool_name) { + outs() << tool_name << ": "; + cl::PrintVersionMessage(); + outs() << "liblldb: " << SBDebugger::GetVersionString() << '\n'; +} + } // namespace int main(int argc, char *argv[]) { @@ -141,6 +161,22 @@ int main(int argc, char *argv[]) { assert(result); #endif + StringRef tool_name = sys::path::filename(argv[0]); + for (int i = 1; i < argc; ++i) { + StringRef arg(argv[i]); + if (arg == "-h" || arg == "--help") { + printHelp(tool_name); + return EXIT_SUCCESS; + } + if (arg == "-v" || arg == "--version") { + printVersion(tool_name); + return EXIT_SUCCESS; + } + WithColor::error(errs()) << "unknown argument '" << arg << "'\n"; + printHelp(tool_name); + return EXIT_FAILURE; + } + // Bring up the debug engine (through the public SB API) so lldb-mcp can host // debug sessions in its own process. SBDebugger::Initialize(); From 773e47127d5fdddf7d4ddb5ffb5b64229311ed84 Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere Date: Wed, 29 Jul 2026 13:12:30 -0500 Subject: [PATCH 2/4] [lldb] Reply to malformed requests and use JSON-RPC error codes (#212678) The transport dropped a message that failed to parse: it logged the error and returned, so a peer waiting on that request hung forever and every message already buffered behind it was discarded. Keep going after a failed parse, and add ReplyWithParseError so a message that is valid JSON but not a valid request is answered against its own id. Unparseable JSON still gets no reply, since it carries no id and JSON-RPC forbids inventing one. Method-not-found was raised with createStringError, which converts through inconvertibleErrorCode() to -32603, so clients could not tell a misspelled method from a server fault. Use the MethodNotFound error, which already carries -32601, and give InvalidParams the -32602 code it was missing. (cherry picked from commit 16f116f476c248c345fd8ffe4b3e863f2700161f) --- lldb/include/lldb/Host/JSONTransport.h | 45 ++++++++++++++++++++-- lldb/include/lldb/Protocol/MCP/Transport.h | 3 ++ lldb/source/Host/common/JSONTransport.cpp | 14 ++++++- lldb/source/Protocol/MCP/Transport.cpp | 29 ++++++++++++++ lldb/unittests/Host/JSONTransportTest.cpp | 21 ++++++++++ 5 files changed, 107 insertions(+), 5 deletions(-) diff --git a/lldb/include/lldb/Host/JSONTransport.h b/lldb/include/lldb/Host/JSONTransport.h index 87adbbc04eeda..2601baa87624c 100644 --- a/lldb/include/lldb/Host/JSONTransport.h +++ b/lldb/include/lldb/Host/JSONTransport.h @@ -68,6 +68,8 @@ class InvalidParams : public llvm::ErrorInfo { public: static char ID; + static constexpr int kErrorCode = -32602; + explicit InvalidParams(std::string method, std::string context) : m_method(std::move(method)), m_context(std::move(context)) {} @@ -83,6 +85,25 @@ class InvalidParams : public llvm::ErrorInfo { std::string m_context; }; +/// An error to indicate that an incoming message could not be parsed as a +/// valid protocol message. +class InvalidMessage : public llvm::ErrorInfo { +public: + static char ID; + + static constexpr int kErrorCode = -32700; + + explicit InvalidMessage(std::string raw_message, std::string reason) + : m_raw_message(std::move(raw_message)), m_reason(std::move(reason)) {} + + void log(llvm::raw_ostream &OS) const override; + std::error_code convertToErrorCode() const override; + +private: + std::string m_raw_message; + std::string m_reason; +}; + /// An error to indicate that no handler was registered for a given method. class MethodNotFound : public llvm::ErrorInfo { public: @@ -139,6 +160,14 @@ class JSONTransport { /// Sends a response to a specific request. virtual llvm::Error Send(const Resp &) = 0; + /// Sends an error response for a message that failed to parse, described by + /// \p reason. Sends nothing if no request id can be recovered from it, since + /// there is then no request to respond to. + virtual llvm::Error ReplyWithParseError(llvm::StringRef raw_message, + llvm::StringRef reason) { + return llvm::Error::success(); + } + /// Implemented to handle incoming messages. (See `RegisterMessageHandler()` /// below). class MessageHandler { @@ -245,8 +274,15 @@ template class IOTransport : public JSONTransport { llvm::Expected message = llvm::json::parse(raw_message); if (!message) { - handler.OnError(message.takeError()); - return; + // Messages are independent, so one that fails to parse must not + // discard those already buffered behind it. + std::string reason = llvm::toString(message.takeError()); + if (llvm::Error error = + this->ReplyWithParseError(raw_message, reason)) + handler.OnError(std::move(error)); + handler.OnError( + llvm::make_error(raw_message, std::move(reason))); + continue; } std::visit([&handler](auto &&msg) { handler.Received(msg); }, *message); @@ -546,7 +582,7 @@ class Binder : public JSONTransport::MessageHandler { auto it = m_event_handlers.find(Proto::KeyFor(evt)); if (it == m_event_handlers.end()) { OnError(llvm::createStringError( - llvm::formatv("no handled for event {0}", toJSON(evt)))); + llvm::formatv("no handler for event {0}", toJSON(evt)))); return; } it->second(evt); @@ -558,7 +594,8 @@ class Binder : public JSONTransport::MessageHandler { std::scoped_lock guard(m_mutex); auto it = m_request_handlers.find(Proto::KeyFor(req)); if (it == m_request_handlers.end()) { - reply(Proto::Make(req, llvm::createStringError("method not found"))); + reply(Proto::Make(req, + llvm::make_error(Proto::KeyFor(req)))); return; } diff --git a/lldb/include/lldb/Protocol/MCP/Transport.h b/lldb/include/lldb/Protocol/MCP/Transport.h index ceadf1dbd82b8..4e55b7939cfe6 100644 --- a/lldb/include/lldb/Protocol/MCP/Transport.h +++ b/lldb/include/lldb/Protocol/MCP/Transport.h @@ -95,6 +95,9 @@ class Transport final void Log(llvm::StringRef message) override; + llvm::Error ReplyWithParseError(llvm::StringRef raw_message, + llvm::StringRef reason) override; + private: LogCallback m_log_callback; }; diff --git a/lldb/source/Host/common/JSONTransport.cpp b/lldb/source/Host/common/JSONTransport.cpp index 22de7fa8cbead..17af066dfa80d 100644 --- a/lldb/source/Host/common/JSONTransport.cpp +++ b/lldb/source/Host/common/JSONTransport.cpp @@ -37,7 +37,19 @@ void InvalidParams::log(raw_ostream &OS) const { << "'"; } std::error_code InvalidParams::convertToErrorCode() const { - return std::make_error_code(std::errc::invalid_argument); + // JSON-RPC Invalid params + return std::error_code(InvalidParams::kErrorCode, std::generic_category()); +} + +char InvalidMessage::ID; + +void InvalidMessage::log(raw_ostream &OS) const { + OS << "invalid message '" << m_raw_message << "': '" << m_reason << "'"; +} + +std::error_code InvalidMessage::convertToErrorCode() const { + // JSON-RPC Parse error + return std::error_code(InvalidMessage::kErrorCode, std::generic_category()); } char MethodNotFound::ID; diff --git a/lldb/source/Protocol/MCP/Transport.cpp b/lldb/source/Protocol/MCP/Transport.cpp index 1dc01a9f59008..47a3ff2b6f77c 100644 --- a/lldb/source/Protocol/MCP/Transport.cpp +++ b/lldb/source/Protocol/MCP/Transport.cpp @@ -22,3 +22,32 @@ void Transport::Log(StringRef message) { if (m_log_callback) m_log_callback(message); } + +llvm::Error Transport::ReplyWithParseError(StringRef raw_message, + StringRef reason) { + llvm::Expected value = json::parse(raw_message); + if (!value) { + // JSON-RPC forbids guessing an id, and malformed JSON carries none. + consumeError(value.takeError()); + return llvm::Error::success(); + } + + const json::Object *object = value->getAsObject(); + if (!object) + return llvm::Error::success(); + + // A message without an id is a notification, which takes no response. + const json::Value *raw_id = object->get("id"); + if (!raw_id) + return llvm::Error::success(); + + Id id; + if (std::optional str = raw_id->getAsString()) + id = str->str(); + else if (std::optional num = raw_id->getAsInteger()) + id = *num; + else + return llvm::Error::success(); + + return Send(Response{id, mcp::Error{eErrorCodeInvalidRequest, reason.str()}}); +} diff --git a/lldb/unittests/Host/JSONTransportTest.cpp b/lldb/unittests/Host/JSONTransportTest.cpp index 532473fa39899..18e2bf5e111ae 100644 --- a/lldb/unittests/Host/JSONTransportTest.cpp +++ b/lldb/unittests/Host/JSONTransportTest.cpp @@ -535,6 +535,19 @@ TEST_F(JSONRPCTransportTest, MalformedRequests) { ASSERT_THAT_ERROR(Run(), Succeeded()); } +TEST_F(JSONRPCTransportTest, MalformedRequestDoesNotDropLaterMessages) { + InSequence seq; + std::string messages = + "notjson\n" + Encode(Message{Request{1, "foo", std::nullopt}}); + ASSERT_THAT_EXPECTED(input.Write(messages.data(), messages.size()), + Succeeded()); + EXPECT_CALL(message_handler, OnError(_)).WillOnce([](llvm::Error err) { + consumeError(std::move(err)); + }); + EXPECT_CALL(message_handler, Received(Request{1, "foo", std::nullopt})); + ASSERT_THAT_ERROR(Run(), Succeeded()); +} + TEST_F(JSONRPCTransportTest, Read) { Write(Message{Req{1, "foo", std::nullopt}}); EXPECT_CALL(message_handler, Received(Req{1, "foo", std::nullopt})); @@ -812,6 +825,14 @@ TEST_F(TransportBinderTest, InBoundAsyncRequestsError) { Run(); } +TEST_F(TransportBinderTest, InBoundRequestUnknownMethod) { + EXPECT_THAT_ERROR(from_remote->Send(Request{4, "nosuch", MyFnParams{1, 2}}), + Succeeded()); + EXPECT_CALL(remote, Received(Response{4, MethodNotFound::kErrorCode, + "method not found: 'nosuch'"})); + Run(); +} + TEST_F(TransportBinderTest, FailPendingRequests) { OutgoingRequest addFn = binder->Bind("add"); From 93dc9b28248b10763a085122ba8217c8f0e85fcf Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere Date: Wed, 29 Jul 2026 13:13:00 -0500 Subject: [PATCH 3/4] [lldb-mcp] Run managed debug sessions synchronously (#212681) A debugger created for an MCP session has no event loop to service asynchronous stops, so a resume returned before the process stopped and every subsequent command failed against a still-running process. That made breakpoint debugging unusable: `run` reported only the launch, and `bt` that followed it errored out. Create these debuggers in synchronous mode, as lldb-dap does. (cherry picked from commit 58a93a1eec10d30dfa358afda73f402fb308a5e4) --- lldb/source/Plugins/Protocol/MCP/Tool.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lldb/source/Plugins/Protocol/MCP/Tool.cpp b/lldb/source/Plugins/Protocol/MCP/Tool.cpp index 7a5c114c57115..ec4599bf040a1 100644 --- a/lldb/source/Plugins/Protocol/MCP/Tool.cpp +++ b/lldb/source/Plugins/Protocol/MCP/Tool.cpp @@ -183,6 +183,10 @@ DebuggerCreateTool::Call(const lldb_protocol::mcp::ToolArguments &) { debugger_sp->SetOutputFile(out); debugger_sp->SetErrorFile(out); + // A debugger driven over MCP has no event loop to service asynchronous + // stops, so a resume must not return before the process has stopped. + debugger_sp->SetAsyncExecution(false); + return createTextResult(to_uri(debugger_sp)); } From 6094b6f8b850b2aafa0c4e1134e6c19843b6a02e Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere Date: Thu, 30 Jul 2026 09:30:32 -0700 Subject: [PATCH 4/4] Adjust for backport --- lldb/unittests/Host/JSONTransportTest.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lldb/unittests/Host/JSONTransportTest.cpp b/lldb/unittests/Host/JSONTransportTest.cpp index 18e2bf5e111ae..498f8a859ce50 100644 --- a/lldb/unittests/Host/JSONTransportTest.cpp +++ b/lldb/unittests/Host/JSONTransportTest.cpp @@ -538,13 +538,13 @@ TEST_F(JSONRPCTransportTest, MalformedRequests) { TEST_F(JSONRPCTransportTest, MalformedRequestDoesNotDropLaterMessages) { InSequence seq; std::string messages = - "notjson\n" + Encode(Message{Request{1, "foo", std::nullopt}}); + "notjson\n" + Encode(Message{Req{1, "foo", std::nullopt}}); ASSERT_THAT_EXPECTED(input.Write(messages.data(), messages.size()), Succeeded()); EXPECT_CALL(message_handler, OnError(_)).WillOnce([](llvm::Error err) { consumeError(std::move(err)); }); - EXPECT_CALL(message_handler, Received(Request{1, "foo", std::nullopt})); + EXPECT_CALL(message_handler, Received(Req{1, "foo", std::nullopt})); ASSERT_THAT_ERROR(Run(), Succeeded()); } @@ -826,10 +826,10 @@ TEST_F(TransportBinderTest, InBoundAsyncRequestsError) { } TEST_F(TransportBinderTest, InBoundRequestUnknownMethod) { - EXPECT_THAT_ERROR(from_remote->Send(Request{4, "nosuch", MyFnParams{1, 2}}), + EXPECT_THAT_ERROR(from_remote->Send(Req{4, "nosuch", MyFnParams{1, 2}}), Succeeded()); - EXPECT_CALL(remote, Received(Response{4, MethodNotFound::kErrorCode, - "method not found: 'nosuch'"})); + EXPECT_CALL(remote, Received(Resp{4, MethodNotFound::kErrorCode, + "method not found: 'nosuch'"})); Run(); }