feat: Add initial MCP Server support

This commit is contained in:
WerWolv 2025-12-16 20:25:46 +01:00
parent 932c281223
commit e696d384c2
15 changed files with 421 additions and 3 deletions

@ -1 +1 @@
Subproject commit c9108712ba16a024dc7aee75f3fc9882629b2703 Subproject commit b0c9416568475a784838e3af8b0021a542e47cd7

View File

@ -57,6 +57,9 @@ set(LIBIMHEX_SOURCES
source/ui/toast.cpp source/ui/toast.cpp
source/ui/banner.cpp source/ui/banner.cpp
source/mcp/client.cpp
source/mcp/server.cpp
source/subcommands/subcommands.cpp source/subcommands/subcommands.cpp
) )

View File

@ -7,6 +7,8 @@
#include <map> #include <map>
#include <string> #include <string>
#include <hex/mcp/server.hpp>
EXPORT_MODULE namespace hex { EXPORT_MODULE namespace hex {
/* Network Communication Interface Registry. Allows adding new communication interface endpoints */ /* Network Communication Interface Registry. Allows adding new communication interface endpoints */
@ -22,4 +24,19 @@ EXPORT_MODULE namespace hex {
} }
namespace ContentRegistry::MCP {
namespace impl {
mcp::Server& getMcpServerInstance();
void setEnabled(bool enabled);
}
bool isEnabled();
bool isConnected();
void registerTool(std::string_view capabilities, std::function<nlohmann::json(const nlohmann::json &params)> function);
}
} }

View File

@ -0,0 +1,15 @@
#pragma once
#include <iostream>
namespace hex::mcp {
class Client {
public:
Client() = default;
~Client() = default;
int run(std::istream &input, std::ostream &output);
};
}

View File

@ -0,0 +1,38 @@
#pragma once
#include <functional>
#include <nlohmann/json.hpp>
#include <wolv/net/socket_server.hpp>
namespace hex::mcp {
class Server {
public:
constexpr static auto McpInternalPort = 19743;
Server();
~Server();
void listen();
void shutdown();
void disconnect();
bool isConnected();
void addPrimitive(std::string type, std::string_view capabilities, std::function<nlohmann::json(const nlohmann::json &params)> function);
private:
nlohmann::json handleInitialize();
void handleNotifications(const std::string &method, const nlohmann::json &params);
struct Primitive {
nlohmann::json capabilities;
std::function<nlohmann::json(const nlohmann::json &params)> function;
};
std::map<std::string, std::map<std::string, Primitive>> m_primitives;
wolv::net::SocketServer m_server;
bool m_connected = false;
};
}

View File

@ -1413,6 +1413,40 @@ namespace hex {
} }
namespace ContentRegistry::MCP {
namespace impl {
mcp::Server& getMcpServerInstance() {
static AutoReset<std::unique_ptr<mcp::Server>> server;
if (*server == nullptr)
server = std::make_unique<mcp::Server>();
return **server;
}
static bool s_mcpEnabled = false;
void setEnabled(bool enabled) {
s_mcpEnabled = enabled;
}
}
bool isEnabled() {
return impl::s_mcpEnabled;
}
bool isConnected() {
return impl::getMcpServerInstance().isConnected();
}
void registerTool(std::string_view capabilities, std::function<nlohmann::json(const nlohmann::json &params)> function) {
impl::getMcpServerInstance().addPrimitive("tools", capabilities, function);
}
}
namespace ContentRegistry::Experiments { namespace ContentRegistry::Experiments {
namespace impl { namespace impl {

View File

@ -0,0 +1,38 @@
#include <hex/mcp/client.hpp>
#include <hex/mcp/server.hpp>
#include <hex.hpp>
#include <string>
#include <cstdlib>
#include <fmt/format.h>
#include <hex/helpers/logger.hpp>
#include <nlohmann/json.hpp>
#include <wolv/net/socket_client.hpp>
namespace hex::mcp {
int Client::run(std::istream &input, std::ostream &output) {
wolv::net::SocketClient client(wolv::net::SocketClient::Type::TCP, true);
client.connect("127.0.0.1", Server::McpInternalPort);
if (!client.isConnected()) {
log::resumeLogging();
log::error("Cannot connect to ImHex. Do you have an instance running and is the MCP server enabled?");
return EXIT_FAILURE;
}
while (true) {
std::string request;
std::getline(input, request);
client.writeString(request);
auto response = client.readString();
if (!response.empty() && response.front() != 0x00)
output << response << std::endl;
}
return EXIT_SUCCESS;
}
}

View File

@ -0,0 +1,226 @@
#include <hex/mcp/server.hpp>
#include <string>
#include <fmt/format.h>
#include <hex/helpers/logger.hpp>
#include <nlohmann/json.hpp>
#include <wolv/net/socket_client.hpp>
#include <hex/api/imhex_api/system.hpp>
namespace hex::mcp {
class JsonRpc {
public:
explicit JsonRpc(const std::string &request) : m_request(request) { }
struct MethodNotFoundException : std::exception {};
struct InvalidParametersException : std::exception {};
std::optional<std::string> execute(auto callback) {
try {
auto requestJson = nlohmann::json::parse(m_request);
if (requestJson.is_array()) {
return handleBatchedMessages(requestJson, callback).transform([](const auto &response) { return response.dump(); });
} else {
return handleMessage(requestJson, callback).transform([](const auto &response) { return response.dump(); });
}
} catch (const MethodNotFoundException &) {
return createErrorMessage(ErrorCode::MethodNotFound, "Method not found").dump();
} catch (const InvalidParametersException &) {
return createErrorMessage(ErrorCode::InvalidParams, "Invalid params").dump();
} catch (const nlohmann::json::parse_error &) {
return createErrorMessage(ErrorCode::ParseError, "Parse error").dump();
} catch (const std::exception &e) {
return createErrorMessage(ErrorCode::InternalError, e.what()).dump();
}
}
private:
std::optional<nlohmann::json> handleMessage(const nlohmann::json &request, auto callback) {
// Validate JSON-RPC request
if (!request.contains("jsonrpc") || request["jsonrpc"] != "2.0" ||
!request.contains("method") || !request["method"].is_string()) {
m_id = request.contains("id") ? std::optional(request["id"].get<int>()) : std::nullopt;
return createErrorMessage(ErrorCode::InvalidRequest, "Invalid Request").dump();
}
m_id = request.contains("id") ? std::optional(request["id"].get<int>()) : std::nullopt;
// Execute the method
auto result = callback(request["method"].get<std::string>(), request.value("params", nlohmann::json::object()));
if (!m_id.has_value())
return std::nullopt;
return createResponseMessage(result);
}
std::optional<nlohmann::json> handleBatchedMessages(const nlohmann::json &request, auto callback) {
if (!request.is_array()) {
return createErrorMessage(ErrorCode::InvalidRequest, "Invalid Request").dump();
}
nlohmann::json responses = nlohmann::json::array();
for (const auto &message : request) {
auto response = handleMessage(message, callback);
if (response.has_value())
responses.push_back(*response);
}
if (responses.empty())
return std::nullopt;
return responses.dump();
}
enum class ErrorCode {
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
};
nlohmann::json createDefaultMessage() {
nlohmann::json message;
message["jsonrpc"] = "2.0";
if (m_id.has_value())
message["id"] = m_id.value();
else
message["id"] = nullptr;
return message;
}
nlohmann::json createErrorMessage(ErrorCode code, const std::string &message) {
auto json = createDefaultMessage();
json["error"] = {
{ "code", int(code) },
{ "message", message }
};
return json;
}
nlohmann::json createResponseMessage(const nlohmann::json &result) {
auto json = createDefaultMessage();
json["result"] = result;
return json;
}
private:
std::string m_request;
std::optional<int> m_id;
};
Server::Server() : m_server(McpInternalPort, 1024, 1, true) {
}
Server::~Server() {
this->shutdown();
}
void Server::listen() {
m_server.accept([this](auto, const std::vector<u8> &data) -> std::vector<u8> {
std::string request(data.begin(), data.end());
log::debug("MCP ----> {}", request);
JsonRpc rpc(request);
auto response = rpc.execute([this](const std::string &method, const nlohmann::json &params) -> nlohmann::json {
if (method == "initialize") {
return handleInitialize();
} else if (method.starts_with("notifications/")) {
handleNotifications(method.substr(14), params);
return {};
} else if (method.ends_with("/list")) {
auto primitiveName = method.substr(0, method.size() - 5);
if (m_primitives.contains(primitiveName)) {
nlohmann::json capabilitiesList = nlohmann::json::array();
for (const auto &[name, primitive] : m_primitives[primitiveName]) {
capabilitiesList.push_back(primitive.capabilities);
}
nlohmann::json result;
result[primitiveName] = capabilitiesList;
return result;
}
} else if (method.ends_with("/call")) {
auto primitive = method.substr(0, method.size() - 5);
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()));
}
}
}
throw JsonRpc::MethodNotFoundException();
});
log::debug("MCP <---- {}", response.value_or("<Nothing>"));
if (response.has_value())
return { response->begin(), response->end() };
else
return std::vector<u8>{ 0x00 };
}, [this](auto) {
log::info("MCP client disconnected");
m_connected = false;
}, true);
}
void Server::shutdown() {
m_server.shutdown();
}
void Server::disconnect() {
m_server.disconnectClients();
}
void Server::addPrimitive(std::string type, std::string_view capabilities, std::function<nlohmann::json(const nlohmann::json &params)> function) {
auto json = nlohmann::json::parse(capabilities);
auto name = json["name"].get<std::string>();
m_primitives[type][name] = {
json,
function
};
}
nlohmann::json Server::handleInitialize() {
constexpr static auto ServerName = "ImHex";
constexpr static auto ProtocolVersion = "2025-06-18";
return {
{ "protocolVersion", ProtocolVersion },
{
"capabilities",
{
{ "tools", nlohmann::json::object() },
},
},
{
"serverInfo", {
{ "name", ServerName },
{ "version", ImHexApi::System::getImHexVersion().get() }
}
}
};
}
void Server::handleNotifications(const std::string &method, [[maybe_unused]] const nlohmann::json &params) {
if (method == "initialized") {
m_connected = true;
}
}
bool Server::isConnected() {
return m_connected;
}
}

View File

@ -30,6 +30,7 @@ namespace hex::plugin::builtin {
void handleValidatePluginCommand(const std::vector<std::string> &args); void handleValidatePluginCommand(const std::vector<std::string> &args);
void handleSaveEditorCommand(const std::vector<std::string> &args); void handleSaveEditorCommand(const std::vector<std::string> &args);
void handleFileInfoCommand(const std::vector<std::string> &args); void handleFileInfoCommand(const std::vector<std::string> &args);
void handleMCPCommand(const std::vector<std::string> &args);
void registerCommandForwarders(); void registerCommandForwarders();

View File

@ -54,6 +54,7 @@
"hex.builtin.achievement.misc.download_from_store.name": "There's an app for that", "hex.builtin.achievement.misc.download_from_store.name": "There's an app for that",
"hex.builtin.achievement.misc.download_from_store.desc": "Download any item from the Content Store", "hex.builtin.achievement.misc.download_from_store.desc": "Download any item from the Content Store",
"hex.builtin.background_service.network_interface": "Network Interface", "hex.builtin.background_service.network_interface": "Network Interface",
"hex.builtin.setting.general.mcp_server": "MCP Server support",
"hex.builtin.background_service.auto_backup": "Auto Backup", "hex.builtin.background_service.auto_backup": "Auto Backup",
"hex.builtin.command.calc.desc": "Calculator", "hex.builtin.command.calc.desc": "Calculator",
"hex.builtin.command.convert.desc": "Unit conversion", "hex.builtin.command.convert.desc": "Unit conversion",

View File

@ -17,6 +17,8 @@
#include <fmt/chrono.h> #include <fmt/chrono.h>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <romfs/romfs.hpp>
#include <toasts/toast_notification.hpp>
namespace hex::plugin::builtin { namespace hex::plugin::builtin {
@ -103,6 +105,16 @@ namespace hex::plugin::builtin {
std::this_thread::sleep_for(std::chrono::seconds(1)); std::this_thread::sleep_for(std::chrono::seconds(1));
} }
void handleMCPServer() {
if (!ContentRegistry::MCP::isEnabled()) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
ContentRegistry::MCP::impl::getMcpServerInstance().disconnect();
return;
}
ContentRegistry::MCP::impl::getMcpServerInstance().listen();
}
} }
void registerBackgroundServices() { void registerBackgroundServices() {
@ -110,12 +122,17 @@ namespace hex::plugin::builtin {
s_networkInterfaceServiceEnabled = value.get<bool>(false); s_networkInterfaceServiceEnabled = value.get<bool>(false);
}); });
ContentRegistry::Settings::onChange("hex.builtin.setting.general", "hex.builtin.setting.general.mcp_server", [](const ContentRegistry::Settings::SettingsValue &value) {
ContentRegistry::MCP::impl::setEnabled(value.get<bool>(false));
});
ContentRegistry::Settings::onChange("hex.builtin.setting.general", "hex.builtin.setting.general.backups.auto_backup_time", [](const ContentRegistry::Settings::SettingsValue &value) { ContentRegistry::Settings::onChange("hex.builtin.setting.general", "hex.builtin.setting.general.backups.auto_backup_time", [](const ContentRegistry::Settings::SettingsValue &value) {
s_autoBackupTime = value.get<int>(0) * 30; s_autoBackupTime = value.get<int>(0) * 30;
}); });
ContentRegistry::BackgroundServices::registerService("hex.builtin.background_service.network_interface", handleNetworkInterfaceService); ContentRegistry::BackgroundServices::registerService("hex.builtin.background_service.network_interface", handleNetworkInterfaceService);
ContentRegistry::BackgroundServices::registerService("hex.builtin.background_service.auto_backup", handleAutoBackup); ContentRegistry::BackgroundServices::registerService("hex.builtin.background_service.auto_backup", handleAutoBackup);
ContentRegistry::BackgroundServices::registerService("hex.builtin.background_service.mcp", handleMCPServer);
EventProviderDirtied::subscribe([](prv::Provider *) { EventProviderDirtied::subscribe([](prv::Provider *) {
s_dataDirty = true; s_dataDirty = true;

View File

@ -1,4 +1,6 @@
#include <iostream>
#include <content/command_line_interface.hpp> #include <content/command_line_interface.hpp>
#include <hex/mcp/client.hpp>
#include <hex/api/imhex_api/system.hpp> #include <hex/api/imhex_api/system.hpp>
#include <hex/api/imhex_api/hex_editor.hpp> #include <hex/api/imhex_api/hex_editor.hpp>
@ -530,6 +532,14 @@ namespace hex::plugin::builtin {
ContentRegistry::Views::setFullScreenView<ViewFullScreenFileInfo>(path); ContentRegistry::Views::setFullScreenView<ViewFullScreenFileInfo>(path);
} }
void handleMCPCommand(const std::vector<std::string> &) {
mcp::Client client;
auto result = client.run(std::cin, std::cout);
std::fprintf(stderr, "MCP Client disconnected!\n");
std::exit(result);
}
void registerCommandForwarders() { void registerCommandForwarders() {
hex::subcommands::registerSubCommand("open", [](const std::vector<std::string> &args){ hex::subcommands::registerSubCommand("open", [](const std::vector<std::string> &args){

View File

@ -768,6 +768,8 @@ namespace hex::plugin::builtin {
ContentRegistry::Settings::add<Widgets::Checkbox>("hex.builtin.setting.general", "hex.builtin.setting.general.network", "hex.builtin.setting.general.network_interface", false); ContentRegistry::Settings::add<Widgets::Checkbox>("hex.builtin.setting.general", "hex.builtin.setting.general.network", "hex.builtin.setting.general.network_interface", false);
ContentRegistry::Settings::add<Widgets::Checkbox>("hex.builtin.setting.general", "hex.builtin.setting.general.network", "hex.builtin.setting.general.mcp_server", false);
#if !defined(OS_WEB) #if !defined(OS_WEB)
ContentRegistry::Settings::add<ServerContactWidget>("hex.builtin.setting.general", "hex.builtin.setting.general.network", "hex.builtin.setting.general.server_contact"); ContentRegistry::Settings::add<ServerContactWidget>("hex.builtin.setting.general", "hex.builtin.setting.general.network", "hex.builtin.setting.general.server_contact");
ContentRegistry::Settings::add<Widgets::Checkbox>("hex.builtin.setting.general", "hex.builtin.setting.general.network", "hex.builtin.setting.general.upload_crash_logs", true); ContentRegistry::Settings::add<Widgets::Checkbox>("hex.builtin.setting.general", "hex.builtin.setting.general.network", "hex.builtin.setting.general.upload_crash_logs", true);

View File

@ -29,6 +29,7 @@
#include <csignal> #include <csignal>
#include <fonts/tabler_icons.hpp> #include <fonts/tabler_icons.hpp>
#include <hex/api/content_registry/communication_interface.hpp>
namespace hex::plugin::builtin { namespace hex::plugin::builtin {
@ -239,6 +240,20 @@ namespace hex::plugin::builtin {
}); });
} }
ContentRegistry::UserInterface::addFooterItem([] {
if (ContentRegistry::MCP::isConnected()) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGuiExt::GetCustomColorU32(ImGuiCustomCol_Highlight));
} else {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
}
if (ContentRegistry::MCP::isEnabled()) {
ImGui::TextUnformatted(ICON_VS_MCP);
}
ImGui::PopStyleColor();
});
if (dbg::debugModeEnabled()) { if (dbg::debugModeEnabled()) {
ContentRegistry::UserInterface::addFooterItem([] { ContentRegistry::UserInterface::addFooterItem([] {
static float framerate = 0; static float framerate = 0;

View File

@ -88,6 +88,7 @@ IMHEX_PLUGIN_SUBCOMMANDS() {
{ "validate-plugin", "", "Validates that a plugin can be loaded", hex::plugin::builtin::handleValidatePluginCommand }, { "validate-plugin", "", "Validates that a plugin can be loaded", hex::plugin::builtin::handleValidatePluginCommand },
{ "save-editor", "", "Opens a pattern file for save file editing", hex::plugin::builtin::handleSaveEditorCommand }, { "save-editor", "", "Opens a pattern file for save file editing", hex::plugin::builtin::handleSaveEditorCommand },
{ "file-info", "i", "Displays information about a file", hex::plugin::builtin::handleFileInfoCommand }, { "file-info", "i", "Displays information about a file", hex::plugin::builtin::handleFileInfoCommand },
{ "mcp", "", "Starts a MCP Server for AI to interact with", hex::plugin::builtin::handleMCPCommand },
}; };
IMHEX_PLUGIN_SETUP_BUILTIN("Built-in", "WerWolv", "Default ImHex functionality") { IMHEX_PLUGIN_SETUP_BUILTIN("Built-in", "WerWolv", "Default ImHex functionality") {