diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ae0e63..deecbb8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,7 @@ find_package(spdlog CONFIG REQUIRED) find_package(fmt CONFIG REQUIRED) find_package(ZLIB REQUIRED) find_package(Lua REQUIRED) +find_package(nlohmann_json CONFIG REQUIRED) file(GLOB_RECURSE HYPERION_SOURCES "src/*.cpp" @@ -54,6 +55,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE fmt::fmt ZLIB::ZLIB ${LUA_LIBRARIES} + nlohmann_json::nlohmann_json ) if(WIN32) diff --git a/README.md b/README.md index 5447bb4..0c67087 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,13 @@ Disassembly: Zydis (x86/x64) + Capstone (ARM, ARM64, MIPS, PPC) - Script API: get_name, set_name, get_insn, get_bytes, get_functions, get_xrefs_to, set_comment, goto_addr, patch_byte, get_segments, get_arch, create_function - See [docs/scripting.md](docs/scripting.md) and [docs/plugins.md](docs/plugins.md) +**MCP Server (Model Context Protocol)** +- Built-in headless MCP server for AI integration (Cursor, Claude Desktop, etc.) +- Access disassembly, decompilation, xrefs, and binary structures directly via AI prompts +- Start the server by passing `--mcp` to the executable: + `./build/Release/Hyperion --mcp` (or equivalent on your platform) +- Supports checking status, finding functions, string references, modifying comments/names, and decompiling over stdio. + **Customization** - Settings panel (Ctrl+,): fonts, colors, keybinds, advanced options - Editable keybinds (press-to-assign, persisted) @@ -152,9 +159,9 @@ Dependencies (pulled via vcpkg): imgui (docking), glfw, zydis, capstone, spdlog, | Platform | Status | |----------|--------| -| Windows x64 | Full support | -| Linux x64 | Builds, full support | -| macOS (Intel + Apple Silicon) | Builds, full support | +| Windows x64 | Full support (UI + MCP) | +| Linux x64 | Builds, full support (UI + MCP) | +| macOS (Intel + Apple Silicon) | Builds, full support (UI + MCP) | ## Status diff --git a/hyperion_mcp.log b/hyperion_mcp.log new file mode 100644 index 0000000..b753767 --- /dev/null +++ b/hyperion_mcp.log @@ -0,0 +1 @@ +[01:23:51.459] [info] Starting MCP stdio server... diff --git a/src/core/analysis/analyzer.cpp b/src/core/analysis/analyzer.cpp index df258f2..18629cd 100644 --- a/src/core/analysis/analyzer.cpp +++ b/src/core/analysis/analyzer.cpp @@ -985,7 +985,7 @@ void Analyzer::recover_structs() { total_size = static_cast(off) + sz; u32 sid = db_.types.add_struct(name, total_size); - u32 field_idx = 0; + [[maybe_unused]] u32 field_idx = 0; for (auto& [off, sz] : fields_set) { std::string fname = fmt::format("field_{:X}", off); u32 type_id = 0; diff --git a/src/core/analysis/rtti.cpp b/src/core/analysis/rtti.cpp index a76ca02..1fe4018 100644 --- a/src/core/analysis/rtti.cpp +++ b/src/core/analysis/rtti.cpp @@ -14,7 +14,7 @@ namespace hype { namespace { -const u8* va_to_ptr(PEImage& img, va_t addr, size_t* max_len = nullptr) { +[[maybe_unused]] const u8* va_to_ptr(PEImage& img, va_t addr, size_t* max_len = nullptr) { for (auto& seg : img.segments) { if (seg.contains(addr)) { size_t off = static_cast(addr - seg.va); @@ -65,6 +65,10 @@ std::string demangle_rtti(const std::string& mangled) { } // namespace +} + +namespace hype { + void RTTIParser::parse(PEImage& img, AnalysisDB& db) { if (img.arch != Arch::X64) return; @@ -78,7 +82,7 @@ void RTTIParser::parse(PEImage& img, AnalysisDB& db) { } void RTTIParser::find_type_descriptors(PEImage& img) { - constexpr std::string_view kPattern = ".?AV"; + [[maybe_unused]] constexpr std::string_view kPattern = ".?AV"; for (auto& seg : img.segments) { if (seg.executable() || seg.data.empty()) continue; diff --git a/src/core/decompiler/dce.cpp b/src/core/decompiler/dce.cpp index 0ac88de..bb21574 100644 --- a/src/core/decompiler/dce.cpp +++ b/src/core/decompiler/dce.cpp @@ -45,8 +45,8 @@ bool DCE::is_dead_stack_op(const PcodeInsn& op, const PcodeFunc&) { } void DCE::run(PcodeFunc& func) { - static constexpr int RSP_ID = 4; - static constexpr int RBP_ID = 5; + [[maybe_unused]] static constexpr int RSP_ID = 4; + [[maybe_unused]] static constexpr int RBP_ID = 5; // Pass 1: eliminate ONLY redundant prologue/epilogue register saves/restores // and flag computations not used by branches. diff --git a/src/core/decompiler/lifter.cpp b/src/core/decompiler/lifter.cpp index 8f6268f..7b90619 100644 --- a/src/core/decompiler/lifter.cpp +++ b/src/core/decompiler/lifter.cpp @@ -5,10 +5,10 @@ namespace hype { -static constexpr int REG_RAX = 0, REG_RCX = 1, REG_RDX = 2, REG_RBX = 3; -static constexpr int REG_RSP = 4, REG_RBP = 5, REG_RSI = 6, REG_RDI = 7; -static constexpr int REG_R8 = 8, REG_R9 = 9, REG_R10 = 10, REG_R11 = 11; -static constexpr int REG_R12 = 12, REG_R13 = 13, REG_R14 = 14, REG_R15 = 15; +[[maybe_unused]] static constexpr int REG_RAX = 0, REG_RCX = 1, REG_RDX = 2, REG_RBX = 3; +[[maybe_unused]] static constexpr int REG_RSP = 4, REG_RBP = 5, REG_RSI = 6, REG_RDI = 7; +[[maybe_unused]] static constexpr int REG_R8 = 8, REG_R9 = 9, REG_R10 = 10, REG_R11 = 11; +[[maybe_unused]] static constexpr int REG_R12 = 12, REG_R13 = 13, REG_R14 = 14, REG_R15 = 15; static constexpr int REG_ZF = 100, REG_CF = 101, REG_SF = 102, REG_OF = 103; static const struct { u16 zreg; const char* name; int id; int size; } kRegTable[] = { diff --git a/src/core/decompiler/lifter_arm64.cpp b/src/core/decompiler/lifter_arm64.cpp index d457f3e..66f94e4 100644 --- a/src/core/decompiler/lifter_arm64.cpp +++ b/src/core/decompiler/lifter_arm64.cpp @@ -7,10 +7,10 @@ namespace hype { -static constexpr int REG_ARM64_SP = ARM64_REG_SP; +[[maybe_unused]] static constexpr int REG_ARM64_SP = ARM64_REG_SP; static constexpr int REG_ARM64_XZR = ARM64_REG_XZR; static constexpr int REG_ARM64_WZR = ARM64_REG_WZR; -static constexpr int REG_ARM64_NZCV = ARM64_REG_NZCV; +[[maybe_unused]] static constexpr int REG_ARM64_NZCV = ARM64_REG_NZCV; Varnode LifterARM64::reg_vn(int reg_id) { if (reg_id == ARM64_REG_INVALID) return vn_const(0); diff --git a/src/core/decompiler/type_infer.cpp b/src/core/decompiler/type_infer.cpp index 3534768..6aff438 100644 --- a/src/core/decompiler/type_infer.cpp +++ b/src/core/decompiler/type_infer.cpp @@ -3,6 +3,32 @@ namespace hype { +DecompType DecompType::make_void() { return {DTypeKind::Void, nullptr, 0, "", false}; } +DecompType DecompType::make_bool() { return {DTypeKind::Bool, nullptr, 0, "", false}; } +DecompType DecompType::make_char() { return {DTypeKind::Char, nullptr, 0, "", false}; } +DecompType DecompType::make_int(int bits, bool sign) { + switch (bits) { + case 8: return {sign ? DTypeKind::Int8 : DTypeKind::UInt8, nullptr, 0, "", false}; + case 16: return {sign ? DTypeKind::Int16 : DTypeKind::UInt16, nullptr, 0, "", false}; + case 32: return {sign ? DTypeKind::Int32 : DTypeKind::UInt32, nullptr, 0, "", false}; + default: return {sign ? DTypeKind::Int64 : DTypeKind::UInt64, nullptr, 0, "", false}; + } +} +DecompType DecompType::make_sizet() { return {DTypeKind::SizeT, nullptr, 0, "", false}; } +DecompType DecompType::make_ptr(DecompType pointee, bool c) { + DecompType t{DTypeKind::Pointer, nullptr, 0, "", c}; + t.inner = std::make_shared(std::move(pointee)); + return t; +} +DecompType DecompType::make_array(DecompType inner, u32 count) { + DecompType t{DTypeKind::Array, nullptr, static_cast(count), "", false}; + t.inner = std::make_shared(std::move(inner)); + return t; +} +DecompType DecompType::make_struct(std::string name) { + return {DTypeKind::Struct, nullptr, 0, std::move(name), false}; +} + int DecompType::bit_width() const { switch (kind) { case DTypeKind::Bool: case DTypeKind::Int8: case DTypeKind::UInt8: case DTypeKind::Char: return 8; @@ -80,7 +106,7 @@ void TypeInfer::init_known_funcs() { {"CreateFileA", void_ptr, {const_char_ptr, uint32, uint32, void_ptr, uint32, uint32, void_ptr}, {"lpFileName", "dwDesiredAccess", "dwShareMode", "lpSecurityAttributes", "dwCreationDisposition", "dwFlagsAndAttributes", "hTemplateFile"}}, - {"CreateFileW", void_ptr, {DecompType::make_ptr(DecompType{DTypeKind::WChar}), uint32, uint32, void_ptr, uint32, uint32, void_ptr}, + {"CreateFileW", void_ptr, {DecompType::make_ptr(DecompType{DTypeKind::WChar, nullptr, 0, "", false}), uint32, uint32, void_ptr, uint32, uint32, void_ptr}, {"lpFileName", "dwDesiredAccess", "dwShareMode", "lpSecurityAttributes", "dwCreationDisposition", "dwFlagsAndAttributes", "hTemplateFile"}}, {"CloseHandle", int32, {void_ptr}, {"hObject"}}, @@ -166,7 +192,7 @@ void TypeInfer::infer_from_calls(const PcodeFunc& func) { void TypeInfer::infer_params(const PcodeFunc& func) { for (auto& p : func.params) - set_type(p.id, DecompType{DTypeKind::Int64}); + set_type(p.id, DecompType{DTypeKind::Int64, nullptr, 0, "", false}); for (auto& blk : func.blocks) { for (auto& op : blk.ops) { @@ -219,7 +245,7 @@ const KnownFunc* TypeInfer::find_known(const std::string& name) const { DecompType TypeInfer::get_type(int var_id) const { auto it = types_.find(var_id); if (it != types_.end()) return it->second; - return DecompType{DTypeKind::Int64}; + return DecompType{DTypeKind::Int64, nullptr, 0, "", false}; } std::string TypeInfer::get_var_name(int var_id) const { @@ -231,7 +257,7 @@ std::string TypeInfer::get_var_name(int var_id) const { void TypeInfer::run(PcodeFunc& func) { types_.clear(); names_.clear(); - ret_type_ = DecompType{DTypeKind::Int64}; + ret_type_ = DecompType{DTypeKind::Int64, nullptr, 0, "", false}; init_known_funcs(); infer_params(func); diff --git a/src/core/decompiler/type_infer.h b/src/core/decompiler/type_infer.h index c492997..509ac00 100644 --- a/src/core/decompiler/type_infer.h +++ b/src/core/decompiler/type_infer.h @@ -12,34 +12,24 @@ enum class DTypeKind : u8 { Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float, Double, - Pointer, Array, FuncPtr, SizeT + Pointer, Array, FuncPtr, Struct, SizeT }; struct DecompType { DTypeKind kind = DTypeKind::Int64; std::shared_ptr inner; int array_count = 0; + std::string struct_name; bool is_const = false; - static DecompType make_void() { return {DTypeKind::Void}; } - static DecompType make_bool() { return {DTypeKind::Bool}; } - static DecompType make_char() { return {DTypeKind::Char}; } - static DecompType make_int(int bits, bool sign = true) { - switch (bits) { - case 8: return {sign ? DTypeKind::Int8 : DTypeKind::UInt8}; - case 16: return {sign ? DTypeKind::Int16 : DTypeKind::UInt16}; - case 32: return {sign ? DTypeKind::Int32 : DTypeKind::UInt32}; - default: return {sign ? DTypeKind::Int64 : DTypeKind::UInt64}; - } - } - static DecompType make_sizet() { return {DTypeKind::SizeT}; } - static DecompType make_ptr(DecompType pointee, bool c = false) { - DecompType t; - t.kind = DTypeKind::Pointer; - t.inner = std::make_shared(std::move(pointee)); - t.is_const = c; - return t; - } + static DecompType make_void(); + static DecompType make_bool(); + static DecompType make_char(); + static DecompType make_int(int bits, bool sign = true); + static DecompType make_sizet(); + static DecompType make_ptr(DecompType pointee, bool c = false); + static DecompType make_array(DecompType inner, u32 count); + static DecompType make_struct(std::string name); std::string to_string() const; bool is_pointer() const { return kind == DTypeKind::Pointer; } @@ -80,7 +70,7 @@ class TypeInfer { std::unordered_map types_; std::unordered_map names_; std::vector known_funcs_; - DecompType ret_type_{DTypeKind::Int64}; + DecompType ret_type_{DTypeKind::Int64, nullptr, 0, "", false}; }; } diff --git a/src/core/disasm/disassembler.h b/src/core/disasm/disassembler.h index bef7e24..e7fc77a 100644 --- a/src/core/disasm/disassembler.h +++ b/src/core/disasm/disassembler.h @@ -10,10 +10,11 @@ namespace hype { enum class InsnType : u8 { Unknown, Nop, Mov, Push, Pop, Call, Ret, Jmp, Jcc, - Cmp, Test, - Add, Sub, Mul, Div, - And, Or, Xor, Not, Shl, Shr, - Lea, Int, Syscall, Other + Cmp, Test, Setcc, + Add, Sub, Mul, Div, Imul, Idiv, + Inc, Dec, + And, Or, Xor, Not, Shl, Shr, Sar, Rol, Ror, + Lea, Int, Syscall, Movsx, Movzx, Other }; enum class OpType : u8 { None, Reg, Imm, Mem }; diff --git a/src/core/loader/elf_loader.cpp b/src/core/loader/elf_loader.cpp index 64d0400..de0d147 100644 --- a/src/core/loader/elf_loader.cpp +++ b/src/core/loader/elf_loader.cpp @@ -29,7 +29,7 @@ constexpr u16 EM_AARCH64 = 183; // Segment types constexpr u32 PT_LOAD = 1; -constexpr u32 PT_DYNAMIC = 2; +[[maybe_unused]] constexpr u32 PT_DYNAMIC = 2; // Segment flags constexpr u32 PF_X = 1; @@ -38,8 +38,8 @@ constexpr u32 PF_R = 4; // Section types constexpr u32 SHT_SYMTAB = 2; -constexpr u32 SHT_STRTAB = 3; -constexpr u32 SHT_DYNAMIC = 6; +[[maybe_unused]] constexpr u32 SHT_STRTAB = 3; +[[maybe_unused]] constexpr u32 SHT_DYNAMIC = 6; constexpr u32 SHT_DYNSYM = 11; // Symbol binding/type @@ -48,11 +48,11 @@ constexpr u8 STB_WEAK = 2; constexpr u8 STT_FUNC = 2; // Dynamic tags -constexpr i64 DT_NEEDED = 1; -constexpr i64 DT_PLTGOT = 3; -constexpr i64 DT_STRTAB = 5; -constexpr i64 DT_JMPREL = 23; -constexpr i64 DT_PLTRELSZ = 2; +[[maybe_unused]] constexpr i64 DT_NEEDED = 1; +[[maybe_unused]] constexpr i64 DT_PLTGOT = 3; +[[maybe_unused]] constexpr i64 DT_STRTAB = 5; +[[maybe_unused]] constexpr i64 DT_JMPREL = 23; +[[maybe_unused]] constexpr i64 DT_PLTRELSZ = 2; #pragma pack(push, 1) struct Elf32_Ehdr { diff --git a/src/core/loader/pe_loader.cpp b/src/core/loader/pe_loader.cpp index 8509ef1..d7e05fc 100644 --- a/src/core/loader/pe_loader.cpp +++ b/src/core/loader/pe_loader.cpp @@ -110,7 +110,7 @@ bool safe_add(size_t a, size_t b, size_t& result) { return result >= a; } -bool safe_add32(u32 a, u32 b, u32& result) { +[[maybe_unused]] bool safe_add32(u32 a, u32 b, u32& result) { result = a + b; return result >= a; } @@ -472,6 +472,9 @@ void PELoader::parse_exceptions(PEImage& img) { struct RtFunc { u32 begin_rva; u32 end_rva; u32 unwind_rva; }; u32 count = pdata.size / sizeof(RtFunc); + [[maybe_unused]] constexpr u32 kMaxExceptions = 500'000; + if (count > kMaxExceptions) count = kMaxExceptions; + for (u32 i = 0; i < count; ++i) { size_t entry_off; if (!safe_add(raw_off, static_cast(i) * sizeof(RtFunc), entry_off)) break; diff --git a/src/debugger/instrumentation_cb.h b/src/debugger/instrumentation_cb.h index 40faee5..0daae54 100644 --- a/src/debugger/instrumentation_cb.h +++ b/src/debugger/instrumentation_cb.h @@ -5,7 +5,9 @@ #ifndef NOMINMAX #define NOMINMAX #endif +#ifdef _WIN32 #include +#endif namespace hype { diff --git a/src/main.cpp b/src/main.cpp index 80f9ab2..3ed594a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,15 +1,36 @@ #include "ui/app.h" +#include "mcp/mcp_server.h" #include +#include +#include +#include int main(int argc, char** argv) { - (void)argv; - spdlog::set_level(spdlog::level::info); - spdlog::set_pattern("[%H:%M:%S.%e] [%l] %v"); + bool run_mcp = false; + for (int i = 1; i < argc; ++i) { + if (std::string_view(argv[i]) == "--mcp") { + run_mcp = true; + } + } + + if (run_mcp) { + // When running MCP over stdio, redirect logs to a file to avoid corrupting stdout JSON + auto file_sink = std::make_shared("hyperion_mcp.log", true); + auto logger = std::make_shared("mcp_logger", file_sink); + spdlog::set_default_logger(logger); + spdlog::set_level(spdlog::level::info); + spdlog::set_pattern("[%H:%M:%S.%e] [%l] %v"); + } else { + spdlog::set_level(spdlog::level::info); + spdlog::set_pattern("[%H:%M:%S.%e] [%l] %v"); + } hype::App app; - if (argc > 1) { - // will be handled via command line in future + if (run_mcp) { + hype::McpServer mcp(app); + mcp.run_stdio(); + return 0; } return app.run(); diff --git a/src/mcp/mcp_server.cpp b/src/mcp/mcp_server.cpp new file mode 100644 index 0000000..f975ba0 --- /dev/null +++ b/src/mcp/mcp_server.cpp @@ -0,0 +1,415 @@ +#include "mcp_server.h" +#include "core/analysis/analyzer.h" +#include "core/decompiler/decompiler.h" +#include +#include +#include + +using json = nlohmann::json; + +namespace hype { + +McpServer::McpServer(App& app) : app_(app) {} + +void McpServer::run_stdio() { + spdlog::info("Starting MCP stdio server..."); + std::string line; + while (std::getline(std::cin, line)) { + if (line.empty()) continue; + try { + handle_message(line); + } catch (const std::exception& e) { + spdlog::error("MCP error: {}", e.what()); + } + } +} + +void McpServer::handle_message(const std::string& msg) { + auto req = json::parse(msg); + if (!req.contains("method")) return; + + std::string method = req["method"]; + json res = { + {"jsonrpc", "2.0"}, + {"id", req["id"]} + }; + + if (method == "initialize") { + res["result"] = { + {"protocolVersion", "2024-11-05"}, + {"serverInfo", { + {"name", "hyperion-mcp"}, + {"version", "0.1.0"} + }}, + {"capabilities", { + {"tools", {}} + }} + }; + } else if (method == "tools/list") { + res["result"] = { + {"tools", { + { + {"name", "ping"}, + {"description", "Ping the Hyperion disassembler MCP server to check if it's alive"}, + {"inputSchema", {{"type", "object"}, {"properties", json::object()}}} + }, + { + {"name", "open_binary"}, + {"description", "Load a binary into the disassembler"}, + {"inputSchema", { + {"type", "object"}, + {"properties", { + {"path", {{"type", "string"}, {"description", "Absolute path to binary"}}} + }}, + {"required", {"path"}} + }} + }, + { + {"name", "get_status"}, + {"description", "Get current disassembly status and loaded file info"}, + {"inputSchema", {{"type", "object"}, {"properties", json::object()}}} + }, + { + {"name", "get_functions"}, + {"description", "Get list of all discovered functions"}, + {"inputSchema", {{"type", "object"}, {"properties", json::object()}}} + }, + { + {"name", "get_strings"}, + {"description", "Get all extracted strings"}, + {"inputSchema", {{"type", "object"}, {"properties", json::object()}}} + }, + { + {"name", "get_xrefs_to"}, + {"description", "Get cross-references to a specific address"}, + {"inputSchema", { + {"type", "object"}, + {"properties", { + {"addr", {{"type", "integer"}, {"description", "Address"}}} + }}, + {"required", {"addr"}} + }} + }, + { + {"name", "set_comment"}, + {"description", "Set a comment at an address"}, + {"inputSchema", { + {"type", "object"}, + {"properties", { + {"addr", {{"type", "integer"}, {"description", "Address"}}}, + {"comment", {{"type", "string"}, {"description", "Comment text"}}} + }}, + {"required", {"addr", "comment"}} + }} + }, + { + {"name", "set_name"}, + {"description", "Rename an address or function"}, + {"inputSchema", { + {"type", "object"}, + {"properties", { + {"addr", {{"type", "integer"}, {"description", "Address"}}}, + {"name", {{"type", "string"}, {"description", "New name"}}} + }}, + {"required", {"addr", "name"}} + }} + }, + { + {"name", "disassemble_at"}, + {"description", "Get the disassembled instruction at an address"}, + {"inputSchema", { + {"type", "object"}, + {"properties", { + {"addr", {{"type", "integer"}, {"description", "Address"}}} + }}, + {"required", {"addr"}} + }} + }, + { + {"name", "decompile_function"}, + {"description", "Decompile a function into C-like pseudo-code"}, + {"inputSchema", { + {"type", "object"}, + {"properties", { + {"addr", {{"type", "integer"}, {"description", "Function start address"}}} + }}, + {"required", {"addr"}} + }} + }, + { + {"name", "get_imports"}, + {"description", "Get all imported functions and libraries"}, + {"inputSchema", {{"type", "object"}, {"properties", json::object()}}} + }, + { + {"name", "get_exports"}, + {"description", "Get all exported functions"}, + {"inputSchema", {{"type", "object"}, {"properties", json::object()}}} + }, + { + {"name", "get_segments"}, + {"description", "Get all memory segments/sections"}, + {"inputSchema", {{"type", "object"}, {"properties", json::object()}}} + }, + { + {"name", "get_bytes"}, + {"description", "Read raw bytes from a specific address"}, + {"inputSchema", { + {"type", "object"}, + {"properties", { + {"addr", {{"type", "integer"}, {"description", "Address"}}}, + {"size", {{"type", "integer"}, {"description", "Number of bytes to read"}}} + }}, + {"required", {"addr", "size"}} + }} + }, + { + {"name", "search_bytes"}, + {"description", "Search for a specific hex pattern in the loaded binary"}, + {"inputSchema", { + {"type", "object"}, + {"properties", { + {"pattern", {{"type", "string"}, {"description", "Hex pattern e.g., 'E8 ? ? ? ? 48 8D'"}}} + }}, + {"required", {"pattern"}} + }} + } + }} + }; + } else if (method == "tools/call") { + std::string tool = req["params"]["name"]; + if (tool == "ping") { + res["result"] = { + {"content", { + {{"type", "text"}, {"text", "pong"}} + }} + }; + } else if (tool == "open_binary") { + std::string path = req["params"]["arguments"]["path"]; + try { + app_.open_file(path.c_str()); + res["result"] = {{"content", {{{"type", "text"}, {"text", "File opened successfully."}}}}}; + } catch (const std::exception& e) { + res["result"] = {{"content", {{{"type", "text"}, {"text", std::string("Error: ") + e.what()}}}}}; + } + } else if (tool == "get_functions") { + if (auto* analyzer = app_.get_analyzer()) { + auto& db = analyzer->db(); + std::lock_guard lk(db.mtx); + json funcs = json::array(); + for (const auto& [entry, f] : db.funcs) { + funcs.push_back({ + {"entry", entry}, + {"name", f.name.empty() ? "sub_" + std::to_string(entry) : f.name}, + {"blocks", f.blocks.size()} + }); + } + res["result"] = {{"content", {{{"type", "text"}, {"text", funcs.dump(2)}}}}}; + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", "No binary loaded."}}}}}; + } + } else if (tool == "get_strings") { + if (auto* analyzer = app_.get_analyzer()) { + auto& db = analyzer->db(); + std::lock_guard lk(db.mtx); + json strings = json::array(); + for (const auto& s : db.strings) { + strings.push_back({ + {"addr", s.first}, + {"string", s.second} + }); + } + res["result"] = {{"content", {{{"type", "text"}, {"text", strings.dump(2)}}}}}; + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", "No binary loaded."}}}}}; + } + } else if (tool == "get_xrefs_to") { + if (auto* analyzer = app_.get_analyzer()) { + uint64_t addr = req["params"]["arguments"]["addr"]; + auto& db = analyzer->db(); + std::lock_guard lk(db.mtx); + json xr = json::array(); + if (db.xrefs_to.count(addr)) { + for (auto& x : db.xrefs_to[addr]) { + xr.push_back({ + {"from", x.from}, + {"to", x.to}, + {"type", static_cast(x.type)} + }); + } + } + res["result"] = {{"content", {{{"type", "text"}, {"text", xr.dump(2)}}}}}; + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", "No binary loaded."}}}}}; + } + } else if (tool == "set_comment") { + if (auto* analyzer = app_.get_analyzer()) { + uint64_t addr = req["params"]["arguments"]["addr"]; + std::string comment = req["params"]["arguments"]["comment"]; + auto& db = analyzer->db(); + std::lock_guard lk(db.mtx); + db.comments[addr] = comment; + res["result"] = {{"content", {{{"type", "text"}, {"text", "Comment set successfully."}}}}}; + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", "No binary loaded."}}}}}; + } + } else if (tool == "set_name") { + if (auto* analyzer = app_.get_analyzer()) { + uint64_t addr = req["params"]["arguments"]["addr"]; + std::string name = req["params"]["arguments"]["name"]; + auto& db = analyzer->db(); + db.set_name(addr, name); + res["result"] = {{"content", {{{"type", "text"}, {"text", "Name set successfully."}}}}}; + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", "No binary loaded."}}}}}; + } + } else if (tool == "disassemble_at") { + if (auto* analyzer = app_.get_analyzer()) { + uint64_t addr = req["params"]["arguments"]["addr"]; + auto& db = analyzer->db(); + std::lock_guard lk(db.mtx); + if (db.insns.count(addr)) { + auto& ins = db.insns[addr]; + std::string mnem = ins.mnemonic; + std::string op = ins.op_str; + res["result"] = {{"content", {{{"type", "text"}, {"text", mnem + " " + op}}}}}; + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", "No instruction found at address."}}}}}; + } + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", "No binary loaded."}}}}}; + } + } else if (tool == "decompile_function") { + if (auto* analyzer = app_.get_analyzer()) { + uint64_t addr = req["params"]["arguments"]["addr"]; + auto& db = analyzer->db(); + std::lock_guard lk(db.mtx); + auto it = db.funcs.find(addr); + if (it != db.funcs.end()) { + Decompiler dec; + auto lines = dec.decompile(it->second, db, &analyzer->rtti_parser()); + std::string full_code; + for (const auto& line : lines) { + full_code += std::string(line.indent * 4, ' ') + line.text + "\n"; + } + if (full_code.empty()) { + res["result"] = {{"content", {{{"type", "text"}, {"text", "Function decompilation resulted in empty output."}}}}}; + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", full_code}}}}}; + } + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", "Function not found at that address."}}}}}; + } + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", "No binary loaded."}}}}}; + } + } else if (tool == "get_imports") { + if (auto* img = app_.get_image()) { + json imports = json::array(); + for (const auto& imp : img->imports) { + imports.push_back({ + {"dll", imp.dll}, + {"name", imp.name}, + {"addr", imp.iat_addr} + }); + } + res["result"] = {{"content", {{{"type", "text"}, {"text", imports.dump(2)}}}}}; + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", "No binary loaded."}}}}}; + } + } else if (tool == "get_exports") { + if (auto* img = app_.get_image()) { + json exports = json::array(); + for (const auto& exp : img->exports) { + exports.push_back({ + {"name", exp.name}, + {"addr", exp.addr}, + {"ordinal", exp.ordinal} + }); + } + res["result"] = {{"content", {{{"type", "text"}, {"text", exports.dump(2)}}}}}; + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", "No binary loaded."}}}}}; + } + } else if (tool == "get_segments") { + if (auto* img = app_.get_image()) { + json segments = json::array(); + for (const auto& seg : img->segments) { + segments.push_back({ + {"name", seg.name}, + {"start", seg.va}, + {"size", seg.size}, + {"flags", seg.flags} + }); + } + res["result"] = {{"content", {{{"type", "text"}, {"text", segments.dump(2)}}}}}; + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", "No binary loaded."}}}}}; + } + } else if (tool == "get_bytes") { + if (auto* img = app_.get_image()) { + uint64_t addr = req["params"]["arguments"]["addr"]; + uint64_t size = req["params"]["arguments"]["size"]; + if (size > 1024) size = 1024; // Limit to 1KB max + + std::vector buffer; + for (const auto& seg : img->segments) { + if (addr >= seg.va && addr < seg.va + seg.size) { + uint64_t offset = addr - seg.va; + uint64_t available = seg.size - offset; + uint64_t read_size = std::min(size, available); + if (seg.file_off + offset + read_size <= img->raw.size()) { + buffer.assign(img->raw.begin() + seg.file_off + offset, + img->raw.begin() + seg.file_off + offset + read_size); + } + break; + } + } + + if (!buffer.empty()) { + std::string hex_out; + char buf[4]; + for (uint8_t b : buffer) { + snprintf(buf, sizeof(buf), "%02X ", b); + hex_out += buf; + } + res["result"] = {{"content", {{{"type", "text"}, {"text", hex_out}}}}}; + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", "Could not read bytes at that address."}}}}}; + } + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", "No binary loaded."}}}}}; + } + } else if (tool == "search_bytes") { + // Simplified return indicating use of standard tools + res["result"] = {{"content", {{{"type", "text"}, {"text", "Binary search not fully exposed over MCP yet. Use Python scripts internally."}}}}}; + } else if (tool == "get_status") { + if (auto* img = app_.get_image()) { + res["result"] = { + {"content", { + {{"type", "text"}, {"text", std::string("Loaded ") + std::to_string(img->segments.size()) + " sections. Is busy: " + (app_.is_busy() ? "true" : "false")}} + }} + }; + } else { + res["result"] = {{"content", {{{"type", "text"}, {"text", "No binary loaded. Hyperion is running headless with MCP support."}}}}}; + } + } else { + res["error"] = { + {"code", -32601}, + {"message", "Tool not found"} + }; + res.erase("result"); + } + } else { + res["error"] = { + {"code", -32601}, + {"message", "Method not found"} + }; + res.erase("result"); + } + + std::cout << res.dump() << "\n"; + std::cout.flush(); +} + +} diff --git a/src/mcp/mcp_server.h b/src/mcp/mcp_server.h new file mode 100644 index 0000000..f33ba93 --- /dev/null +++ b/src/mcp/mcp_server.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include "ui/app.h" + +namespace hype { +class McpServer { +public: + McpServer(App& app); + void run_stdio(); +private: + void handle_message(const std::string& msg); + App& app_; +}; +} diff --git a/src/ui/app.cpp b/src/ui/app.cpp index 4e7bfd7..8c31fc2 100644 --- a/src/ui/app.cpp +++ b/src/ui/app.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -222,6 +223,7 @@ int App::run() { if (show_bookmarks_) show_bookmarks_dlg(); if (show_sigs_) show_sigs_dlg(); if (show_apply_type_) show_apply_type_dlg(); + if (show_mcp_) show_mcp_dlg(); // status bar { @@ -561,6 +563,18 @@ void App::render_menubar() { if (ImGui::MenuItem("Auto-save", nullptr, autosave_enabled_)) autosave_enabled_ = !autosave_enabled_; ImGui::Separator(); + if (ImGui::MenuItem("Toggle Hex", "H", false, analyzer_ != nullptr)) dv_.cmd_toggle_hex(); + if (ImGui::MenuItem("NOP Out", nullptr, false, analyzer_ != nullptr)) dv_.cmd_nop(); + ImGui::Separator(); + if (ImGui::MenuItem("Rebase...", nullptr, false, img_ != nullptr)) show_rebase_ = true; + if (ImGui::MenuItem("Apply Signatures", nullptr, false, analyzer_ != nullptr)) { + analyzer_->apply_signatures(); + out_.log("Signatures re-applied"); + } + ImGui::Separator(); + if (ImGui::MenuItem("Auto-save", nullptr, autosave_enabled_)) + autosave_enabled_ = !autosave_enabled_; + ImGui::Separator(); if (ImGui::MenuItem("Settings", "Ctrl+,")) settings_panel_.show(); ImGui::EndMenu(); @@ -577,6 +591,8 @@ void App::render_menubar() { sigmaker_.visible() = true; if (ImGui::MenuItem("Debugger", nullptr, false, true)) dbgp_.visible() = true; + if (ImGui::MenuItem("MCP Server", nullptr, false, true)) + show_mcp_ = true; ImGui::Separator(); if (ImGui::BeginMenu("Theme")) { if (ImGui::MenuItem("Binary Ninja", nullptr, g_theme == Theme::BinaryNinja)) { @@ -1375,6 +1391,98 @@ void App::show_apply_type_dlg() { } } +void App::show_mcp_dlg() { + ImGui::SetNextWindowSize(ImVec2(550, 0), ImGuiCond_Once); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + + if (ImGui::Begin("MCP Server Integration###mcp", &show_mcp_, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::TextWrapped("Model Context Protocol (MCP) allows AI assistants like Cursor and Claude to directly reverse-engineer binaries using Hyperion's headless decompiler pipeline."); + ImGui::Spacing(); + + std::filesystem::path exe_path = std::filesystem::current_path() / "build" / "Release" / "Hyperion.exe"; +#if defined(__APPLE__) || defined(__linux__) + exe_path = std::filesystem::current_path() / "build" / "Hyperion"; +#endif + ImGui::TextDisabled("Executable Path:"); + ImGui::TextWrapped("%s", exe_path.string().c_str()); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::Button("Install to Cursor (.cursor/mcp.json)", ImVec2(-1, 35))) { + try { + std::filesystem::create_directory(".cursor"); + nlohmann::json mcp_config = nlohmann::json::object(); + if (std::filesystem::exists(".cursor/mcp.json")) { + std::ifstream in(".cursor/mcp.json"); + try { in >> mcp_config; } catch(...) {} + } + + if (!mcp_config.contains("mcpServers")) mcp_config["mcpServers"] = nlohmann::json::object(); + + mcp_config["mcpServers"]["hyperion"] = { + {"command", exe_path.string()}, + {"args", {"--mcp"}} + }; + + std::ofstream out(".cursor/mcp.json"); + out << mcp_config.dump(2); + out_.log("Created/Updated .cursor/mcp.json. Restart Cursor or reload its window to connect."); + } catch (const std::exception& e) { + out_.log(fmt::format("Error configuring Cursor MCP: {}", e.what())); + } + } + + ImGui::Spacing(); + + if (ImGui::Button("Install to Claude Desktop", ImVec2(-1, 35))) { + std::filesystem::path claude_path; +#ifdef _WIN32 + if (const char* appdata = std::getenv("APPDATA")) { + claude_path = std::filesystem::path(appdata) / "Claude" / "claude_desktop_config.json"; + } +#elif defined(__APPLE__) + if (const char* home = std::getenv("HOME")) { + claude_path = std::filesystem::path(home) / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"; + } +#endif + if (!claude_path.empty()) { + try { + std::filesystem::create_directories(claude_path.parent_path()); + nlohmann::json claude_config = nlohmann::json::object(); + if (std::filesystem::exists(claude_path)) { + std::ifstream in(claude_path); + try { in >> claude_config; } catch(...) {} + } + if (!claude_config.contains("mcpServers")) claude_config["mcpServers"] = nlohmann::json::object(); + + claude_config["mcpServers"]["hyperion"] = { + {"command", exe_path.string()}, + {"args", {"--mcp"}} + }; + + std::ofstream out(claude_path); + out << claude_config.dump(2); + out_.log(fmt::format("Updated {}. Restart Claude Desktop.", claude_path.string())); + } catch (const std::exception& e) { + out_.log(fmt::format("Error configuring Claude MCP: {}", e.what())); + } + } else { + out_.log("Claude Desktop config path not found on this OS."); + } + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::Button("Close", ImVec2(100, 0))) { + show_mcp_ = false; + } + } + ImGui::End(); +} + void App::compare_with() { if (!analyzer_) return; auto p = open_dialog(); diff --git a/src/ui/app.h b/src/ui/app.h index 3e605cf..776f1a9 100644 --- a/src/ui/app.h +++ b/src/ui/app.h @@ -48,9 +48,14 @@ class App { App(); ~App(); int run(); - -private: + + // Headless/MCP API void open_file(const char* path); + bool is_busy() const { return busy_; } + Analyzer* get_analyzer() const { return analyzer_.get(); } + PEImage* get_image() const { return img_.get(); } + Database& get_database() { return database_; } + void render_menubar(); void render_dockspace(); void build_default_layout(ImGuiID dock_id); @@ -66,6 +71,7 @@ class App { void show_bookmarks_dlg(); void show_sigs_dlg(); void show_apply_type_dlg(); + void show_mcp_dlg(); void compare_with(); void sync_panels(va_t addr); va_t find_func_for(va_t addr); @@ -135,6 +141,7 @@ class App { bool show_sigs_ = false; bool show_apply_type_ = false; bool show_plugin_manager_ = false; + bool show_mcp_ = false; bool layout_built_ = false; char goto_buf_[64] = {}; char rename_buf_[256] = {}; diff --git a/vcpkg.json b/vcpkg.json index 4d4ddc7..484b6f5 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -12,6 +12,7 @@ "spdlog", "fmt", "zlib", - "lua" + "lua", + "nlohmann-json" ] }