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.
- mcp_pfsense-0.1.0/.github/workflows/ci.yml +34 -0
- mcp_pfsense-0.1.0/.gitignore +12 -0
- mcp_pfsense-0.1.0/LICENSE +21 -0
- mcp_pfsense-0.1.0/PKG-INFO +156 -0
- mcp_pfsense-0.1.0/README.md +123 -0
- mcp_pfsense-0.1.0/pyproject.toml +65 -0
- mcp_pfsense-0.1.0/src/mcp_pfsense/__init__.py +1 -0
- mcp_pfsense-0.1.0/src/mcp_pfsense/client.py +152 -0
- mcp_pfsense-0.1.0/src/mcp_pfsense/config.py +28 -0
- mcp_pfsense-0.1.0/src/mcp_pfsense/server.py +196 -0
- mcp_pfsense-0.1.0/src/mcp_pfsense/tools/__init__.py +1 -0
- mcp_pfsense-0.1.0/src/mcp_pfsense/tools/dhcp.py +74 -0
- mcp_pfsense-0.1.0/src/mcp_pfsense/tools/dns.py +59 -0
- mcp_pfsense-0.1.0/src/mcp_pfsense/tools/firewall.py +81 -0
- mcp_pfsense-0.1.0/src/mcp_pfsense/tools/monitoring.py +56 -0
- mcp_pfsense-0.1.0/src/mcp_pfsense/tools/system.py +26 -0
- mcp_pfsense-0.1.0/tests/__init__.py +0 -0
- mcp_pfsense-0.1.0/tests/conftest.py +30 -0
- mcp_pfsense-0.1.0/tests/sample_data.py +244 -0
- mcp_pfsense-0.1.0/tests/test_config.py +33 -0
- mcp_pfsense-0.1.0/tests/test_dhcp.py +84 -0
- mcp_pfsense-0.1.0/tests/test_dns.py +59 -0
- mcp_pfsense-0.1.0/tests/test_firewall.py +83 -0
- mcp_pfsense-0.1.0/tests/test_monitoring.py +66 -0
- mcp_pfsense-0.1.0/tests/test_system.py +61 -0
|
@@ -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,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
|
+
[](https://pypi.org/project/mcp-pfsense/)
|
|
37
|
+
[](https://pypi.org/project/mcp-pfsense/)
|
|
38
|
+
[](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
|
+
[](https://pypi.org/project/mcp-pfsense/)
|
|
4
|
+
[](https://pypi.org/project/mcp-pfsense/)
|
|
5
|
+
[](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"
|