diff --git a/fuzzforge-cli/src/fuzzforge_cli/tui/app.py b/fuzzforge-cli/src/fuzzforge_cli/tui/app.py index 75b8d27..27822e7 100644 --- a/fuzzforge-cli/src/fuzzforge_cli/tui/app.py +++ b/fuzzforge-cli/src/fuzzforge_cli/tui/app.py @@ -96,7 +96,8 @@ class FuzzForgeApp(App[None]): /* Modal screens */ AgentSetupScreen, AgentUnlinkScreen, - HubManagerScreen, LinkHubScreen, CloneHubScreen { + HubManagerScreen, LinkHubScreen, CloneHubScreen, + BuildImageScreen { align: center middle; } @@ -130,6 +131,30 @@ class FuzzForgeApp(App[None]): overflow-y: auto; } + #build-dialog { + width: 100; + height: 80%; + border: thick #4699fc; + background: $surface; + padding: 2 3; + } + + #build-log { + height: 1fr; + border: round $panel; + margin: 1 0; + } + + #build-subtitle { + color: $text-muted; + margin-bottom: 1; + } + + #build-status { + height: 1; + margin-top: 1; + } + .dialog-title { text-style: bold; text-align: center; @@ -168,6 +193,7 @@ class FuzzForgeApp(App[None]): Binding("q", "quit", "Quit"), Binding("h", "manage_hubs", "Hub Manager"), Binding("r", "refresh", "Refresh"), + Binding("enter", "select_row", "Select", show=False), ] def compose(self) -> ComposeResult: @@ -194,7 +220,9 @@ class FuzzForgeApp(App[None]): def on_mount(self) -> None: """Populate tables on startup.""" self._agent_rows: list[_AgentRow] = [] - self.query_one("#hub-panel").border_title = "Hub Servers" + # hub row data: (server_name, image, hub_name) | None for group headers + self._hub_rows: list[tuple[str, str, str] | None] = [] + self.query_one("#hub-panel").border_title = "Hub Servers [dim](Enter to build)[/dim]" self.query_one("#agents-panel").border_title = "AI Agents" self._refresh_agents() self._refresh_hub() @@ -220,6 +248,7 @@ class FuzzForgeApp(App[None]): def _refresh_hub(self) -> None: """Refresh the hub servers table, grouped by source hub.""" + self._hub_rows = [] table = self.query_one("#hub-table", DataTable) table.clear(columns=True) table.add_columns("Server", "Image", "Hub", "Status") @@ -275,6 +304,7 @@ class FuzzForgeApp(App[None]): style="bold", ) table.add_row(header, "", "", "") + self._hub_rows.append(None) # group header — not selectable # Tool rows for server, is_ready, status_text in statuses: @@ -287,7 +317,7 @@ class FuzzForgeApp(App[None]): elif is_ready: status_cell = Text("✓ Ready", style="green") else: - status_cell = Text(f"✗ {status_text}", style="red") + status_cell = Text(f"✗ {status_text}", style="red dim") table.add_row( f" {name}", @@ -295,13 +325,17 @@ class FuzzForgeApp(App[None]): hub_name, status_cell, ) + self._hub_rows.append((name, image, hub_name)) def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: - """Handle row selection on the agents table.""" - if event.data_table.id != "agents-table": - return + """Handle row selection on agents and hub tables.""" + if event.data_table.id == "agents-table": + self._handle_agent_row(event.cursor_row) + elif event.data_table.id == "hub-table": + self._handle_hub_row(event.cursor_row) - idx = event.cursor_row + def _handle_agent_row(self, idx: int) -> None: + """Open agent setup/unlink for the selected agent row.""" if idx < 0 or idx >= len(self._agent_rows): return @@ -322,6 +356,32 @@ class FuzzForgeApp(App[None]): callback=self._on_agent_changed, ) + def _handle_hub_row(self, idx: int) -> None: + """Open the build dialog for the selected hub tool row.""" + if idx < 0 or idx >= len(self._hub_rows): + return + row_data = self._hub_rows[idx] + if row_data is None: + return # group header row — ignore + + server_name, image, hub_name = row_data + if hub_name == "manual": + self.notify("Manual servers must be built outside FuzzForge") + return + + from fuzzforge_cli.tui.screens.build_image import BuildImageScreen + + self.push_screen( + BuildImageScreen(server_name, image, hub_name), + callback=self._on_image_built, + ) + + def _on_image_built(self, success: bool) -> None: + """Refresh hub status after a build attempt.""" + self._refresh_hub() + if success: + self.notify("Image built successfully", severity="information") + def on_button_pressed(self, event: Button.Pressed) -> None: """Handle button presses.""" if event.button.id == "btn-hub-manager": diff --git a/fuzzforge-cli/src/fuzzforge_cli/tui/helpers.py b/fuzzforge-cli/src/fuzzforge_cli/tui/helpers.py index 16a6e97..f5e0003 100644 --- a/fuzzforge-cli/src/fuzzforge_cli/tui/helpers.py +++ b/fuzzforge-cli/src/fuzzforge_cli/tui/helpers.py @@ -287,20 +287,80 @@ def get_default_hubs_dir() -> Path: return get_fuzzforge_user_dir() / "hubs" +def _discover_hub_dirs() -> list[Path]: + """Scan known hub directories for cloned repos. + + Checks both the current global location (``~/.fuzzforge/hubs/``) and the + legacy workspace-local location (``/.fuzzforge/hubs/``) so that hubs + cloned before the global-dir migration are still found. + + :return: List of hub directory paths (each is a direct child with a ``.git`` + sub-directory). + + """ + candidates: list[Path] = [] + for base in (get_fuzzforge_user_dir() / "hubs", get_fuzzforge_dir() / "hubs"): + if base.is_dir(): + for entry in base.iterdir(): + if entry.is_dir() and (entry / ".git").is_dir(): + candidates.append(entry) + return candidates + + def load_hubs_registry() -> dict[str, Any]: """Load the hubs registry from disk. + If the registry file does not exist, auto-recovers it by scanning known hub + directories and rebuilding entries for any discovered hubs. This handles + the migration from the old workspace-local ``/.fuzzforge/hubs.json`` + path to the global ``~/.fuzzforge/hubs.json`` path, as well as any case + where the registry was lost. + :return: Registry dict with ``hubs`` key containing a list of hub entries. """ path = get_hubs_registry_path() - if not path.exists(): + if path.exists(): + try: + data: dict[str, Any] = json.loads(path.read_text()) + return data + except (json.JSONDecodeError, OSError): + pass + + # Registry missing — attempt to rebuild from discovered hub directories. + discovered = _discover_hub_dirs() + if not discovered: return {"hubs": []} + + hubs: list[dict[str, Any]] = [] + for hub_dir in discovered: + name = hub_dir.name + # Try to read the git remote URL + git_url: str = "" + try: + import subprocess as _sp + r = _sp.run( + ["git", "-C", str(hub_dir), "remote", "get-url", "origin"], + check=False, capture_output=True, text=True, timeout=5, + ) + if r.returncode == 0: + git_url = r.stdout.strip() + except Exception: + pass + hubs.append({ + "name": name, + "path": str(hub_dir), + "git_url": git_url, + "is_default": name == FUZZFORGE_DEFAULT_HUB_NAME, + }) + + registry: dict[str, Any] = {"hubs": hubs} + # Persist so we don't re-scan on every load try: - data: dict[str, Any] = json.loads(path.read_text()) - return data - except (json.JSONDecodeError, OSError): - return {"hubs": []} + save_hubs_registry(registry) + except OSError: + pass + return registry def save_hubs_registry(registry: dict[str, Any]) -> None: @@ -566,3 +626,62 @@ def _remove_hub_servers_from_config(hub_name: str) -> int: config_path.write_text(json.dumps(config, indent=2)) return before - after + + +def find_dockerfile_for_server(server_name: str, hub_name: str) -> Path | None: + """Find the Dockerfile for a hub server tool. + + Looks up the hub path from the registry, then scans for + ``category//Dockerfile``. + + :param server_name: Tool name (e.g. ``"nmap-mcp"``). + :param hub_name: Hub name as stored in the registry. + :return: Absolute path to the Dockerfile, or ``None`` if not found. + + """ + registry = load_hubs_registry() + hub_entry = next( + (h for h in registry.get("hubs", []) if h.get("name") == hub_name), + None, + ) + if not hub_entry: + return None + + hub_path = Path(hub_entry["path"]) + for dockerfile in hub_path.rglob("Dockerfile"): + rel = dockerfile.relative_to(hub_path) + parts = rel.parts + if len(parts) == 3 and parts[1] == server_name: + return dockerfile + + return None + + +def build_image( + image: str, + dockerfile: Path, + *, + engine: str | None = None, +) -> subprocess.Popen[str]: + """Start a non-blocking ``docker/podman build`` subprocess. + + Returns the running :class:`subprocess.Popen` object so the caller + can stream ``stdout`` / ``stderr`` lines incrementally. + + :param image: Image tag (e.g. ``"nmap-mcp:latest"``). + :param dockerfile: Path to the ``Dockerfile``. + :param engine: ``"docker"`` or ``"podman"`` (auto-detected if ``None``). + :return: Running subprocess with merged stdout+stderr. + + """ + if engine is None: + engine = os.environ.get("FUZZFORGE_ENGINE__TYPE", "docker").lower() + engine = "podman" if engine == "podman" else "docker" + + context_dir = str(dockerfile.parent) + return subprocess.Popen( + [engine, "build", "-t", image, context_dir], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) diff --git a/fuzzforge-cli/src/fuzzforge_cli/tui/screens/build_image.py b/fuzzforge-cli/src/fuzzforge_cli/tui/screens/build_image.py new file mode 100644 index 0000000..4a66e82 --- /dev/null +++ b/fuzzforge-cli/src/fuzzforge_cli/tui/screens/build_image.py @@ -0,0 +1,103 @@ +"""Build-image modal screen for FuzzForge TUI. + +Provides a modal dialog that runs ``docker/podman build`` for a single +hub tool and streams the build log into a scrollable log area. + +""" + +from __future__ import annotations + +from pathlib import Path + +from textual import work +from textual.app import ComposeResult +from textual.containers import Horizontal, Vertical +from textual.screen import ModalScreen +from textual.widgets import Button, Label, Log + +from fuzzforge_cli.tui.helpers import build_image, find_dockerfile_for_server + + +class BuildImageScreen(ModalScreen[bool]): + """Modal that builds a Docker/Podman image and streams the build log.""" + + BINDINGS = [("escape", "cancel", "Close")] + + def __init__(self, server_name: str, image: str, hub_name: str) -> None: + super().__init__() + self._server_name = server_name + self._image = image + self._hub_name = hub_name + + def compose(self) -> ComposeResult: + """Compose the build dialog layout.""" + with Vertical(id="build-dialog"): + yield Label(f"Build {self._image}", classes="dialog-title") + yield Label( + f"Hub: {self._hub_name} • Tool: {self._server_name}", + id="build-subtitle", + ) + yield Log(id="build-log", auto_scroll=True) + yield Label("", id="build-status") + with Horizontal(classes="dialog-buttons"): + yield Button("Close", variant="default", id="btn-close", disabled=True) + + def on_mount(self) -> None: + """Start the build as soon as the screen is shown.""" + self._start_build() + + def action_cancel(self) -> None: + """Only dismiss when the build is not running (Close button enabled).""" + close_btn = self.query_one("#btn-close", Button) + if not close_btn.disabled: + self.dismiss(False) + + def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle Close button.""" + if event.button.id == "btn-close": + self.dismiss(self._succeeded) + + @work(thread=True) + def _start_build(self) -> None: + """Run the build in a background thread and stream output.""" + self._succeeded = False + log = self.query_one("#build-log", Log) + status = self.query_one("#build-status", Label) + + dockerfile = find_dockerfile_for_server(self._server_name, self._hub_name) + if dockerfile is None: + log.write_line(f"ERROR: Dockerfile not found for '{self._server_name}' in hub '{self._hub_name}'") + status.update("[red]Build failed — Dockerfile not found[/red]") + self.query_one("#btn-close", Button).disabled = False + return + + log.write_line(f"$ {self._get_engine()} build -t {self._image} {dockerfile.parent}") + log.write_line("") + + try: + proc = build_image(self._image, dockerfile) + except FileNotFoundError as exc: + log.write_line(f"ERROR: {exc}") + status.update("[red]Build failed — engine not found[/red]") + self.query_one("#btn-close", Button).disabled = False + return + + assert proc.stdout is not None + for line in proc.stdout: + log.write_line(line.rstrip()) + + proc.wait() + + if proc.returncode == 0: + self._succeeded = True + status.update(f"[green]✓ Built {self._image} successfully[/green]") + else: + status.update(f"[red]✗ Build failed (exit {proc.returncode})[/red]") + + self.query_one("#btn-close", Button).disabled = False + + @staticmethod + def _get_engine() -> str: + import os + engine = os.environ.get("FUZZFORGE_ENGINE__TYPE", "docker").lower() + return "podman" if engine == "podman" else "docker" diff --git a/hub-config.json b/hub-config.json index 1538889..1bc90ec 100644 --- a/hub-config.json +++ b/hub-config.json @@ -1,502 +1 @@ -{ - "servers": [ - { - "name": "nmap-mcp", - "description": "Network reconnaissance using Nmap - port scanning, service detection, OS fingerprinting", - "type": "docker", - "image": "nmap-mcp:latest", - "category": "reconnaissance", - "capabilities": [ - "NET_RAW" - ], - "enabled": true - }, - { - "name": "binwalk-mcp", - "description": "Firmware extraction and analysis using Binwalk - file signatures, entropy analysis, embedded file extraction", - "type": "docker", - "image": "binwalk-mcp:latest", - "category": "binary-analysis", - "capabilities": [], - "volumes": [ - "~/.fuzzforge/hub/workspace:/data" - ], - "enabled": true - }, - { - "name": "yara-mcp", - "description": "Pattern matching and malware classification using YARA rules", - "type": "docker", - "image": "yara-mcp:latest", - "category": "binary-analysis", - "capabilities": [], - "volumes": [ - "~/.fuzzforge/hub/workspace:/data" - ], - "enabled": true - }, - { - "name": "capa-mcp", - "description": "Static capability detection using capa - identifies malware capabilities in binaries", - "type": "docker", - "image": "capa-mcp:latest", - "category": "binary-analysis", - "capabilities": [], - "volumes": [ - "~/.fuzzforge/hub/workspace:/data" - ], - "enabled": true - }, - { - "name": "radare2-mcp", - "description": "Binary analysis and reverse engineering using radare2", - "type": "docker", - "image": "radare2-mcp:latest", - "category": "binary-analysis", - "capabilities": [], - "volumes": [ - "~/.fuzzforge/hub/workspace:/data" - ], - "enabled": true - }, - { - "name": "ghidra-mcp", - "description": "Advanced binary decompilation and reverse engineering using Ghidra", - "type": "docker", - "image": "ghcr.io/clearbluejar/pyghidra-mcp:latest", - "category": "binary-analysis", - "capabilities": [], - "volumes": [ - "~/.fuzzforge/hub/workspace:/data" - ], - "enabled": true - }, - { - "name": "searchsploit-mcp", - "description": "CVE and exploit search using SearchSploit / Exploit-DB", - "type": "docker", - "image": "searchsploit-mcp:latest", - "category": "exploitation", - "capabilities": [], - "volumes": [ - "~/.fuzzforge/hub/workspace:/data" - ], - "enabled": true - }, - { - "name": "nuclei-mcp", - "description": "Vulnerability scanning using Nuclei templates", - "type": "docker", - "image": "nuclei-mcp:latest", - "category": "web-security", - "capabilities": [ - "NET_RAW" - ], - "volumes": [ - "~/.fuzzforge/hub/workspace:/data" - ], - "enabled": true - }, - { - "name": "trivy-mcp", - "description": "Container and filesystem vulnerability scanning using Trivy", - "type": "docker", - "image": "trivy-mcp:latest", - "category": "cloud-security", - "capabilities": [], - "volumes": [ - "~/.fuzzforge/hub/workspace:/data" - ], - "enabled": true - }, - { - "name": "gitleaks-mcp", - "description": "Secret and credential detection in code and firmware using Gitleaks", - "type": "docker", - "image": "gitleaks-mcp:latest", - "category": "secrets", - "capabilities": [], - "volumes": [ - "~/.fuzzforge/hub/workspace:/data" - ], - "enabled": true - }, - { - "name": "bloodhound-mcp", - "description": "bloodhound-mcp \u2014 active-directory", - "type": "docker", - "image": "bloodhound-mcp:latest", - "category": "active-directory", - "capabilities": [], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "ida-mcp", - "description": "ida-mcp \u2014 binary-analysis", - "type": "docker", - "image": "ida-mcp:latest", - "category": "binary-analysis", - "capabilities": [], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "daml-viewer-mcp", - "description": "daml-viewer-mcp \u2014 blockchain", - "type": "docker", - "image": "daml-viewer-mcp:latest", - "category": "blockchain", - "capabilities": [], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "medusa-mcp", - "description": "medusa-mcp \u2014 blockchain", - "type": "docker", - "image": "medusa-mcp:latest", - "category": "blockchain", - "capabilities": [], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "solazy-mcp", - "description": "solazy-mcp \u2014 blockchain", - "type": "docker", - "image": "solazy-mcp:latest", - "category": "blockchain", - "capabilities": [], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "prowler-mcp", - "description": "prowler-mcp \u2014 cloud-security", - "type": "docker", - "image": "prowler-mcp:latest", - "category": "cloud-security", - "capabilities": [], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "roadrecon-mcp", - "description": "roadrecon-mcp \u2014 cloud-security", - "type": "docker", - "image": "roadrecon-mcp:latest", - "category": "cloud-security", - "capabilities": [], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "semgrep-mcp", - "description": "semgrep-mcp \u2014 code-security", - "type": "docker", - "image": "semgrep-mcp:latest", - "category": "code-security", - "capabilities": [], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "boofuzz-mcp", - "description": "boofuzz-mcp \u2014 fuzzing", - "type": "docker", - "image": "boofuzz-mcp:latest", - "category": "fuzzing", - "capabilities": [], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "dharma-mcp", - "description": "dharma-mcp \u2014 fuzzing", - "type": "docker", - "image": "dharma-mcp:latest", - "category": "fuzzing", - "capabilities": [], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "dnstwist-mcp", - "description": "dnstwist-mcp \u2014 osint", - "type": "docker", - "image": "dnstwist-mcp:latest", - "category": "osint", - "capabilities": [], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "maigret-mcp", - "description": "maigret-mcp \u2014 osint", - "type": "docker", - "image": "maigret-mcp:latest", - "category": "osint", - "capabilities": [], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "hashcat-mcp", - "description": "hashcat-mcp \u2014 password-cracking", - "type": "docker", - "image": "hashcat-mcp:latest", - "category": "password-cracking", - "capabilities": [], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "externalattacker-mcp", - "description": "externalattacker-mcp \u2014 reconnaissance", - "type": "docker", - "image": "externalattacker-mcp:latest", - "category": "reconnaissance", - "capabilities": [ - "NET_RAW" - ], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "masscan-mcp", - "description": "masscan-mcp \u2014 reconnaissance", - "type": "docker", - "image": "masscan-mcp:latest", - "category": "reconnaissance", - "capabilities": [ - "NET_RAW" - ], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "networksdb-mcp", - "description": "networksdb-mcp \u2014 reconnaissance", - "type": "docker", - "image": "networksdb-mcp:latest", - "category": "reconnaissance", - "capabilities": [ - "NET_RAW" - ], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "pd-tools-mcp", - "description": "pd-tools-mcp \u2014 reconnaissance", - "type": "docker", - "image": "pd-tools-mcp:latest", - "category": "reconnaissance", - "capabilities": [ - "NET_RAW" - ], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "shodan-mcp", - "description": "shodan-mcp \u2014 reconnaissance", - "type": "docker", - "image": "shodan-mcp:latest", - "category": "reconnaissance", - "capabilities": [ - "NET_RAW" - ], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "whatweb-mcp", - "description": "whatweb-mcp \u2014 reconnaissance", - "type": "docker", - "image": "whatweb-mcp:latest", - "category": "reconnaissance", - "capabilities": [ - "NET_RAW" - ], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "zoomeye-mcp", - "description": "zoomeye-mcp \u2014 reconnaissance", - "type": "docker", - "image": "zoomeye-mcp:latest", - "category": "reconnaissance", - "capabilities": [ - "NET_RAW" - ], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "otx-mcp", - "description": "otx-mcp \u2014 threat-intel", - "type": "docker", - "image": "otx-mcp:latest", - "category": "threat-intel", - "capabilities": [], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "virustotal-mcp", - "description": "virustotal-mcp \u2014 threat-intel", - "type": "docker", - "image": "virustotal-mcp:latest", - "category": "threat-intel", - "capabilities": [], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "burp-mcp", - "description": "burp-mcp \u2014 web-security", - "type": "docker", - "image": "burp-mcp:latest", - "category": "web-security", - "capabilities": [ - "NET_RAW" - ], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "ffuf-mcp", - "description": "ffuf-mcp \u2014 web-security", - "type": "docker", - "image": "ffuf-mcp:latest", - "category": "web-security", - "capabilities": [ - "NET_RAW" - ], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "nikto-mcp", - "description": "nikto-mcp \u2014 web-security", - "type": "docker", - "image": "nikto-mcp:latest", - "category": "web-security", - "capabilities": [ - "NET_RAW" - ], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "sqlmap-mcp", - "description": "sqlmap-mcp \u2014 web-security", - "type": "docker", - "image": "sqlmap-mcp:latest", - "category": "web-security", - "capabilities": [ - "NET_RAW" - ], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - }, - { - "name": "waybackurls-mcp", - "description": "waybackurls-mcp \u2014 web-security", - "type": "docker", - "image": "waybackurls-mcp:latest", - "category": "web-security", - "capabilities": [ - "NET_RAW" - ], - "volumes": [ - "/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data" - ], - "enabled": true, - "source_hub": "mcp-security-hub" - } - ], - "default_timeout": 300, - "cache_tools": true -} \ No newline at end of file +{"servers": []}