LLDB mainline
Plugins/Protocol/MCP/Tool.cpp
Go to the documentation of this file.
1//===- Tool.cpp -----------------------------------------------------------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
9#include "Tool.h"
10#include "lldb/Core/Debugger.h"
11#include "lldb/Host/File.h"
18#include "llvm/ADT/StringRef.h"
19#include "llvm/Support/Error.h"
20#include <cstdint>
21#include <optional>
22
23using namespace lldb_private;
24using namespace lldb_protocol;
25using namespace lldb_private::mcp;
26using namespace lldb;
27using namespace llvm;
28
29namespace {
30
31static constexpr StringLiteral kSchemeAndHost = "lldb-mcp://debugger/";
32
33struct CommandToolArguments {
34 /// Either an id like '1' or a uri like 'lldb-mcp://debugger/1'.
35 std::string debugger;
36 std::string command;
37};
38
39bool fromJSON(const json::Value &V, CommandToolArguments &A, json::Path P) {
40 json::ObjectMapper O(V, P);
41 return O && O.mapOptional("debugger", A.debugger) &&
42 O.mapOptional("command", A.command);
43}
44
45/// Helper function to create a CallToolResult from a string output.
47createTextResult(std::string output, bool is_error = false) {
49 text_result.content.emplace_back(
50 lldb_protocol::mcp::TextContent{{std::move(output)}});
51 text_result.isError = is_error;
52 return text_result;
53}
54
55std::string to_uri(DebuggerSP debugger) {
56 return (kSchemeAndHost + std::to_string(debugger->GetID())).str();
57}
58
59} // namespace
60
61Expected<lldb_protocol::mcp::CallToolResult>
63 if (!std::holds_alternative<json::Value>(args))
64 return createStringError("CommandTool requires arguments");
65
66 json::Path::Root root;
67
68 CommandToolArguments arguments;
69 if (!fromJSON(std::get<json::Value>(args), arguments, root))
70 return root.getError();
71
72 lldb::DebuggerSP debugger_sp;
73
74 if (!arguments.debugger.empty()) {
75 llvm::StringRef debugger_specifier = arguments.debugger;
76 debugger_specifier.consume_front(kSchemeAndHost);
77 uint32_t debugger_id = 0;
78 if (debugger_specifier.consumeInteger(10, debugger_id))
79 return createStringError(
80 formatv("malformed debugger specifier {0}", arguments.debugger));
81
82 debugger_sp = Debugger::FindDebuggerWithID(debugger_id);
83 } else {
84 for (size_t i = 0; i < Debugger::GetNumDebuggers(); i++) {
85 debugger_sp = Debugger::GetDebuggerAtIndex(i);
86 if (debugger_sp)
87 break;
88 }
89 }
90
91 if (!debugger_sp)
92 return createStringError("no debugger found");
93
94 // FIXME: Disallow certain commands and their aliases.
95 CommandReturnObject result(/*colors=*/false);
96 debugger_sp->GetCommandInterpreter().HandleCommand(arguments.command.c_str(),
97 eLazyBoolYes, result);
98
99 std::string output;
100 StringRef output_str = result.GetOutputString();
101 if (!output_str.empty())
102 output += output_str.str();
103
104 std::string err_str = result.GetErrorString();
105 if (!err_str.empty()) {
106 if (!output.empty())
107 output += '\n';
108 output += err_str;
109 }
110
111 return createTextResult(output, !result.Succeeded());
112}
113
114std::optional<json::Value> CommandTool::GetSchema() const {
115 using namespace llvm::json;
116 Object properties{
117 {"debugger",
118 Object{{"type", "string"},
119 {"description",
120 "The debugger ID or URI to a specific debug session. If not "
121 "specified, the first debugger will be used."}}},
122 {"command",
123 Object{{"type", "string"}, {"description", "An lldb command to run."}}}};
124 Object schema{{"type", "object"}, {"properties", std::move(properties)}};
125 return schema;
126}
127
128Expected<lldb_protocol::mcp::CallToolResult>
130 llvm::json::Path::Root root;
131
132 // Return a nested Markdown list with debuggers and target.
133 // Example output:
134 //
135 // - lldb-mcp://debugger/1
136 // - lldb-mcp://debugger/2
137 //
138 // FIXME: Use Structured Content when we adopt protocol version 2025-06-18.
139 std::string output;
140 llvm::raw_string_ostream os(output);
141
142 const size_t num_debuggers = Debugger::GetNumDebuggers();
143 for (size_t i = 0; i < num_debuggers; ++i) {
145 if (!debugger_sp)
146 continue;
147
148 os << "- " << to_uri(debugger_sp) << '\n';
149 }
150
151 return createTextResult(output);
152}
153
154/// Opens the platform null device with the given options, or nullptr on error.
156 llvm::Expected<lldb::FileUP> file =
158 if (!file) {
159 llvm::consumeError(file.takeError());
160 return nullptr;
161 }
162 return std::move(*file);
163}
164
165Expected<lldb_protocol::mcp::CallToolResult>
167 // Redirect the new debugger's stdio to the null device so its prompt and
168 // async output can't corrupt an MCP stream sharing the host's stdout. Command
169 // results flow through CommandReturnObject and are unaffected. Open the null
170 // files first so a failure can't leave a created debugger on the real stdio.
171 // The single write-only null file backs both stdout and stderr.
174 if (!in || !out)
175 return createStringError(
176 "failed to open the null device for debugger stdio");
177
179 if (!debugger_sp)
180 return createStringError("failed to create debugger");
181
182 debugger_sp->SetInputFile(in);
183 debugger_sp->SetOutputFile(out);
184 debugger_sp->SetErrorFile(out);
185
186 // A debugger driven over MCP has no event loop to service asynchronous
187 // stops, so a resume must not return before the process has stopped.
188 debugger_sp->SetAsyncExecution(false);
189
190 return createTextResult(to_uri(debugger_sp));
191}
192
193Expected<lldb_protocol::mcp::CallToolResult>
195 if (!std::holds_alternative<json::Value>(args))
196 return createStringError("DebuggerDeleteTool requires arguments");
197
198 const json::Object *arguments = std::get<json::Value>(args).getAsObject();
199 if (!arguments)
200 return createStringError("DebuggerDeleteTool requires arguments");
201
202 std::optional<StringRef> debugger = arguments->getString("debugger");
203 if (!debugger)
204 return createStringError("DebuggerDeleteTool requires a debugger");
205
206 StringRef specifier = *debugger;
207 specifier.consume_front(kSchemeAndHost);
208 uint32_t debugger_id = 0;
209 if (specifier.consumeInteger(10, debugger_id))
210 return createStringError(
211 formatv("malformed debugger specifier {0}", *debugger));
212
213 lldb::DebuggerSP debugger_sp = Debugger::FindDebuggerWithID(debugger_id);
214 if (!debugger_sp)
215 return createStringError("no debugger found");
216
217 Debugger::Destroy(debugger_sp);
218 return createTextResult(formatv("deleted {0}", *debugger).str());
219}
220
221std::optional<json::Value> DebuggerDeleteTool::GetSchema() const {
222 using namespace llvm::json;
223 Object properties{
224 {"debugger",
225 Object{{"type", "string"},
226 {"description", "The debugger ID or URI to destroy."}}}};
227 Object schema{{"type", "object"},
228 {"properties", std::move(properties)},
229 {"required", Array{"debugger"}}};
230 return schema;
231}
static lldb::FileSP openNull(File::OpenOptions options)
Opens the platform null device with the given options, or nullptr on error.
std::string GetErrorString(bool with_diagnostics=true) const
Return the errors as a string.
llvm::StringRef GetOutputString() const
static lldb::DebuggerSP GetDebuggerAtIndex(size_t index)
static lldb::DebuggerSP CreateInstance(lldb::LogOutputCallback log_callback=nullptr, void *baton=nullptr)
Definition Debugger.cpp:941
static lldb::DebuggerSP FindDebuggerWithID(lldb::user_id_t id)
static void Destroy(lldb::DebuggerSP &debugger_sp)
Definition Debugger.cpp:985
static size_t GetNumDebuggers()
A file utility class.
Definition FileSpec.h:57
static const char * DEV_NULL
Definition FileSystem.h:32
int Open(const char *path, int flags, int mode=0600)
Wraps open in a platform-independent way.
static FileSystem & Instance()
llvm::Expected< lldb_protocol::mcp::CallToolResult > Call(const lldb_protocol::mcp::ToolArguments &args) override
std::optional< llvm::json::Value > GetSchema() const override
llvm::Expected< lldb_protocol::mcp::CallToolResult > Call(const lldb_protocol::mcp::ToolArguments &args) override
std::optional< llvm::json::Value > GetSchema() const override
llvm::Expected< lldb_protocol::mcp::CallToolResult > Call(const lldb_protocol::mcp::ToolArguments &args) override
llvm::Expected< lldb_protocol::mcp::CallToolResult > Call(const lldb_protocol::mcp::ToolArguments &args) override
A class that represents a running process on the host machine.
bool fromJSON(const llvm::json::Value &value, SymbolValue &data, llvm::json::Path path)
std::variant< std::monostate, llvm::json::Value > ToolArguments
Definition Protocol.h:195
std::shared_ptr< lldb_private::Debugger > DebuggerSP
std::shared_ptr< lldb_private::File > FileSP
The server’s response to a tool call.
Definition Protocol.h:304
std::vector< ContentBlock > content
A list of content objects that represent the unstructured result of the tool call.
Definition Protocol.h:307
bool isError
Whether the tool call ended in an error.
Definition Protocol.h:321
Text provided to or from an LLM.
Definition Protocol.h:174