instances_use previously called register_instance which made a blocking safe_get (30s timeout) to validate the connection. If the Ghidra server was slow or unresponsive, this could hang the MCP tool call indefinitely from the client's perspective. Now instances_use creates a lazy stub entry and sets the port immediately — pure in-memory, no network I/O. The first actual tool call validates the connection naturally. Also fix background discovery thread using request_timeout (30s) instead of discovery_timeout (0.5s) per port — worst case went from 300s to 5s per scan cycle.
229 lines
6.8 KiB
Python
229 lines
6.8 KiB
Python
"""GhydraMCP Server - FastMCP server composing all mixins.
|
|
|
|
This module creates and configures the FastMCP server by composing
|
|
all domain-specific mixins into a single MCP server.
|
|
"""
|
|
|
|
import signal
|
|
import sys
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from fastmcp import FastMCP
|
|
|
|
from .config import GhydraConfig, get_config, set_config
|
|
from .mixins import (
|
|
AnalysisMixin,
|
|
BookmarksMixin,
|
|
CursorsMixin,
|
|
DataMixin,
|
|
DataTypesMixin,
|
|
DockerMixin,
|
|
FunctionsMixin,
|
|
InstancesMixin,
|
|
MemoryMixin,
|
|
NamespacesMixin,
|
|
SegmentsMixin,
|
|
StructsMixin,
|
|
SymbolsMixin,
|
|
VariablesMixin,
|
|
XrefsMixin,
|
|
)
|
|
|
|
|
|
def create_server(
|
|
name: str = "GhydraMCP",
|
|
config: Optional[GhydraConfig] = None,
|
|
) -> FastMCP:
|
|
"""Create and configure the GhydraMCP server.
|
|
|
|
Args:
|
|
name: Server name
|
|
config: Optional configuration override
|
|
|
|
Returns:
|
|
Configured FastMCP server instance
|
|
"""
|
|
if config:
|
|
set_config(config)
|
|
|
|
# Create the FastMCP server
|
|
mcp = FastMCP(name)
|
|
|
|
# Instantiate all mixins
|
|
instances_mixin = InstancesMixin()
|
|
functions_mixin = FunctionsMixin()
|
|
data_mixin = DataMixin()
|
|
structs_mixin = StructsMixin()
|
|
analysis_mixin = AnalysisMixin()
|
|
memory_mixin = MemoryMixin()
|
|
xrefs_mixin = XrefsMixin()
|
|
cursors_mixin = CursorsMixin()
|
|
docker_mixin = DockerMixin()
|
|
symbols_mixin = SymbolsMixin()
|
|
segments_mixin = SegmentsMixin()
|
|
variables_mixin = VariablesMixin()
|
|
namespaces_mixin = NamespacesMixin()
|
|
bookmarks_mixin = BookmarksMixin()
|
|
datatypes_mixin = DataTypesMixin()
|
|
|
|
# Register all mixins with the server
|
|
# Each mixin registers its tools, resources, and prompts
|
|
instances_mixin.register_all(mcp)
|
|
functions_mixin.register_all(mcp)
|
|
data_mixin.register_all(mcp)
|
|
structs_mixin.register_all(mcp)
|
|
analysis_mixin.register_all(mcp)
|
|
memory_mixin.register_all(mcp)
|
|
xrefs_mixin.register_all(mcp)
|
|
cursors_mixin.register_all(mcp)
|
|
docker_mixin.register_all(mcp)
|
|
symbols_mixin.register_all(mcp)
|
|
segments_mixin.register_all(mcp)
|
|
variables_mixin.register_all(mcp)
|
|
namespaces_mixin.register_all(mcp)
|
|
bookmarks_mixin.register_all(mcp)
|
|
datatypes_mixin.register_all(mcp)
|
|
|
|
# Optional feedback collection
|
|
cfg = get_config()
|
|
if cfg.feedback_enabled:
|
|
try:
|
|
from fastmcp_feedback import add_feedback_tools
|
|
|
|
db_path = Path(cfg.feedback_db_path)
|
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
add_feedback_tools(mcp, database_url=f"sqlite:///{db_path}")
|
|
except ImportError:
|
|
pass # fastmcp-feedback not installed — skip silently
|
|
|
|
return mcp
|
|
|
|
|
|
def _periodic_discovery(interval: int = 30):
|
|
"""Background thread for periodic instance discovery.
|
|
|
|
Uses a short timeout per port so a full scan completes quickly
|
|
even when most ports are unreachable.
|
|
|
|
Args:
|
|
interval: Seconds between discovery attempts
|
|
"""
|
|
import requests as _requests
|
|
|
|
from .mixins.base import GhydraMixinBase
|
|
|
|
config = get_config()
|
|
|
|
while True:
|
|
time.sleep(interval)
|
|
try:
|
|
# Quick scan — use discovery_timeout (0.5s), NOT request_timeout (30s)
|
|
for port in config.quick_discovery_range:
|
|
try:
|
|
url = f"http://{config.ghidra_host}:{port}/"
|
|
resp = _requests.get(
|
|
url,
|
|
timeout=config.discovery_timeout,
|
|
headers={"Accept": "application/json"},
|
|
)
|
|
if resp.ok:
|
|
response = resp.json()
|
|
if response.get("success", False):
|
|
with GhydraMixinBase._instances_lock:
|
|
if port not in GhydraMixinBase._instances:
|
|
GhydraMixinBase._instances[port] = {
|
|
"url": url.rstrip("/"),
|
|
"project": response.get("project", ""),
|
|
"file": response.get("file", ""),
|
|
"discovered_at": time.time(),
|
|
}
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _handle_sigint(signum, frame):
|
|
"""Handle SIGINT gracefully."""
|
|
print("\nShutting down GhydraMCP...", file=sys.stderr)
|
|
sys.exit(0)
|
|
|
|
|
|
def main():
|
|
"""Main entry point for the GhydraMCP server."""
|
|
import shutil
|
|
|
|
try:
|
|
from importlib.metadata import version
|
|
package_version = version("ghydramcp")
|
|
except Exception:
|
|
package_version = "2025.12.1"
|
|
|
|
print(f"🔬 GhydraMCP v{package_version}", file=sys.stderr)
|
|
print(" AI-assisted reverse engineering bridge for Ghidra", file=sys.stderr)
|
|
|
|
# Check Docker availability
|
|
docker_available = shutil.which("docker") is not None
|
|
if docker_available:
|
|
print(" 🐳 Docker available (use docker_* tools for container management)", file=sys.stderr)
|
|
else:
|
|
print(" ⚠ Docker not found (container management disabled)", file=sys.stderr)
|
|
|
|
config = get_config()
|
|
|
|
if config.feedback_enabled:
|
|
print(f" 📋 Feedback collection: {config.feedback_db_path}", file=sys.stderr)
|
|
|
|
# Create and configure the server
|
|
mcp = create_server()
|
|
|
|
# Initial instance discovery
|
|
print(f" Discovering Ghidra instances on {config.ghidra_host}...", file=sys.stderr)
|
|
|
|
from .core.http_client import safe_get
|
|
from .mixins.base import GhydraMixinBase
|
|
|
|
found = 0
|
|
for port in config.quick_discovery_range:
|
|
try:
|
|
response = safe_get(port, "")
|
|
if response.get("success", False):
|
|
GhydraMixinBase._instances[port] = {
|
|
"url": f"http://{config.ghidra_host}:{port}",
|
|
"project": response.get("project", ""),
|
|
"file": response.get("file", ""),
|
|
"discovered_at": time.time(),
|
|
}
|
|
found += 1
|
|
print(f" ✓ Found instance on port {port}", file=sys.stderr)
|
|
except Exception:
|
|
pass
|
|
|
|
if found == 0:
|
|
print(" ⚠ No Ghidra instances found (they can be discovered later)", file=sys.stderr)
|
|
else:
|
|
print(f" Found {found} Ghidra instance(s)", file=sys.stderr)
|
|
|
|
# Start background discovery thread
|
|
discovery_thread = threading.Thread(
|
|
target=_periodic_discovery,
|
|
daemon=True,
|
|
name="GhydraMCP-Discovery",
|
|
)
|
|
discovery_thread.start()
|
|
|
|
# Set up signal handler
|
|
signal.signal(signal.SIGINT, _handle_sigint)
|
|
|
|
print(" Starting MCP server...", file=sys.stderr)
|
|
|
|
# Run the server
|
|
mcp.run(transport="stdio")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|