mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-03-14 21:05:55 +00:00
Compare commits
2 Commits
refactor/r
...
feature/tu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9376645197 | ||
|
|
3e0d1cd02f |
413
USAGE.md
413
USAGE.md
@@ -1,8 +1,9 @@
|
||||
# FuzzForge AI Usage Guide
|
||||
|
||||
This guide covers everything you need to know to get started with FuzzForge AI - from installation to running your first security research workflow with AI.
|
||||
This guide covers everything you need to know to get started with FuzzForge AI — from installation to linking your first MCP hub and running security research workflows with AI.
|
||||
|
||||
> **FuzzForge is designed to be used with AI agents** (GitHub Copilot, Claude, etc.) via MCP.
|
||||
> A terminal UI (`fuzzforge ui`) is provided for managing agents and hubs.
|
||||
> The CLI is available for advanced users but the primary experience is through natural language interaction with your AI assistant.
|
||||
|
||||
---
|
||||
@@ -12,8 +13,17 @@ This guide covers everything you need to know to get started with FuzzForge AI -
|
||||
- [Quick Start](#quick-start)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation](#installation)
|
||||
- [Building Modules](#building-modules)
|
||||
- [MCP Server Configuration](#mcp-server-configuration)
|
||||
- [Terminal UI](#terminal-ui)
|
||||
- [Launching the UI](#launching-the-ui)
|
||||
- [Dashboard](#dashboard)
|
||||
- [Agent Setup](#agent-setup)
|
||||
- [Hub Manager](#hub-manager)
|
||||
- [MCP Hub System](#mcp-hub-system)
|
||||
- [What is an MCP Hub?](#what-is-an-mcp-hub)
|
||||
- [FuzzingLabs Security Hub](#fuzzinglabs-security-hub)
|
||||
- [Linking a Custom Hub](#linking-a-custom-hub)
|
||||
- [Building Hub Images](#building-hub-images)
|
||||
- [MCP Server Configuration (CLI)](#mcp-server-configuration-cli)
|
||||
- [GitHub Copilot](#github-copilot)
|
||||
- [Claude Code (CLI)](#claude-code-cli)
|
||||
- [Claude Desktop](#claude-desktop)
|
||||
@@ -27,7 +37,7 @@ This guide covers everything you need to know to get started with FuzzForge AI -
|
||||
## Quick Start
|
||||
|
||||
> **Prerequisites:** You need [uv](https://docs.astral.sh/uv/) and [Docker](https://docs.docker.com/get-docker/) installed.
|
||||
> See the [Prerequisites](#prerequisites) section for installation instructions.
|
||||
> See the [Prerequisites](#prerequisites) section for details.
|
||||
|
||||
```bash
|
||||
# 1. Clone and install
|
||||
@@ -35,20 +45,29 @@ git clone https://github.com/FuzzingLabs/fuzzforge_ai.git
|
||||
cd fuzzforge_ai
|
||||
uv sync
|
||||
|
||||
# 2. Build the module images (one-time setup)
|
||||
make build-modules
|
||||
# 2. Launch the terminal UI
|
||||
uv run fuzzforge ui
|
||||
|
||||
# 3. Install MCP for your AI agent
|
||||
uv run fuzzforge mcp install copilot # For VS Code + GitHub Copilot
|
||||
# OR
|
||||
uv run fuzzforge mcp install claude-code # For Claude Code CLI
|
||||
|
||||
# 4. Restart your AI agent (VS Code, Claude, etc.)
|
||||
|
||||
# 5. Start talking to your AI:
|
||||
# "List available FuzzForge modules"
|
||||
# 3. Press 'h' → "FuzzingLabs Hub" to clone & link the default security hub
|
||||
# 4. Select an agent row and press Enter to link it
|
||||
# 5. Restart your AI agent and start talking:
|
||||
# "What security tools are available?"
|
||||
# "Scan this binary with binwalk and yara"
|
||||
# "Analyze this Rust crate for fuzzable functions"
|
||||
# "Start fuzzing the parse_input function"
|
||||
```
|
||||
|
||||
Or do it entirely from the command line:
|
||||
|
||||
```bash
|
||||
# Install MCP for your AI agent
|
||||
uv run fuzzforge mcp install copilot # For VS Code + GitHub Copilot
|
||||
# OR
|
||||
uv run fuzzforge mcp install claude-code # For Claude Code CLI
|
||||
|
||||
# Build hub tool images
|
||||
./scripts/build-hub-images.sh
|
||||
|
||||
# Restart your AI agent — done!
|
||||
```
|
||||
|
||||
> **Note:** FuzzForge uses Docker by default. Podman is also supported via `--engine podman`.
|
||||
@@ -59,9 +78,10 @@ uv run fuzzforge mcp install claude-code # For Claude Code CLI
|
||||
|
||||
Before installing FuzzForge AI, ensure you have:
|
||||
|
||||
- **Python 3.12+** - [Download Python](https://www.python.org/downloads/)
|
||||
- **uv** package manager - [Install uv](https://docs.astral.sh/uv/)
|
||||
- **Docker** - Container runtime ([Install Docker](https://docs.docker.com/get-docker/))
|
||||
- **Python 3.12+** — [Download Python](https://www.python.org/downloads/)
|
||||
- **uv** package manager — [Install uv](https://docs.astral.sh/uv/)
|
||||
- **Docker** — Container runtime ([Install Docker](https://docs.docker.com/get-docker/))
|
||||
- **Git** — For cloning hub repositories
|
||||
|
||||
### Installing uv
|
||||
|
||||
@@ -115,74 +135,164 @@ uv run fuzzforge --help
|
||||
|
||||
---
|
||||
|
||||
## Building Modules
|
||||
## Terminal UI
|
||||
|
||||
FuzzForge modules are containerized security tools. After cloning, you need to build them once:
|
||||
FuzzForge ships with a terminal user interface (TUI) built on [Textual](https://textual.textualize.io/) for managing AI agents and MCP hub servers from a single dashboard.
|
||||
|
||||
### Build All Modules
|
||||
### Launching the UI
|
||||
|
||||
```bash
|
||||
# From the fuzzforge_ai directory
|
||||
make build-modules
|
||||
uv run fuzzforge ui
|
||||
```
|
||||
|
||||
This builds all available modules:
|
||||
- `fuzzforge-rust-analyzer` - Analyzes Rust code for fuzzable functions
|
||||
- `fuzzforge-cargo-fuzzer` - Runs cargo-fuzz on Rust crates
|
||||
- `fuzzforge-harness-validator` - Validates generated fuzzing harnesses
|
||||
- `fuzzforge-crash-analyzer` - Analyzes crash inputs
|
||||
### Dashboard
|
||||
|
||||
### Build a Single Module
|
||||
The main screen is split into two panels:
|
||||
|
||||
```bash
|
||||
# Build a specific module
|
||||
cd fuzzforge-modules/rust-analyzer
|
||||
make build
|
||||
```
|
||||
| Panel | Content |
|
||||
|-------|---------|
|
||||
| **AI Agents** (left) | Shows GitHub Copilot, Claude Desktop, and Claude Code with live link status and config file path |
|
||||
| **Hub Servers** (right) | Shows all configured MCP hub tools with Docker image name, source hub, and build status (✓ Ready / ✗ Not built) |
|
||||
|
||||
### Verify Modules are Built
|
||||
### Keyboard Shortcuts
|
||||
|
||||
```bash
|
||||
# List built module images
|
||||
docker images | grep fuzzforge
|
||||
```
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Enter` | **Select** — Act on the selected row (setup/unlink an agent) |
|
||||
| `h` | **Hub Manager** — Open the hub management screen |
|
||||
| `r` | **Refresh** — Re-check all agent and hub statuses |
|
||||
| `q` | **Quit** |
|
||||
|
||||
You should see something like:
|
||||
```
|
||||
fuzzforge-rust-analyzer 0.1.0 abc123def456 2 minutes ago 850 MB
|
||||
fuzzforge-cargo-fuzzer 0.1.0 789ghi012jkl 2 minutes ago 1.2 GB
|
||||
...
|
||||
```
|
||||
### Agent Setup
|
||||
|
||||
Select an agent row in the AI Agents table and press `Enter`:
|
||||
|
||||
- **If the agent is not linked** → a setup dialog opens asking for your container engine (Docker or Podman), then installs the FuzzForge MCP configuration
|
||||
- **If the agent is already linked** → a confirmation dialog offers to unlink it (removes the `fuzzforge` entry without touching other MCP servers)
|
||||
|
||||
The setup auto-detects:
|
||||
- FuzzForge installation root
|
||||
- Docker/Podman socket path
|
||||
- Hub configuration from `hub-config.json`
|
||||
|
||||
### Hub Manager
|
||||
|
||||
Press `h` to open the hub manager. This is where you manage your MCP hub repositories:
|
||||
|
||||
| Button | Action |
|
||||
|--------|--------|
|
||||
| **FuzzingLabs Hub** | One-click clone of the official [mcp-security-hub](https://github.com/FuzzingLabs/mcp-security-hub) repository — clones to `~/.fuzzforge/hubs/mcp-security-hub`, scans for tools, and registers them in `hub-config.json` |
|
||||
| **Link Path** | Link any local directory as a hub — enter a name and path, FuzzForge scans it for `category/tool-name/Dockerfile` patterns |
|
||||
| **Clone URL** | Clone any git repository and link it as a hub |
|
||||
| **Remove** | Unlink the selected hub and remove its servers from the configuration |
|
||||
|
||||
The hub table shows:
|
||||
- **Name** — Hub name (★ prefix for the default hub)
|
||||
- **Path** — Local directory path
|
||||
- **Servers** — Number of MCP tools discovered
|
||||
- **Source** — Git URL or "local"
|
||||
|
||||
---
|
||||
|
||||
## MCP Server Configuration
|
||||
## MCP Hub System
|
||||
|
||||
FuzzForge integrates with AI agents through the Model Context Protocol (MCP). Configure your preferred AI agent to use FuzzForge tools.
|
||||
### What is an MCP Hub?
|
||||
|
||||
An MCP hub is a directory containing one or more containerized MCP tools, organized by category:
|
||||
|
||||
```
|
||||
my-hub/
|
||||
├── category-a/
|
||||
│ ├── tool-1/
|
||||
│ │ └── Dockerfile
|
||||
│ └── tool-2/
|
||||
│ └── Dockerfile
|
||||
├── category-b/
|
||||
│ └── tool-3/
|
||||
│ └── Dockerfile
|
||||
└── ...
|
||||
```
|
||||
|
||||
FuzzForge scans for the pattern `category/tool-name/Dockerfile` and auto-generates server configuration entries for each discovered tool.
|
||||
|
||||
### FuzzingLabs Security Hub
|
||||
|
||||
The default MCP hub is [mcp-security-hub](https://github.com/FuzzingLabs/mcp-security-hub), maintained by FuzzingLabs. It includes **40+ security tools** across categories:
|
||||
|
||||
| Category | Tools |
|
||||
|----------|-------|
|
||||
| **Reconnaissance** | nmap, masscan, shodan, zoomeye, whatweb, pd-tools, externalattacker, networksdb |
|
||||
| **Binary Analysis** | binwalk, yara, capa, radare2, ghidra, ida |
|
||||
| **Code Security** | semgrep, rust-analyzer, harness-tester, cargo-fuzzer, crash-analyzer |
|
||||
| **Web Security** | nuclei, nikto, sqlmap, ffuf, burp, waybackurls |
|
||||
| **Fuzzing** | boofuzz, dharma |
|
||||
| **Exploitation** | searchsploit |
|
||||
| **Secrets** | gitleaks |
|
||||
| **Cloud Security** | trivy, prowler, roadrecon |
|
||||
| **OSINT** | maigret, dnstwist |
|
||||
| **Threat Intel** | virustotal, otx |
|
||||
| **Password Cracking** | hashcat |
|
||||
| **Blockchain** | medusa, solazy, daml-viewer |
|
||||
|
||||
**Clone it via the UI:**
|
||||
|
||||
1. `uv run fuzzforge ui`
|
||||
2. Press `h` → click **FuzzingLabs Hub**
|
||||
3. Wait for the clone to finish — servers are auto-registered
|
||||
|
||||
**Or clone manually:**
|
||||
|
||||
```bash
|
||||
git clone git@github.com:FuzzingLabs/mcp-security-hub.git ~/.fuzzforge/hubs/mcp-security-hub
|
||||
```
|
||||
|
||||
### Linking a Custom Hub
|
||||
|
||||
You can link any directory that follows the `category/tool-name/Dockerfile` layout:
|
||||
|
||||
**Via the UI:**
|
||||
|
||||
1. Press `h` → **Link Path**
|
||||
2. Enter a name and the directory path
|
||||
|
||||
**Via the CLI (planned):** Not yet available — use the UI.
|
||||
|
||||
### Building Hub Images
|
||||
|
||||
After linking a hub, you need to build the Docker images before the tools can be used:
|
||||
|
||||
```bash
|
||||
# Build all images from the default security hub
|
||||
./scripts/build-hub-images.sh
|
||||
|
||||
# Or build a single tool image
|
||||
docker build -t semgrep-mcp:latest mcp-security-hub/code-security/semgrep-mcp/
|
||||
```
|
||||
|
||||
The dashboard hub table shows ✓ Ready for built images and ✗ Not built for missing ones.
|
||||
|
||||
---
|
||||
|
||||
## MCP Server Configuration (CLI)
|
||||
|
||||
If you prefer the command line over the TUI, you can configure agents directly:
|
||||
|
||||
### GitHub Copilot
|
||||
|
||||
```bash
|
||||
# That's it! Just run this command:
|
||||
uv run fuzzforge mcp install copilot
|
||||
```
|
||||
|
||||
The command auto-detects everything:
|
||||
- **FuzzForge root** - Where FuzzForge is installed
|
||||
- **Modules path** - Defaults to `fuzzforge_ai/fuzzforge-modules`
|
||||
- **Docker socket** - Auto-detects `/var/run/docker.sock`
|
||||
The command auto-detects:
|
||||
- **FuzzForge root** — Where FuzzForge is installed
|
||||
- **Docker socket** — Auto-detects `/var/run/docker.sock`
|
||||
|
||||
**Optional overrides** (usually not needed):
|
||||
**Optional overrides:**
|
||||
```bash
|
||||
uv run fuzzforge mcp install copilot \
|
||||
--modules /path/to/modules \
|
||||
--engine podman # if using Podman instead of Docker
|
||||
uv run fuzzforge mcp install copilot --engine podman
|
||||
```
|
||||
|
||||
**After installation:**
|
||||
1. Restart VS Code
|
||||
2. Open GitHub Copilot Chat
|
||||
3. FuzzForge tools are now available!
|
||||
**After installation:** Restart VS Code. FuzzForge tools appear in GitHub Copilot Chat.
|
||||
|
||||
### Claude Code (CLI)
|
||||
|
||||
@@ -190,143 +300,89 @@ uv run fuzzforge mcp install copilot \
|
||||
uv run fuzzforge mcp install claude-code
|
||||
```
|
||||
|
||||
Installs to `~/.claude.json` so FuzzForge tools are available from any directory.
|
||||
|
||||
**After installation:**
|
||||
1. Run `claude` from any directory
|
||||
2. FuzzForge tools are now available!
|
||||
Installs to `~/.claude.json`. FuzzForge tools are available from any directory after restarting Claude.
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
```bash
|
||||
# Automatic installation
|
||||
uv run fuzzforge mcp install claude-desktop
|
||||
|
||||
# Verify
|
||||
uv run fuzzforge mcp status
|
||||
```
|
||||
|
||||
**After installation:**
|
||||
1. Restart Claude Desktop
|
||||
2. FuzzForge tools are now available!
|
||||
**After installation:** Restart Claude Desktop.
|
||||
|
||||
### Check MCP Status
|
||||
### Check Status
|
||||
|
||||
```bash
|
||||
uv run fuzzforge mcp status
|
||||
```
|
||||
|
||||
Shows configuration status for all supported AI agents:
|
||||
|
||||
```
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Agent ┃ Config Path ┃ Status ┃ FuzzForge Configured ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ GitHub Copilot │ ~/.config/Code/User/mcp.json │ ✓ Exists │ ✓ Yes │
|
||||
│ Claude Desktop │ ~/.config/Claude/claude_desktop_config... │ Not found │ - │
|
||||
│ Claude Code │ ~/.claude.json │ ✓ Exists │ ✓ Yes │
|
||||
└──────────────────────┴───────────────────────────────────────────┴──────────────┴─────────────────────────┘
|
||||
```
|
||||
|
||||
### Generate Config Without Installing
|
||||
|
||||
```bash
|
||||
# Preview the configuration that would be installed
|
||||
uv run fuzzforge mcp generate copilot
|
||||
uv run fuzzforge mcp generate claude-desktop
|
||||
uv run fuzzforge mcp generate claude-code
|
||||
```
|
||||
|
||||
### Remove MCP Configuration
|
||||
### Remove Configuration
|
||||
|
||||
```bash
|
||||
uv run fuzzforge mcp uninstall copilot
|
||||
uv run fuzzforge mcp uninstall claude-desktop
|
||||
uv run fuzzforge mcp uninstall claude-code
|
||||
uv run fuzzforge mcp uninstall claude-desktop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using FuzzForge with AI
|
||||
|
||||
Once MCP is configured, you interact with FuzzForge through natural language with your AI assistant.
|
||||
Once MCP is configured and hub images are built, interact with FuzzForge through natural language with your AI assistant.
|
||||
|
||||
### Example Conversations
|
||||
|
||||
**Discover available tools:**
|
||||
```
|
||||
You: "What FuzzForge modules are available?"
|
||||
AI: Uses list_modules → "I found 4 modules: rust-analyzer, cargo-fuzzer,
|
||||
harness-validator, and crash-analyzer..."
|
||||
You: "What security tools are available in FuzzForge?"
|
||||
AI: Queries hub tools → "I found 15 tools across categories: nmap for
|
||||
port scanning, binwalk for firmware analysis, semgrep for code
|
||||
scanning, cargo-fuzzer for Rust fuzzing..."
|
||||
```
|
||||
|
||||
**Analyze code for fuzzing targets:**
|
||||
**Analyze a binary:**
|
||||
```
|
||||
You: "Extract and analyze this firmware image"
|
||||
AI: Uses binwalk to extract → yara for pattern matching → capa for
|
||||
capability detection → "Found 3 embedded filesystems, 2 YARA
|
||||
matches for known vulnerabilities..."
|
||||
```
|
||||
|
||||
**Fuzz Rust code:**
|
||||
```
|
||||
You: "Analyze this Rust crate for functions I should fuzz"
|
||||
AI: Uses execute_module("rust-analyzer") → "I found 3 good fuzzing candidates:
|
||||
- parse_input() in src/parser.rs - handles untrusted input
|
||||
- decode_message() in src/codec.rs - complex parsing logic
|
||||
..."
|
||||
```
|
||||
AI: Uses rust-analyzer → "Found 3 fuzzable entry points..."
|
||||
|
||||
**Generate and validate harnesses:**
|
||||
```
|
||||
You: "Generate a fuzzing harness for the parse_input function"
|
||||
AI: Creates harness code, then uses execute_module("harness-validator")
|
||||
→ "Here's a harness that compiles successfully..."
|
||||
```
|
||||
|
||||
**Run continuous fuzzing:**
|
||||
```
|
||||
You: "Start fuzzing parse_input for 10 minutes"
|
||||
AI: Uses start_continuous_module("cargo-fuzzer") → "Started fuzzing session abc123"
|
||||
|
||||
You: "How's the fuzzing going?"
|
||||
AI: Uses get_continuous_status("abc123") → "Running for 5 minutes:
|
||||
- 150,000 executions
|
||||
- 2 crashes found
|
||||
- 45% edge coverage"
|
||||
|
||||
You: "Stop and show me the crashes"
|
||||
AI: Uses stop_continuous_module("abc123") → "Found 2 unique crashes..."
|
||||
AI: Uses cargo-fuzzer → "Fuzzing session started. 2 crashes found..."
|
||||
```
|
||||
|
||||
### Available MCP Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_modules` | List all available security modules |
|
||||
| `execute_module` | Run a module once and get results |
|
||||
| `start_continuous_module` | Start a long-running module (e.g., fuzzing) |
|
||||
| `get_continuous_status` | Check status of a continuous session |
|
||||
| `stop_continuous_module` | Stop a continuous session |
|
||||
| `list_continuous_sessions` | List all active sessions |
|
||||
| `get_execution_results` | Retrieve results from an execution |
|
||||
| `execute_workflow` | Run a multi-step workflow |
|
||||
**Scan for vulnerabilities:**
|
||||
```
|
||||
You: "Scan this codebase with semgrep for security issues"
|
||||
AI: Uses semgrep-mcp → "Found 5 findings: 2 high severity SQL injection
|
||||
patterns, 3 medium severity hardcoded secrets..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI Reference
|
||||
|
||||
> **Note:** The CLI is for advanced users. Most users should interact with FuzzForge through their AI assistant.
|
||||
### UI Command
|
||||
|
||||
```bash
|
||||
uv run fuzzforge ui # Launch the terminal dashboard
|
||||
```
|
||||
|
||||
### MCP Commands
|
||||
|
||||
```bash
|
||||
uv run fuzzforge mcp status # Check configuration status
|
||||
uv run fuzzforge mcp install <agent> # Install MCP config
|
||||
uv run fuzzforge mcp status # Check agent configuration status
|
||||
uv run fuzzforge mcp install <agent> # Install MCP config (copilot|claude-code|claude-desktop)
|
||||
uv run fuzzforge mcp uninstall <agent> # Remove MCP config
|
||||
uv run fuzzforge mcp generate <agent> # Preview config without installing
|
||||
```
|
||||
|
||||
### Module Commands
|
||||
|
||||
```bash
|
||||
uv run fuzzforge modules list # List available modules
|
||||
uv run fuzzforge modules info <module> # Show module details
|
||||
uv run fuzzforge modules run <module> --assets . # Run a module
|
||||
```
|
||||
|
||||
### Project Commands
|
||||
|
||||
```bash
|
||||
@@ -343,14 +399,13 @@ uv run fuzzforge project results <id> # Get execution results
|
||||
Configure FuzzForge using environment variables:
|
||||
|
||||
```bash
|
||||
# Project paths
|
||||
export FUZZFORGE_MODULES_PATH=/path/to/modules
|
||||
# Storage path for projects and execution results
|
||||
export FUZZFORGE_STORAGE_PATH=/path/to/storage
|
||||
|
||||
# Container engine (Docker is default)
|
||||
export FUZZFORGE_ENGINE__TYPE=docker # or podman
|
||||
|
||||
# Podman-specific settings (only needed if using Podman under Snap)
|
||||
# Podman-specific settings
|
||||
export FUZZFORGE_ENGINE__GRAPHROOT=~/.fuzzforge/containers/storage
|
||||
export FUZZFORGE_ENGINE__RUNROOT=~/.fuzzforge/containers/run
|
||||
```
|
||||
@@ -384,66 +439,62 @@ Error: Permission denied connecting to Docker socket
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Add your user to the docker group
|
||||
sudo usermod -aG docker $USER
|
||||
|
||||
# Log out and back in for changes to take effect
|
||||
# Then verify:
|
||||
# Log out and back in, then verify:
|
||||
docker run --rm hello-world
|
||||
```
|
||||
|
||||
### No Modules Found
|
||||
### Hub Images Not Built
|
||||
|
||||
```
|
||||
No modules found.
|
||||
```
|
||||
The dashboard shows ✗ Not built for tools:
|
||||
|
||||
**Solution:**
|
||||
1. Build the modules first: `make build-modules`
|
||||
2. Check the modules path: `uv run fuzzforge modules list`
|
||||
3. Verify images exist: `docker images | grep fuzzforge`
|
||||
```bash
|
||||
# Build all hub images
|
||||
./scripts/build-hub-images.sh
|
||||
|
||||
# Or build a single tool
|
||||
docker build -t <tool-name>:latest mcp-security-hub/<category>/<tool-name>/
|
||||
```
|
||||
|
||||
### MCP Server Not Starting
|
||||
|
||||
Check the MCP configuration:
|
||||
```bash
|
||||
# Check agent configuration
|
||||
uv run fuzzforge mcp status
|
||||
```
|
||||
|
||||
Verify the configuration file path exists and contains valid JSON.
|
||||
|
||||
### Module Container Fails to Build
|
||||
|
||||
```bash
|
||||
# Build module container manually to see errors
|
||||
cd fuzzforge-modules/<module-name>
|
||||
docker build -t <module-name> .
|
||||
# Verify the config file path exists and contains valid JSON
|
||||
cat ~/.config/Code/User/mcp.json # Copilot
|
||||
cat ~/.claude.json # Claude Code
|
||||
```
|
||||
|
||||
### Using Podman Instead of Docker
|
||||
|
||||
If you prefer Podman:
|
||||
```bash
|
||||
# Use --engine podman with CLI
|
||||
# Install with Podman engine
|
||||
uv run fuzzforge mcp install copilot --engine podman
|
||||
|
||||
# Or set environment variable
|
||||
export FUZZFORGE_ENGINE=podman
|
||||
```
|
||||
|
||||
### Check Logs
|
||||
### Hub Registry
|
||||
|
||||
FuzzForge stores linked hub information in `~/.fuzzforge/hubs.json`. If something goes wrong:
|
||||
|
||||
FuzzForge stores execution logs in the storage directory:
|
||||
```bash
|
||||
ls -la ~/.fuzzforge/storage/<project-id>/<execution-id>/
|
||||
# View registry
|
||||
cat ~/.fuzzforge/hubs.json
|
||||
|
||||
# Reset registry
|
||||
rm ~/.fuzzforge/hubs.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- 📖 Read the [Module SDK Guide](fuzzforge-modules/fuzzforge-modules-sdk/README.md) to create custom modules
|
||||
- 🎬 Check the demos in the [README](README.md)
|
||||
- 🖥️ Launch `uv run fuzzforge ui` and explore the dashboard
|
||||
- 🔒 Clone the [mcp-security-hub](https://github.com/FuzzingLabs/mcp-security-hub) for 40+ security tools
|
||||
- 💬 Join our [Discord](https://discord.gg/8XEX33UUwZ) for support
|
||||
|
||||
---
|
||||
|
||||
@@ -8,6 +8,7 @@ requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"fuzzforge-mcp==0.0.1",
|
||||
"rich>=14.0.0",
|
||||
"textual>=1.0.0",
|
||||
"typer==0.20.1",
|
||||
]
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ def main(
|
||||
envvar="FUZZFORGE_STORAGE__PATH",
|
||||
help="Path to the storage directory.",
|
||||
),
|
||||
] = Path.home() / ".fuzzforge" / "storage",
|
||||
] = Path.cwd() / ".fuzzforge" / "storage",
|
||||
context: TyperContext = None, # type: ignore[assignment]
|
||||
) -> None:
|
||||
"""FuzzForge AI - Security research orchestration platform.
|
||||
@@ -42,7 +42,7 @@ def main(
|
||||
Discover and execute MCP hub tools for security research.
|
||||
|
||||
"""
|
||||
storage = LocalStorage(storage_path=storage_path)
|
||||
storage = LocalStorage(base_path=storage_path)
|
||||
|
||||
context.obj = Context(
|
||||
storage=storage,
|
||||
@@ -52,3 +52,19 @@ def main(
|
||||
|
||||
application.add_typer(mcp.application)
|
||||
application.add_typer(projects.application)
|
||||
|
||||
|
||||
@application.command(
|
||||
name="ui",
|
||||
help="Launch the FuzzForge terminal interface.",
|
||||
)
|
||||
def launch_ui() -> None:
|
||||
"""Launch the interactive FuzzForge TUI dashboard.
|
||||
|
||||
Provides a visual dashboard showing AI agent connection status
|
||||
and hub server availability, with wizards for setup and configuration.
|
||||
|
||||
"""
|
||||
from fuzzforge_cli.tui.app import FuzzForgeApp
|
||||
|
||||
FuzzForgeApp().run()
|
||||
|
||||
@@ -169,7 +169,7 @@ def _generate_mcp_config(
|
||||
|
||||
# Self-contained storage paths for FuzzForge containers
|
||||
# This isolates FuzzForge from system Podman and avoids snap issues
|
||||
fuzzforge_home = Path.home() / ".fuzzforge"
|
||||
fuzzforge_home = Path.cwd() / ".fuzzforge"
|
||||
graphroot = fuzzforge_home / "containers" / "storage"
|
||||
runroot = fuzzforge_home / "containers" / "run"
|
||||
|
||||
|
||||
1
fuzzforge-cli/src/fuzzforge_cli/tui/__init__.py
Normal file
1
fuzzforge-cli/src/fuzzforge_cli/tui/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""FuzzForge terminal user interface."""
|
||||
360
fuzzforge-cli/src/fuzzforge_cli/tui/app.py
Normal file
360
fuzzforge-cli/src/fuzzforge_cli/tui/app.py
Normal file
@@ -0,0 +1,360 @@
|
||||
"""FuzzForge TUI application.
|
||||
|
||||
Main terminal user interface for FuzzForge, providing a dashboard
|
||||
with AI agent connection status, hub server availability, and
|
||||
hub management capabilities.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from rich.text import Text
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Horizontal, Vertical, VerticalScroll
|
||||
from textual.widgets import Button, DataTable, Footer, Header, Label
|
||||
|
||||
from fuzzforge_cli.tui.helpers import (
|
||||
check_agent_status,
|
||||
check_hub_image,
|
||||
find_fuzzforge_root,
|
||||
get_agent_configs,
|
||||
load_hub_config,
|
||||
)
|
||||
|
||||
# Agent config entries stored alongside their linked status for row mapping
|
||||
_AgentRow = tuple[str, "AIAgent", "Path", str, bool] # noqa: F821
|
||||
|
||||
|
||||
class FuzzForgeApp(App):
|
||||
"""FuzzForge AI terminal user interface."""
|
||||
|
||||
TITLE = "FuzzForge AI"
|
||||
SUB_TITLE = "Security Research Orchestration"
|
||||
|
||||
CSS = """
|
||||
Screen {
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
#main {
|
||||
height: 1fr;
|
||||
margin: 1 2;
|
||||
}
|
||||
|
||||
.panel {
|
||||
width: 1fr;
|
||||
border: round #4699fc;
|
||||
padding: 1 2;
|
||||
margin: 0 0 1 0;
|
||||
}
|
||||
|
||||
#hub-panel {
|
||||
height: 12;
|
||||
}
|
||||
|
||||
#hub-table {
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
#agents-panel {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
text-style: bold;
|
||||
color: #4699fc;
|
||||
text-align: left;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#hub-title-bar {
|
||||
height: auto;
|
||||
align: center middle;
|
||||
margin: 0 0 1 0;
|
||||
}
|
||||
|
||||
#btn-hub-manager {
|
||||
min-width: 40;
|
||||
margin-right: 2;
|
||||
}
|
||||
|
||||
#btn-fuzzinglabs-hub {
|
||||
min-width: 30;
|
||||
}
|
||||
|
||||
#agents-table {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Modal screens */
|
||||
AgentSetupScreen, AgentUnlinkScreen,
|
||||
HubManagerScreen, LinkHubScreen, CloneHubScreen {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
#setup-dialog, #unlink-dialog {
|
||||
width: 56;
|
||||
height: auto;
|
||||
max-height: 80%;
|
||||
border: thick #4699fc;
|
||||
background: $surface;
|
||||
padding: 2 3;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#hub-manager-dialog {
|
||||
width: 100;
|
||||
height: auto;
|
||||
max-height: 85%;
|
||||
border: thick #4699fc;
|
||||
background: $surface;
|
||||
padding: 2 3;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#link-dialog, #clone-dialog {
|
||||
width: 72;
|
||||
height: auto;
|
||||
max-height: 80%;
|
||||
border: thick #4699fc;
|
||||
background: $surface;
|
||||
padding: 2 3;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
text-style: bold;
|
||||
text-align: center;
|
||||
color: #4699fc;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
margin-top: 1;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
RadioSet {
|
||||
height: auto;
|
||||
margin: 0 0 1 2;
|
||||
}
|
||||
|
||||
Input {
|
||||
margin: 0 0 1 0;
|
||||
}
|
||||
|
||||
.dialog-buttons {
|
||||
layout: horizontal;
|
||||
height: 3;
|
||||
align: center middle;
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
.dialog-buttons Button {
|
||||
margin: 0 1;
|
||||
min-width: 14;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("q", "quit", "Quit"),
|
||||
Binding("h", "manage_hubs", "Hub Manager"),
|
||||
Binding("r", "refresh", "Refresh"),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the dashboard layout."""
|
||||
yield Header()
|
||||
with VerticalScroll(id="main"):
|
||||
with Vertical(id="hub-panel", classes="panel"):
|
||||
yield DataTable(id="hub-table")
|
||||
with Horizontal(id="hub-title-bar"):
|
||||
yield Button(
|
||||
"Hub Manager (h)",
|
||||
variant="primary",
|
||||
id="btn-hub-manager",
|
||||
)
|
||||
yield Button(
|
||||
"FuzzingLabs Hub",
|
||||
variant="primary",
|
||||
id="btn-fuzzinglabs-hub",
|
||||
)
|
||||
with Vertical(id="agents-panel", classes="panel"):
|
||||
yield DataTable(id="agents-table")
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Populate tables on startup."""
|
||||
self._agent_rows: list[_AgentRow] = []
|
||||
self.query_one("#hub-panel").border_title = "Hub Servers"
|
||||
self.query_one("#agents-panel").border_title = "AI Agents"
|
||||
self._refresh_agents()
|
||||
self._refresh_hub()
|
||||
|
||||
def _refresh_agents(self) -> None:
|
||||
"""Refresh the AI agents status table."""
|
||||
table = self.query_one("#agents-table", DataTable)
|
||||
table.clear(columns=True)
|
||||
table.add_columns("Agent", "Status", "Config Path")
|
||||
table.cursor_type = "row"
|
||||
|
||||
self._agent_rows = []
|
||||
for display_name, agent, config_path, servers_key in get_agent_configs():
|
||||
is_linked, status_text = check_agent_status(config_path, servers_key)
|
||||
if is_linked:
|
||||
status_cell = Text(f"✓ {status_text}", style="green")
|
||||
else:
|
||||
status_cell = Text(f"✗ {status_text}", style="red")
|
||||
table.add_row(display_name, status_cell, str(config_path))
|
||||
self._agent_rows.append(
|
||||
(display_name, agent, config_path, servers_key, is_linked)
|
||||
)
|
||||
|
||||
def _refresh_hub(self) -> None:
|
||||
"""Refresh the hub servers table, grouped by source hub."""
|
||||
table = self.query_one("#hub-table", DataTable)
|
||||
table.clear(columns=True)
|
||||
table.add_columns("Server", "Image", "Hub", "Status")
|
||||
|
||||
try:
|
||||
fuzzforge_root = find_fuzzforge_root()
|
||||
hub_config = load_hub_config(fuzzforge_root)
|
||||
except Exception:
|
||||
table.add_row(
|
||||
Text("Error loading config", style="red"), "", "", ""
|
||||
)
|
||||
return
|
||||
|
||||
servers = hub_config.get("servers", [])
|
||||
if not servers:
|
||||
table.add_row(
|
||||
Text("No servers — press h", style="dim"), "", "", ""
|
||||
)
|
||||
return
|
||||
|
||||
# Group servers by source hub
|
||||
groups: dict[str, list[dict]] = defaultdict(list)
|
||||
for server in servers:
|
||||
source = server.get("source_hub", "manual")
|
||||
groups[source].append(server)
|
||||
|
||||
for hub_name, hub_servers in groups.items():
|
||||
ready_count = 0
|
||||
total = len(hub_servers)
|
||||
|
||||
statuses: list[tuple[dict, bool, str]] = []
|
||||
for server in hub_servers:
|
||||
enabled = server.get("enabled", True)
|
||||
if not enabled:
|
||||
statuses.append((server, False, "Disabled"))
|
||||
else:
|
||||
is_ready, status_text = check_hub_image(
|
||||
server.get("image", "")
|
||||
)
|
||||
if is_ready:
|
||||
ready_count += 1
|
||||
statuses.append((server, is_ready, status_text))
|
||||
|
||||
# Group header row
|
||||
if hub_name == "manual":
|
||||
header = Text(
|
||||
f"▼ 📦 Local config ({ready_count}/{total} ready)",
|
||||
style="bold",
|
||||
)
|
||||
else:
|
||||
header = Text(
|
||||
f"▼ 🔗 {hub_name} ({ready_count}/{total} ready)",
|
||||
style="bold",
|
||||
)
|
||||
table.add_row(header, "", "", "")
|
||||
|
||||
# Tool rows
|
||||
for server, is_ready, status_text in statuses:
|
||||
name = server.get("name", "unknown")
|
||||
image = server.get("image", "unknown")
|
||||
enabled = server.get("enabled", True)
|
||||
|
||||
if not enabled:
|
||||
status_cell = Text("Disabled", style="dim")
|
||||
elif is_ready:
|
||||
status_cell = Text("✓ Ready", style="green")
|
||||
else:
|
||||
status_cell = Text(f"✗ {status_text}", style="red")
|
||||
|
||||
table.add_row(
|
||||
f" {name}",
|
||||
Text(image, style="dim"),
|
||||
hub_name,
|
||||
status_cell,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
idx = event.cursor_row
|
||||
if idx < 0 or idx >= len(self._agent_rows):
|
||||
return
|
||||
|
||||
display_name, agent, _config_path, _servers_key, is_linked = self._agent_rows[idx]
|
||||
|
||||
if is_linked:
|
||||
from fuzzforge_cli.tui.screens.agent_setup import AgentUnlinkScreen
|
||||
|
||||
self.push_screen(
|
||||
AgentUnlinkScreen(agent, display_name),
|
||||
callback=self._on_agent_changed,
|
||||
)
|
||||
else:
|
||||
from fuzzforge_cli.tui.screens.agent_setup import AgentSetupScreen
|
||||
|
||||
self.push_screen(
|
||||
AgentSetupScreen(agent, display_name),
|
||||
callback=self._on_agent_changed,
|
||||
)
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button presses."""
|
||||
if event.button.id == "btn-hub-manager":
|
||||
self.action_manage_hubs()
|
||||
elif event.button.id == "btn-fuzzinglabs-hub":
|
||||
self.action_add_fuzzinglabs_hub()
|
||||
|
||||
def action_add_fuzzinglabs_hub(self) -> None:
|
||||
"""Open the clone dialog pre-filled with the FuzzingLabs hub URL."""
|
||||
from fuzzforge_cli.tui.screens.hub_manager import CloneHubScreen
|
||||
|
||||
self.push_screen(
|
||||
CloneHubScreen(
|
||||
default_url="https://github.com/FuzzingLabs/mcp-security-hub",
|
||||
default_name="mcp-security-hub",
|
||||
is_default=True,
|
||||
),
|
||||
callback=self._on_hub_changed,
|
||||
)
|
||||
|
||||
def action_manage_hubs(self) -> None:
|
||||
"""Open the hub manager."""
|
||||
from fuzzforge_cli.tui.screens.hub_manager import HubManagerScreen
|
||||
|
||||
self.push_screen(HubManagerScreen(), callback=self._on_hub_changed)
|
||||
|
||||
def _on_agent_changed(self, result: str | None) -> None:
|
||||
"""Handle agent setup/unlink completion."""
|
||||
if result:
|
||||
self.notify(result)
|
||||
self._refresh_agents()
|
||||
|
||||
def _on_hub_changed(self, result: str | None) -> None:
|
||||
"""Handle hub manager completion — refresh the hub table."""
|
||||
self._refresh_hub()
|
||||
|
||||
def action_refresh(self) -> None:
|
||||
"""Refresh all status panels."""
|
||||
self._refresh_agents()
|
||||
self._refresh_hub()
|
||||
self.notify("Status refreshed")
|
||||
535
fuzzforge-cli/src/fuzzforge_cli/tui/helpers.py
Normal file
535
fuzzforge-cli/src/fuzzforge_cli/tui/helpers.py
Normal file
@@ -0,0 +1,535 @@
|
||||
"""Shared helpers for FuzzForge TUI and CLI.
|
||||
|
||||
Provides utility functions for checking AI agent configuration status,
|
||||
hub server image availability, installing/removing MCP configurations,
|
||||
and managing linked MCP hub repositories.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fuzzforge_cli.commands.mcp import (
|
||||
AIAgent,
|
||||
_detect_docker_socket,
|
||||
_detect_podman_socket,
|
||||
_find_fuzzforge_root,
|
||||
_generate_mcp_config,
|
||||
_get_claude_code_user_mcp_path,
|
||||
_get_claude_desktop_mcp_path,
|
||||
_get_copilot_mcp_path,
|
||||
)
|
||||
|
||||
# --- Hub Management Constants ---
|
||||
|
||||
FUZZFORGE_DEFAULT_HUB_URL = "git@github.com:FuzzingLabs/mcp-security-hub.git"
|
||||
FUZZFORGE_DEFAULT_HUB_NAME = "mcp-security-hub"
|
||||
|
||||
|
||||
def get_fuzzforge_dir() -> Path:
|
||||
"""Return the project-local ``.fuzzforge/`` directory.
|
||||
|
||||
Uses the current working directory so that each project gets its
|
||||
own isolated FuzzForge configuration, hubs, and storage — similar
|
||||
to how ``.git/`` or ``.venv/`` work.
|
||||
|
||||
:return: ``Path.cwd() / ".fuzzforge"``
|
||||
|
||||
"""
|
||||
return Path.cwd() / ".fuzzforge"
|
||||
|
||||
# Categories that typically need NET_RAW capability for network access
|
||||
_NET_RAW_CATEGORIES = {"reconnaissance", "web-security"}
|
||||
|
||||
# Directories to skip when scanning a hub for MCP tool Dockerfiles
|
||||
_SCAN_SKIP_DIRS = {
|
||||
".git",
|
||||
".github",
|
||||
"scripts",
|
||||
"tests",
|
||||
"examples",
|
||||
"meta",
|
||||
"__pycache__",
|
||||
"node_modules",
|
||||
".venv",
|
||||
}
|
||||
|
||||
|
||||
def get_agent_configs() -> list[tuple[str, AIAgent, Path, str]]:
|
||||
"""Return agent display configs with resolved paths.
|
||||
|
||||
Each tuple contains:
|
||||
- Display name
|
||||
- AIAgent enum value
|
||||
- Config file path
|
||||
- Servers JSON key
|
||||
|
||||
:return: List of agent configuration tuples.
|
||||
|
||||
"""
|
||||
return [
|
||||
("GitHub Copilot", AIAgent.COPILOT, _get_copilot_mcp_path(), "servers"),
|
||||
("Claude Desktop", AIAgent.CLAUDE_DESKTOP, _get_claude_desktop_mcp_path(), "mcpServers"),
|
||||
("Claude Code", AIAgent.CLAUDE_CODE, _get_claude_code_user_mcp_path(), "mcpServers"),
|
||||
]
|
||||
|
||||
|
||||
def check_agent_status(config_path: Path, servers_key: str) -> tuple[bool, str]:
|
||||
"""Check whether an AI agent has FuzzForge configured.
|
||||
|
||||
:param config_path: Path to the agent's MCP config file.
|
||||
:param servers_key: JSON key for the servers dict (e.g. "servers" or "mcpServers").
|
||||
:return: Tuple of (is_linked, status_description).
|
||||
|
||||
"""
|
||||
if not config_path.exists():
|
||||
return False, "Not configured"
|
||||
try:
|
||||
config = json.loads(config_path.read_text())
|
||||
servers = config.get(servers_key, {})
|
||||
if "fuzzforge" in servers:
|
||||
return True, "Linked"
|
||||
return False, "Config exists, not linked"
|
||||
except json.JSONDecodeError:
|
||||
return False, "Invalid config file"
|
||||
|
||||
|
||||
def check_hub_image(image: str) -> tuple[bool, str]:
|
||||
"""Check whether a Docker image exists locally.
|
||||
|
||||
:param image: Docker image name (e.g. "semgrep-mcp:latest").
|
||||
:return: Tuple of (is_ready, status_description).
|
||||
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "image", "inspect", image],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True, "Ready"
|
||||
return False, "Not built"
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "Timeout"
|
||||
except FileNotFoundError:
|
||||
return False, "Docker not found"
|
||||
|
||||
|
||||
def load_hub_config(fuzzforge_root: Path) -> dict[str, Any]:
|
||||
"""Load hub-config.json from the FuzzForge root.
|
||||
|
||||
:param fuzzforge_root: Path to fuzzforge-oss directory.
|
||||
:return: Parsed hub configuration dict, empty dict on error.
|
||||
|
||||
"""
|
||||
config_path = fuzzforge_root / "hub-config.json"
|
||||
if not config_path.exists():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(config_path.read_text())
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
|
||||
def find_fuzzforge_root() -> Path:
|
||||
"""Find the FuzzForge installation root directory.
|
||||
|
||||
:return: Path to the fuzzforge-oss directory.
|
||||
|
||||
"""
|
||||
return _find_fuzzforge_root()
|
||||
|
||||
|
||||
def install_agent_config(agent: AIAgent, engine: str, force: bool = False) -> str:
|
||||
"""Install FuzzForge MCP configuration for an AI agent.
|
||||
|
||||
:param agent: Target AI agent.
|
||||
:param engine: Container engine type ("docker" or "podman").
|
||||
:param force: Overwrite existing configuration.
|
||||
:return: Result message string.
|
||||
|
||||
"""
|
||||
fuzzforge_root = _find_fuzzforge_root()
|
||||
|
||||
if agent == AIAgent.COPILOT:
|
||||
config_path = _get_copilot_mcp_path()
|
||||
servers_key = "servers"
|
||||
elif agent == AIAgent.CLAUDE_CODE:
|
||||
config_path = _get_claude_code_user_mcp_path()
|
||||
servers_key = "mcpServers"
|
||||
else:
|
||||
config_path = _get_claude_desktop_mcp_path()
|
||||
servers_key = "mcpServers"
|
||||
|
||||
socket = _detect_docker_socket() if engine == "docker" else _detect_podman_socket()
|
||||
|
||||
server_config = _generate_mcp_config(
|
||||
fuzzforge_root=fuzzforge_root,
|
||||
engine_type=engine,
|
||||
engine_socket=socket,
|
||||
)
|
||||
|
||||
if config_path.exists():
|
||||
try:
|
||||
existing = json.loads(config_path.read_text())
|
||||
except json.JSONDecodeError:
|
||||
return f"Error: Invalid JSON in {config_path}"
|
||||
|
||||
servers = existing.get(servers_key, {})
|
||||
if "fuzzforge" in servers and not force:
|
||||
return "Already configured (use force to overwrite)"
|
||||
|
||||
if servers_key not in existing:
|
||||
existing[servers_key] = {}
|
||||
existing[servers_key]["fuzzforge"] = server_config
|
||||
full_config = existing
|
||||
else:
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
full_config = {servers_key: {"fuzzforge": server_config}}
|
||||
|
||||
config_path.write_text(json.dumps(full_config, indent=4))
|
||||
return f"Installed FuzzForge for {agent.value}"
|
||||
|
||||
|
||||
def uninstall_agent_config(agent: AIAgent) -> str:
|
||||
"""Remove FuzzForge MCP configuration from an AI agent.
|
||||
|
||||
:param agent: Target AI agent.
|
||||
:return: Result message string.
|
||||
|
||||
"""
|
||||
if agent == AIAgent.COPILOT:
|
||||
config_path = _get_copilot_mcp_path()
|
||||
servers_key = "servers"
|
||||
elif agent == AIAgent.CLAUDE_CODE:
|
||||
config_path = _get_claude_code_user_mcp_path()
|
||||
servers_key = "mcpServers"
|
||||
else:
|
||||
config_path = _get_claude_desktop_mcp_path()
|
||||
servers_key = "mcpServers"
|
||||
|
||||
if not config_path.exists():
|
||||
return "Configuration file not found"
|
||||
|
||||
try:
|
||||
config = json.loads(config_path.read_text())
|
||||
except json.JSONDecodeError:
|
||||
return "Error: Invalid JSON in config file"
|
||||
|
||||
servers = config.get(servers_key, {})
|
||||
if "fuzzforge" not in servers:
|
||||
return "FuzzForge is not configured for this agent"
|
||||
|
||||
del servers["fuzzforge"]
|
||||
config_path.write_text(json.dumps(config, indent=4))
|
||||
return f"Removed FuzzForge from {agent.value}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hub Management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_hubs_registry_path() -> Path:
|
||||
"""Return path to the hubs registry file (``.fuzzforge/hubs.json``).
|
||||
|
||||
:return: Path to the registry JSON file.
|
||||
|
||||
"""
|
||||
return get_fuzzforge_dir() / "hubs.json"
|
||||
|
||||
|
||||
def get_default_hubs_dir() -> Path:
|
||||
"""Return default directory for cloned hubs (``.fuzzforge/hubs/``).
|
||||
|
||||
:return: Path to the default hubs directory.
|
||||
|
||||
"""
|
||||
return get_fuzzforge_dir() / "hubs"
|
||||
|
||||
|
||||
def load_hubs_registry() -> dict[str, Any]:
|
||||
"""Load the hubs registry from disk.
|
||||
|
||||
:return: Registry dict with ``hubs`` key containing a list of hub entries.
|
||||
|
||||
"""
|
||||
path = get_hubs_registry_path()
|
||||
if not path.exists():
|
||||
return {"hubs": []}
|
||||
try:
|
||||
return json.loads(path.read_text())
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return {"hubs": []}
|
||||
|
||||
|
||||
def save_hubs_registry(registry: dict[str, Any]) -> None:
|
||||
"""Save the hubs registry to disk.
|
||||
|
||||
:param registry: Registry dict to persist.
|
||||
|
||||
"""
|
||||
path = get_hubs_registry_path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(registry, indent=2))
|
||||
|
||||
|
||||
def scan_hub_for_servers(hub_path: Path) -> list[dict[str, Any]]:
|
||||
"""Scan a hub directory for MCP tool Dockerfiles.
|
||||
|
||||
Looks for the ``category/tool-name/Dockerfile`` pattern and generates
|
||||
a server configuration entry for each discovered tool.
|
||||
|
||||
:param hub_path: Root directory of the hub repository.
|
||||
:return: Sorted list of server configuration dicts.
|
||||
|
||||
"""
|
||||
servers: list[dict[str, Any]] = []
|
||||
|
||||
if not hub_path.is_dir():
|
||||
return servers
|
||||
|
||||
for dockerfile in sorted(hub_path.rglob("Dockerfile")):
|
||||
rel = dockerfile.relative_to(hub_path)
|
||||
parts = rel.parts
|
||||
|
||||
# Expected layout: category/tool-name/Dockerfile (exactly 3 parts)
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
|
||||
category, tool_name, _ = parts
|
||||
|
||||
if category in _SCAN_SKIP_DIRS:
|
||||
continue
|
||||
|
||||
capabilities: list[str] = []
|
||||
if category in _NET_RAW_CATEGORIES:
|
||||
capabilities = ["NET_RAW"]
|
||||
|
||||
servers.append(
|
||||
{
|
||||
"name": tool_name,
|
||||
"description": f"{tool_name} — {category}",
|
||||
"type": "docker",
|
||||
"image": f"{tool_name}:latest",
|
||||
"category": category,
|
||||
"capabilities": capabilities,
|
||||
"volumes": [f"{get_fuzzforge_dir()}/hub/workspace:/data"],
|
||||
"enabled": True,
|
||||
}
|
||||
)
|
||||
|
||||
return servers
|
||||
|
||||
|
||||
def link_hub(
|
||||
name: str,
|
||||
path: str | Path,
|
||||
git_url: str | None = None,
|
||||
is_default: bool = False,
|
||||
) -> str:
|
||||
"""Link a hub directory and add its servers to hub-config.json.
|
||||
|
||||
:param name: Display name for the hub.
|
||||
:param path: Local directory path containing the hub.
|
||||
:param git_url: Optional git remote URL (for tracking).
|
||||
:param is_default: Whether this is the default FuzzingLabs hub.
|
||||
:return: Result message string.
|
||||
|
||||
"""
|
||||
hub_path = Path(path).resolve()
|
||||
|
||||
if not hub_path.is_dir():
|
||||
return f"Error: directory not found: {hub_path}"
|
||||
|
||||
# Update registry
|
||||
registry = load_hubs_registry()
|
||||
hubs = registry.get("hubs", [])
|
||||
|
||||
# Remove existing entry with same name
|
||||
hubs = [h for h in hubs if h.get("name") != name]
|
||||
|
||||
hubs.append(
|
||||
{
|
||||
"name": name,
|
||||
"path": str(hub_path),
|
||||
"git_url": git_url,
|
||||
"is_default": is_default,
|
||||
}
|
||||
)
|
||||
|
||||
registry["hubs"] = hubs
|
||||
save_hubs_registry(registry)
|
||||
|
||||
# Scan and update hub-config.json
|
||||
scanned = scan_hub_for_servers(hub_path)
|
||||
if not scanned:
|
||||
return f"Linked '{name}' (0 servers found)"
|
||||
|
||||
try:
|
||||
added = _merge_servers_into_hub_config(name, scanned)
|
||||
except Exception as exc:
|
||||
return f"Linked '{name}' but config update failed: {exc}"
|
||||
|
||||
return f"Linked '{name}' — {added} new servers added ({len(scanned)} scanned)"
|
||||
|
||||
|
||||
def unlink_hub(name: str) -> str:
|
||||
"""Unlink a hub and remove its servers from hub-config.json.
|
||||
|
||||
:param name: Name of the hub to unlink.
|
||||
:return: Result message string.
|
||||
|
||||
"""
|
||||
registry = load_hubs_registry()
|
||||
hubs = registry.get("hubs", [])
|
||||
|
||||
if not any(h.get("name") == name for h in hubs):
|
||||
return f"Hub '{name}' is not linked"
|
||||
|
||||
hubs = [h for h in hubs if h.get("name") != name]
|
||||
registry["hubs"] = hubs
|
||||
save_hubs_registry(registry)
|
||||
|
||||
try:
|
||||
removed = _remove_hub_servers_from_config(name)
|
||||
except Exception:
|
||||
removed = 0
|
||||
|
||||
return f"Unlinked '{name}' — {removed} server(s) removed"
|
||||
|
||||
|
||||
def clone_hub(
|
||||
git_url: str,
|
||||
dest: Path | None = None,
|
||||
name: str | None = None,
|
||||
) -> tuple[bool, str, Path | None]:
|
||||
"""Clone a git hub repository.
|
||||
|
||||
If the destination already exists and is a git repo, pulls instead.
|
||||
|
||||
:param git_url: Git remote URL to clone.
|
||||
:param dest: Destination directory (auto-derived from URL if *None*).
|
||||
:param name: Hub name (auto-derived from URL if *None*).
|
||||
:return: Tuple of ``(success, message, clone_path)``.
|
||||
|
||||
"""
|
||||
if name is None:
|
||||
name = git_url.rstrip("/").split("/")[-1]
|
||||
if name.endswith(".git"):
|
||||
name = name[:-4]
|
||||
|
||||
if dest is None:
|
||||
dest = get_default_hubs_dir() / name
|
||||
|
||||
if dest.exists():
|
||||
if (dest / ".git").is_dir():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(dest), "pull"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True, f"Updated existing clone at {dest}", dest
|
||||
return False, f"Git pull failed: {result.stderr.strip()}", None
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "Git pull timed out", None
|
||||
except FileNotFoundError:
|
||||
return False, "Git not found", None
|
||||
return False, f"Directory already exists (not a git repo): {dest}", None
|
||||
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "clone", git_url, str(dest)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True, f"Cloned to {dest}", dest
|
||||
return False, f"Git clone failed: {result.stderr.strip()}", None
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "Git clone timed out (5 min limit)", None
|
||||
except FileNotFoundError:
|
||||
return False, "Git not found on PATH", None
|
||||
|
||||
|
||||
def _merge_servers_into_hub_config(
|
||||
hub_name: str,
|
||||
servers: list[dict[str, Any]],
|
||||
) -> int:
|
||||
"""Merge scanned servers into hub-config.json.
|
||||
|
||||
Only adds servers whose name does not already exist in the config.
|
||||
New entries are tagged with ``source_hub`` for later removal.
|
||||
|
||||
:param hub_name: Name of the source hub (used for tagging).
|
||||
:param servers: List of server dicts from :func:`scan_hub_for_servers`.
|
||||
:return: Number of newly added servers.
|
||||
|
||||
"""
|
||||
fuzzforge_root = find_fuzzforge_root()
|
||||
config_path = fuzzforge_root / "hub-config.json"
|
||||
|
||||
if config_path.exists():
|
||||
try:
|
||||
config = json.loads(config_path.read_text())
|
||||
except json.JSONDecodeError:
|
||||
config = {"servers": [], "default_timeout": 300, "cache_tools": True}
|
||||
else:
|
||||
config = {"servers": [], "default_timeout": 300, "cache_tools": True}
|
||||
|
||||
existing = config.get("servers", [])
|
||||
existing_names = {s.get("name") for s in existing}
|
||||
|
||||
added = 0
|
||||
for server in servers:
|
||||
if server["name"] not in existing_names:
|
||||
server["source_hub"] = hub_name
|
||||
existing.append(server)
|
||||
existing_names.add(server["name"])
|
||||
added += 1
|
||||
|
||||
config["servers"] = existing
|
||||
config_path.write_text(json.dumps(config, indent=2))
|
||||
return added
|
||||
|
||||
|
||||
def _remove_hub_servers_from_config(hub_name: str) -> int:
|
||||
"""Remove servers belonging to a hub from hub-config.json.
|
||||
|
||||
Only removes servers tagged with the given ``source_hub`` value.
|
||||
Manually-added servers (without a tag) are preserved.
|
||||
|
||||
:param hub_name: Name of the hub whose servers should be removed.
|
||||
:return: Number of servers removed.
|
||||
|
||||
"""
|
||||
fuzzforge_root = find_fuzzforge_root()
|
||||
config_path = fuzzforge_root / "hub-config.json"
|
||||
|
||||
if not config_path.exists():
|
||||
return 0
|
||||
|
||||
try:
|
||||
config = json.loads(config_path.read_text())
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
|
||||
existing = config.get("servers", [])
|
||||
before = len(existing)
|
||||
config["servers"] = [s for s in existing if s.get("source_hub") != hub_name]
|
||||
after = len(config["servers"])
|
||||
|
||||
config_path.write_text(json.dumps(config, indent=2))
|
||||
return before - after
|
||||
1
fuzzforge-cli/src/fuzzforge_cli/tui/screens/__init__.py
Normal file
1
fuzzforge-cli/src/fuzzforge_cli/tui/screens/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""TUI screens for FuzzForge."""
|
||||
96
fuzzforge-cli/src/fuzzforge_cli/tui/screens/agent_setup.py
Normal file
96
fuzzforge-cli/src/fuzzforge_cli/tui/screens/agent_setup.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""Agent setup and unlink modal screens for FuzzForge TUI.
|
||||
|
||||
Provides context-aware modals that receive the target agent directly
|
||||
from the dashboard row selection — no redundant agent picker needed.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Label, RadioButton, RadioSet
|
||||
|
||||
from fuzzforge_cli.commands.mcp import AIAgent
|
||||
from fuzzforge_cli.tui.helpers import install_agent_config, uninstall_agent_config
|
||||
|
||||
|
||||
class AgentSetupScreen(ModalScreen[str | None]):
|
||||
"""Modal for linking a specific agent — only asks for engine choice."""
|
||||
|
||||
BINDINGS = [("escape", "cancel", "Cancel")]
|
||||
|
||||
def __init__(self, agent: AIAgent, display_name: str) -> None:
|
||||
super().__init__()
|
||||
self._agent = agent
|
||||
self._display_name = display_name
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the setup dialog layout."""
|
||||
with Vertical(id="setup-dialog"):
|
||||
yield Label(f"Setup {self._display_name}", classes="dialog-title")
|
||||
|
||||
yield Label("Container Engine:", classes="field-label")
|
||||
yield RadioSet(
|
||||
RadioButton("Docker", value=True),
|
||||
RadioButton("Podman"),
|
||||
id="engine-select",
|
||||
)
|
||||
|
||||
with Horizontal(classes="dialog-buttons"):
|
||||
yield Button("Install", variant="primary", id="btn-install")
|
||||
yield Button("Cancel", variant="default", id="btn-cancel")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button clicks."""
|
||||
if event.button.id == "btn-cancel":
|
||||
self.dismiss(None)
|
||||
elif event.button.id == "btn-install":
|
||||
self._do_install()
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
"""Dismiss the dialog without action."""
|
||||
self.dismiss(None)
|
||||
|
||||
def _do_install(self) -> None:
|
||||
"""Execute the installation."""
|
||||
engine_set = self.query_one("#engine-select", RadioSet)
|
||||
engine = "docker" if engine_set.pressed_index <= 0 else "podman"
|
||||
result = install_agent_config(self._agent, engine, force=True)
|
||||
self.dismiss(result)
|
||||
|
||||
|
||||
class AgentUnlinkScreen(ModalScreen[str | None]):
|
||||
"""Confirmation modal for unlinking a specific agent."""
|
||||
|
||||
BINDINGS = [("escape", "cancel", "Cancel")]
|
||||
|
||||
def __init__(self, agent: AIAgent, display_name: str) -> None:
|
||||
super().__init__()
|
||||
self._agent = agent
|
||||
self._display_name = display_name
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the unlink confirmation layout."""
|
||||
with Vertical(id="unlink-dialog"):
|
||||
yield Label(f"Unlink {self._display_name}?", classes="dialog-title")
|
||||
yield Label(
|
||||
f"This will remove the FuzzForge MCP configuration from {self._display_name}.",
|
||||
)
|
||||
|
||||
with Horizontal(classes="dialog-buttons"):
|
||||
yield Button("Unlink", variant="warning", id="btn-unlink")
|
||||
yield Button("Cancel", variant="default", id="btn-cancel")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button clicks."""
|
||||
if event.button.id == "btn-cancel":
|
||||
self.dismiss(None)
|
||||
elif event.button.id == "btn-unlink":
|
||||
result = uninstall_agent_config(self._agent)
|
||||
self.dismiss(result)
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
"""Dismiss without action."""
|
||||
self.dismiss(None)
|
||||
299
fuzzforge-cli/src/fuzzforge_cli/tui/screens/hub_manager.py
Normal file
299
fuzzforge-cli/src/fuzzforge_cli/tui/screens/hub_manager.py
Normal file
@@ -0,0 +1,299 @@
|
||||
"""Hub management screens for FuzzForge TUI.
|
||||
|
||||
Provides modal dialogs for managing linked MCP hub repositories:
|
||||
- HubManagerScreen: list, add, remove linked hubs
|
||||
- LinkHubScreen: link a local directory as a hub
|
||||
- CloneHubScreen: clone a git repo and link it (defaults to FuzzingLabs hub)
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from rich.text import Text
|
||||
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, DataTable, Input, Label, Static
|
||||
|
||||
from fuzzforge_cli.tui.helpers import (
|
||||
FUZZFORGE_DEFAULT_HUB_NAME,
|
||||
FUZZFORGE_DEFAULT_HUB_URL,
|
||||
clone_hub,
|
||||
link_hub,
|
||||
load_hubs_registry,
|
||||
scan_hub_for_servers,
|
||||
unlink_hub,
|
||||
)
|
||||
|
||||
|
||||
class HubManagerScreen(ModalScreen[str | None]):
|
||||
"""Modal screen for managing linked MCP hubs."""
|
||||
|
||||
BINDINGS = [("escape", "cancel", "Close")]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the hub manager layout."""
|
||||
with Vertical(id="hub-manager-dialog"):
|
||||
yield Label("Hub Manager", classes="dialog-title")
|
||||
yield DataTable(id="hubs-table")
|
||||
yield Label("", id="hub-status")
|
||||
with Horizontal(classes="dialog-buttons"):
|
||||
yield Button(
|
||||
"FuzzingLabs Hub",
|
||||
variant="primary",
|
||||
id="btn-clone-default",
|
||||
)
|
||||
yield Button("Link Path", variant="default", id="btn-link")
|
||||
yield Button("Clone URL", variant="default", id="btn-clone")
|
||||
yield Button("Remove", variant="primary", id="btn-remove")
|
||||
yield Button("Close", variant="default", id="btn-close")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Populate the hubs table on startup."""
|
||||
self._refresh_hubs()
|
||||
|
||||
def _refresh_hubs(self) -> None:
|
||||
"""Refresh the linked hubs table."""
|
||||
table = self.query_one("#hubs-table", DataTable)
|
||||
table.clear(columns=True)
|
||||
table.add_columns("Name", "Path", "Servers", "Source")
|
||||
table.cursor_type = "row"
|
||||
|
||||
registry = load_hubs_registry()
|
||||
hubs = registry.get("hubs", [])
|
||||
|
||||
if not hubs:
|
||||
table.add_row(
|
||||
Text("No hubs linked", style="dim"),
|
||||
Text("Press 'FuzzingLabs Hub' to get started", style="dim"),
|
||||
"",
|
||||
"",
|
||||
)
|
||||
return
|
||||
|
||||
for hub in hubs:
|
||||
name = hub.get("name", "unknown")
|
||||
path = hub.get("path", "")
|
||||
git_url = hub.get("git_url", "")
|
||||
is_default = hub.get("is_default", False)
|
||||
|
||||
hub_path = Path(path)
|
||||
if hub_path.is_dir():
|
||||
servers = scan_hub_for_servers(hub_path)
|
||||
count = str(len(servers))
|
||||
else:
|
||||
count = Text("dir missing", style="yellow")
|
||||
|
||||
source = git_url or "local"
|
||||
if is_default:
|
||||
name_cell = Text(f"★ {name}", style="bold")
|
||||
else:
|
||||
name_cell = Text(name)
|
||||
|
||||
table.add_row(name_cell, path, count, source)
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Route button actions."""
|
||||
if event.button.id == "btn-close":
|
||||
self.dismiss("refreshed")
|
||||
elif event.button.id == "btn-clone-default":
|
||||
self.app.push_screen(
|
||||
CloneHubScreen(
|
||||
FUZZFORGE_DEFAULT_HUB_URL,
|
||||
FUZZFORGE_DEFAULT_HUB_NAME,
|
||||
is_default=True,
|
||||
),
|
||||
callback=self._on_hub_action,
|
||||
)
|
||||
elif event.button.id == "btn-link":
|
||||
self.app.push_screen(
|
||||
LinkHubScreen(),
|
||||
callback=self._on_hub_action,
|
||||
)
|
||||
elif event.button.id == "btn-clone":
|
||||
self.app.push_screen(
|
||||
CloneHubScreen(),
|
||||
callback=self._on_hub_action,
|
||||
)
|
||||
elif event.button.id == "btn-remove":
|
||||
self._remove_selected()
|
||||
|
||||
def _on_hub_action(self, result: str | None) -> None:
|
||||
"""Handle result from a sub-screen."""
|
||||
if result:
|
||||
self.query_one("#hub-status", Label).update(result)
|
||||
self.app.notify(result)
|
||||
self._refresh_hubs()
|
||||
|
||||
def _remove_selected(self) -> None:
|
||||
"""Remove the currently selected hub."""
|
||||
table = self.query_one("#hubs-table", DataTable)
|
||||
registry = load_hubs_registry()
|
||||
hubs = registry.get("hubs", [])
|
||||
|
||||
if not hubs:
|
||||
self.app.notify("No hubs to remove", severity="warning")
|
||||
return
|
||||
|
||||
idx = table.cursor_row
|
||||
if idx is None or idx < 0 or idx >= len(hubs):
|
||||
self.app.notify("Select a hub to remove", severity="warning")
|
||||
return
|
||||
|
||||
name = hubs[idx].get("name", "")
|
||||
result = unlink_hub(name)
|
||||
self.query_one("#hub-status", Label).update(result)
|
||||
self._refresh_hubs()
|
||||
self.app.notify(result)
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
"""Close the hub manager."""
|
||||
self.dismiss("refreshed")
|
||||
|
||||
|
||||
class LinkHubScreen(ModalScreen[str | None]):
|
||||
"""Modal for linking a local directory as an MCP hub."""
|
||||
|
||||
BINDINGS = [("escape", "cancel", "Cancel")]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the link dialog layout."""
|
||||
with Vertical(id="link-dialog"):
|
||||
yield Label("Link Local Hub", classes="dialog-title")
|
||||
|
||||
yield Label("Hub Name:", classes="field-label")
|
||||
yield Input(placeholder="my-hub", id="name-input")
|
||||
|
||||
yield Label("Directory Path:", classes="field-label")
|
||||
yield Input(placeholder="/path/to/hub-directory", id="path-input")
|
||||
|
||||
yield Label("", id="link-status")
|
||||
with Horizontal(classes="dialog-buttons"):
|
||||
yield Button("Link", variant="primary", id="btn-link")
|
||||
yield Button("Cancel", variant="default", id="btn-cancel")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button clicks."""
|
||||
if event.button.id == "btn-cancel":
|
||||
self.dismiss(None)
|
||||
elif event.button.id == "btn-link":
|
||||
self._do_link()
|
||||
|
||||
def _do_link(self) -> None:
|
||||
"""Execute the link operation."""
|
||||
name = self.query_one("#name-input", Input).value.strip()
|
||||
path = self.query_one("#path-input", Input).value.strip()
|
||||
|
||||
if not name:
|
||||
self.app.notify("Please enter a hub name", severity="warning")
|
||||
return
|
||||
if not path:
|
||||
self.app.notify("Please enter a directory path", severity="warning")
|
||||
return
|
||||
|
||||
result = link_hub(name, path)
|
||||
self.dismiss(result)
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
"""Dismiss without action."""
|
||||
self.dismiss(None)
|
||||
|
||||
|
||||
class CloneHubScreen(ModalScreen[str | None]):
|
||||
"""Modal for cloning a git hub repository and linking it.
|
||||
|
||||
When instantiated with *is_default=True* and FuzzingLabs URL,
|
||||
provides a one-click setup for the standard security hub.
|
||||
|
||||
"""
|
||||
|
||||
BINDINGS = [("escape", "cancel", "Cancel")]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
default_url: str = "",
|
||||
default_name: str = "",
|
||||
is_default: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._default_url = default_url
|
||||
self._default_name = default_name
|
||||
self._is_default = is_default
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the clone dialog layout."""
|
||||
title = "Clone FuzzingLabs Hub" if self._is_default else "Clone Git Hub"
|
||||
with Vertical(id="clone-dialog"):
|
||||
yield Label(title, classes="dialog-title")
|
||||
|
||||
yield Label("Git URL:", classes="field-label")
|
||||
yield Input(
|
||||
value=self._default_url,
|
||||
placeholder="git@github.com:org/repo.git",
|
||||
id="url-input",
|
||||
)
|
||||
|
||||
yield Label("Hub Name (optional):", classes="field-label")
|
||||
yield Input(
|
||||
value=self._default_name,
|
||||
placeholder="auto-detect from URL",
|
||||
id="name-input",
|
||||
)
|
||||
|
||||
yield Static("", id="clone-status")
|
||||
with Horizontal(classes="dialog-buttons"):
|
||||
yield Button(
|
||||
"Clone & Link",
|
||||
variant="primary",
|
||||
id="btn-clone",
|
||||
)
|
||||
yield Button("Cancel", variant="default", id="btn-cancel")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button clicks."""
|
||||
if event.button.id == "btn-cancel":
|
||||
self.dismiss(None)
|
||||
elif event.button.id == "btn-clone":
|
||||
self._start_clone()
|
||||
|
||||
def _start_clone(self) -> None:
|
||||
"""Validate input and start the async clone operation."""
|
||||
url = self.query_one("#url-input", Input).value.strip()
|
||||
if not url:
|
||||
self.app.notify("Please enter a git URL", severity="warning")
|
||||
return
|
||||
|
||||
self.query_one("#btn-clone", Button).disabled = True
|
||||
self.query_one("#clone-status", Static).update("⏳ Cloning repository...")
|
||||
self._do_clone(url)
|
||||
|
||||
@work(thread=True)
|
||||
def _do_clone(self, url: str) -> None:
|
||||
"""Clone the repo in a background thread."""
|
||||
name_input = self.query_one("#name-input", Input).value.strip()
|
||||
name = name_input or None
|
||||
|
||||
success, msg, path = clone_hub(url, name=name)
|
||||
if success and path:
|
||||
hub_name = name or path.name
|
||||
link_result = link_hub(
|
||||
hub_name,
|
||||
path,
|
||||
git_url=url,
|
||||
is_default=self._is_default,
|
||||
)
|
||||
self.app.call_from_thread(self.dismiss, f"✓ {link_result}")
|
||||
else:
|
||||
self.app.call_from_thread(self._on_clone_failed, msg)
|
||||
|
||||
def _on_clone_failed(self, msg: str) -> None:
|
||||
"""Handle a failed clone — re-enable the button and show the error."""
|
||||
self.query_one("#clone-status", Static).update(f"✗ {msg}")
|
||||
self.query_one("#btn-clone", Button).disabled = False
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
"""Dismiss without action."""
|
||||
self.dismiss(None)
|
||||
421
hub-config.json
421
hub-config.json
@@ -6,7 +6,9 @@
|
||||
"type": "docker",
|
||||
"image": "nmap-mcp:latest",
|
||||
"category": "reconnaissance",
|
||||
"capabilities": ["NET_RAW"],
|
||||
"capabilities": [
|
||||
"NET_RAW"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -16,7 +18,9 @@
|
||||
"image": "binwalk-mcp:latest",
|
||||
"category": "binary-analysis",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"volumes": [
|
||||
"~/.fuzzforge/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -26,7 +30,9 @@
|
||||
"image": "yara-mcp:latest",
|
||||
"category": "binary-analysis",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"volumes": [
|
||||
"~/.fuzzforge/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -36,7 +42,9 @@
|
||||
"image": "capa-mcp:latest",
|
||||
"category": "binary-analysis",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"volumes": [
|
||||
"~/.fuzzforge/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -46,7 +54,9 @@
|
||||
"image": "radare2-mcp:latest",
|
||||
"category": "binary-analysis",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"volumes": [
|
||||
"~/.fuzzforge/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -56,7 +66,9 @@
|
||||
"image": "ghcr.io/clearbluejar/pyghidra-mcp:latest",
|
||||
"category": "binary-analysis",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"volumes": [
|
||||
"~/.fuzzforge/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -66,7 +78,9 @@
|
||||
"image": "searchsploit-mcp:latest",
|
||||
"category": "exploitation",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"volumes": [
|
||||
"~/.fuzzforge/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -75,8 +89,12 @@
|
||||
"type": "docker",
|
||||
"image": "nuclei-mcp:latest",
|
||||
"category": "web-security",
|
||||
"capabilities": ["NET_RAW"],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"capabilities": [
|
||||
"NET_RAW"
|
||||
],
|
||||
"volumes": [
|
||||
"~/.fuzzforge/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -86,7 +104,9 @@
|
||||
"image": "trivy-mcp:latest",
|
||||
"category": "cloud-security",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"volumes": [
|
||||
"~/.fuzzforge/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -96,10 +116,387 @@
|
||||
"image": "gitleaks-mcp:latest",
|
||||
"category": "secrets",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user