Author: Jonas Devlieghere Date: 2025-07-11T15:49:27-07:00 New Revision: 3c4c2fada26f479be7c2f9744f5b7364f7612446
URL: https://github.com/llvm/llvm-project/commit/3c4c2fada26f479be7c2f9744f5b7364f7612446 DIFF: https://github.com/llvm/llvm-project/commit/3c4c2fada26f479be7c2f9744f5b7364f7612446.diff LOG: [lldb] Expose debuggers and target as resources through MCP (#148075) Expose debuggers and target as resources through MCP. This has two advantages: 1. Enables returning data in a structured way. Although tools can return structured data with the latest revision of the protocol, we might not be able to update before the majority of clients has adopted it. 2. Enables the user to specify a resource themselves, rather than letting the model guess which debugger instance it should use. This PR exposes a resource for debuggers and targets. The following URI returns information about a given debugger instance: ``` lldb://debugger/<debugger id> ``` For example: ``` { uri: "lldb://debugger/0" mimeType: "application/json" text: "{"debugger_id":0,"num_targets":2}" } ``` The following URI returns information about a given target: ``` lldb://debugger/<debugger id>/target/<target id> ``` For example: ``` { uri: "lldb://debugger/0/target/0" mimeType: "application/json" text: "{"arch":"arm64-apple-macosx26.0.0","debugger_id":0,"path":"/Users/jonas/llvm/build-ra/bin/count","target_id":0}" } ``` Added: lldb/source/Plugins/Protocol/MCP/Resource.cpp lldb/source/Plugins/Protocol/MCP/Resource.h Modified: lldb/include/lldb/Core/Debugger.h lldb/include/lldb/Target/Target.h lldb/source/Plugins/Protocol/MCP/CMakeLists.txt lldb/source/Plugins/Protocol/MCP/MCPError.cpp lldb/source/Plugins/Protocol/MCP/MCPError.h lldb/source/Plugins/Protocol/MCP/Protocol.cpp lldb/source/Plugins/Protocol/MCP/Protocol.h lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.h lldb/source/Plugins/Protocol/MCP/Tool.cpp lldb/source/Plugins/Protocol/MCP/Tool.h lldb/unittests/Protocol/ProtocolMCPServerTest.cpp lldb/unittests/Protocol/ProtocolMCPTest.cpp Removed: ################################################################################ diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h index 504f936fe317a..250ad64b76d9a 100644 --- a/lldb/include/lldb/Core/Debugger.h +++ b/lldb/include/lldb/Core/Debugger.h @@ -367,7 +367,7 @@ class Debugger : public std::enable_shared_from_this<Debugger>, bool GetNotifyVoid() const; - const std::string &GetInstanceName() { return m_instance_name; } + const std::string &GetInstanceName() const { return m_instance_name; } bool GetShowInlineDiagnostics() const; diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h index 0d4e11b65339e..a1d881375b08b 100644 --- a/lldb/include/lldb/Target/Target.h +++ b/lldb/include/lldb/Target/Target.h @@ -1093,7 +1093,7 @@ class Target : public std::enable_shared_from_this<Target>, Architecture *GetArchitecturePlugin() const { return m_arch.GetPlugin(); } - Debugger &GetDebugger() { return m_debugger; } + Debugger &GetDebugger() const { return m_debugger; } size_t ReadMemoryFromFileCache(const Address &addr, void *dst, size_t dst_len, Status &error); diff --git a/lldb/source/Plugins/Protocol/MCP/CMakeLists.txt b/lldb/source/Plugins/Protocol/MCP/CMakeLists.txt index db31a7a69cb33..e104fb527e57a 100644 --- a/lldb/source/Plugins/Protocol/MCP/CMakeLists.txt +++ b/lldb/source/Plugins/Protocol/MCP/CMakeLists.txt @@ -2,6 +2,7 @@ add_lldb_library(lldbPluginProtocolServerMCP PLUGIN MCPError.cpp Protocol.cpp ProtocolServerMCP.cpp + Resource.cpp Tool.cpp LINK_COMPONENTS diff --git a/lldb/source/Plugins/Protocol/MCP/MCPError.cpp b/lldb/source/Plugins/Protocol/MCP/MCPError.cpp index 5ed850066b659..659b53a14fe23 100644 --- a/lldb/source/Plugins/Protocol/MCP/MCPError.cpp +++ b/lldb/source/Plugins/Protocol/MCP/MCPError.cpp @@ -14,6 +14,7 @@ namespace lldb_private::mcp { char MCPError::ID; +char UnsupportedURI::ID; MCPError::MCPError(std::string message, int64_t error_code) : m_message(message), m_error_code(error_code) {} @@ -31,4 +32,14 @@ protocol::Error MCPError::toProtcolError() const { return error; } +UnsupportedURI::UnsupportedURI(std::string uri) : m_uri(uri) {} + +void UnsupportedURI::log(llvm::raw_ostream &OS) const { + OS << "unsupported uri: " << m_uri; +} + +std::error_code UnsupportedURI::convertToErrorCode() const { + return llvm::inconvertibleErrorCode(); +} + } // namespace lldb_private::mcp diff --git a/lldb/source/Plugins/Protocol/MCP/MCPError.h b/lldb/source/Plugins/Protocol/MCP/MCPError.h index 2a76a7b087e20..f4db13d6deade 100644 --- a/lldb/source/Plugins/Protocol/MCP/MCPError.h +++ b/lldb/source/Plugins/Protocol/MCP/MCPError.h @@ -8,6 +8,7 @@ #include "Protocol.h" #include "llvm/Support/Error.h" +#include "llvm/Support/FormatVariadic.h" #include <string> namespace lldb_private::mcp { @@ -16,7 +17,7 @@ class MCPError : public llvm::ErrorInfo<MCPError> { public: static char ID; - MCPError(std::string message, int64_t error_code); + MCPError(std::string message, int64_t error_code = kInternalError); void log(llvm::raw_ostream &OS) const override; std::error_code convertToErrorCode() const override; @@ -25,9 +26,25 @@ class MCPError : public llvm::ErrorInfo<MCPError> { protocol::Error toProtcolError() const; + static constexpr int64_t kResourceNotFound = -32002; + static constexpr int64_t kInternalError = -32603; + private: std::string m_message; int64_t m_error_code; }; +class UnsupportedURI : public llvm::ErrorInfo<UnsupportedURI> { +public: + static char ID; + + UnsupportedURI(std::string uri); + + void log(llvm::raw_ostream &OS) const override; + std::error_code convertToErrorCode() const override; + +private: + std::string m_uri; +}; + } // namespace lldb_private::mcp diff --git a/lldb/source/Plugins/Protocol/MCP/Protocol.cpp b/lldb/source/Plugins/Protocol/MCP/Protocol.cpp index d66c931a0b284..e42e1bf1118cf 100644 --- a/lldb/source/Plugins/Protocol/MCP/Protocol.cpp +++ b/lldb/source/Plugins/Protocol/MCP/Protocol.cpp @@ -107,8 +107,36 @@ bool fromJSON(const llvm::json::Value &V, ToolCapability &TC, return O && O.map("listChanged", TC.listChanged); } +llvm::json::Value toJSON(const ResourceCapability &RC) { + return llvm::json::Object{{"listChanged", RC.listChanged}, + {"subscribe", RC.subscribe}}; +} + +bool fromJSON(const llvm::json::Value &V, ResourceCapability &RC, + llvm::json::Path P) { + llvm::json::ObjectMapper O(V, P); + return O && O.map("listChanged", RC.listChanged) && + O.map("subscribe", RC.subscribe); +} + llvm::json::Value toJSON(const Capabilities &C) { - return llvm::json::Object{{"tools", C.tools}}; + return llvm::json::Object{{"tools", C.tools}, {"resources", C.resources}}; +} + +bool fromJSON(const llvm::json::Value &V, Resource &R, llvm::json::Path P) { + llvm::json::ObjectMapper O(V, P); + return O && O.map("uri", R.uri) && O.map("name", R.name) && + O.mapOptional("description", R.description) && + O.mapOptional("mimeType", R.mimeType); +} + +llvm::json::Value toJSON(const Resource &R) { + llvm::json::Object Result{{"uri", R.uri}, {"name", R.name}}; + if (R.description) + Result.insert({"description", R.description}); + if (R.mimeType) + Result.insert({"mimeType", R.mimeType}); + return Result; } bool fromJSON(const llvm::json::Value &V, Capabilities &C, llvm::json::Path P) { @@ -116,6 +144,30 @@ bool fromJSON(const llvm::json::Value &V, Capabilities &C, llvm::json::Path P) { return O && O.map("tools", C.tools); } +llvm::json::Value toJSON(const ResourceContents &RC) { + llvm::json::Object Result{{"uri", RC.uri}, {"text", RC.text}}; + if (RC.mimeType) + Result.insert({"mimeType", RC.mimeType}); + return Result; +} + +bool fromJSON(const llvm::json::Value &V, ResourceContents &RC, + llvm::json::Path P) { + llvm::json::ObjectMapper O(V, P); + return O && O.map("uri", RC.uri) && O.map("text", RC.text) && + O.mapOptional("mimeType", RC.mimeType); +} + +llvm::json::Value toJSON(const ResourceResult &RR) { + return llvm::json::Object{{"contents", RR.contents}}; +} + +bool fromJSON(const llvm::json::Value &V, ResourceResult &RR, + llvm::json::Path P) { + llvm::json::ObjectMapper O(V, P); + return O && O.map("contents", RR.contents); +} + llvm::json::Value toJSON(const TextContent &TC) { return llvm::json::Object{{"type", "text"}, {"text", TC.text}}; } diff --git a/lldb/source/Plugins/Protocol/MCP/Protocol.h b/lldb/source/Plugins/Protocol/MCP/Protocol.h index cb790dc4e5596..ffe621bee1c2a 100644 --- a/lldb/source/Plugins/Protocol/MCP/Protocol.h +++ b/lldb/source/Plugins/Protocol/MCP/Protocol.h @@ -76,17 +76,75 @@ struct ToolCapability { llvm::json::Value toJSON(const ToolCapability &); bool fromJSON(const llvm::json::Value &, ToolCapability &, llvm::json::Path); +struct ResourceCapability { + /// Whether this server supports notifications for changes to the resources + /// list. + bool listChanged = false; + + /// Whether subscriptions are supported. + bool subscribe = false; +}; + +llvm::json::Value toJSON(const ResourceCapability &); +bool fromJSON(const llvm::json::Value &, ResourceCapability &, + 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 Capabilities { - /// Present if the server offers any tools to call. + /// Tool capabilities of the server. ToolCapability tools; + + /// Resource capabilities of the server. + ResourceCapability resources; }; llvm::json::Value toJSON(const Capabilities &); bool fromJSON(const llvm::json::Value &, Capabilities &, llvm::json::Path); +/// A known resource that the server is capable of reading. +struct Resource { + /// The URI of this resource. + std::string uri; + + /// A human-readable name for this resource. + std::string name; + + /// A description of what this resource represents. + std::optional<std::string> description; + + /// The MIME type of this resource, if known. + std::optional<std::string> mimeType; +}; + +llvm::json::Value toJSON(const Resource &); +bool fromJSON(const llvm::json::Value &, Resource &, llvm::json::Path); + +/// The contents of a specific resource or sub-resource. +struct ResourceContents { + /// The URI of this resource. + std::string uri; + + /// The text of the item. This must only be set if the item can actually be + /// represented as text (not binary data). + std::string text; + + /// The MIME type of this resource, if known. + std::optional<std::string> mimeType; +}; + +llvm::json::Value toJSON(const ResourceContents &); +bool fromJSON(const llvm::json::Value &, ResourceContents &, llvm::json::Path); + +/// The server's response to a resources/read request from the client. +struct ResourceResult { + std::vector<ResourceContents> contents; +}; + +llvm::json::Value toJSON(const ResourceResult &); +bool fromJSON(const llvm::json::Value &, ResourceResult &, llvm::json::Path); + /// Text provided to or from an LLM. struct TextContent { /// The text content of the message. diff --git a/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp b/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp index 3180341b50b91..0e5a3631e6387 100644 --- a/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp +++ b/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp @@ -28,20 +28,29 @@ ProtocolServerMCP::ProtocolServerMCP() : ProtocolServer() { AddRequestHandler("initialize", std::bind(&ProtocolServerMCP::InitializeHandler, this, std::placeholders::_1)); + AddRequestHandler("tools/list", std::bind(&ProtocolServerMCP::ToolsListHandler, this, std::placeholders::_1)); AddRequestHandler("tools/call", std::bind(&ProtocolServerMCP::ToolsCallHandler, this, std::placeholders::_1)); + + AddRequestHandler("resources/list", + std::bind(&ProtocolServerMCP::ResourcesListHandler, this, + std::placeholders::_1)); + AddRequestHandler("resources/read", + std::bind(&ProtocolServerMCP::ResourcesReadHandler, this, + std::placeholders::_1)); AddNotificationHandler( "notifications/initialized", [](const protocol::Notification &) { LLDB_LOG(GetLog(LLDBLog::Host), "MCP initialization complete"); }); + AddTool( std::make_unique<CommandTool>("lldb_command", "Run an lldb command.")); - AddTool(std::make_unique<DebuggerListTool>( - "lldb_debugger_list", "List debugger instances with their debugger_id.")); + + AddResourceProvider(std::make_unique<DebuggerResourceProvider>()); } ProtocolServerMCP::~ProtocolServerMCP() { llvm::consumeError(Stop()); } @@ -75,7 +84,7 @@ ProtocolServerMCP::Handle(protocol::Request request) { } return make_error<MCPError>( - llvm::formatv("no handler for request: {0}", request.method).str(), 1); + llvm::formatv("no handler for request: {0}", request.method).str()); } void ProtocolServerMCP::Handle(protocol::Notification notification) { @@ -216,7 +225,7 @@ ProtocolServerMCP::HandleData(llvm::StringRef data) { response.takeError(), [&](const MCPError &err) { protocol_error = err.toProtcolError(); }, [&](const llvm::ErrorInfoBase &err) { - protocol_error.error.code = -1; + protocol_error.error.code = MCPError::kInternalError; protocol_error.error.message = err.message(); }); protocol_error.id = request->id; @@ -244,6 +253,9 @@ ProtocolServerMCP::HandleData(llvm::StringRef data) { protocol::Capabilities ProtocolServerMCP::GetCapabilities() { protocol::Capabilities capabilities; capabilities.tools.listChanged = true; + // FIXME: Support sending notifications when a debugger/target are + // added/removed. + capabilities.resources.listChanged = false; return capabilities; } @@ -255,6 +267,15 @@ void ProtocolServerMCP::AddTool(std::unique_ptr<Tool> tool) { m_tools[tool->GetName()] = std::move(tool); } +void ProtocolServerMCP::AddResourceProvider( + std::unique_ptr<ResourceProvider> resource_provider) { + std::lock_guard<std::mutex> guard(m_server_mutex); + + if (!resource_provider) + return; + m_resource_providers.push_back(std::move(resource_provider)); +} + void ProtocolServerMCP::AddRequestHandler(llvm::StringRef method, RequestHandler handler) { std::lock_guard<std::mutex> guard(m_server_mutex); @@ -327,3 +348,63 @@ ProtocolServerMCP::ToolsCallHandler(const protocol::Request &request) { return response; } + +llvm::Expected<protocol::Response> +ProtocolServerMCP::ResourcesListHandler(const protocol::Request &request) { + protocol::Response response; + + llvm::json::Array resources; + + std::lock_guard<std::mutex> guard(m_server_mutex); + for (std::unique_ptr<ResourceProvider> &resource_provider_up : + m_resource_providers) { + for (const protocol::Resource &resource : + resource_provider_up->GetResources()) + resources.push_back(resource); + } + response.result.emplace( + llvm::json::Object{{"resources", std::move(resources)}}); + + return response; +} + +llvm::Expected<protocol::Response> +ProtocolServerMCP::ResourcesReadHandler(const protocol::Request &request) { + protocol::Response response; + + if (!request.params) + return llvm::createStringError("no resource parameters"); + + const json::Object *param_obj = request.params->getAsObject(); + if (!param_obj) + return llvm::createStringError("no resource parameters"); + + const json::Value *uri = param_obj->get("uri"); + if (!uri) + return llvm::createStringError("no resource uri"); + + llvm::StringRef uri_str = uri->getAsString().value_or(""); + if (uri_str.empty()) + return llvm::createStringError("no resource uri"); + + std::lock_guard<std::mutex> guard(m_server_mutex); + for (std::unique_ptr<ResourceProvider> &resource_provider_up : + m_resource_providers) { + llvm::Expected<protocol::ResourceResult> result = + resource_provider_up->ReadResource(uri_str); + if (result.errorIsA<UnsupportedURI>()) { + llvm::consumeError(result.takeError()); + continue; + } + if (!result) + return result.takeError(); + + protocol::Response response; + response.result.emplace(std::move(*result)); + return response; + } + + return make_error<MCPError>( + llvm::formatv("no resource handler for uri: {0}", uri_str).str(), + MCPError::kResourceNotFound); +} diff --git a/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.h b/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.h index d55882cc8ab09..e273f6e2a8d37 100644 --- a/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.h +++ b/lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.h @@ -10,6 +10,7 @@ #define LLDB_PLUGINS_PROTOCOL_MCP_PROTOCOLSERVERMCP_H #include "Protocol.h" +#include "Resource.h" #include "Tool.h" #include "lldb/Core/ProtocolServer.h" #include "lldb/Host/MainLoop.h" @@ -46,6 +47,8 @@ class ProtocolServerMCP : public ProtocolServer { std::function<void(const protocol::Notification &)>; void AddTool(std::unique_ptr<Tool> tool); + void AddResourceProvider(std::unique_ptr<ResourceProvider> resource_provider); + void AddRequestHandler(llvm::StringRef method, RequestHandler handler); void AddNotificationHandler(llvm::StringRef method, NotificationHandler handler); @@ -61,11 +64,17 @@ class ProtocolServerMCP : public ProtocolServer { llvm::Expected<protocol::Response> InitializeHandler(const protocol::Request &); + llvm::Expected<protocol::Response> ToolsListHandler(const protocol::Request &); llvm::Expected<protocol::Response> ToolsCallHandler(const protocol::Request &); + llvm::Expected<protocol::Response> + ResourcesListHandler(const protocol::Request &); + llvm::Expected<protocol::Response> + ResourcesReadHandler(const protocol::Request &); + protocol::Capabilities GetCapabilities(); llvm::StringLiteral kName = "lldb-mcp"; @@ -89,6 +98,7 @@ class ProtocolServerMCP : public ProtocolServer { std::mutex m_server_mutex; llvm::StringMap<std::unique_ptr<Tool>> m_tools; + std::vector<std::unique_ptr<ResourceProvider>> m_resource_providers; llvm::StringMap<RequestHandler> m_request_handlers; llvm::StringMap<NotificationHandler> m_notification_handlers; diff --git a/lldb/source/Plugins/Protocol/MCP/Resource.cpp b/lldb/source/Plugins/Protocol/MCP/Resource.cpp new file mode 100644 index 0000000000000..d75d5b6dd6a41 --- /dev/null +++ b/lldb/source/Plugins/Protocol/MCP/Resource.cpp @@ -0,0 +1,217 @@ +// 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 +// +//===----------------------------------------------------------------------===// + +#include "Resource.h" +#include "MCPError.h" +#include "lldb/Core/Debugger.h" +#include "lldb/Core/Module.h" +#include "lldb/Target/Platform.h" + +using namespace lldb_private::mcp; + +namespace { +struct DebuggerResource { + uint64_t debugger_id = 0; + std::string name; + uint64_t num_targets = 0; +}; + +llvm::json::Value toJSON(const DebuggerResource &DR) { + llvm::json::Object Result{{"debugger_id", DR.debugger_id}, + {"num_targets", DR.num_targets}}; + if (!DR.name.empty()) + Result.insert({"name", DR.name}); + return Result; +} + +struct TargetResource { + size_t debugger_id = 0; + size_t target_idx = 0; + bool selected = false; + bool dummy = false; + std::string arch; + std::string path; + std::string platform; +}; + +llvm::json::Value toJSON(const TargetResource &TR) { + llvm::json::Object Result{{"debugger_id", TR.debugger_id}, + {"target_idx", TR.target_idx}, + {"selected", TR.selected}, + {"dummy", TR.dummy}}; + if (!TR.arch.empty()) + Result.insert({"arch", TR.arch}); + if (!TR.path.empty()) + Result.insert({"path", TR.path}); + if (!TR.platform.empty()) + Result.insert({"platform", TR.platform}); + return Result; +} +} // namespace + +static constexpr llvm::StringLiteral kMimeTypeJSON = "application/json"; + +template <typename... Args> +static llvm::Error createStringError(const char *format, Args &&...args) { + return llvm::createStringError( + llvm::formatv(format, std::forward<Args>(args)...).str()); +} + +static llvm::Error createUnsupportedURIError(llvm::StringRef uri) { + return llvm::make_error<UnsupportedURI>(uri.str()); +} + +protocol::Resource +DebuggerResourceProvider::GetDebuggerResource(Debugger &debugger) { + const lldb::user_id_t debugger_id = debugger.GetID(); + + protocol::Resource resource; + resource.uri = llvm::formatv("lldb://debugger/{0}", debugger_id); + resource.name = debugger.GetInstanceName(); + resource.description = + llvm::formatv("Information about debugger instance {0}: {1}", debugger_id, + debugger.GetInstanceName()); + resource.mimeType = kMimeTypeJSON; + return resource; +} + +protocol::Resource +DebuggerResourceProvider::GetTargetResource(size_t target_idx, Target &target) { + const size_t debugger_id = target.GetDebugger().GetID(); + + std::string target_name = llvm::formatv("target {0}", target_idx); + + if (Module *exe_module = target.GetExecutableModulePointer()) + target_name = exe_module->GetFileSpec().GetFilename().GetString(); + + protocol::Resource resource; + resource.uri = + llvm::formatv("lldb://debugger/{0}/target/{1}", debugger_id, target_idx); + resource.name = target_name; + resource.description = + llvm::formatv("Information about target {0} in debugger instance {1}", + target_idx, debugger_id); + resource.mimeType = kMimeTypeJSON; + return resource; +} + +std::vector<protocol::Resource> DebuggerResourceProvider::GetResources() const { + std::vector<protocol::Resource> resources; + + const size_t num_debuggers = Debugger::GetNumDebuggers(); + for (size_t i = 0; i < num_debuggers; ++i) { + lldb::DebuggerSP debugger_sp = Debugger::GetDebuggerAtIndex(i); + if (!debugger_sp) + continue; + resources.emplace_back(GetDebuggerResource(*debugger_sp)); + + TargetList &target_list = debugger_sp->GetTargetList(); + const size_t num_targets = target_list.GetNumTargets(); + for (size_t j = 0; j < num_targets; ++j) { + lldb::TargetSP target_sp = target_list.GetTargetAtIndex(j); + if (!target_sp) + continue; + resources.emplace_back(GetTargetResource(j, *target_sp)); + } + } + + return resources; +} + +llvm::Expected<protocol::ResourceResult> +DebuggerResourceProvider::ReadResource(llvm::StringRef uri) const { + + auto [protocol, path] = uri.split("://"); + + if (protocol != "lldb") + return createUnsupportedURIError(uri); + + llvm::SmallVector<llvm::StringRef, 4> components; + path.split(components, '/'); + + if (components.size() < 2) + return createUnsupportedURIError(uri); + + if (components[0] != "debugger") + return createUnsupportedURIError(uri); + + size_t debugger_idx; + if (components[1].getAsInteger(0, debugger_idx)) + return createStringError("invalid debugger id '{0}': {1}", components[1], + path); + + if (components.size() > 3) { + if (components[2] != "target") + return createUnsupportedURIError(uri); + + size_t target_idx; + if (components[3].getAsInteger(0, target_idx)) + return createStringError("invalid target id '{0}': {1}", components[3], + path); + + return ReadTargetResource(uri, debugger_idx, target_idx); + } + + return ReadDebuggerResource(uri, debugger_idx); +} + +llvm::Expected<protocol::ResourceResult> +DebuggerResourceProvider::ReadDebuggerResource(llvm::StringRef uri, + lldb::user_id_t debugger_id) { + lldb::DebuggerSP debugger_sp = Debugger::FindDebuggerWithID(debugger_id); + if (!debugger_sp) + return createStringError("invalid debugger id: {0}", debugger_id); + + DebuggerResource debugger_resource; + debugger_resource.debugger_id = debugger_id; + debugger_resource.name = debugger_sp->GetInstanceName(); + debugger_resource.num_targets = debugger_sp->GetTargetList().GetNumTargets(); + + protocol::ResourceContents contents; + contents.uri = uri; + contents.mimeType = kMimeTypeJSON; + contents.text = llvm::formatv("{0}", toJSON(debugger_resource)); + + protocol::ResourceResult result; + result.contents.push_back(contents); + return result; +} + +llvm::Expected<protocol::ResourceResult> +DebuggerResourceProvider::ReadTargetResource(llvm::StringRef uri, + lldb::user_id_t debugger_id, + size_t target_idx) { + + lldb::DebuggerSP debugger_sp = Debugger::FindDebuggerWithID(debugger_id); + if (!debugger_sp) + return createStringError("invalid debugger id: {0}", debugger_id); + + TargetList &target_list = debugger_sp->GetTargetList(); + lldb::TargetSP target_sp = target_list.GetTargetAtIndex(target_idx); + if (!target_sp) + return createStringError("invalid target idx: {0}", target_idx); + + TargetResource target_resource; + target_resource.debugger_id = debugger_id; + target_resource.target_idx = target_idx; + target_resource.arch = target_sp->GetArchitecture().GetTriple().str(); + target_resource.dummy = target_sp->IsDummyTarget(); + target_resource.selected = target_sp == debugger_sp->GetSelectedTarget(); + + if (Module *exe_module = target_sp->GetExecutableModulePointer()) + target_resource.path = exe_module->GetFileSpec().GetPath(); + if (lldb::PlatformSP platform_sp = target_sp->GetPlatform()) + target_resource.platform = platform_sp->GetName(); + + protocol::ResourceContents contents; + contents.uri = uri; + contents.mimeType = kMimeTypeJSON; + contents.text = llvm::formatv("{0}", toJSON(target_resource)); + + protocol::ResourceResult result; + result.contents.push_back(contents); + return result; +} diff --git a/lldb/source/Plugins/Protocol/MCP/Resource.h b/lldb/source/Plugins/Protocol/MCP/Resource.h new file mode 100644 index 0000000000000..5ac38e7e878ff --- /dev/null +++ b/lldb/source/Plugins/Protocol/MCP/Resource.h @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// 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_PLUGINS_PROTOCOL_MCP_RESOURCE_H +#define LLDB_PLUGINS_PROTOCOL_MCP_RESOURCE_H + +#include "Protocol.h" +#include "lldb/lldb-private.h" +#include <vector> + +namespace lldb_private::mcp { + +class ResourceProvider { +public: + ResourceProvider() = default; + virtual ~ResourceProvider() = default; + + virtual std::vector<protocol::Resource> GetResources() const = 0; + virtual llvm::Expected<protocol::ResourceResult> + ReadResource(llvm::StringRef uri) const = 0; +}; + +class DebuggerResourceProvider : public ResourceProvider { +public: + using ResourceProvider::ResourceProvider; + virtual ~DebuggerResourceProvider() = default; + + virtual std::vector<protocol::Resource> GetResources() const override; + virtual llvm::Expected<protocol::ResourceResult> + ReadResource(llvm::StringRef uri) const override; + +private: + static protocol::Resource GetDebuggerResource(Debugger &debugger); + static protocol::Resource GetTargetResource(size_t target_idx, + Target &target); + + static llvm::Expected<protocol::ResourceResult> + ReadDebuggerResource(llvm::StringRef uri, lldb::user_id_t debugger_id); + static llvm::Expected<protocol::ResourceResult> + ReadTargetResource(llvm::StringRef uri, lldb::user_id_t debugger_id, + size_t target_idx); +}; + +} // namespace lldb_private::mcp + +#endif diff --git a/lldb/source/Plugins/Protocol/MCP/Tool.cpp b/lldb/source/Plugins/Protocol/MCP/Tool.cpp index 5c4626cf66b32..eecd56141d2ed 100644 --- a/lldb/source/Plugins/Protocol/MCP/Tool.cpp +++ b/lldb/source/Plugins/Protocol/MCP/Tool.cpp @@ -65,7 +65,7 @@ CommandTool::Call(const protocol::ToolArguments &args) { return root.getError(); lldb::DebuggerSP debugger_sp = - Debugger::GetDebuggerAtIndex(arguments.debugger_id); + Debugger::FindDebuggerWithID(arguments.debugger_id); if (!debugger_sp) return createStringError( llvm::formatv("no debugger with id {0}", arguments.debugger_id)); @@ -101,50 +101,3 @@ std::optional<llvm::json::Value> CommandTool::GetSchema() const { {"required", std::move(required)}}; return schema; } - -llvm::Expected<protocol::TextResult> -DebuggerListTool::Call(const protocol::ToolArguments &args) { - if (!std::holds_alternative<std::monostate>(args)) - return createStringError("DebuggerListTool takes no arguments"); - - llvm::json::Path::Root root; - - // Return a nested Markdown list with debuggers and target. - // Example output: - // - // - debugger 0 - // - target 0 /path/to/foo - // - target 1 - // - debugger 1 - // - target 0 /path/to/bar - // - // FIXME: Use Structured Content when we adopt protocol version 2025-06-18. - std::string output; - llvm::raw_string_ostream os(output); - - const size_t num_debuggers = Debugger::GetNumDebuggers(); - for (size_t i = 0; i < num_debuggers; ++i) { - lldb::DebuggerSP debugger_sp = Debugger::GetDebuggerAtIndex(i); - if (!debugger_sp) - continue; - - os << "- debugger " << i << '\n'; - - TargetList &target_list = debugger_sp->GetTargetList(); - const size_t num_targets = target_list.GetNumTargets(); - for (size_t j = 0; j < num_targets; ++j) { - lldb::TargetSP target_sp = target_list.GetTargetAtIndex(j); - if (!target_sp) - continue; - os << " - target " << j; - if (target_sp == target_list.GetSelectedTarget()) - os << " (selected)"; - // Append the module path if we have one. - if (Module *exe_module = target_sp->GetExecutableModulePointer()) - os << " " << exe_module->GetFileSpec().GetPath(); - os << '\n'; - } - } - - return createTextResult(output); -} diff --git a/lldb/source/Plugins/Protocol/MCP/Tool.h b/lldb/source/Plugins/Protocol/MCP/Tool.h index 74ab04b472522..d0f639adad24e 100644 --- a/lldb/source/Plugins/Protocol/MCP/Tool.h +++ b/lldb/source/Plugins/Protocol/MCP/Tool.h @@ -48,15 +48,6 @@ class CommandTool : public mcp::Tool { virtual std::optional<llvm::json::Value> GetSchema() const override; }; -class DebuggerListTool : public mcp::Tool { -public: - using mcp::Tool::Tool; - ~DebuggerListTool() = default; - - virtual llvm::Expected<protocol::TextResult> - Call(const protocol::ToolArguments &args) override; -}; - } // namespace lldb_private::mcp #endif diff --git a/lldb/unittests/Protocol/ProtocolMCPServerTest.cpp b/lldb/unittests/Protocol/ProtocolMCPServerTest.cpp index 8e61379b5c731..51eb6275e811a 100644 --- a/lldb/unittests/Protocol/ProtocolMCPServerTest.cpp +++ b/lldb/unittests/Protocol/ProtocolMCPServerTest.cpp @@ -7,6 +7,7 @@ //===----------------------------------------------------------------------===// #include "Plugins/Platform/MacOSX/PlatformRemoteMacOSX.h" +#include "Plugins/Protocol/MCP/MCPError.h" #include "Plugins/Protocol/MCP/ProtocolServerMCP.h" #include "TestingSupport/Host/SocketTestUtilities.h" #include "TestingSupport/SubsystemRAII.h" @@ -28,6 +29,7 @@ class TestProtocolServerMCP : public lldb_private::mcp::ProtocolServerMCP { public: using ProtocolServerMCP::AddNotificationHandler; using ProtocolServerMCP::AddRequestHandler; + using ProtocolServerMCP::AddResourceProvider; using ProtocolServerMCP::AddTool; using ProtocolServerMCP::GetSocket; using ProtocolServerMCP::ProtocolServerMCP; @@ -61,6 +63,38 @@ class TestTool : public mcp::Tool { } }; +class TestResourceProvider : public mcp::ResourceProvider { + using mcp::ResourceProvider::ResourceProvider; + + virtual std::vector<Resource> GetResources() const override { + std::vector<Resource> resources; + + Resource resource; + resource.uri = "lldb://foo/bar"; + resource.name = "name"; + resource.description = "description"; + resource.mimeType = "application/json"; + + resources.push_back(resource); + return resources; + } + + virtual llvm::Expected<ResourceResult> + ReadResource(llvm::StringRef uri) const override { + if (uri != "lldb://foo/bar") + return llvm::make_error<mcp::UnsupportedURI>(uri.str()); + + ResourceContents contents; + contents.uri = "lldb://foo/bar"; + contents.mimeType = "application/json"; + contents.text = "foobar"; + + ResourceResult result; + result.contents.push_back(contents); + return result; + } +}; + /// Test tool that returns an error. class ErrorTool : public mcp::Tool { public: @@ -118,6 +152,7 @@ class ProtocolServerMCPTest : public ::testing::Test { connection.name = llvm::formatv("{0}:0", k_localhost).str(); m_server_up = std::make_unique<TestProtocolServerMCP>(); m_server_up->AddTool(std::make_unique<TestTool>("test", "test tool")); + m_server_up->AddResourceProvider(std::make_unique<TestResourceProvider>()); ASSERT_THAT_ERROR(m_server_up->Start(connection), llvm::Succeeded()); // Connect to the server over a TCP socket. @@ -148,7 +183,7 @@ TEST_F(ProtocolServerMCPTest, Intialization) { llvm::StringLiteral request = R"json({"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"lldb-unit","version":"0.1.0"}},"jsonrpc":"2.0","id":0})json"; llvm::StringLiteral response = - R"json({"jsonrpc":"2.0","id":0,"result":{"capabilities":{"tools":{"listChanged":true}},"protocolVersion":"2024-11-05","serverInfo":{"name":"lldb-mcp","version":"0.1.0"}}})json"; + R"json( {"id":0,"jsonrpc":"2.0","result":{"capabilities":{"resources":{"listChanged":false,"subscribe":false},"tools":{"listChanged":true}},"protocolVersion":"2024-11-05","serverInfo":{"name":"lldb-mcp","version":"0.1.0"}}})json"; ASSERT_THAT_ERROR(Write(request), llvm::Succeeded()); @@ -168,7 +203,7 @@ TEST_F(ProtocolServerMCPTest, ToolsList) { llvm::StringLiteral request = R"json({"method":"tools/list","params":{},"jsonrpc":"2.0","id":1})json"; llvm::StringLiteral response = - R"json( {"id":1,"jsonrpc":"2.0","result":{"tools":[{"description":"test tool","inputSchema":{"type":"object"},"name":"test"},{"description":"List debugger instances with their debugger_id.","inputSchema":{"type":"object"},"name":"lldb_debugger_list"},{"description":"Run an lldb command.","inputSchema":{"properties":{"arguments":{"type":"string"},"debugger_id":{"type":"number"}},"required":["debugger_id"],"type":"object"},"name":"lldb_command"}]}})json"; + R"json({"id":1,"jsonrpc":"2.0","result":{"tools":[{"description":"test tool","inputSchema":{"type":"object"},"name":"test"},{"description":"Run an lldb command.","inputSchema":{"properties":{"arguments":{"type":"string"},"debugger_id":{"type":"number"}},"required":["debugger_id"],"type":"object"},"name":"lldb_command"}]}})json"; ASSERT_THAT_ERROR(Write(request), llvm::Succeeded()); @@ -188,7 +223,7 @@ TEST_F(ProtocolServerMCPTest, ResourcesList) { llvm::StringLiteral request = R"json({"method":"resources/list","params":{},"jsonrpc":"2.0","id":2})json"; llvm::StringLiteral response = - R"json({"error":{"code":1,"message":"no handler for request: resources/list"},"id":2,"jsonrpc":"2.0"})json"; + R"json({"id":2,"jsonrpc":"2.0","result":{"resources":[{"description":"description","mimeType":"application/json","name":"name","uri":"lldb://foo/bar"}]}})json"; ASSERT_THAT_ERROR(Write(request), llvm::Succeeded()); @@ -230,7 +265,7 @@ TEST_F(ProtocolServerMCPTest, ToolsCallError) { llvm::StringLiteral request = R"json({"method":"tools/call","params":{"name":"error","arguments":{"arguments":"foo","debugger_id":0}},"jsonrpc":"2.0","id":11})json"; llvm::StringLiteral response = - R"json({"error":{"code":-1,"message":"error"},"id":11,"jsonrpc":"2.0"})json"; + R"json({"error":{"code":-32603,"message":"error"},"id":11,"jsonrpc":"2.0"})json"; ASSERT_THAT_ERROR(Write(request), llvm::Succeeded()); diff --git a/lldb/unittests/Protocol/ProtocolMCPTest.cpp b/lldb/unittests/Protocol/ProtocolMCPTest.cpp index 14cc240dd3628..ddc5a411a5c31 100644 --- a/lldb/unittests/Protocol/ProtocolMCPTest.cpp +++ b/lldb/unittests/Protocol/ProtocolMCPTest.cpp @@ -230,3 +230,101 @@ TEST(ProtocolMCPTest, ResponseWithError) { EXPECT_EQ(response.error->code, deserialized_response->error->code); EXPECT_EQ(response.error->message, deserialized_response->error->message); } + +TEST(ProtocolMCPTest, Resource) { + Resource resource; + resource.uri = "resource://example/test"; + resource.name = "Test Resource"; + resource.description = "A test resource for unit testing"; + resource.mimeType = "text/plain"; + + llvm::Expected<Resource> deserialized_resource = roundtripJSON(resource); + ASSERT_THAT_EXPECTED(deserialized_resource, llvm::Succeeded()); + + EXPECT_EQ(resource.uri, deserialized_resource->uri); + EXPECT_EQ(resource.name, deserialized_resource->name); + EXPECT_EQ(resource.description, deserialized_resource->description); + EXPECT_EQ(resource.mimeType, deserialized_resource->mimeType); +} + +TEST(ProtocolMCPTest, ResourceWithoutOptionals) { + Resource resource; + resource.uri = "resource://example/minimal"; + resource.name = "Minimal Resource"; + + llvm::Expected<Resource> deserialized_resource = roundtripJSON(resource); + ASSERT_THAT_EXPECTED(deserialized_resource, llvm::Succeeded()); + + EXPECT_EQ(resource.uri, deserialized_resource->uri); + EXPECT_EQ(resource.name, deserialized_resource->name); + EXPECT_FALSE(deserialized_resource->description.has_value()); + EXPECT_FALSE(deserialized_resource->mimeType.has_value()); +} + +TEST(ProtocolMCPTest, ResourceContents) { + ResourceContents contents; + contents.uri = "resource://example/content"; + contents.text = "This is the content of the resource"; + contents.mimeType = "text/plain"; + + llvm::Expected<ResourceContents> deserialized_contents = + roundtripJSON(contents); + ASSERT_THAT_EXPECTED(deserialized_contents, llvm::Succeeded()); + + EXPECT_EQ(contents.uri, deserialized_contents->uri); + EXPECT_EQ(contents.text, deserialized_contents->text); + EXPECT_EQ(contents.mimeType, deserialized_contents->mimeType); +} + +TEST(ProtocolMCPTest, ResourceContentsWithoutMimeType) { + ResourceContents contents; + contents.uri = "resource://example/content-no-mime"; + contents.text = "Content without mime type specified"; + + llvm::Expected<ResourceContents> deserialized_contents = + roundtripJSON(contents); + ASSERT_THAT_EXPECTED(deserialized_contents, llvm::Succeeded()); + + EXPECT_EQ(contents.uri, deserialized_contents->uri); + EXPECT_EQ(contents.text, deserialized_contents->text); + EXPECT_FALSE(deserialized_contents->mimeType.has_value()); +} + +TEST(ProtocolMCPTest, ResourceResult) { + ResourceContents contents1; + contents1.uri = "resource://example/content1"; + contents1.text = "First resource content"; + contents1.mimeType = "text/plain"; + + ResourceContents contents2; + contents2.uri = "resource://example/content2"; + contents2.text = "Second resource content"; + contents2.mimeType = "application/json"; + + ResourceResult result; + result.contents = {contents1, contents2}; + + llvm::Expected<ResourceResult> deserialized_result = roundtripJSON(result); + ASSERT_THAT_EXPECTED(deserialized_result, llvm::Succeeded()); + + ASSERT_EQ(result.contents.size(), deserialized_result->contents.size()); + + EXPECT_EQ(result.contents[0].uri, deserialized_result->contents[0].uri); + EXPECT_EQ(result.contents[0].text, deserialized_result->contents[0].text); + EXPECT_EQ(result.contents[0].mimeType, + deserialized_result->contents[0].mimeType); + + EXPECT_EQ(result.contents[1].uri, deserialized_result->contents[1].uri); + EXPECT_EQ(result.contents[1].text, deserialized_result->contents[1].text); + EXPECT_EQ(result.contents[1].mimeType, + deserialized_result->contents[1].mimeType); +} + +TEST(ProtocolMCPTest, ResourceResultEmpty) { + ResourceResult result; + + llvm::Expected<ResourceResult> deserialized_result = roundtripJSON(result); + ASSERT_THAT_EXPECTED(deserialized_result, llvm::Succeeded()); + + EXPECT_TRUE(deserialized_result->contents.empty()); +} _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits