MCP Registration
Single-source registration for exploit MCP tools: add @audit_tool or @require_allowlist() in tools/mcp_tools/<family>.py only. mcp_exploit_server.py auto-discovers via tools/mcp_tools/registry.collect_tools() and fails CI if a tool lacks the gate.
ToolContext
tools/mcp_tools/registry.py:102-111 — frozen dataclass injected into every register_*_tools(mcp, *, ctx):
@dataclass(frozen=True)
class ToolContext:
workspace: Path
config: dict[str, Any] | None
search: ExploitSearch
nvd: NVDClient
researcher: WebResearcher
audit_tool: Any
require_allowlist: Any
Built in mcp_exploit_server.create_mcp_server (mcp_exploit_server.py:135-143):
require_allowlist = make_require_allowlist(workspace, config)
audit_tool = make_audit_tool(workspace)
ctx = ToolContext(workspace, config, search, nvd, researcher, audit_tool, require_allowlist)
Families do from tools.mcp_tools.registry import * and use local audit_tool / require_allowlist captured from ctx (tools/mcp_tools/terminal.py:185-192, etc.).
The Two Decorators
@require_allowlist(target_param="target_ip") — structured target gate
Factory make_require_allowlist(workspace, config) (tools/kernel/audit.py:232-321, re-exported via tools/mcp_shared). Usage:
@require_allowlist()— target param defaults totarget_ip(e.g.tools/mcp_tools/recon.py:20)@require_allowlist("domain")— domain families (e.g.tools/mcp_tools/domain.py:292)
Behavior (tools/kernel/audit.py:239-321):
- Binds handler
*args/**kwargsviainspect.signature(fn).bindand reads the named target param (bound.arguments.get(target_param, "")). - Calls
_check_allowlist(target_ip, config)(tools/kernel/allowlist.py:67-80): ifexploit.require_explicit_allowlistisFalse→ allowed; else target must be in_allowed_target_list(config)viais_target_in_allowlist. Empty allowlist with flagTruefails closed. - Writes
startedaudit record (approved=allowed) with_redact_args(dict(bound.arguments))before gating; onnot allowedreturnsBLOCKED: ... ATTEMPT_ID: preflight. - On allowed, awaits/calls handler, then inspects result string for blocked markers (
_result_is_blocked) and writescompletedorblocked(approved=not blocked). Handles both sync andasynchandlers, preserving__signature__for FastMCP introspection and setting__wrapped_require_allowlist__/__wrapped_audit_tool__.
@audit_tool — audit for tools without a structured target
Factory make_audit_tool(workspace) (tools/kernel/audit.py:324-387, re-exported). Applied as bare @audit_tool (no call) to free-text command, callback-host, and local-only tools:
run_exploit_terminal,run_as_root,apt_install,generate_payload(lhost),msfconsole_command(RHOSTS),write_python_file,run_hash_crack
Behavior:
- Binds args, derives touched hosts via
_extract_audit_target(bound)(tools/kernel/audit.py:212-229): extractsRHOSTS/RHOST+ pivot hosts fromcommand/script_contentpluslhostarg. - Writes
startedrecord (approved=True), calls handler, then writesblocked/completedbased on_result_is_blocked(result).
Both decorators set __wrapped_audit_tool__ = True (and __wrapped_require_allowlist__ for the allowlist variant) so AST validation can detect them by substring audit_tool / require_allowlist (tools/mcp_tools/registry.py:384).
Choosing the gate (AGENTS.md rule 4)
- Structured
target_ipparam →@require_allowlist() - Structured
domainparam →@require_allowlist("domain") - Free-text command / script /
lhost/dc_ip→@audit_tool+ manualcheck_targets_allowlist([...], config)on extracted hosts inside the body (seedocs/mcp/security.md) - No target touch →
@audit_toolonly or nothing for pure queries (list_workspace,list_attack_modules)
Discovery — collect_tools()
tools/mcp_tools/registry.py:391-405:
def collect_tools() -> list[Any]:
registrars = _discover_tool_registrars()
errs = _validate_mcp_tool_decorators()
if errs:
raise RuntimeError("MCP tool decorator check failed:\n" + "\n".join(errs))
return registrars
_discover_tool_registrars() (tools/mcp_tools/registry.py:311-342)
- Returns cached
list(_TOOL_REGISTRARS)if already populated (via decorator or prior discovery). - Otherwise walks
tools.mcp_toolspackage viapkgutil.iter_modules(_pkg.__path__), skipsregistryand subpackages, imports eachtools.mcp_tools.<modname>(swallowsExceptionper module), and collects every callable whose attribute name matchesregister_*_tools(attr.startswith("register_") and attr.endswith("_tools")). - Appends to module global
_TOOL_REGISTRARS: list[Any] = [](tools/mcp_tools/registry.py:290), so subsequent calls are cached.register_tool_family(fn)decorator (tools/mcp_tools/registry.py:293-308) also appends explicitly.
Consumed in mcp_exploit_server.py:153-157:
for registrar in collect_tools():
try:
registrar(mcp, ctx=ctx)
except Exception:
logger.warning("MCP tool registration failed for %s", registrar.__name__, exc_info=True)
One bad family never breaks the rest; plugins follow same pattern (mcp_exploit_server.py:159-171).
_validate_mcp_tool_decorators() (tools/mcp_tools/registry.py:345-388)
Static ast check over every tools/mcp_tools/*.py (skips registry.py, __init__.py):
ast.parse(py.read_text()), walkFunctionDef/AsyncFunctionDef, collectdecorator_list.ast.unparse(decorator)lowercased;has_mcp_tool = "mcp.tool" in low or ".tool(" in low;has_audit = "audit_tool" in low or "require_allowlist" in low.- If
has_mcp_tool and not has_audit, appendf"{py.name}:{node.lineno} {node.name} has @mcp.tool but lacks @audit_tool/@require_allowlist".
collect_tools() raises RuntimeError with those errors → CI fails if a tool lacks the gate. This is the single enforcement point for AGENTS.md rule 4.
Shared Helpers Re-Exported via registry
tools/mcp_tools/registry.py:60-87, 413-511 re-exports for families' from tools.mcp_tools.registry import *:
ToolContext+_run_with_pgrp_timeout(compatibility shim that honorsmcp_exploit_server._run_with_pgrp_timeoutmonkeypatch,tools/mcp_tools/registry.py:113-136)_get_model_router/_get_model_client/_resolve_consult_aliases/_multi_model_enabled/_chat_content/_truncate_text/_skills_config/_runtime_skills_enabled_ensure_workspace_dirs(tools/mcp_tools/registry.py:276-279→ createsplans/exploits/modules/campaigns)_attempt_dir,_extract_msf_rhosts,_extract_scanner_targets,check_targets_allowlist,ps_quotevalidate_target,validate_target_or_ip,is_target_in_allowlist,is_fqdn,resolve_target_to_ip,preflight_command_check,is_subdomain_of- stdlib modules
asyncio, datetime, json, os, re, signal, socket, time, _ssl_module, Path, Any
Adding a New Tool
-
In
tools/mcp_tools/<family>.py, insideregister_<family>_tools(mcp, *, ctx), add:@mcp.tool() @require_allowlist() # or @audit_tool + manual check_targets_allowlist def my_tool(target_ip: str, ...) -> str: ... -
Validate inputs (
validate_target_or_ipon target args, regex on free-text,shlex+ shell-metachar rejection foroptions). -
Run subprocesses via
_run_with_pgrp_timeout(argv_list, timeout, ...)withtext=True, never a shell string. -
Write artifacts under
_attempt_dir(workspace)per attempt. -
No edit to
mcp_exploit_server.pyorregistry.pyneeded. Tests: mocksubprocess.Popen/_run_with_pgrp_timeout, never live Nmap.
Related Docs
docs/mcp/servers/exploit.mddocs/mcp/security.mddocs/mcp/tool-families/*.md