Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ def start_server(self, bv):
self.server.binary_ops.register_view(bv)
except Exception:
pass
_show_popup("MCP Server", "Server is already running.")
return
self.server.binary_ops.current_view = bv
try:
Expand All @@ -68,14 +67,7 @@ def start_server(self, bv):
self.server.start()
global _mcp_user_stopped
_mcp_user_stopped = False
bn.log_info(
f"MCP server started successfully on http://{self.config.server.host}:{self.config.server.port}"
)
_set_status_indicator(True)
_show_popup(
"MCP Server Started",
f"Running at http://{self.config.server.host}:{self.config.server.port}",
)
except Exception as e:
bn.log_error(f"Failed to start MCP server: {e!s}")
_show_popup("MCP Server Error", f"Failed to start: {e}")
Expand Down Expand Up @@ -133,6 +125,9 @@ def _try_autostart_for_bv(bv):
if _mcp_user_stopped:
bn.log_debug("MCP Max autostart suppressed due to manual stop")
return
if _is_server_running():
plugin.server.binary_ops.current_view = bv
return
plugin.start_server(bv)
except Exception as e:
bn.log_error(f"MCP Max autostart failed: {e}")
Expand Down Expand Up @@ -603,7 +598,7 @@ class _MCPMaxUINotification(ui.UIContextNotification):
def __init__(self):
super().__init__()
ui.UIContext.registerNotification(self)

def _get_active_bv(self):
try:
ctx = ui.UIContext.activeContext()
Expand Down
19 changes: 16 additions & 3 deletions plugin/core/binary_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,29 @@ def current_view(self) -> bn.BinaryView | None:

@current_view.setter
def current_view(self, bv: bn.BinaryView | None):
self._current_view = bv
if bv is self._current_view:
return
if bv:
bn.log_info(f"Set current binary view: {bv.file.filename}")
previous_filename = self._view_filename(self._current_view)
try:
self._register_view(bv)
except Exception:
pass
self._current_view = bv
current_filename = self._view_filename(bv)
if current_filename != previous_filename:
bn.log_info(f"Set current binary view: {current_filename}")
else:
self._current_view = None
bn.log_info("Cleared current binary view")

@staticmethod
def _view_filename(bv: bn.BinaryView | None) -> str | None:
try:
return str(bv.file.filename) if bv and bv.file else None
except Exception:
return None

def load_binary(self, filepath: str) -> bn.BinaryView:
"""Load a binary file using the appropriate method based on the Binary Ninja API version"""
try:
Expand Down Expand Up @@ -220,7 +233,7 @@ def list_open_binaries(self) -> list[dict[str, str]]:
vb_canon = vb
entries.append((canonical_id, fn, bool(vb_canon is self._current_view)))
# Sort by filename for stable ordering
entries.sort(key=lambda t: (t[1] or ""))
entries.sort(key=lambda t: t[1] or "")
for cid, fn, active in entries:
items.append({"id": cid, "filename": fn, "active": active})
return items
Expand Down
4 changes: 3 additions & 1 deletion plugin/server/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2371,7 +2371,9 @@ def start(self):
self.thread = threading.Thread(target=self.server.serve_forever)
self.thread.daemon = True
self.thread.start()
bn.log_info(f"Server started on {self.config.server.host}:{self.config.server.port}")
bn.log_info(
f"MCP server started on http://{self.config.server.host}:{self.config.server.port}"
)

def stop(self):
"""Stop the HTTP server and clean up resources."""
Expand Down
86 changes: 86 additions & 0 deletions tests/test_binary_operations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import importlib.util
import sys
import types
import unittest
from pathlib import Path


def _load_binary_operations_module():
info_logs = []
binaryninja = types.ModuleType("binaryninja")
binaryninja.log_info = info_logs.append
binaryninja.log_warn = lambda _message: None
binaryninja.log_error = lambda _message: None

def binaryninja_type(name):
value = type(name, (), {})
setattr(binaryninja, name, value)
return value

binaryninja.__getattr__ = binaryninja_type
sys.modules["binaryninja"] = binaryninja

enums = types.ModuleType("binaryninja.enums")
enums.StructureVariant = type("StructureVariant", (), {})
enums.TypeClass = type("TypeClass", (), {})
sys.modules[enums.__name__] = enums

for package in ("plugin", "plugin.core", "plugin.utils"):
module = types.ModuleType(package)
module.__path__ = []
sys.modules[package] = module

config = types.ModuleType("plugin.core.config")
config.BinaryNinjaConfig = object
sys.modules[config.__name__] = config

string_utils = types.ModuleType("plugin.utils.string_utils")
string_utils.escape_non_ascii = lambda value: value
sys.modules[string_utils.__name__] = string_utils

path = Path(__file__).parents[1] / "plugin" / "core" / "binary_operations.py"
spec = importlib.util.spec_from_file_location("plugin.core.binary_operations", path)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module, info_logs


class FakeView:
def __init__(self, filename):
self.file = types.SimpleNamespace(filename=filename)


class BinaryOperationsTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.module, cls.info_logs = _load_binary_operations_module()

def setUp(self):
self.info_logs.clear()
self.operations = self.module.BinaryOperations(object())

def test_first_assignment_remains_active_and_repeat_is_noop(self):
view = FakeView("sample.bndb")

self.operations.current_view = view
self.operations.current_view = view

self.assertIs(self.operations.current_view, view)
self.assertEqual(len(self.operations._views_by_id), 1)
self.assertEqual(self.info_logs, ["Set current binary view: sample.bndb"])

def test_replacement_view_for_same_file_updates_silently(self):
first_view = FakeView("sample.bndb")
replacement_view = FakeView("sample.bndb")

self.operations.current_view = first_view
self.operations.current_view = replacement_view

self.assertIs(self.operations.current_view, replacement_view)
self.assertEqual(len(self.operations._views_by_id), 1)
self.assertEqual(self.info_logs, ["Set current binary view: sample.bndb"])


if __name__ == "__main__":
unittest.main()
Loading