feat: Add initial MCP commands to query, open select and read data

This commit is contained in:
WerWolv
2025-12-17 16:03:57 +01:00
parent 2047a41498
commit d775b80a44
9 changed files with 265 additions and 2 deletions

View File

@@ -6,6 +6,40 @@
namespace hex::mcp {
struct TextContent {
std::string text;
operator nlohmann::json() const {
nlohmann::json result;
result["content"] = nlohmann::json::array({
nlohmann::json::object({
{ "type", "text" },
{ "text", text }
})
});
return result;
}
};
struct StructuredContent {
std::string text;
nlohmann::json data;
operator nlohmann::json() const {
nlohmann::json result;
result["content"] = nlohmann::json::array({
nlohmann::json::object({
{ "type", "text" },
{ "text", text }
})
});
result["structuredContent"] = data;
return result;
}
};
class Server {
public:
constexpr static auto McpInternalPort = 19743;

View File

@@ -56,7 +56,7 @@ namespace hex::mcp {
if (!m_id.has_value())
return std::nullopt;
return createResponseMessage(result);
return createResponseMessage(result.is_null() ? nlohmann::json::object() : result);
}
std::optional<nlohmann::json> handleBatchedMessages(const nlohmann::json &request, auto callback) {
@@ -154,7 +154,9 @@ namespace hex::mcp {
if (auto primitiveIt = m_primitives.find(primitive); primitiveIt != m_primitives.end()) {
auto name = params.value("name", "");
if (auto functionIt = primitiveIt->second.find(name); functionIt != primitiveIt->second.end()) {
return functionIt->second.function(params.value("arguments", nlohmann::json::object()));
auto result = functionIt->second.function(params.value("arguments", nlohmann::json::object()));
return result.is_null() ? nlohmann::json::object() : result;
}
}
}

View File

@@ -18,6 +18,7 @@ add_imhex_plugin(
source/content/command_palette_commands.cpp
source/content/command_line_interface.cpp
source/content/communication_interface.cpp
source/content/mcp_tools.cpp
source/content/data_inspector.cpp
source/content/differing_byte_searcher.cpp
source/content/pl_builtin_functions.cpp

View File

@@ -0,0 +1,50 @@
{
"name": "list_open_data_sources",
"title": "List Open Data Sources",
"description": "Lists all currently open data sources with their name and a handle that can be used to reference them in other tools.",
"inputSchema": {
"type": "object",
"properties": {}
},
"outputSchema": {
"type": "object",
"properties": {
"data_sources": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the data source"
},
"type": {
"type": "string",
"description": "Internal type of the data source"
},
"size": {
"type": "number",
"description": "Size of the data source in bytes"
},
"is_writable": {
"type": "boolean",
"description": "Whether the data source is writable"
},
"handle": {
"type": "number",
"description": "Handle of the data source to reference it in other tools"
}
},
"required": [
"name",
"type",
"size",
"is_writable",
"handle"
]
}
}
},
"required": ["data_sources"]
}
}

View File

@@ -0,0 +1,19 @@
{
"name": "open_file",
"title": "Open File",
"description": "Opens a file from the filesystem, given the file path in ImHex. This is the first step that always needs to be done first before any of the other tools can be used. A file stays open until it's closed by the user.",
"inputSchema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path of the file to open"
}
},
"required": ["file_path"]
},
"outputSchema": {
"type": "object",
"properties": {}
}
}

View File

@@ -0,0 +1,37 @@
{
"name": "read_data",
"title": "Read Binary Data",
"description": "Reads data from the currently selected data source at the specified address and length. The data is returned as a base64-encoded string. The maximum size that can be read at once is 16MiB, if more data is requested, the call will only return the first 16MiB.",
"inputSchema": {
"type": "object",
"properties": {
"address": {
"type": "number",
"description": "Address to read from in the selected data source"
},
"size": {
"type": "number",
"description": "Number of bytes to read from the selected data source"
}
},
"required": ["address", "size"]
},
"outputSchema": {
"type": "object",
"properties": {
"handle": {
"type": "number",
"description": "Handle of the data source the data was read from"
},
"data": {
"type": "string",
"description": "Base64-encoded string of the read data"
},
"data_size": {
"type": "number",
"description": "Number of bytes that were actually read"
}
},
"required": ["handle", "data"]
}
}

View File

@@ -0,0 +1,25 @@
{
"name": "select_data_source",
"title": "Select a Data Source given its handle",
"description": "Selects one of the currently open data sources by its handle so that it can be used in subsequent operations. When running other tools that operate on a data source, this selected data source will be used. The returned handle is the one that was selected. If it failed to select the data source, the old handle is returned. That usually means that that handle doesn't exist (because it hasn't been opened yet or because it was closed already)",
"inputSchema": {
"type": "object",
"properties": {
"handle": {
"type": "number",
"description": "Handle of the data source to select"
}
},
"required": ["handle"]
},
"outputSchema": {
"type": "object",
"properties": {
"selected_handle": {
"type": "number",
"description": "Handle of the selected data source"
}
},
"required": ["selected_handle"]
}
}

View File

@@ -0,0 +1,93 @@
#include <content/providers/file_provider.hpp>
#include <hex/api/content_registry/communication_interface.hpp>
#include <hex/api/imhex_api/provider.hpp>
#include <hex/helpers/crypto.hpp>
#include <romfs/romfs.hpp>
#include <wolv/literals.hpp>
namespace hex::plugin::builtin {
using namespace wolv::literals;
void registerMCPTools() {
ContentRegistry::MCP::registerTool(romfs::get("mcp/tools/open_file.json").string(), [](const nlohmann::json &data) -> nlohmann::json {
auto filePath = data.at("file_path").get<std::string>();
auto provider = ImHexApi::Provider::createProvider("hex.builtin.provider.file", true);
if (auto *fileProvider = dynamic_cast<FileProvider*>(provider.get()); fileProvider != nullptr) {
fileProvider->setPath(filePath);
ImHexApi::Provider::openProvider(provider);
}
return mcp::TextContent {
.text = "File opened"
};
});
ContentRegistry::MCP::registerTool(romfs::get("mcp/tools/list_open_data_sources.json").string(), [](const nlohmann::json &) -> nlohmann::json {
const auto &providers = ImHexApi::Provider::getProviders();
nlohmann::json array = nlohmann::json::array();
for (const auto &provider : providers) {
nlohmann::json providerInfo;
providerInfo["name"] = provider->getName();
providerInfo["type"] = provider->getTypeName().get();
providerInfo["size"] = provider->getSize();
providerInfo["is_writable"] = provider->isWritable();
providerInfo["handle"] = provider->getID();
array.push_back(providerInfo);
}
nlohmann::json result;
result["data_sources"] = array;
return mcp::StructuredContent {
.text = result.dump(),
.data = result
};
});
ContentRegistry::MCP::registerTool(romfs::get("mcp/tools/select_data_source.json").string(), [](const nlohmann::json &data) -> nlohmann::json {
const auto &providers = ImHexApi::Provider::getProviders();
auto handle = data.at("handle").get<u64>();
for (size_t i = 0; i < providers.size(); i++) {
if (providers[i]->getID() == handle) {
ImHexApi::Provider::setCurrentProvider(static_cast<i64>(i));
break;
}
}
nlohmann::json result = { "selected_handle", ImHexApi::Provider::get()->getID() };
return mcp::StructuredContent {
.text = result.dump(),
.data = result
};
});
ContentRegistry::MCP::registerTool(romfs::get("mcp/tools/read_data.json").string(), [](const nlohmann::json &data) -> nlohmann::json {
auto address = data.at("address").get<u64>();
auto size = data.at("size").get<u64>();
size = std::min<size_t>(size, 16_MiB);
auto provider = ImHexApi::Provider::get();
std::vector<u8> buffer(std::min(size, provider->getActualSize() - address));
provider->read(address, buffer.data(), buffer.size());
auto base64 = crypt::encode64(buffer);
nlohmann::json result = {
"handle", provider->getID(),
"data", std::string(base64.begin(), base64.end()),
"data_size", buffer.size()
};
return mcp::StructuredContent {
.text = result.dump(),
.data = result
};
});
}
}

View File

@@ -40,6 +40,7 @@ namespace hex::plugin::builtin {
void registerThemes();
void registerBackgroundServices();
void registerNetworkEndpoints();
void registerMCPTools();
void registerFileHandlers();
void registerProjectHandlers();
void registerAchievements();
@@ -140,6 +141,7 @@ IMHEX_PLUGIN_SETUP_BUILTIN("Built-in", "WerWolv", "Default ImHex functionality")
registerStyleHandlers();
registerBackgroundServices();
registerNetworkEndpoints();
registerMCPTools();
registerFileHandlers();
registerProjectHandlers();
registerCommandForwarders();