From a824809294f329dcad1099b2038b0667af02249b Mon Sep 17 00:00:00 2001
From: AFredefon
Date: Mon, 16 Mar 2026 02:08:20 +0100
Subject: [PATCH 1/6] feat(mcp): add project assets storage and output
directory management
---
fuzzforge-mcp/src/fuzzforge_mcp/storage.py | 80 ++++++++++++++++++-
.../src/fuzzforge_mcp/tools/projects.py | 4 +-
2 files changed, 78 insertions(+), 6 deletions(-)
diff --git a/fuzzforge-mcp/src/fuzzforge_mcp/storage.py b/fuzzforge-mcp/src/fuzzforge_mcp/storage.py
index d4228d1..dbf0bce 100644
--- a/fuzzforge-mcp/src/fuzzforge_mcp/storage.py
+++ b/fuzzforge-mcp/src/fuzzforge_mcp/storage.py
@@ -13,9 +13,11 @@ from __future__ import annotations
import json
import logging
+from datetime import UTC, datetime
from pathlib import Path
from tarfile import open as Archive # noqa: N812
from typing import Any
+from uuid import uuid4
logger = logging.getLogger("fuzzforge-mcp")
@@ -79,6 +81,7 @@ class LocalStorage:
storage_path = self._get_project_path(project_path)
storage_path.mkdir(parents=True, exist_ok=True)
(storage_path / "runs").mkdir(parents=True, exist_ok=True)
+ (storage_path / "output").mkdir(parents=True, exist_ok=True)
# Create .gitignore to avoid committing large files
gitignore_path = storage_path / ".gitignore"
@@ -86,6 +89,7 @@ class LocalStorage:
gitignore_path.write_text(
"# FuzzForge storage - ignore large/temporary files\n"
"runs/\n"
+ "output/\n"
"!config.json\n"
)
@@ -141,17 +145,85 @@ class LocalStorage:
logger.info("Set project assets: %s -> %s", project_path.name, assets_path)
return assets_path
- def list_executions(self, project_path: Path) -> list[str]:
- """List all execution IDs for a project.
+ def get_project_output_path(self, project_path: Path) -> Path | None:
+ """Get the output directory path for a project.
+
+ Returns the path to the writable output directory that is mounted
+ into hub tool containers at /app/output.
:param project_path: Path to the project directory.
- :returns: List of execution IDs.
+ :returns: Path to output directory, or None if project not initialized.
+
+ """
+ output_path = self._get_project_path(project_path) / "output"
+ if output_path.exists():
+ return output_path
+ return None
+
+ def record_execution(
+ self,
+ project_path: Path,
+ server_name: str,
+ tool_name: str,
+ arguments: dict[str, Any],
+ result: dict[str, Any],
+ ) -> str:
+ """Record an execution result to the project's runs directory.
+
+ :param project_path: Path to the project directory.
+ :param server_name: Hub server name.
+ :param tool_name: Tool name that was executed.
+ :param arguments: Arguments passed to the tool.
+ :param result: Execution result dictionary.
+ :returns: Execution ID.
+
+ """
+ execution_id = f"{datetime.now(tz=UTC).strftime('%Y%m%dT%H%M%SZ')}_{uuid4().hex[:8]}"
+ run_dir = self._get_project_path(project_path) / "runs" / execution_id
+ run_dir.mkdir(parents=True, exist_ok=True)
+
+ metadata = {
+ "execution_id": execution_id,
+ "timestamp": datetime.now(tz=UTC).isoformat(),
+ "server": server_name,
+ "tool": tool_name,
+ "arguments": arguments,
+ "success": result.get("success", False),
+ "result": result,
+ }
+ (run_dir / "metadata.json").write_text(json.dumps(metadata, indent=2, default=str))
+
+ logger.info("Recorded execution %s: %s:%s", execution_id, server_name, tool_name)
+ return execution_id
+
+ def list_executions(self, project_path: Path) -> list[dict[str, Any]]:
+ """List all executions for a project with summary metadata.
+
+ :param project_path: Path to the project directory.
+ :returns: List of execution summaries (id, timestamp, server, tool, success).
"""
runs_dir = self._get_project_path(project_path) / "runs"
if not runs_dir.exists():
return []
- return [d.name for d in runs_dir.iterdir() if d.is_dir()]
+
+ executions: list[dict[str, Any]] = []
+ for run_dir in sorted(runs_dir.iterdir(), reverse=True):
+ if not run_dir.is_dir():
+ continue
+ meta_path = run_dir / "metadata.json"
+ if meta_path.exists():
+ meta = json.loads(meta_path.read_text())
+ executions.append({
+ "execution_id": meta.get("execution_id", run_dir.name),
+ "timestamp": meta.get("timestamp"),
+ "server": meta.get("server"),
+ "tool": meta.get("tool"),
+ "success": meta.get("success"),
+ })
+ else:
+ executions.append({"execution_id": run_dir.name})
+ return executions
def get_execution_results(
self,
diff --git a/fuzzforge-mcp/src/fuzzforge_mcp/tools/projects.py b/fuzzforge-mcp/src/fuzzforge_mcp/tools/projects.py
index 2530922..9d7709c 100644
--- a/fuzzforge-mcp/src/fuzzforge_mcp/tools/projects.py
+++ b/fuzzforge-mcp/src/fuzzforge_mcp/tools/projects.py
@@ -85,9 +85,9 @@ async def set_project_assets(assets_path: str) -> dict[str, Any]:
async def list_executions() -> dict[str, Any]:
"""List all executions for the current project.
- Returns a list of execution IDs that can be used to retrieve results.
+ Returns execution summaries including server, tool, timestamp, and success status.
- :return: List of execution IDs.
+ :return: List of execution summaries.
"""
storage = get_storage()
From 7924e442456f496fcf2dc3eacfc5fcbe22743db2 Mon Sep 17 00:00:00 2001
From: AFredefon
Date: Mon, 16 Mar 2026 02:09:04 +0100
Subject: [PATCH 2/6] feat(hub): volume mounts, get_agent_context convention,
category filter
---
fuzzforge-mcp/src/fuzzforge_mcp/tools/hub.py | 92 ++++++++++++++++++--
1 file changed, 87 insertions(+), 5 deletions(-)
diff --git a/fuzzforge-mcp/src/fuzzforge_mcp/tools/hub.py b/fuzzforge-mcp/src/fuzzforge_mcp/tools/hub.py
index 33d724a..dbdf491 100644
--- a/fuzzforge-mcp/src/fuzzforge_mcp/tools/hub.py
+++ b/fuzzforge-mcp/src/fuzzforge_mcp/tools/hub.py
@@ -20,10 +20,41 @@ from fuzzforge_mcp.dependencies import get_project_path, get_settings, get_stora
mcp: FastMCP = FastMCP()
+# Name of the convention tool that hub servers can implement to provide
+# rich usage context for AI agents (known issues, workflow tips, rules, etc.).
+_AGENT_CONTEXT_TOOL = "get_agent_context"
+
# Global hub executor instance (lazy initialization)
_hub_executor: HubExecutor | None = None
+async def _fetch_agent_context(
+ executor: HubExecutor,
+ server_name: str,
+ tools: list[Any],
+) -> str | None:
+ """Call get_agent_context if the server provides it.
+
+ Returns the context string, or None if the server doesn't implement
+ the convention or the call fails.
+ """
+ if not any(t.name == _AGENT_CONTEXT_TOOL for t in tools):
+ return None
+ try:
+ result = await executor.execute_tool(
+ identifier=f"hub:{server_name}:{_AGENT_CONTEXT_TOOL}",
+ arguments={},
+ )
+ if result.success and result.result:
+ content = result.result.get("content", [])
+ if content and isinstance(content, list):
+ text: str = content[0].get("text", "")
+ return text
+ except Exception: # noqa: BLE001, S110 - best-effort context fetch
+ pass
+ return None
+
+
def _get_hub_executor() -> HubExecutor:
"""Get or create the hub executor instance.
@@ -50,12 +81,15 @@ def _get_hub_executor() -> HubExecutor:
@mcp.tool
-async def list_hub_servers() -> dict[str, Any]:
+async def list_hub_servers(category: str | None = None) -> dict[str, Any]:
"""List all registered MCP hub servers.
Returns information about configured hub servers, including
their connection type, status, and discovered tool count.
+ :param category: Optional category to filter by (e.g. "binary-analysis",
+ "web-security", "reconnaissance"). Only servers in this category
+ are returned.
:return: Dictionary with list of hub servers.
"""
@@ -63,6 +97,9 @@ async def list_hub_servers() -> dict[str, Any]:
executor = _get_hub_executor()
servers = executor.list_servers()
+ if category:
+ servers = [s for s in servers if s.get("category") == category]
+
return {
"servers": servers,
"count": len(servers),
@@ -93,7 +130,14 @@ async def discover_hub_tools(server_name: str | None = None) -> dict[str, Any]:
if server_name:
tools = await executor.discover_server_tools(server_name)
- return {
+
+ # Convention: auto-fetch agent context if server provides it.
+ agent_context = await _fetch_agent_context(executor, server_name, tools)
+
+ # Hide the convention tool from the agent's tool list.
+ visible_tools = [t for t in tools if t.name != "get_agent_context"]
+
+ result: dict[str, Any] = {
"server": server_name,
"tools": [
{
@@ -102,15 +146,24 @@ async def discover_hub_tools(server_name: str | None = None) -> dict[str, Any]:
"description": t.description,
"parameters": [p.model_dump() for p in t.parameters],
}
- for t in tools
+ for t in visible_tools
],
- "count": len(tools),
+ "count": len(visible_tools),
}
+ if agent_context:
+ result["agent_context"] = agent_context
+ return result
else:
results = await executor.discover_all_tools()
all_tools = []
+ contexts: dict[str, str] = {}
for server, tools in results.items():
+ ctx = await _fetch_agent_context(executor, server, tools)
+ if ctx:
+ contexts[server] = ctx
for tool in tools:
+ if tool.name == "get_agent_context":
+ continue
all_tools.append({
"identifier": tool.identifier,
"name": tool.name,
@@ -119,11 +172,14 @@ async def discover_hub_tools(server_name: str | None = None) -> dict[str, Any]:
"parameters": [p.model_dump() for p in tool.parameters],
})
- return {
+ result = {
"servers_discovered": len(results),
"tools": all_tools,
"count": len(all_tools),
}
+ if contexts:
+ result["agent_contexts"] = contexts
+ return result
except Exception as e:
if isinstance(e, ToolError):
@@ -183,6 +239,11 @@ async def execute_hub_tool(
Always use /app/uploads/ or /app/samples/ when
passing file paths to hub tools — do NOT use the host path.
+ Tool outputs are persisted to a writable shared volume:
+ - /app/output/ (writable — extraction results, reports, etc.)
+ Files written here survive container destruction and are available
+ to subsequent tool calls. The host path is .fuzzforge/output/.
+
"""
try:
executor = _get_hub_executor()
@@ -191,6 +252,7 @@ async def execute_hub_tool(
# Mounts the assets directory at the standard paths used by hub tools:
# /app/uploads — binwalk, and other tools that use UPLOAD_DIR
# /app/samples — yara, capa, and other tools that use SAMPLES_DIR
+ # /app/output — writable volume for tool outputs (persists across calls)
extra_volumes: list[str] = []
try:
storage = get_storage()
@@ -202,6 +264,9 @@ async def execute_hub_tool(
f"{assets_str}:/app/uploads:ro",
f"{assets_str}:/app/samples:ro",
]
+ output_path = storage.get_project_output_path(project_path)
+ if output_path:
+ extra_volumes.append(f"{output_path!s}:/app/output:rw")
except Exception: # noqa: BLE001 - never block tool execution due to asset injection failure
extra_volumes = []
@@ -212,6 +277,20 @@ async def execute_hub_tool(
extra_volumes=extra_volumes or None,
)
+ # Record execution history for list_executions / get_execution_results.
+ try:
+ storage = get_storage()
+ project_path = get_project_path()
+ storage.record_execution(
+ project_path=project_path,
+ server_name=result.server_name,
+ tool_name=result.tool_name,
+ arguments=arguments or {},
+ result=result.to_dict(),
+ )
+ except Exception: # noqa: BLE001, S110 - never fail the tool call due to recording issues
+ pass
+
return result.to_dict()
except Exception as e:
@@ -372,6 +451,9 @@ async def start_hub_server(server_name: str) -> dict[str, Any]:
f"{assets_str}:/app/uploads:ro",
f"{assets_str}:/app/samples:ro",
]
+ output_path = storage.get_project_output_path(project_path)
+ if output_path:
+ extra_volumes.append(f"{output_path!s}:/app/output:rw")
except Exception: # noqa: BLE001 - never block server start due to asset injection failure
extra_volumes = []
From a51c495d34fc85b26c8402eadcce1614331bca8f Mon Sep 17 00:00:00 2001
From: AFredefon
Date: Mon, 16 Mar 2026 02:10:16 +0100
Subject: [PATCH 3/6] feat(mcp): update application instructions and hub config
---
.../src/fuzzforge_mcp/application.py | 41 +-
hub-config.json | 514 +++++++++++++++++-
2 files changed, 549 insertions(+), 6 deletions(-)
diff --git a/fuzzforge-mcp/src/fuzzforge_mcp/application.py b/fuzzforge-mcp/src/fuzzforge_mcp/application.py
index 57dcf0c..bd49a55 100644
--- a/fuzzforge-mcp/src/fuzzforge_mcp/application.py
+++ b/fuzzforge-mcp/src/fuzzforge_mcp/application.py
@@ -47,15 +47,46 @@ FuzzForge is a security research orchestration platform. Use these tools to:
Typical workflow:
1. Initialize a project with `init_project`
-2. Set project assets with `set_project_assets` (optional, only needed once for the source directory)
+2. Set project assets with `set_project_assets` — path to the directory containing
+ target files (firmware images, binaries, source code, etc.)
3. List available hub servers with `list_hub_servers`
4. Discover tools from servers with `discover_hub_tools`
5. Execute hub tools with `execute_hub_tool`
-Hub workflow:
-1. List available hub servers with `list_hub_servers`
-2. Discover tools from servers with `discover_hub_tools`
-3. Execute hub tools with `execute_hub_tool`
+Agent context convention:
+When you call `discover_hub_tools`, some servers return an `agent_context` field
+with usage tips, known issues, rule templates, and workflow guidance. Always read
+this context before using the server's tools.
+
+File access in containers:
+- Assets set via `set_project_assets` are mounted read-only at `/app/uploads/` and `/app/samples/`
+- A writable output directory is mounted at `/app/output/` — use it for extraction results, reports, etc.
+- Always use container paths (e.g. `/app/uploads/file`) when passing file arguments to hub tools
+
+Stateful tools:
+- Some tools (e.g. radare2-mcp) require multi-step sessions. Use `start_hub_server` to launch
+ a persistent container, then `execute_hub_tool` calls reuse that container. Stop with `stop_hub_server`.
+
+Firmware analysis pipeline (when analyzing firmware images):
+1. **binwalk-mcp** (`binwalk_scan` + `binwalk_extract`) — identify and extract filesystem from firmware
+2. **yara-mcp** (`yara_scan_with_rules`) — scan extracted files with vulnerability rules to prioritize targets
+3. **radare2-mcp** (persistent session) — confirm dangerous code paths
+4. **searchsploit-mcp** (`search_exploitdb`) — query version strings from radare2 against ExploitDB
+ Run steps 3 and 4 outputs feed into a final triage summary.
+
+radare2-mcp agent context (upstream tool — no embedded context):
+- Start a persistent session with `start_hub_server("radare2-mcp")` before any calls.
+- IMPORTANT: the `open_file` tool requires the parameter name `file_path` (with underscore),
+ not `filepath`. Example: `execute_hub_tool("hub:radare2-mcp:open_file", {"file_path": "/app/output/..."})`
+- Workflow: `open_file` → `analyze` → `list_imports` → `xrefs_to` → `run_command` with `pdf @ `.
+- Static binary fallback: firmware binaries are often statically linked. When `list_imports`
+ returns an empty result, fall back to `list_symbols` and search for dangerous function names
+ (system, strcpy, gets, popen, sprintf) in the output. Then use `xrefs_to` on their addresses.
+- For string extraction, use `run_command` with `iz` (data section strings).
+ The `list_all_strings` tool may return garbled output for large binaries.
+- For decompilation, use `run_command` with `pdc @ ` (pseudo-C) or `pdf @ `
+ (annotated disassembly). The `decompile` tool may fail with "not available in current mode".
+- Stop the session with `stop_hub_server("radare2-mcp")` when done.
""",
lifespan=lifespan,
)
diff --git a/hub-config.json b/hub-config.json
index 1bc90ec..f922a66 100644
--- a/hub-config.json
+++ b/hub-config.json
@@ -1 +1,513 @@
-{"servers": []}
+{
+ "servers": [
+ {
+ "name": "bloodhound-mcp",
+ "description": "bloodhound-mcp \u2014 active-directory",
+ "type": "docker",
+ "image": "bloodhound-mcp:latest",
+ "category": "active-directory",
+ "capabilities": [],
+ "volumes": [
+ "/home/afredefon/.fuzzforge/hub/workspace:/data"
+ ],
+ "enabled": true,
+ "source_hub": "mcp-security-hub"
+ },
+ {
+ "name": "binwalk-mcp",
+ "description": "binwalk-mcp \u2014 binary-analysis",
+ "type": "docker",
+ "image": "binwalk-mcp:latest",
+ "category": "binary-analysis",
+ "capabilities": [],
+ "volumes": [
+ "/home/afredefon/.fuzzforge/hub/workspace:/data"
+ ],
+ "enabled": true,
+ "source_hub": "mcp-security-hub"
+ },
+ {
+ "name": "capa-mcp",
+ "description": "capa-mcp \u2014 binary-analysis",
+ "type": "docker",
+ "image": "capa-mcp:latest",
+ "category": "binary-analysis",
+ "capabilities": [],
+ "volumes": [
+ "/home/afredefon/.fuzzforge/hub/workspace:/data"
+ ],
+ "enabled": true,
+ "source_hub": "mcp-security-hub"
+ },
+ {
+ "name": "ghidra-mcp",
+ "description": "ghidra-mcp \u2014 binary-analysis",
+ "type": "docker",
+ "image": "ghidra-mcp:latest",
+ "category": "binary-analysis",
+ "capabilities": [],
+ "volumes": [
+ "/home/afredefon/.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/.fuzzforge/hub/workspace:/data"
+ ],
+ "enabled": true,
+ "source_hub": "mcp-security-hub"
+ },
+ {
+ "name": "radare2-mcp",
+ "description": "radare2-mcp \u2014 binary-analysis",
+ "type": "docker",
+ "image": "radare2-mcp:latest",
+ "category": "binary-analysis",
+ "capabilities": [],
+ "volumes": [
+ "/home/afredefon/.fuzzforge/hub/workspace:/data"
+ ],
+ "enabled": true,
+ "source_hub": "mcp-security-hub"
+ },
+ {
+ "name": "yara-mcp",
+ "description": "yara-mcp \u2014 binary-analysis",
+ "type": "docker",
+ "image": "yara-mcp:latest",
+ "category": "binary-analysis",
+ "capabilities": [],
+ "volumes": [
+ "/home/afredefon/.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/.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/.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/.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/.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/.fuzzforge/hub/workspace:/data"
+ ],
+ "enabled": true,
+ "source_hub": "mcp-security-hub"
+ },
+ {
+ "name": "trivy-mcp",
+ "description": "trivy-mcp \u2014 cloud-security",
+ "type": "docker",
+ "image": "trivy-mcp:latest",
+ "category": "cloud-security",
+ "capabilities": [],
+ "volumes": [
+ "/home/afredefon/.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/.fuzzforge/hub/workspace:/data"
+ ],
+ "enabled": true,
+ "source_hub": "mcp-security-hub"
+ },
+ {
+ "name": "searchsploit-mcp",
+ "description": "searchsploit-mcp \u2014 exploitation",
+ "type": "docker",
+ "image": "searchsploit-mcp:latest",
+ "category": "exploitation",
+ "capabilities": [],
+ "volumes": [
+ "/home/afredefon/.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/.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/.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/.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/.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/.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/.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/.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/.fuzzforge/hub/workspace:/data"
+ ],
+ "enabled": true,
+ "source_hub": "mcp-security-hub"
+ },
+ {
+ "name": "nmap-mcp",
+ "description": "nmap-mcp \u2014 reconnaissance",
+ "type": "docker",
+ "image": "nmap-mcp:latest",
+ "category": "reconnaissance",
+ "capabilities": [
+ "NET_RAW"
+ ],
+ "volumes": [
+ "/home/afredefon/.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/.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/.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/.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/.fuzzforge/hub/workspace:/data"
+ ],
+ "enabled": true,
+ "source_hub": "mcp-security-hub"
+ },
+ {
+ "name": "gitleaks-mcp",
+ "description": "gitleaks-mcp \u2014 secrets",
+ "type": "docker",
+ "image": "gitleaks-mcp:latest",
+ "category": "secrets",
+ "capabilities": [],
+ "volumes": [
+ "/home/afredefon/.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/.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/.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/.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/.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/.fuzzforge/hub/workspace:/data"
+ ],
+ "enabled": true,
+ "source_hub": "mcp-security-hub"
+ },
+ {
+ "name": "nuclei-mcp",
+ "description": "nuclei-mcp \u2014 web-security",
+ "type": "docker",
+ "image": "nuclei-mcp:latest",
+ "category": "web-security",
+ "capabilities": [
+ "NET_RAW"
+ ],
+ "volumes": [
+ "/home/afredefon/.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/.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/.fuzzforge/hub/workspace:/data"
+ ],
+ "enabled": true,
+ "source_hub": "mcp-security-hub"
+ }
+ ]
+}
\ No newline at end of file
From c59b6ba81ac0ef80a5c6583511df285e198d8159 Mon Sep 17 00:00:00 2001
From: AFredefon
Date: Tue, 17 Mar 2026 04:21:06 +0100
Subject: [PATCH 4/6] fix(mcp): fix mypy type error in executions resource and
improve tool docstrings
---
.../src/fuzzforge_mcp/resources/executions.py | 6 ++---
.../src/fuzzforge_mcp/tools/projects.py | 22 +++++++++----------
2 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/fuzzforge-mcp/src/fuzzforge_mcp/resources/executions.py b/fuzzforge-mcp/src/fuzzforge_mcp/resources/executions.py
index a720761..61df782 100644
--- a/fuzzforge-mcp/src/fuzzforge_mcp/resources/executions.py
+++ b/fuzzforge-mcp/src/fuzzforge_mcp/resources/executions.py
@@ -30,10 +30,10 @@ async def list_executions() -> list[dict[str, Any]]:
return [
{
- "execution_id": exec_id,
- "has_results": storage.get_execution_results(project_path, exec_id) is not None,
+ "execution_id": entry["execution_id"],
+ "has_results": storage.get_execution_results(project_path, entry["execution_id"]) is not None,
}
- for exec_id in execution_ids
+ for entry in execution_ids
]
except Exception as exception:
diff --git a/fuzzforge-mcp/src/fuzzforge_mcp/tools/projects.py b/fuzzforge-mcp/src/fuzzforge_mcp/tools/projects.py
index 9d7709c..52b6c4a 100644
--- a/fuzzforge-mcp/src/fuzzforge_mcp/tools/projects.py
+++ b/fuzzforge-mcp/src/fuzzforge_mcp/tools/projects.py
@@ -15,15 +15,14 @@ mcp: FastMCP = FastMCP()
@mcp.tool
async def init_project(project_path: str | None = None) -> dict[str, Any]:
- """Initialize a new FuzzForge project.
+ """Initialize a new FuzzForge project workspace.
- Creates a `.fuzzforge/` directory inside the project for storing:
- - config.json: Project configuration
- - runs/: Execution results
+ Creates a `.fuzzforge/` directory for storing configuration and execution results.
+ Call this once before using hub tools. The project path is a working directory
+ for FuzzForge state — it does not need to contain the files you want to analyze.
+ Use `set_project_assets` separately to specify the target files.
- This should be called before executing hub tools.
-
- :param project_path: Path to the project directory. If not provided, uses current directory.
+ :param project_path: Working directory for FuzzForge state. Defaults to current directory.
:return: Project initialization result.
"""
@@ -51,12 +50,13 @@ async def init_project(project_path: str | None = None) -> dict[str, Any]:
@mcp.tool
async def set_project_assets(assets_path: str) -> dict[str, Any]:
- """Set the initial assets (source code) for a project.
+ """Set the directory containing target files to analyze.
- This sets the DEFAULT source directory that will be mounted into
- hub tool containers via volume mounts.
+ Points FuzzForge to the directory with your analysis targets
+ (firmware images, binaries, source code, etc.). This directory
+ is mounted read-only into hub tool containers.
- :param assets_path: Path to the project source directory.
+ :param assets_path: Path to the directory containing files to analyze.
:return: Result including stored assets path.
"""
From 575b90f8d44f7cd1d6249ef505f69716b79c0da6 Mon Sep 17 00:00:00 2001
From: AFredefon
Date: Tue, 17 Mar 2026 07:58:26 +0100
Subject: [PATCH 5/6] docs: rewrite README for hub-centric architecture, remove
demo GIFs
---
README.md | 242 +++++++++++++++++++------------------------
assets/demopart1.gif | Bin 368961 -> 0 bytes
assets/demopart2.gif | Bin 2252477 -> 0 bytes
3 files changed, 108 insertions(+), 134 deletions(-)
delete mode 100644 assets/demopart1.gif
delete mode 100644 assets/demopart2.gif
diff --git a/README.md b/README.md
index a180513..86951e5 100644
--- a/README.md
+++ b/README.md
@@ -17,9 +17,9 @@
Overview •
Features •
+ Security Hub •
Installation •
Usage Guide •
- Modules •
Contributing
@@ -32,39 +32,44 @@
## 🚀 Overview
-**FuzzForge AI** is an open-source runtime that enables AI agents (GitHub Copilot, Claude, etc.) to orchestrate security research workflows through the **Model Context Protocol (MCP)**.
+**FuzzForge AI** is an open-source MCP server that enables AI agents (GitHub Copilot, Claude, etc.) to orchestrate security research workflows through the **Model Context Protocol (MCP)**.
-### The Core: Modules
+FuzzForge connects your AI assistant to **MCP tool hubs** — collections of containerized security tools that the agent can discover, chain, and execute autonomously. Instead of manually running security tools, describe what you want and let your AI assistant handle it.
-At the heart of FuzzForge are **modules** - containerized security tools that AI agents can discover, configure, and orchestrate. Each module encapsulates a specific security capability (static analysis, fuzzing, crash analysis, etc.) and runs in an isolated container.
+### The Core: Hub Architecture
-- **🔌 Plug & Play**: Modules are self-contained - just pull and run
-- **🤖 AI-Native**: Designed for AI agent orchestration via MCP
-- **🔗 Composable**: Chain modules together into automated workflows
-- **📦 Extensible**: Build custom modules with the Python SDK
+FuzzForge acts as a **meta-MCP server** — a single MCP endpoint that gives your AI agent access to tools from multiple MCP hub servers. Each hub server is a containerized security tool (Binwalk, YARA, Radare2, Nmap, etc.) that the agent can discover at runtime.
-FuzzForge AI handles module discovery, execution, and result collection. Security modules (developed separately) provide the actual security tooling - from static analyzers to fuzzers to crash triagers.
+- **🔍 Discovery**: The agent lists available hub servers and discovers their tools
+- **🤖 AI-Native**: Hub tools provide agent context — usage tips, workflow guidance, and domain knowledge
+- **🔗 Composable**: Chain tools from different hubs into automated pipelines
+- **📦 Extensible**: Add your own MCP servers to the hub registry
-Instead of manually running security tools, describe what you want and let your AI assistant handle it.
+### 🎬 Use Case: Firmware Vulnerability Research
+
+> **Scenario**: Analyze a firmware image to find security vulnerabilities — fully automated by an AI agent.
+
+```
+User: "Search for vulnerabilities in firmware.bin"
+
+Agent → Binwalk: Extract filesystem from firmware image
+Agent → YARA: Scan extracted files for vulnerability patterns
+Agent → Radare2: Trace dangerous function calls in prioritized binaries
+Agent → Report: 8 vulnerabilities found (2 critical, 4 high, 2 medium)
+```
### 🎬 Use Case: Rust Fuzzing Pipeline
> **Scenario**: Fuzz a Rust crate to discover vulnerabilities using AI-assisted harness generation and parallel fuzzing.
-
-
- | 1️⃣ Analyze, Generate & Validate Harnesses |
- 2️⃣ Run Parallel Continuous Fuzzing |
-
-
-  |
-  |
-
-
- | AI agent analyzes code, generates harnesses, and validates they compile |
- Multiple fuzzing sessions run in parallel with live metrics |
-
-
+```
+User: "Fuzz the blurhash crate for vulnerabilities"
+
+Agent → Rust Analyzer: Identify fuzzable functions and attack surface
+Agent → Harness Gen: Generate and validate fuzzing harnesses
+Agent → Cargo Fuzzer: Run parallel coverage-guided fuzzing sessions
+Agent → Crash Analysis: Deduplicate and triage discovered crashes
+```
---
@@ -82,13 +87,13 @@ If you find FuzzForge useful, please **star the repo** to support development!
| Feature | Description |
|---------|-------------|
-| 🤖 **AI-Native** | Built for MCP - works with GitHub Copilot, Claude, and any MCP-compatible agent |
-| 📦 **Containerized** | Each module runs in isolation via Docker or Podman |
-| 🔄 **Continuous Mode** | Long-running tasks (fuzzing) with real-time metrics streaming |
-| 🔗 **Workflows** | Chain multiple modules together in automated pipelines |
-| 🛠️ **Extensible** | Create custom modules with the Python SDK |
-| 🏠 **Local First** | All execution happens on your machine - no cloud required |
-| 🔒 **Secure** | Sandboxed containers with no network access by default |
+| 🤖 **AI-Native** | Built for MCP — works with GitHub Copilot, Claude, and any MCP-compatible agent |
+| 🔌 **Hub System** | Connect to MCP tool hubs — each hub brings dozens of containerized security tools |
+| 🔍 **Tool Discovery** | Agents discover available tools at runtime with built-in usage guidance |
+| 🔗 **Pipelines** | Chain tools from different hubs into automated multi-step workflows |
+| 🔄 **Persistent Sessions** | Long-running tools (Radare2, fuzzers) with stateful container sessions |
+| 🏠 **Local First** | All execution happens on your machine — no cloud required |
+| 🔒 **Sandboxed** | Every tool runs in an isolated container via Docker or Podman |
---
@@ -102,27 +107,57 @@ If you find FuzzForge useful, please **star the repo** to support development!
▼
┌─────────────────────────────────────────────────────────────────┐
│ FuzzForge MCP Server │
-│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
-│ │list_modules │ │execute_module│ │start_continuous_module │ │
-│ └─────────────┘ └──────────────┘ └────────────────────────┘ │
+│ │
+│ Projects Hub Discovery Hub Execution │
+│ ┌──────────────┐ ┌──────────────────┐ ┌───────────────────┐ │
+│ │init_project │ │list_hub_servers │ │execute_hub_tool │ │
+│ │set_assets │ │discover_hub_tools│ │start_hub_server │ │
+│ │list_results │ │get_tool_schema │ │stop_hub_server │ │
+│ └──────────────┘ └──────────────────┘ └───────────────────┘ │
└───────────────────────────┬─────────────────────────────────────┘
- │
+ │ Docker/Podman
▼
┌─────────────────────────────────────────────────────────────────┐
-│ FuzzForge Runner │
-│ Container Engine (Docker/Podman) │
-└───────────────────────────┬─────────────────────────────────────┘
- │
- ┌───────────────────┼───────────────────┐
- ▼ ▼ ▼
-┌───────────────┐ ┌───────────────┐ ┌───────────────┐
-│ Module A │ │ Module B │ │ Module C │
-│ (Container) │ │ (Container) │ │ (Container) │
-└───────────────┘ └───────────────┘ └───────────────┘
+│ MCP Hub Servers │
+│ │
+│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
+│ │ Binwalk │ │ YARA │ │ Radare2 │ │ Nmap │ │
+│ │ 6 tools │ │ 5 tools │ │ 32 tools │ │ 8 tools │ │
+│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
+│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
+│ │ Nuclei │ │ SQLMap │ │ Trivy │ │ ... │ │
+│ │ 7 tools │ │ 8 tools │ │ 7 tools │ │ 36 hubs │ │
+│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
+└─────────────────────────────────────────────────────────────────┘
```
---
+## 🔧 MCP Security Hub
+
+FuzzForge ships with built-in support for the **[MCP Security Hub](https://github.com/FuzzingLabs/mcp-security-hub)** — a collection of 36 production-ready, Dockerized MCP servers covering offensive security:
+
+| Category | Servers | Examples |
+|----------|---------|----------|
+| 🔍 **Reconnaissance** | 8 | Nmap, Masscan, Shodan, WhatWeb |
+| 🌐 **Web Security** | 6 | Nuclei, SQLMap, ffuf, Nikto |
+| 🔬 **Binary Analysis** | 6 | Radare2, Binwalk, YARA, Capa, Ghidra |
+| ⛓️ **Blockchain** | 3 | Medusa, Solazy, DAML Viewer |
+| ☁️ **Cloud Security** | 3 | Trivy, Prowler, RoadRecon |
+| 💻 **Code Security** | 1 | Semgrep |
+| 🔑 **Secrets Detection** | 1 | Gitleaks |
+| 💥 **Exploitation** | 1 | SearchSploit |
+| 🎯 **Fuzzing** | 2 | Boofuzz, Dharma |
+| 🕵️ **OSINT** | 2 | Maigret, DNSTwist |
+| 🛡️ **Threat Intel** | 2 | VirusTotal, AlienVault OTX |
+| 🏰 **Active Directory** | 1 | BloodHound |
+
+> 185+ individual tools accessible through a single MCP connection.
+
+The hub is open source and can be extended with your own MCP servers. See the [mcp-security-hub repository](https://github.com/FuzzingLabs/mcp-security-hub) for details.
+
+---
+
## 📦 Installation
### Prerequisites
@@ -140,11 +175,20 @@ cd fuzzforge_ai
# Install dependencies
uv sync
-
-# Build module images
-make build-modules
```
+### Link the Security Hub
+
+```bash
+# Clone the MCP Security Hub
+git clone https://github.com/FuzzingLabs/mcp-security-hub.git ~/.fuzzforge/hubs/mcp-security-hub
+
+# Build the Docker images for the hub tools
+./scripts/build-hub-images.sh
+```
+
+Or use the terminal UI (`uv run fuzzforge ui`) to link hubs interactively.
+
### Configure MCP for Your AI Agent
```bash
@@ -165,81 +209,20 @@ uv run fuzzforge mcp status
---
-## 📦 Modules
+## 🧑💻 Usage
-FuzzForge modules are containerized security tools that AI agents can orchestrate. The module ecosystem is designed around a simple principle: **the OSS runtime orchestrates, enterprise modules execute**.
+Once installed, just talk to your AI agent:
-### Module Ecosystem
-
-| | FuzzForge AI | FuzzForge Enterprise Modules |
-|---|---|---|
-| **What** | Runtime & MCP server | Security research modules |
-| **License** | Apache 2.0 | BSL 1.1 (Business Source License) |
-| **Compatibility** | ✅ Runs any compatible module | ✅ Works with FuzzForge AI |
-
-**Enterprise modules** are developed separately and provide production-ready security tooling:
-
-| Category | Modules | Description |
-|----------|---------|-------------|
-| 🔍 **Static Analysis** | Rust Analyzer, Solidity Analyzer, Cairo Analyzer | Code analysis and fuzzable function detection |
-| 🎯 **Fuzzing** | Cargo Fuzzer, Honggfuzz, AFL++ | Coverage-guided fuzz testing |
-| 💥 **Crash Analysis** | Crash Triager, Root Cause Analyzer | Automated crash deduplication and analysis |
-| 🔐 **Vulnerability Detection** | Pattern Matcher, Taint Analyzer | Security vulnerability scanning |
-| 📝 **Reporting** | Report Generator, SARIF Exporter | Automated security report generation |
-
-> 💡 **Build your own modules!** The FuzzForge SDK allows you to create custom modules that integrate seamlessly with FuzzForge AI. See [Creating Custom Modules](#-creating-custom-modules).
-
-### Execution Modes
-
-Modules run in two execution modes:
-
-#### One-shot Execution
-
-Run a module once and get results:
-
-```python
-result = execute_module("my-analyzer", assets_path="/path/to/project")
+```
+"What security tools are available?"
+"Scan this firmware image for vulnerabilities"
+"Analyze this binary with radare2"
+"Run nuclei against https://example.com"
```
-#### Continuous Execution
+The agent will use FuzzForge to discover the right hub tools, chain them into a pipeline, and return results — all without you touching a terminal.
-For long-running tasks like fuzzing, with real-time metrics:
-
-```python
-# Start continuous execution
-session = start_continuous_module("my-fuzzer",
- assets_path="/path/to/project",
- configuration={"target": "my_target"})
-
-# Check status with live metrics
-status = get_continuous_status(session["session_id"])
-
-# Stop and collect results
-stop_continuous_module(session["session_id"])
-```
-
----
-
-## 🛠️ Creating Custom Modules
-
-Build your own security modules with the FuzzForge SDK:
-
-```python
-from fuzzforge_modules_sdk import FuzzForgeModule, FuzzForgeModuleResults
-
-class MySecurityModule(FuzzForgeModule):
- def _run(self, resources):
- self.emit_event("started", target=resources[0].path)
-
- # Your analysis logic here
- results = self.analyze(resources)
-
- self.emit_progress(100, status="completed",
- message=f"Analysis complete")
- return FuzzForgeModuleResults.SUCCESS
-```
-
-📖 See the [Module SDK Guide](fuzzforge-modules/fuzzforge-modules-sdk/README.md) for details.
+See the [Usage Guide](USAGE.md) for detailed setup and advanced workflows.
---
@@ -247,26 +230,17 @@ class MySecurityModule(FuzzForgeModule):
```
fuzzforge_ai/
-├── fuzzforge-cli/ # Command-line interface
+├── fuzzforge-mcp/ # MCP server — the core of FuzzForge
+├── fuzzforge-cli/ # Command-line interface & terminal UI
├── fuzzforge-common/ # Shared abstractions (containers, storage)
-├── fuzzforge-mcp/ # MCP server for AI agents
-├── fuzzforge-modules/ # Security modules
-│ └── fuzzforge-modules-sdk/ # Module development SDK
-├── fuzzforge-runner/ # Local execution engine
-├── fuzzforge-types/ # Type definitions & schemas
-└── demo/ # Demo projects for testing
+├── fuzzforge-runner/ # Container execution engine (Docker/Podman)
+├── fuzzforge-tests/ # Integration tests
+├── mcp-security-hub/ # Default hub: 36 offensive security MCP servers
+└── scripts/ # Hub image build scripts
```
---
-## 🗺️ What's Next
-
-**[MCP Security Hub](https://github.com/FuzzingLabs/mcp-security-hub) integration** — Bridge 175+ offensive security tools (Nmap, Nuclei, Ghidra, and more) into FuzzForge workflows, all orchestrated by AI agents.
-
-See [ROADMAP.md](ROADMAP.md) for the full roadmap.
-
----
-
## 🤝 Contributing
We welcome contributions from the community!
@@ -274,7 +248,7 @@ We welcome contributions from the community!
- 🐛 Report bugs via [GitHub Issues](../../issues)
- 💡 Suggest features or improvements
- 🔧 Submit pull requests
-- 📦 Share your custom modules
+- 🔌 Add new MCP servers to the [Security Hub](https://github.com/FuzzingLabs/mcp-security-hub)
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
diff --git a/assets/demopart1.gif b/assets/demopart1.gif
deleted file mode 100644
index db09ca04c2b0f5a44defe30b7f67e440273740b9..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 368961
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 wUi3
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 |