Skip to content

[lldb] Expose debuggers and target as resources through MCP #148075

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 11, 2025

Conversation

JDevlieghere
Copy link
Member

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}"
}

@JDevlieghere JDevlieghere requested a review from ashgti July 10, 2025 23:01
@llvmbot llvmbot added the lldb label Jul 10, 2025
@llvmbot
Copy link
Member

llvmbot commented Jul 10, 2025

@llvm/pr-subscribers-lldb

Author: Jonas Devlieghere (JDevlieghere)

Changes

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/&lt;debugger id&gt;

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/&lt;debugger id&gt;/target/&lt;target id&gt;

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}"
}

Patch is 33.55 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/148075.diff

13 Files Affected:

  • (modified) lldb/source/Plugins/Protocol/MCP/CMakeLists.txt (+1)
  • (modified) lldb/source/Plugins/Protocol/MCP/MCPError.cpp (+11)
  • (modified) lldb/source/Plugins/Protocol/MCP/MCPError.h (+14)
  • (modified) lldb/source/Plugins/Protocol/MCP/Protocol.cpp (+53-1)
  • (modified) lldb/source/Plugins/Protocol/MCP/Protocol.h (+59-1)
  • (modified) lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.cpp (+80-2)
  • (modified) lldb/source/Plugins/Protocol/MCP/ProtocolServerMCP.h (+10)
  • (added) lldb/source/Plugins/Protocol/MCP/Resource.cpp (+166)
  • (added) lldb/source/Plugins/Protocol/MCP/Resource.h (+51)
  • (modified) lldb/source/Plugins/Protocol/MCP/Tool.cpp (-47)
  • (modified) lldb/source/Plugins/Protocol/MCP/Tool.h (-9)
  • (modified) lldb/unittests/Protocol/ProtocolMCPServerTest.cpp (+38-3)
  • (modified) lldb/unittests/Protocol/ProtocolMCPTest.cpp (+103)
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..05a047ec881a9 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 {
@@ -30,4 +31,17 @@ class MCPError : public llvm::ErrorInfo<MCPError> {
   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..6099d429672bb 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()); }
@@ -244,6 +253,7 @@ ProtocolServerMCP::HandleData(llvm::StringRef data) {
 protocol::Capabilities ProtocolServerMCP::GetCapabilities() {
   protocol::Capabilities capabilities;
   capabilities.tools.listChanged = true;
+  capabilities.resources.listChanged = false;
   return capabilities;
 }
 
@@ -255,6 +265,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 +346,62 @@ 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(), 1);
+}
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..8cb4e3916b1b1
--- /dev/null
+++ b/lldb/source/Plugins/Protocol/MCP/Resource.cpp
@@ -0,0 +1,166 @@
+// 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"
+
+using namespace lldb_private::mcp;
+
+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(lldb::user_id_t debugger_id) {
+  protocol::Resource resource;
+  resource.uri = llvm::formatv("lldb://debugger/{0}", debugger_id);
+  resource.name = llvm::formatv("debugger {0}", debugger_id);
+  resource.description =
+      llvm::formatv("Information about debugger instance {0}", debugger_id);
+  resource.mimeType = "application/json";
+  return resource;
+}
+
+protocol::Resource
+DebuggerResourceProvider::GetTargetResource(lldb::user_id_t debugger_id,
+                                            lldb::user_id_t target_id) {
+  protocol::Resource resource;
+  resource.uri =
+      llvm::formatv("lldb://debugger/{0}/target/{1}", debugger_id, target_id);
+  resource.name = llvm::formatv("target {0}", target_id);
+  resource.description =
+      llvm::formatv("Information about target {0} in debugger instance {1}",
+                    target_id, debugger_id);
+  resource.mimeType = "application/json";
+  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(i));
+
+    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(i, j));
+    }
+  }
+
+  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);
+
+  lldb::user_id_t debugger_id;
+  if (components[1].getAsInteger(0, debugger_id))
+    return createStringError("invalid debugger id '{0}': {1}", components[1],
+                             path);
+
+  if (components.size() > 3) {
+    if (components[2] != "target")
+      return createUnsupportedURIError(uri);
+
+    lldb::user_id_t target_id;
+    if (components[3].getAsInteger(0, target_id))
+      return createStringError("invalid target id '{0}': {1}", components[3],
+                               path);
+
+    return ReadTargetResource(uri, debugger_id, target_id);
+  }
+
+  return ReadDebuggerResource(uri, debugger_id);
+}
+
+llvm::Expected<protocol::ResourceResult>
+DebuggerResourceProvider::ReadDebuggerResource(llvm::StringRef uri,
+                                               lldb::user_id_t debugger_id) {
+  lldb::DebuggerSP debugger_sp = Debugger::GetDebuggerAtIndex(debugger_id);
+  if (!debugger_sp)
+    return createStringError("invalid debugger id: {0}", debugger_id);
+
+  TargetList &target_list = debugger_sp->GetTargetList();
+  const size_t num_targets = target_list.GetNumTargets();
+
+  llvm::json::Value value = llvm::json::Object{{"debugger_id", debugger_id},
+                                               {"num_targets", num_targets}};
+
+  std::string json = llvm::formatv("{0}", value);
+
+  protocol::ResourceContents contents;
+  contents.uri = uri;
+  contents.mimeType = "application/json";
+  contents.text = json;
+
+  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,
+                                             lldb::user_id_t target_id) {
+
+  lldb::DebuggerSP debugger_sp = Debugger::GetDebuggerAtIndex(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_id);
+  if (!target_sp)
+    return createStringError("invalid target id: {0}", target_id);
+
+  llvm::json::Object object{
+      {"debugger_id", debugger_id},
+      {"target_id", target_id},
+      {"arch", target_sp->GetArchitecture().GetTriple().str()}};
+
+  if (Module *exe_module = target_sp->GetExecutableModulePointer())
+    object.insert({"path", exe_module->GetFileSpec().GetPath()});
+
+  llvm::json::Value value = std::move(object);
+  std::string json = llvm::formatv("{0}", value);
+
+  protocol::ResourceContents contents;
+  contents.uri = uri;
+  contents.mimeType = "application/json";
+  contents.text = json;
+
+  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...
[truncated]

@@ -244,6 +253,7 @@ ProtocolServerMCP::HandleData(llvm::StringRef data) {
protocol::Capabilities ProtocolServerMCP::GetCapabilities() {
protocol::Capabilities capabilities;
capabilities.tools.listChanged = true;
capabilities.resources.listChanged = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we support sending notifications when a debugger/target are added/removed? Otherwise the client could get out of sync with the state of lldb.

Maybe as a FIXME/TODO if we can't easily do that now.

@@ -30,4 +31,17 @@ class MCPError : public llvm::ErrorInfo<MCPError> {
int64_t m_error_code;
};

class UnsupportedURI : public llvm::ErrorInfo<UnsupportedURI> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the MCP spec, there are 2 error codes for resources:

  • Resource not found: -32002
  • Internal errors: -32603

See https://modelcontextprotocol.io/specification/2025-06-18/server/resources#error-handling

Should we use those here?

Comment on lines +115 to +118
std::optional<std::string> description;

/// The MIME type of this resource, if known.
std::optional<std::string> mimeType;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been using std::optional less in lldb-dap's protocols because here isn't much of a meaningful difference between an empty string and std::nullopt. Not a required change, but I find it makes it easier to think about since I don't have to double check if field != std::nullopt && !field->empty(), I can just do !field.empty().

When encoding toJSON we can check if the string is empty and not include it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me do this as a followup for all of MCP 👍

Comment on lines +59 to +108
TargetList &target_list = debugger_sp->GetTargetList();
const size_t num_targets = target_list.GetNumTargets();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its possible to add/remove targets from the debugger. Is there a more stable way to identify them than their position in the target list?

If you remove a target, say target 2 of 3, does the list compact?

Just wondering what happens if this list is out of sync with the client.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe we have anything more stable. My hope is to rely on the listChanged notification in the future, so the client knows the resource went away.


llvm::Expected<protocol::ResourceResult>
DebuggerResourceProvider::ReadResource(llvm::StringRef uri) const {
auto [protocol, path] = uri.split("://");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I almost wonder if we could use

static std::optional<URI> Parse(llvm::StringRef uri);

but I don't think we're planning on setting the host at the moment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it treats the first component as the hostname, which is why I didn't use it. Not sure if there's a good way to distinguish the two.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add the target to the input scheme?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't because, unlike for the debugger, you can do target select.

Comment on lines 97 to 98
lldb::user_id_t target_id;
if (components[3].getAsInteger(0, target_id))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would be helpful to have support for lldb://debugger/0/target/selected as a shorthand for the selected target.

Or in theory lldb://debugger/0/target could return info for the selected target.

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}"
}
```
Copy link
Contributor

@ashgti ashgti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@JDevlieghere JDevlieghere merged commit 3c4c2fa into llvm:main Jul 11, 2025
9 checks passed
@JDevlieghere JDevlieghere deleted the mcp-resources branch July 11, 2025 22:49
JDevlieghere added a commit to swiftlang/llvm-project that referenced this pull request Jul 31, 2025
)

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}"
}
```

(cherry picked from commit 3c4c2fa)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants