Read-only pfSense MCP server work-in-progress for safely exposing selected pfSense status data to an MCP client.
The current codebase provides the read-only configuration, authenticated WebGUI session client, and parsers needed for the first MCP resources/tools. It intentionally favors conservative behavior over broad access: no pfSense mutation APIs are implemented.
Implemented so far:
- Safe local configuration loading from a dotenv-style
.envfile. - HTTPS-only pfSense base URL validation by default.
- Read-only mode enforcement via
PFSENSE_MCP_READ_ONLY=true. - Password redaction in configuration representations.
- Authenticated pfSense WebGUI login helpers using pfSense's
__csrf_magictoken. - Cookie-preserving WebGUI client transport using Python stdlib
urllib. - Relative-path validation for WebGUI page fetches.
- Read-only ARP table retrieval and parsing from
/diag_arp.php. - Read-only DHCP lease retrieval and parsing from
/status_dhcp_leases.php. - Read-only firewall state retrieval and parsing from
/diag_dump_states.php, with optional exact-IP filtering and state-kill action links stripped from results. - Read-only firewall log retrieval and parsing from
/status_logs_filter.php, with optional exact-IP/action/interface/protocol filtering and bounded results. - Read-only firewall alias retrieval and parsing from
/firewall_aliases.php, with mutating action links omitted from results. - Read-only firewall rule retrieval and parsing from
/firewall_rules.php, optionally for one interface tab, with mutating action links omitted from results. - Passive read-only troubleshooting reports that correlate ARP, DHCP, firewall states/logs, aliases, and rules without active probes or configuration changes.
- MCP stdio server entrypoint with read-only login-check, ARP, DHCP, firewall-state, firewall-log, firewall-alias, firewall-rule, host-diagnosis, and health-report tools annotated as non-destructive.
- Deterministic pytest coverage for configuration, auth helpers, WebGUI client behavior, ARP parsing, DHCP parsing, firewall-state parsing, firewall inspection parsing, passive troubleshooting, and MCP tool handler registration.
Not implemented yet:
- pfSense REST API integration.
- Interface/VLAN, route/gateway, and NDP read-only tools.
- Any mutating pfSense action. Mutations are intentionally out of scope unless explicitly approved later.
This repository is designed for a cautious homelab security workflow.
- Read-only is the default and expected operating mode.
- Local secrets belong only in
.env, which is gitignored. - Do not commit real pfSense credentials, API keys, cookies, CSRF tokens, or exported configs containing secrets.
- WebGUI requests are limited to relative paths and reject absolute URLs and parent-directory traversal.
- Firewall state inspection is read-only: parser output omits WebGUI state-kill action links, validates optional IP filters as single IP addresses, and caps returned states.
- Firewall log/rule/alias inspection is read-only: parser output omits WebGUI action URLs and labels that would enable mutation, validates optional filters before WebGUI fetches where applicable, and caps returned log entries.
- MCP tools are annotated with
readOnlyHint=TrueanddestructiveHint=Falsefor compatible clients. - TLS verification is enabled by default. Disable it only deliberately for local/self-signed homelab testing.
- MCP stdout should remain clean JSON-RPC when the MCP server entrypoint is added; diagnostics should go to stderr.
- Python 3.11+
mcpPython SDK for the stdio MCP serverpytestfor testspylintfor lintingbanditfor Python static security scanningdetect-secretsfor repository secret scanningpip-auditfor Python dependency vulnerability auditingpre-commitfor local commit-time guardrails
Create a local .env file in the repository root:
PFSENSE_BASE_URL=https://192.168.1.1:8843
PFSENSE_USERNAME=<read-only-pfsense-username>
PFSENSE_PASSWORD=<read-only-pfsense-password>
PFSENSE_MCP_READ_ONLY=true
# Optional safety/runtime knobs. Defaults shown.
PFSENSE_VERIFY_TLS=true
PFSENSE_TIMEOUT_SECONDS=15.0
PFSENSE_MAX_RESPONSE_BYTES=2000000Recommended file permissions:
chmod 600 .envNotes:
PFSENSE_BASE_URLmust be an HTTPS origin URL only: scheme, host, and optional port; no username, password, path, query, or fragment.- HTTPS is required by default.
PFSENSE_MCP_READ_ONLYdefaults totruewhen omitted, but keeping it explicit is clearer.PFSENSE_VERIFY_TLS=falseis available for local self-signed certificates, but only use it after accepting the MITM risk.PFSENSE_MCP_ENV_PATHcan point the installed console script at a gitignored env file when the process is launched from another working directory.- Use a dedicated pfSense read-only account.
From the repository root:
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install --upgrade pip
python3 -m pip install -e ".[dev]"
pre-commit installBecause the package uses a src/ layout, either run tools with the configured project settings or set PYTHONPATH=src for direct Python snippets.
Run the MCP server over stdio:
cd /home/alex/repos/pfsense-mcp-server
python3 -m pfsense_mcp.serverIf the project is installed in a virtual environment, the console script is also available:
pfsense-mcp-serverCurrent read-only MCP tools:
pfsense_check_webgui_login— returns reachability/authentication metadata only:reachable,authenticated,base_url_host,read_only, anderror_typeon failure. It never returns exception messages, passwords, cookies, or CSRF tokens.pfsense_get_arp_table— returns parsed ARP table entries from/diag_arp.php; failures return safeerror_typemetadata only.pfsense_get_dhcp_leases— returns parsed DHCP lease entries from/status_dhcp_leases.php; failures return safeerror_typemetadata only.pfsense_get_firewall_states— returns parsed active firewall states from/diag_dump_states.php, optionally exact-filtered byip_addressand capped bylimit(max 200); action links for killing states are never returned.pfsense_get_firewall_logs— returns parsed firewall log entries from/status_logs_filter.php, optionally filtered by exactip_address,action(pass,block,reject),interface, and protocol prefix; returned entries are capped bylimit(max 200).pfsense_get_firewall_aliases— returns parsed firewall aliases from/firewall_aliases.php; action links for editing/copying/deleting aliases are never returned.pfsense_get_firewall_rules— returns parsed firewall rules from/firewall_rules.php, optionally for one safe interface token; action links for editing/toggling/deleting rules are never returned.pfsense_diagnose_host— correlates passive evidence for one exact IP address across ARP, DHCP leases, active firewall states, recent firewall logs, aliases, and candidate firewall rules. Optionaldestination_portandprotocolnarrow log/rule evidence. It does not ping, run traceroute, perform DNS lookups, or change pfSense state.pfsense_get_health_report— returns a passive health summary from the available WebGUI pages: ARP/DHCP counts, active state count, recent firewall log counts, block/reject count, alias/rule counts, disabled-rule count, per-component collection status, and active checks that were intentionally not performed.
Example Hermes MCP configuration snippet, for review only until explicitly applied:
mcp_servers:
pfsense:
command: "python3"
args: ["-m", "pfsense_mcp.server"]
env:
PFSENSE_MCP_ENV_PATH: "/home/alex/repos/pfsense-mcp-server/.env"
timeout: 120
connect_timeout: 60Load local configuration:
from pfsense_mcp import load_config
config = load_config(".env")
print(config) # password is redactedParse an exported or fixture ARP table HTML page:
from pfsense_mcp import parse_arp_table
entries = parse_arp_table(html)
for entry in entries:
print(entry.ip_address, entry.mac_address, entry.interface)Parse an exported or fixture DHCP leases HTML page:
from pfsense_mcp import parse_dhcp_leases
leases = parse_dhcp_leases(html)
for lease in leases:
print(lease.ip_address, lease.mac_address, lease.hostname, lease.online)Fetch read-only WebGUI data through the authenticated client:
from pfsense_mcp import PfSenseWebGuiClient, load_config
config = load_config(".env")
client = PfSenseWebGuiClient(config)
arp_entries = client.get_arp_table()
dhcp_leases = client.get_dhcp_leases()
matching_states = client.get_firewall_states(ip_address="192.168.1.202", limit=25)
blocked_logs = client.get_firewall_logs(action="block", limit=25)
aliases = client.get_firewall_aliases()
wan_rules = client.get_firewall_rules(interface="wan")Build a passive troubleshooting report from already-collected data:
from pfsense_mcp.troubleshooting import diagnose_host
host_report = diagnose_host(
"192.168.1.202",
arp_entries=arp_entries,
dhcp_leases=dhcp_leases,
firewall_states=matching_states,
firewall_logs=blocked_logs,
firewall_aliases=aliases,
firewall_rules=wan_rules,
destination_port="443",
protocol="tcp",
)
print(host_report["status"], host_report["issues"])For self-signed local pfSense certificates, prefer the .env setting below only when you understand the MITM risk:
PFSENSE_VERIFY_TLS=falseFor one-off Python snippets, you can still pass an explicit transport:
from pfsense_mcp.webgui import PfSenseWebGuiClient, UrlLibWebGuiTransport
from pfsense_mcp import load_config
config = load_config(".env")
transport = UrlLibWebGuiTransport(verify_tls=False)
client = PfSenseWebGuiClient(config, transport=transport)Run the full local verification gate:
make verifyOr run checks individually:
python3 -m pytest -q
PYTHONPATH=src python3 -m pylint src tests
python3 -m bandit --configfile pyproject.toml --recursive src
python3 -m pip_audit . --strict
python3 -m detect_secrets scan --baseline .secrets.baseline --force-use-all-plugins
git diff --checkRun all configured pre-commit hooks manually:
pre-commit run --all-filesThe GitHub Actions CI workflow runs tests, pylint, Bandit, pip-audit, and the detect-secrets baseline on pushes and pull requests.
For implementation work, use a strict read-first/TDD workflow:
- Add focused tests before production behavior.
- Run the focused test and confirm it fails for the expected reason.
- Implement the minimal read-only code path.
- Run focused tests, then the full test suite.
- Run
make verify,pre-commit run --all-files, andgit diff --check. - Review the diff for credentials, unsafe shell execution,
eval/exec, pickle usage, unsafe SQL string formatting, and unintended pfSense mutation paths. - Commit only after validation is clean.
Likely next read-only steps:
- Add interface and VLAN attribution.
- Add route and gateway status views.
- Add passive DNS resolver/forwarder, VPN status, and CARP/HA status views where safe WebGUI pages are available.
- Add NDP/IPv6 neighbor parsing.
- Revisit pfSense REST API support if the REST package/path is enabled in the target environment.
GitHub: git@github.com:stepanov1975/pfsense-mcp-server.git