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, 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, 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 namespace lldb_private::mcp { @@ -16,7 +17,7 @@ class MCPError : public llvm::ErrorInfo { 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 { 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 { +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 description; + + /// The MIME type of this resource, if known. + std::optional 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 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 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("lldb_command", "Run an lldb command.")); - AddTool(std::make_unique( - "lldb_debugger_list", "List debugger instances with their debugger_id.")); + + AddResourceProvider(std::make_unique()); } ProtocolServerMCP::~ProtocolServerMCP() { llvm::consumeError(Stop()); } @@ -75,7 +84,7 @@ ProtocolServerMCP::Handle(protocol::Request request) { } return make_error( - 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) { m_tools[tool->GetName()] = std::move(tool); } +void ProtocolServerMCP::AddResourceProvider( + std::unique_ptr resource_provider) { + std::lock_guard 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 guard(m_server_mutex); @@ -327,3 +348,63 @@ ProtocolServerMCP::ToolsCallHandler(const protocol::Request &request) { return response; } + +llvm::Expected +ProtocolServerMCP::ResourcesListHandler(const protocol::Request &request) { + protocol::Response response; + + llvm::json::Array resources; + + std::lock_guard guard(m_server_mutex); + for (std::unique_ptr &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 +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 guard(m_server_mutex); + for (std::unique_ptr &resource_provider_up : + m_resource_providers) { + llvm::Expected result = + resource_provider_up->ReadResource(uri_str); + if (result.errorIsA()) { + llvm::consumeError(result.takeError()); + continue; + } + if (!result) + return result.takeError(); + + protocol::Response response; + response.result.emplace(std::move(*result)); + return response; + } + + return make_error( + 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 AddTool(std::unique_ptr tool); + void AddResourceProvider(std::unique_ptr 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 InitializeHandler(const protocol::Request &); + llvm::Expected ToolsListHandler(const protocol::Request &); llvm::Expected ToolsCallHandler(const protocol::Request &); + llvm::Expected + ResourcesListHandler(const protocol::Request &); + llvm::Expected + 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> m_tools; + std::vector> m_resource_providers; llvm::StringMap m_request_handlers; llvm::StringMap 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 +static llvm::Error createStringError(const char *format, Args &&...args) { + return llvm::createStringError( + llvm::formatv(format, std::forward(args)...).str()); +} + +static llvm::Error createUnsupportedURIError(llvm::StringRef uri) { + return llvm::make_error(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 DebuggerResourceProvider::GetResources() const { + std::vector 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 +DebuggerResourceProvider::ReadResource(llvm::StringRef uri) const { + + auto [protocol, path] = uri.split("://"); + + if (protocol != "lldb") + return createUnsupportedURIError(uri); + + llvm::SmallVector 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 +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 +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 + +namespace lldb_private::mcp { + +class ResourceProvider { +public: + ResourceProvider() = default; + virtual ~ResourceProvider() = default; + + virtual std::vector GetResources() const = 0; + virtual llvm::Expected + ReadResource(llvm::StringRef uri) const = 0; +}; + +class DebuggerResourceProvider : public ResourceProvider { +public: + using ResourceProvider::ResourceProvider; + virtual ~DebuggerResourceProvider() = default; + + virtual std::vector GetResources() const override; + virtual llvm::Expected + 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 + ReadDebuggerResource(llvm::StringRef uri, lldb::user_id_t debugger_id); + static llvm::Expected + 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 CommandTool::GetSchema() const { {"required", std::move(required)}}; return schema; } - -llvm::Expected -DebuggerListTool::Call(const protocol::ToolArguments &args) { - if (!std::holds_alternative(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 GetSchema() const override; }; -class DebuggerListTool : public mcp::Tool { -public: - using mcp::Tool::Tool; - ~DebuggerListTool() = default; - - virtual llvm::Expected - 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 GetResources() const override { + std::vector 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 + ReadResource(llvm::StringRef uri) const override { + if (uri != "lldb://foo/bar") + return llvm::make_error(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(); m_server_up->AddTool(std::make_unique("test", "test tool")); + m_server_up->AddResourceProvider(std::make_unique()); 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 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 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 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 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 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 deserialized_result = roundtripJSON(result); + ASSERT_THAT_EXPECTED(deserialized_result, llvm::Succeeded()); + + EXPECT_TRUE(deserialized_result->contents.empty()); +}