Thanks for your interest in contributing! This project is actively growing and new tools are welcome. Every contribution — whether a new tool, bug fix, or documentation improvement — is appreciated.
Add a new file inside the tools/ directory:
tools/
└── my_tool.py
Implement your tool using the standard pattern:
from utils.helpers import is_valid_domain , normalize_domain
def your_tool_name(domain: str) -> dict:
"""
One clear sentence describing what this tool does.
"""
try:
domain = normalize_domain(domain)
if not is_valid_domain(domain):
return {"success": False, "error": "Invalid domain format"}
result = {}
return {
"success": True,
"domain": domain,
# additional fields
}
except Exception as e:
return {"success": False, "error": str(e)}For tools that operate on IP addresses, validate using Python's built-in ipaddress module:
import ipaddress
try:
ip = str(ipaddress.ip_address(ip_address.strip()))
except ValueError:
return {"success": False, "error": "Invalid IP address format"}Import and register the tool in server.py:
from tools.file_name import tool_name
mcp.tool()(tool_name)Create a corresponding test file inside the tests/ directory:
tests/
└── test_my_tool.py
Run the tests:
pytest tests/test_my_tool.py -vAll tests should pass without any warning or error.
Every new tool must include at least:
- One happy-path test (valid input, expected output)
- One failure-path test (invalid input or error handling)
Test the tool using MCP Inspector:
fastmcp dev inspector server.pyVerify that:
- The tool appears in the available tools list
- Inputs are validated correctly
- Expected results are returned
- Error handling works as intended
- Add the tool to the relevant tools-table in
README.md - Add the relevant info in
mcp.json - If the tool requires an API key, document how to obtain and configure it in the README & mcp.json
If your tool requires new environment variables, add them to .env.example & server.json with placeholder values:
SHODAN_API_KEY=your_api_key_here
VIRUSTOTAL_API_KEY=your_api_key_hereNever commit real API keys, secrets, or credentials to the repository.
Additional steps on top of How to Add a New Tool, do those first.
Add an entry to TOOL_REGISTRY in tools/signals/registry.py:
from tools.your_tool_file import your_tool_name
from tools.signals.your_tool_file import your_tool_extractor
TOOL_REGISTRY = [
...
{
"name": "your_tool",
"fn": your_tool_name,
"wave": 1,
"args": lambda domain, results: (domain,),
"extractor": your_tool_extractor,
},
]wave: waves run in order (1 -> 2 -> 3), tools within a wave run in parallel. Pick by category, not just by whether the tool needsdomain:- Wave 1, lightweight API and infrastructure record lookups (
whois,dns,ssl,email_security,asn). - Wave 2, aggressive port/tech scans and throttled log aggregators (
ports,techstack,ct_logs; crt.sh rate-limits). - Wave 3, tools that depend on Wave 1/2 output (
ip_reputation, which needs an IP resolved byasn/dns).
- Wave 1, lightweight API and infrastructure record lookups (
should_run/skip_reason: use when the tool can't run without another tool's successful output, e.g.ip_reputationneeds an IP address, so it only runs ifasn/dnsresolved one. Ifshould_runreturnsFalse,fnis never called and the tool is recorded as skipped withskip_reasoninstead of a result.
Add a file in tools/signals/:
def your_tool_extractor(result, signals):
if not result.get("success"):
return
signals["your_signal_key"] = result.get("some_field")Add your_signal_key with a default value to the base signals dict in tools/signals/extractor.py, otherwise _format_signals_block() will KeyError on skipped/failed runs. Match the default to the signal's type, not just None: None for a not-yet-known scalar (domain_expiry_days), [] for a list (dns_missing_records, open_ports), {} for grouped data (email_security), 0 for a counter (subdomain_count), False for a flag (ip_reputation_flagged).
Append plain-English strings to signals["auto_warnings"] when a value crosses a risk threshold. Follow the tiered pattern in ssl_extractor:
if expiry < 0:
signals["auto_warnings"].append(f"SSL certificate expired {abs(expiry)} days ago — all HTTPS traffic at risk")
elif expiry < 14:
signals["auto_warnings"].append(f"SSL certificate expires in {expiry} days — CRITICAL, renew immediately")- Add a line for your signal in
_format_signals_block()intools/fullrecon_tool.py; required, or the LLM never sees it. - Update
tools/prompts/threat_analysis.pyonly if the signal needs its own correlation rule (see "EVIDENCE QUALITY RULES").
- Add the tool to
Included in full_reconinREADME.md, notStandalone Tools.
- Unit test the tool (happy path + failure path).
- Test the extractor directly with mocked
resultdicts, asserting onsignals. - Update
mock_signals_fullintests/test_full_recon.pyif you added a new signal key.
pytest tests/ -v- Fork the repo
- Create a branch:
git checkout -b tool/your-tool-name
- Add your tool to
tools/following the pattern above - Test it in the MCP Inspector:
fastmcp dev inspector server.py
- Add unit test for tool in
tests/. - Update the tools table in
README.md - Update the relevant info in
mcp.json - Update the relevant info in
server.json - Update
requirements.txtif you added a new dependency - Update
.env.exampleif your tool needs an API key - Open a pull request with a short description
- Tool follows the existing pattern in
tools/ - Input is validated (
is_valid_domain()oripaddressmodule) - Returns
{"success": True/False, ...}on all code paths - Exceptions handled with
try/except— server must never crash - Unit test added in
tests/. - Dependencies are minimal — reuse existing libraries where possible
- Tools table updated in
README.md -
mcp.jsonupdated if applicable -
server.jsonupdated if applicable -
requirements.txtupdated if a new dependency was added -
.env.exampleupdated if a new API key is required
| Rule | Detail |
|---|---|
| Consistency | Every tool returns {"success": True/False, ...} |
| Validation | Always validate inputs before making external calls |
| Error handling | Catch exceptions and return {"success": False, "error": str(e)} |
| Dependencies | Check if a library is already used before adding a new one |
| Legal | Use scanme.nmap.org for port scanning tests — the only public host officially permitted for Nmap testing |
| API keys | Never hardcode keys; always use os.getenv("YOUR_API_KEY") |
Open an issue on GitHub or reach out on LinkedIn: Gaohar Imran