mcp-pfsense 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.11", "3.12", "3.13"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: pip install -e ".[dev]"
26
+
27
+ - name: Lint
28
+ run: ruff check .
29
+
30
+ - name: Type check
31
+ run: mypy src/
32
+
33
+ - name: Test
34
+ run: pytest -v
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ *.egg
8
+ .venv/
9
+ .env
10
+ .mypy_cache/
11
+ .pytest_cache/
12
+ .ruff_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Antonio Mello
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-pfsense
3
+ Version: 0.1.0
4
+ Summary: MCP server for managing pfSense firewalls through AI assistants
5
+ Project-URL: Homepage, https://github.com/antonio-mello-ai/mcp-pfsense
6
+ Project-URL: Repository, https://github.com/antonio-mello-ai/mcp-pfsense
7
+ Project-URL: Issues, https://github.com/antonio-mello-ai/mcp-pfsense/issues
8
+ Author-email: Antonio Mello <antonio.mello@felhen.com.br>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,firewall,infrastructure,llm,mcp,pfsense,server
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: System :: Networking :: Firewalls
20
+ Classifier: Topic :: System :: Systems Administration
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: httpx>=0.27.0
23
+ Requires-Dist: mcp>=1.0.0
24
+ Requires-Dist: pydantic-settings>=2.0.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: build; extra == 'dev'
27
+ Requires-Dist: mypy>=1.13; extra == 'dev'
28
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
29
+ Requires-Dist: pytest>=8.0; extra == 'dev'
30
+ Requires-Dist: ruff>=0.8; extra == 'dev'
31
+ Requires-Dist: twine; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # mcp-pfsense
35
+
36
+ [![PyPI](https://img.shields.io/pypi/v/mcp-pfsense)](https://pypi.org/project/mcp-pfsense/)
37
+ [![Python](https://img.shields.io/pypi/pyversions/mcp-pfsense)](https://pypi.org/project/mcp-pfsense/)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
+
40
+ MCP server for managing **pfSense firewalls** through AI assistants like Claude, ChatGPT, and Copilot.
41
+
42
+ > **Requires**: [pfrest](https://github.com/pfrest/pfSense-pkg-RESTAPI) package installed on your pfSense instance (provides the REST API).
43
+
44
+ ## Features
45
+
46
+ **17 tools** across 6 categories:
47
+
48
+ | Category | Tools | Description |
49
+ |----------|-------|-------------|
50
+ | **System** | `get_system_status`, `get_interfaces` | Version, CPU, memory, uptime, temperature, network interfaces |
51
+ | **Firewall** | `list_firewall_rules`, `add_firewall_rule`, `delete_firewall_rule`, `list_firewall_aliases` | Rule management with interface filtering, alias listing |
52
+ | **DHCP** | `list_dhcp_leases`, `list_dhcp_static_mappings`, `add_dhcp_static_mapping`, `delete_dhcp_static_mapping` | Active leases, IP reservations |
53
+ | **DNS** | `list_dns_host_overrides`, `add_dns_host_override`, `delete_dns_host_override` | Unbound DNS Resolver host overrides |
54
+ | **Monitoring** | `get_gateway_status`, `get_arp_table`, `list_services` | Gateway health, connected devices, service status |
55
+ | **Services** | `restart_service` | Restart any pfSense service |
56
+
57
+ ### Safety
58
+
59
+ All destructive operations (delete rules, delete mappings, restart services) require **two-step confirmation** — the tool returns a warning on first call and only executes when called again with `confirm=true`.
60
+
61
+ ## Installation
62
+
63
+ ```bash
64
+ # Using uvx (recommended)
65
+ uvx mcp-pfsense
66
+
67
+ # Using pip
68
+ pip install mcp-pfsense
69
+ ```
70
+
71
+ ### Prerequisites
72
+
73
+ 1. **pfSense** with [pfrest](https://github.com/pfrest/pfSense-pkg-RESTAPI) package installed
74
+ 2. A user account with API access (typically `admin`)
75
+
76
+ ## Configuration
77
+
78
+ Set environment variables:
79
+
80
+ | Variable | Required | Default | Description |
81
+ |----------|----------|---------|-------------|
82
+ | `PFSENSE_HOST` | Yes | — | pfSense hostname or IP |
83
+ | `PFSENSE_PASSWORD` | Yes | — | API user password |
84
+ | `PFSENSE_USERNAME` | No | `admin` | API username |
85
+ | `PFSENSE_PORT` | No | `443` | API port |
86
+ | `PFSENSE_SCHEME` | No | `https` | `http` or `https` |
87
+ | `PFSENSE_VERIFY_SSL` | No | `false` | Verify SSL certificate |
88
+
89
+ ### Claude Desktop
90
+
91
+ Add to `claude_desktop_config.json`:
92
+
93
+ ```json
94
+ {
95
+ "mcpServers": {
96
+ "pfsense": {
97
+ "command": "uvx",
98
+ "args": ["mcp-pfsense"],
99
+ "env": {
100
+ "PFSENSE_HOST": "10.10.10.1",
101
+ "PFSENSE_PASSWORD": "your-password"
102
+ }
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ ### Claude Code
109
+
110
+ ```bash
111
+ claude mcp add pfsense -- uvx mcp-pfsense
112
+ ```
113
+
114
+ Then set environment variables in your shell or `.env` file.
115
+
116
+ ## Usage Examples
117
+
118
+ Once connected, ask your AI assistant:
119
+
120
+ - *"What's the pfSense system status?"*
121
+ - *"Show me all firewall rules on the LAN interface"*
122
+ - *"List active DHCP leases"*
123
+ - *"Add a DNS entry for nas.home.lan pointing to 10.10.10.50"*
124
+ - *"What devices are connected to the network?"* (ARP table)
125
+ - *"Show gateway health and latency"*
126
+ - *"Create a firewall rule to allow TCP port 8080 on LAN"*
127
+ - *"Reserve IP 10.10.10.60 for MAC aa:bb:cc:dd:ee:20"*
128
+
129
+ ## API Compatibility
130
+
131
+ - **pfSense**: 2.7.x (tested on 2.7.2)
132
+ - **pfrest**: v2.x (REST API v2)
133
+ - **Python**: 3.11+
134
+
135
+ > **Note**: pfrest runs on nginx (port 80 by default), separate from the pfSense WebGUI (lighttpd on port 443). If your pfrest is configured on a non-standard port, set `PFSENSE_PORT` and `PFSENSE_SCHEME` accordingly.
136
+
137
+ ## Development
138
+
139
+ ```bash
140
+ git clone https://github.com/antonio-mello-ai/mcp-pfsense.git
141
+ cd mcp-pfsense
142
+ python -m venv .venv
143
+ source .venv/bin/activate
144
+ pip install -e ".[dev]"
145
+
146
+ # Run tests
147
+ pytest
148
+
149
+ # Lint and type check
150
+ ruff check .
151
+ mypy src/
152
+ ```
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,123 @@
1
+ # mcp-pfsense
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/mcp-pfsense)](https://pypi.org/project/mcp-pfsense/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/mcp-pfsense)](https://pypi.org/project/mcp-pfsense/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ MCP server for managing **pfSense firewalls** through AI assistants like Claude, ChatGPT, and Copilot.
8
+
9
+ > **Requires**: [pfrest](https://github.com/pfrest/pfSense-pkg-RESTAPI) package installed on your pfSense instance (provides the REST API).
10
+
11
+ ## Features
12
+
13
+ **17 tools** across 6 categories:
14
+
15
+ | Category | Tools | Description |
16
+ |----------|-------|-------------|
17
+ | **System** | `get_system_status`, `get_interfaces` | Version, CPU, memory, uptime, temperature, network interfaces |
18
+ | **Firewall** | `list_firewall_rules`, `add_firewall_rule`, `delete_firewall_rule`, `list_firewall_aliases` | Rule management with interface filtering, alias listing |
19
+ | **DHCP** | `list_dhcp_leases`, `list_dhcp_static_mappings`, `add_dhcp_static_mapping`, `delete_dhcp_static_mapping` | Active leases, IP reservations |
20
+ | **DNS** | `list_dns_host_overrides`, `add_dns_host_override`, `delete_dns_host_override` | Unbound DNS Resolver host overrides |
21
+ | **Monitoring** | `get_gateway_status`, `get_arp_table`, `list_services` | Gateway health, connected devices, service status |
22
+ | **Services** | `restart_service` | Restart any pfSense service |
23
+
24
+ ### Safety
25
+
26
+ All destructive operations (delete rules, delete mappings, restart services) require **two-step confirmation** — the tool returns a warning on first call and only executes when called again with `confirm=true`.
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ # Using uvx (recommended)
32
+ uvx mcp-pfsense
33
+
34
+ # Using pip
35
+ pip install mcp-pfsense
36
+ ```
37
+
38
+ ### Prerequisites
39
+
40
+ 1. **pfSense** with [pfrest](https://github.com/pfrest/pfSense-pkg-RESTAPI) package installed
41
+ 2. A user account with API access (typically `admin`)
42
+
43
+ ## Configuration
44
+
45
+ Set environment variables:
46
+
47
+ | Variable | Required | Default | Description |
48
+ |----------|----------|---------|-------------|
49
+ | `PFSENSE_HOST` | Yes | — | pfSense hostname or IP |
50
+ | `PFSENSE_PASSWORD` | Yes | — | API user password |
51
+ | `PFSENSE_USERNAME` | No | `admin` | API username |
52
+ | `PFSENSE_PORT` | No | `443` | API port |
53
+ | `PFSENSE_SCHEME` | No | `https` | `http` or `https` |
54
+ | `PFSENSE_VERIFY_SSL` | No | `false` | Verify SSL certificate |
55
+
56
+ ### Claude Desktop
57
+
58
+ Add to `claude_desktop_config.json`:
59
+
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "pfsense": {
64
+ "command": "uvx",
65
+ "args": ["mcp-pfsense"],
66
+ "env": {
67
+ "PFSENSE_HOST": "10.10.10.1",
68
+ "PFSENSE_PASSWORD": "your-password"
69
+ }
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ ### Claude Code
76
+
77
+ ```bash
78
+ claude mcp add pfsense -- uvx mcp-pfsense
79
+ ```
80
+
81
+ Then set environment variables in your shell or `.env` file.
82
+
83
+ ## Usage Examples
84
+
85
+ Once connected, ask your AI assistant:
86
+
87
+ - *"What's the pfSense system status?"*
88
+ - *"Show me all firewall rules on the LAN interface"*
89
+ - *"List active DHCP leases"*
90
+ - *"Add a DNS entry for nas.home.lan pointing to 10.10.10.50"*
91
+ - *"What devices are connected to the network?"* (ARP table)
92
+ - *"Show gateway health and latency"*
93
+ - *"Create a firewall rule to allow TCP port 8080 on LAN"*
94
+ - *"Reserve IP 10.10.10.60 for MAC aa:bb:cc:dd:ee:20"*
95
+
96
+ ## API Compatibility
97
+
98
+ - **pfSense**: 2.7.x (tested on 2.7.2)
99
+ - **pfrest**: v2.x (REST API v2)
100
+ - **Python**: 3.11+
101
+
102
+ > **Note**: pfrest runs on nginx (port 80 by default), separate from the pfSense WebGUI (lighttpd on port 443). If your pfrest is configured on a non-standard port, set `PFSENSE_PORT` and `PFSENSE_SCHEME` accordingly.
103
+
104
+ ## Development
105
+
106
+ ```bash
107
+ git clone https://github.com/antonio-mello-ai/mcp-pfsense.git
108
+ cd mcp-pfsense
109
+ python -m venv .venv
110
+ source .venv/bin/activate
111
+ pip install -e ".[dev]"
112
+
113
+ # Run tests
114
+ pytest
115
+
116
+ # Lint and type check
117
+ ruff check .
118
+ mypy src/
119
+ ```
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,65 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mcp-pfsense"
7
+ version = "0.1.0"
8
+ description = "MCP server for managing pfSense firewalls through AI assistants"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ authors = [
13
+ { name = "Antonio Mello", email = "antonio.mello@felhen.com.br" },
14
+ ]
15
+ keywords = ["mcp", "pfsense", "firewall", "ai", "llm", "server", "infrastructure"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: System Administrators",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: System :: Networking :: Firewalls",
25
+ "Topic :: System :: Systems Administration",
26
+ ]
27
+ dependencies = [
28
+ "mcp>=1.0.0",
29
+ "httpx>=0.27.0",
30
+ "pydantic-settings>=2.0.0",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ dev = [
35
+ "pytest>=8.0",
36
+ "pytest-asyncio>=0.24",
37
+ "ruff>=0.8",
38
+ "mypy>=1.13",
39
+ "build",
40
+ "twine",
41
+ ]
42
+
43
+ [project.scripts]
44
+ mcp-pfsense = "mcp_pfsense.server:main"
45
+
46
+ [project.urls]
47
+ Homepage = "https://github.com/antonio-mello-ai/mcp-pfsense"
48
+ Repository = "https://github.com/antonio-mello-ai/mcp-pfsense"
49
+ Issues = "https://github.com/antonio-mello-ai/mcp-pfsense/issues"
50
+
51
+ [tool.ruff]
52
+ target-version = "py311"
53
+ line-length = 100
54
+
55
+ [tool.ruff.lint]
56
+ select = ["E", "F", "I", "UP", "B", "SIM"]
57
+
58
+ [tool.mypy]
59
+ python_version = "3.11"
60
+ strict = true
61
+ warn_return_any = true
62
+ warn_unused_configs = true
63
+
64
+ [tool.pytest.ini_options]
65
+ testpaths = ["tests"]
@@ -0,0 +1 @@
1
+ """MCP server for pfSense firewall management."""
@@ -0,0 +1,152 @@
1
+ """pfSense REST API client wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from mcp_pfsense.config import PfSenseConfig
10
+
11
+
12
+ class PfSenseClient:
13
+ """Thin wrapper around the pfSense REST API (pfrest package).
14
+
15
+ Uses Basic Auth and communicates via JSON with the /api/v2 endpoints.
16
+ """
17
+
18
+ def __init__(self, config: PfSenseConfig) -> None:
19
+ self._config = config
20
+ self._client: httpx.Client | None = None
21
+
22
+ @property
23
+ def client(self) -> httpx.Client:
24
+ """Lazy-initialize and return the HTTP client."""
25
+ if self._client is None:
26
+ self._client = httpx.Client(
27
+ base_url=self._config.base_url,
28
+ auth=(self._config.username, self._config.password),
29
+ verify=self._config.verify_ssl,
30
+ timeout=30.0,
31
+ )
32
+ return self._client
33
+
34
+ def _get(self, path: str, **params: Any) -> dict[str, Any]:
35
+ """Make a GET request and return the response data."""
36
+ resp = self.client.get(path, params=params or None)
37
+ resp.raise_for_status()
38
+ body: dict[str, Any] = resp.json()
39
+ return body
40
+
41
+ def _post(self, path: str, **data: Any) -> dict[str, Any]:
42
+ """Make a POST request and return the response data."""
43
+ resp = self.client.post(path, json=data or None)
44
+ resp.raise_for_status()
45
+ body: dict[str, Any] = resp.json()
46
+ return body
47
+
48
+ def _patch(self, path: str, **data: Any) -> dict[str, Any]:
49
+ """Make a PATCH request and return the response data."""
50
+ resp = self.client.patch(path, json=data or None)
51
+ resp.raise_for_status()
52
+ body: dict[str, Any] = resp.json()
53
+ return body
54
+
55
+ def _delete(self, path: str, **params: Any) -> dict[str, Any]:
56
+ """Make a DELETE request and return the response data."""
57
+ resp = self.client.delete(path, params=params or None)
58
+ resp.raise_for_status()
59
+ body: dict[str, Any] = resp.json()
60
+ return body
61
+
62
+ # --- System ---
63
+
64
+ def get_system_version(self) -> dict[str, Any]:
65
+ """Get pfSense version info."""
66
+ return self._get("/system/version")
67
+
68
+ def get_system_status(self) -> dict[str, Any]:
69
+ """Get system status (CPU, memory, uptime, temperature)."""
70
+ return self._get("/status/system")
71
+
72
+ # --- Interfaces ---
73
+
74
+ def get_interfaces(self) -> dict[str, Any]:
75
+ """List all network interfaces."""
76
+ return self._get("/interface")
77
+
78
+ # --- Firewall ---
79
+
80
+ def get_firewall_rules(self) -> dict[str, Any]:
81
+ """List all firewall rules."""
82
+ return self._get("/firewall/rule")
83
+
84
+ def create_firewall_rule(self, **params: Any) -> dict[str, Any]:
85
+ """Create a firewall rule."""
86
+ return self._post("/firewall/rule", **params)
87
+
88
+ def delete_firewall_rule(self, rule_id: int) -> dict[str, Any]:
89
+ """Delete a firewall rule by tracker ID."""
90
+ return self._delete("/firewall/rule", id=rule_id)
91
+
92
+ # --- DHCP ---
93
+
94
+ def get_dhcp_leases(self) -> dict[str, Any]:
95
+ """List active DHCP leases."""
96
+ return self._get("/status/dhcp_leases")
97
+
98
+ def get_dhcp_static_mappings(self, interface: str | None = None) -> dict[str, Any]:
99
+ """List DHCP static mappings."""
100
+ if interface:
101
+ return self._get("/services/dhcpd/static_mapping", interface=interface)
102
+ return self._get("/services/dhcpd/static_mapping")
103
+
104
+ def create_dhcp_static_mapping(self, **params: Any) -> dict[str, Any]:
105
+ """Create a DHCP static mapping."""
106
+ return self._post("/services/dhcpd/static_mapping", **params)
107
+
108
+ def delete_dhcp_static_mapping(self, mapping_id: int) -> dict[str, Any]:
109
+ """Delete a DHCP static mapping."""
110
+ return self._delete("/services/dhcpd/static_mapping", id=mapping_id)
111
+
112
+ # --- DNS ---
113
+
114
+ def get_dns_host_overrides(self) -> dict[str, Any]:
115
+ """List DNS Resolver host overrides."""
116
+ return self._get("/services/unbound/host_override")
117
+
118
+ def create_dns_host_override(self, **params: Any) -> dict[str, Any]:
119
+ """Create a DNS host override."""
120
+ return self._post("/services/unbound/host_override", **params)
121
+
122
+ def delete_dns_host_override(self, override_id: int) -> dict[str, Any]:
123
+ """Delete a DNS host override."""
124
+ return self._delete("/services/unbound/host_override", id=override_id)
125
+
126
+ # --- Gateways ---
127
+
128
+ def get_gateway_status(self) -> dict[str, Any]:
129
+ """Get gateway status (dual-WAN health)."""
130
+ return self._get("/status/gateway")
131
+
132
+ # --- ARP ---
133
+
134
+ def get_arp_table(self) -> dict[str, Any]:
135
+ """Get ARP table (connected devices)."""
136
+ return self._get("/diagnostics/arp_table")
137
+
138
+ # --- Services ---
139
+
140
+ def get_services_status(self) -> dict[str, Any]:
141
+ """List all services and their status."""
142
+ return self._get("/status/service")
143
+
144
+ def restart_service(self, name: str) -> dict[str, Any]:
145
+ """Restart a service by name."""
146
+ return self._post("/status/service", name=name, action="restart")
147
+
148
+ # --- Firewall Aliases ---
149
+
150
+ def get_firewall_aliases(self) -> dict[str, Any]:
151
+ """List firewall aliases."""
152
+ return self._get("/firewall/alias")
@@ -0,0 +1,28 @@
1
+ """Configuration for pfSense MCP server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic_settings import BaseSettings
6
+
7
+
8
+ class PfSenseConfig(BaseSettings):
9
+ """pfSense connection settings loaded from environment variables."""
10
+
11
+ host: str
12
+ username: str = "admin"
13
+ password: str = ""
14
+ port: int = 443
15
+ verify_ssl: bool = False
16
+ scheme: str = "https"
17
+
18
+ model_config = {"env_prefix": "PFSENSE_"}
19
+
20
+ @classmethod
21
+ def from_env(cls) -> PfSenseConfig:
22
+ """Create config from environment variables."""
23
+ return cls() # type: ignore[call-arg]
24
+
25
+ @property
26
+ def base_url(self) -> str:
27
+ """Build the base URL for the pfSense REST API."""
28
+ return f"{self.scheme}://{self.host}:{self.port}/api/v2"