diff --git a/lib/libimhex/include/hex/mcp/server.hpp b/lib/libimhex/include/hex/mcp/server.hpp index 2b62ee82c..959a5d931 100644 --- a/lib/libimhex/include/hex/mcp/server.hpp +++ b/lib/libimhex/include/hex/mcp/server.hpp @@ -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; diff --git a/lib/libimhex/source/mcp/server.cpp b/lib/libimhex/source/mcp/server.cpp index 86aaa2088..bdae493e7 100644 --- a/lib/libimhex/source/mcp/server.cpp +++ b/lib/libimhex/source/mcp/server.cpp @@ -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 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; } } } diff --git a/plugins/builtin/CMakeLists.txt b/plugins/builtin/CMakeLists.txt index 12ceb3fb6..92088a29e 100644 --- a/plugins/builtin/CMakeLists.txt +++ b/plugins/builtin/CMakeLists.txt @@ -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 diff --git a/plugins/builtin/romfs/mcp/tools/list_open_data_sources.json b/plugins/builtin/romfs/mcp/tools/list_open_data_sources.json new file mode 100644 index 000000000..e51871438 --- /dev/null +++ b/plugins/builtin/romfs/mcp/tools/list_open_data_sources.json @@ -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"] + } +} \ No newline at end of file diff --git a/plugins/builtin/romfs/mcp/tools/open_file.json b/plugins/builtin/romfs/mcp/tools/open_file.json new file mode 100644 index 000000000..b12c053e2 --- /dev/null +++ b/plugins/builtin/romfs/mcp/tools/open_file.json @@ -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": {} + } +} \ No newline at end of file diff --git a/plugins/builtin/romfs/mcp/tools/read_data.json b/plugins/builtin/romfs/mcp/tools/read_data.json new file mode 100644 index 000000000..2c58f1efc --- /dev/null +++ b/plugins/builtin/romfs/mcp/tools/read_data.json @@ -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"] + } +} \ No newline at end of file diff --git a/plugins/builtin/romfs/mcp/tools/select_data_source.json b/plugins/builtin/romfs/mcp/tools/select_data_source.json new file mode 100644 index 000000000..b193a9065 --- /dev/null +++ b/plugins/builtin/romfs/mcp/tools/select_data_source.json @@ -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"] + } +} \ No newline at end of file diff --git a/plugins/builtin/source/content/mcp_tools.cpp b/plugins/builtin/source/content/mcp_tools.cpp new file mode 100644 index 000000000..f9c220a79 --- /dev/null +++ b/plugins/builtin/source/content/mcp_tools.cpp @@ -0,0 +1,93 @@ +#include +#include +#include +#include +#include +#include + +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(); + + auto provider = ImHexApi::Provider::createProvider("hex.builtin.provider.file", true); + if (auto *fileProvider = dynamic_cast(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(); + + for (size_t i = 0; i < providers.size(); i++) { + if (providers[i]->getID() == handle) { + ImHexApi::Provider::setCurrentProvider(static_cast(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(); + auto size = data.at("size").get(); + + size = std::min(size, 16_MiB); + + auto provider = ImHexApi::Provider::get(); + std::vector 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 + }; + }); + } + +} diff --git a/plugins/builtin/source/plugin_builtin.cpp b/plugins/builtin/source/plugin_builtin.cpp index 99e3a245e..60bd04320 100644 --- a/plugins/builtin/source/plugin_builtin.cpp +++ b/plugins/builtin/source/plugin_builtin.cpp @@ -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();