mirror of https://github.com/WerWolv/ImHex
feat: Add initial MCP Server support
This commit is contained in:
parent
932c281223
commit
e696d384c2
|
|
@ -1 +1 @@
|
|||
Subproject commit c9108712ba16a024dc7aee75f3fc9882629b2703
|
||||
Subproject commit b0c9416568475a784838e3af8b0021a542e47cd7
|
||||
|
|
@ -57,6 +57,9 @@ set(LIBIMHEX_SOURCES
|
|||
source/ui/toast.cpp
|
||||
source/ui/banner.cpp
|
||||
|
||||
source/mcp/client.cpp
|
||||
source/mcp/server.cpp
|
||||
|
||||
source/subcommands/subcommands.cpp
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include <hex/mcp/server.hpp>
|
||||
|
||||
EXPORT_MODULE namespace hex {
|
||||
|
||||
/* 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 ¶ms)> function);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -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 ¶ms)> function);
|
||||
|
||||
private:
|
||||
nlohmann::json handleInitialize();
|
||||
void handleNotifications(const std::string &method, const nlohmann::json ¶ms);
|
||||
|
||||
struct Primitive {
|
||||
nlohmann::json capabilities;
|
||||
std::function<nlohmann::json(const nlohmann::json ¶ms)> function;
|
||||
};
|
||||
|
||||
std::map<std::string, std::map<std::string, Primitive>> m_primitives;
|
||||
|
||||
wolv::net::SocketServer m_server;
|
||||
bool m_connected = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -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 ¶ms)> function) {
|
||||
impl::getMcpServerInstance().addPrimitive("tools", capabilities, function);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace ContentRegistry::Experiments {
|
||||
|
||||
namespace impl {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ¶ms) -> 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 ¶ms)> 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 ¶ms) {
|
||||
if (method == "initialized") {
|
||||
m_connected = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool Server::isConnected() {
|
||||
return m_connected;
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ namespace hex::plugin::builtin {
|
|||
void handleValidatePluginCommand(const std::vector<std::string> &args);
|
||||
void handleSaveEditorCommand(const std::vector<std::string> &args);
|
||||
void handleFileInfoCommand(const std::vector<std::string> &args);
|
||||
void handleMCPCommand(const std::vector<std::string> &args);
|
||||
|
||||
void registerCommandForwarders();
|
||||
|
||||
|
|
|
|||
|
|
@ -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.desc": "Download any item from the Content Store",
|
||||
"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.command.calc.desc": "Calculator",
|
||||
"hex.builtin.command.convert.desc": "Unit conversion",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
#include <fmt/chrono.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <romfs/romfs.hpp>
|
||||
#include <toasts/toast_notification.hpp>
|
||||
|
||||
namespace hex::plugin::builtin {
|
||||
|
||||
|
|
@ -103,6 +105,16 @@ namespace hex::plugin::builtin {
|
|||
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() {
|
||||
|
|
@ -110,12 +122,17 @@ namespace hex::plugin::builtin {
|
|||
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) {
|
||||
s_autoBackupTime = value.get<int>(0) * 30;
|
||||
});
|
||||
|
||||
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.mcp", handleMCPServer);
|
||||
|
||||
EventProviderDirtied::subscribe([](prv::Provider *) {
|
||||
s_dataDirty = true;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
#include <iostream>
|
||||
#include <content/command_line_interface.hpp>
|
||||
#include <hex/mcp/client.hpp>
|
||||
|
||||
#include <hex/api/imhex_api/system.hpp>
|
||||
#include <hex/api/imhex_api/hex_editor.hpp>
|
||||
|
|
@ -530,6 +532,14 @@ namespace hex::plugin::builtin {
|
|||
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() {
|
||||
hex::subcommands::registerSubCommand("open", [](const std::vector<std::string> &args){
|
||||
|
|
|
|||
|
|
@ -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.mcp_server", false);
|
||||
|
||||
#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<Widgets::Checkbox>("hex.builtin.setting.general", "hex.builtin.setting.general.network", "hex.builtin.setting.general.upload_crash_logs", true);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
|
||||
#include <csignal>
|
||||
#include <fonts/tabler_icons.hpp>
|
||||
#include <hex/api/content_registry/communication_interface.hpp>
|
||||
|
||||
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()) {
|
||||
ContentRegistry::UserInterface::addFooterItem([] {
|
||||
static float framerate = 0;
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ IMHEX_PLUGIN_SUBCOMMANDS() {
|
|||
{ "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 },
|
||||
{ "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") {
|
||||
|
|
|
|||
Loading…
Reference in New Issue