tui: in-UI image building, hub registry auto-recovery, clean hub-config

This commit is contained in:
AFredefon
2026-03-11 03:02:36 +01:00
parent f192771b9b
commit a3441676a3
4 changed files with 295 additions and 514 deletions

View File

@@ -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":

View File

@@ -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 (``<cwd>/.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 ``<cwd>/.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/<server_name>/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,
)

View File

@@ -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"

View File

@@ -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
}
{"servers": []}