Metadata-Version: 2.4
Name: node804-panos-mcp
Version: 0.1.6
Summary: Token-efficient MCP server for Palo Alto Networks PAN-OS firewalls and Panorama
Project-URL: Homepage, https://github.com/Node804/node804-panos-mcp
Project-URL: Repository, https://github.com/Node804/node804-panos-mcp
Project-URL: Issues, https://github.com/Node804/node804-panos-mcp/issues
Author-email: Michael Pope <me@michaelpope.cv>
License-Expression: MIT
License-File: LICENSE
Keywords: firewall,mcp,model-context-protocol,network-security,palo-alto,pan-os,panorama,panos
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: System :: Networking :: Firewalls
Classifier: Topic :: System :: Systems Administration
Classifier: Typing :: Typed
Requires-Python: >=3.13
Requires-Dist: httpx<1.0,>=0.28.1
Requires-Dist: keyring<30,>=25.0
Requires-Dist: mcp[cli]<2.0,>=1.3.0
Requires-Dist: node804-mcp-toolkit<1.0,>=0.1.0
Requires-Dist: pan-os-python<2.0,>=1.12.0
Requires-Dist: pydantic<3.0,>=2.10.6
Requires-Dist: python-dotenv<2.0,>=1.0.0
Requires-Dist: setuptools>=68
Provides-Extra: dev
Requires-Dist: mypy<2.0,>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio<2.0,>=0.23; extra == 'dev'
Requires-Dist: pytest-cov<9.0,>=5.0; extra == 'dev'
Requires-Dist: pytest<11.0,>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: tiktoken<1.0,>=0.7; extra == 'dev'
Description-Content-Type: text/markdown

# PAN-OS MCP Server

[![CI](https://github.com/Node804/node804-panos-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/Node804/node804-panos-mcp/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/node804-panos-mcp)](https://pypi.org/project/node804-panos-mcp/)
[![Python](https://img.shields.io/pypi/pyversions/node804-panos-mcp)](https://pypi.org/project/node804-panos-mcp/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)


A token-efficient [MCP](https://modelcontextprotocol.io/) server for **Palo Alto Networks PAN-OS firewalls and Panorama**, built around three principles:

1. **Lean responses by default.** Every list-returning tool exposes only the fields you actually need to answer the question. Verbose responses are opt-in. Typical 40-70% token reduction vs. dump-everything alternatives.
2. **Default-deny RBAC.** With no environment configuration, the server exposes only read tools. Writes, commits, and raw API access require explicit opt-in via `PANOS_MODE`.
3. **TLS verification on by default.** Self-signed certs require an explicit opt-out or a custom CA bundle — no silent man-in-the-middle exposure.

**137 tools** across system, diagnostics/troubleshooting, network, security, objects, NAT, user-ID, admin, VPN, Panorama, logs, threat, certificates, licenses, utility, and aggregation surfaces.

## Permission Modes

| Mode | Tools | Description |
|------|-------|-------------|
| `read` *(default)* | 89 | Read-only inspection, live troubleshooting (policy-match / NAT-match / FIB-lookup tests, session table, config diff, rule hit counts), and aggregation summaries. No writes. |
| `standard` | 99 | Read + object and tag CRUD (address objects, service objects, tags). No policy rules. |
| `full` | 130 | Standard + policy rule CRUD (security, NAT, PBF, QoS, decryption, static routes; Panorama pre/post rules). **All writes go to candidate config — no live impact without commit.** |
| `admin` | 137 | Full + `commit`, push, `revert_config`, and the raw `set_config` / `delete_config` / `run_op_command` escape hatches. **This is the live-impact tier.** |

Tools above the active mode are not registered with the MCP server — the LLM never sees them, so there is nothing to call. Default mode is `read`.

## Setup

### Prerequisites

- **[uv](https://docs.astral.sh/uv/)** (recommended) — manages Python and dependencies for you
  - or **Python 3.13+** with pip
- A **PAN-OS firewall or Panorama** with the XML API enabled, plus an API key (see below)

### Getting a PAN-OS API key

PAN-OS issues API keys via the `/api/?type=keygen` endpoint. The key inherits the admin account's RBAC — there is no separate "API role," so a read-only admin's key cannot push config even if `PANOS_MODE=admin`.

> **Best practice:** create a dedicated service account (e.g. `apiread`) with a **Superuser (read-only)** dynamic role for `PANOS_MODE=read`. Issue a separate key from an account with write permissions for higher tiers.

**curl:**

```bash
curl -k -X POST "https://fw.example.com/api/?type=keygen" \
  --data-urlencode "user=apiread" \
  --data-urlencode "password=<password>"
```

**PowerShell:**

```powershell
$body = @{ user = 'apiread'; password = '<password>' }
(Invoke-RestMethod -Method Post -Uri 'https://fw.example.com/api/?type=keygen' -Body $body -SkipCertificateCheck).response.result.key
```

Copy the returned `<key>` into `PANOS_API_KEY`. The same endpoint works against Panorama — point it at the Panorama hostname. Keys do not expire by default but are invalidated if the admin's password changes or the account is deleted.

### Environment Variables

| Variable | Required | Description |
|----------|----------|-------------|
| `PANOS_HOST` | Yes\* | Single-firewall hostname. \*Not required in multi-firewall mode (see [Multi-firewall configuration](#multi-firewall-configuration)). |
| `PANOS_API_KEY` | Yes\* | Single-firewall API key. Stored in the OS keychain in multi-firewall mode. |
| `PANOS_MODE` | No | RBAC mode: `read`, `standard`, `full`, or `admin`. Default: `read`. |
| `PANOS_TLS_VERIFY` | No | `true` / `false` / `1` / `0` / `yes` / `no`. Default: `true`. |
| `PANOS_TLS_CA` | No | Path to a custom PEM CA bundle. Default: system trust store. |
| `PANOS_FIREWALLS_CONFIG` | No | Override path for `firewalls.json`. Default: `~/.config/panos-mcp/firewalls.json`. |
| `PANOS_AUDIT_LOG` | No | Path to a JSON-lines audit log. Every tool call is logged with sanitized args, success/error state, and timing. Disabled when unset. |
| `PANOS_ENABLE_RAW_CONFIG` | No | Register `set_config` / `delete_config` (also requires `PANOS_MODE=admin`). Default: `false`. |
| `PANOS_ENABLE_RAW_OPS` | No | Register `run_op_command` (also requires `PANOS_MODE=admin`). Default: `false`. |
| `PANOS_RAW_XPATH_ALLOW` | No | Comma-separated XPath prefixes that `set_config` / `delete_config` are restricted to. Default: unset (all non-denied). |

The raw escape hatches are **off by default and gated twice**: they register only when their `PANOS_ENABLE_RAW_*` flag is truthy *and* `PANOS_MODE=admin`, and even then an internal denylist blocks destructive verbs and protected config subtrees. See [`tools/_guards.py`](src/panos_mcp/tools/_guards.py).

### Install

**Option A — uv (recommended).** No install step: `uvx` fetches the package from PyPI on first run and caches it. Pin the version so upgrades are deliberate:

```bash
uvx node804-panos-mcp==0.1.6
```

**Option B — pip:**

```bash
pip install node804-panos-mcp
```

**Option C — from source (development):**

```bash
git clone https://github.com/Node804/node804-panos-mcp.git
cd node804-panos-mcp
uv sync          # or: pip install -e ".[dev]"
```

Each option provides the `node804-panos-mcp` command and the `panos_mcp` Python module (runnable as `python -m panos_mcp`). The package pulls in its dependency `node804-mcp-toolkit` — shared RBAC, audit-logging, TLS, and lean-response utilities — automatically.

> Until `node804-mcp-toolkit` is published to PyPI, install it from source first: `pip install git+https://github.com/Node804/node804-mcp-toolkit.git`

### Claude Desktop

1. Open your Claude Desktop config file:
   - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
   - **Mac:** `~/Library/Application Support/Claude/claude_desktop_config.json`

2. Add the server under `mcpServers` (create the file if it doesn't exist):

```json
{
  "mcpServers": {
    "PAN-OS": {
      "command": "uvx",
      "args": ["node804-panos-mcp==0.1.6"],
      "env": {
        "PANOS_HOST": "fw.example.com",
        "PANOS_API_KEY": "LUFRPT1xxx...",
        "PANOS_MODE": "read"
      }
    }
  }
}
```

If you installed with pip instead of uv, use `"command": "python"` with `"args": ["-m", "panos_mcp"]`.

3. Restart Claude Desktop — the server starts automatically when Claude needs it. It starts in **read-only mode** by default; raise `PANOS_MODE` to enable writes.

### Claude Code (CLI)

```bash
claude mcp add node804-panos-mcp \
  -e PANOS_HOST=fw.example.com \
  -e PANOS_API_KEY=LUFRPT1xxx... \
  -e PANOS_MODE=read \
  -- uvx node804-panos-mcp==0.1.6
```

Verify with `claude mcp list`.

### Verifying It Works

Once connected, ask your LLM:

- **"What's my access level?"** → calls `server_status`, showing the active mode, live tool counts, TLS posture, and configured firewalls
- **"What firewalls are configured?"** → calls `list_firewalls`, confirming the registry and which targets require an explicit `firewall` argument

If either fails, see [Troubleshooting](#troubleshooting).

## Token efficiency

Every list-returning tool accepts these parameters (from `node804-mcp-toolkit`):

| Parameter | Default | Effect |
|---|---|---|
| `verbose` | `false` | When `true`, returns every field the underlying API exposes. Default returns only the fields most relevant to the tool's purpose. |
| `fields` | `null` | Explicit projection. `["name", "action", "disabled"]` returns exactly those fields. Overrides both `verbose` and the default whitelist. |
| `pagination.limit` | `100` | Max items per response. Hard ceiling: 1000. |
| `pagination.offset` | `0` | Skip count. Use with `limit` to page through inventories. |
| `pattern` | `null` | Server-side substring filter on the item's name field. Far cheaper than fetching the inventory and filtering client-side. |

Responses are wrapped as `{total, count, items}` so the LLM knows when pagination truncated the result and whether to fetch more. Single-record tools (`get_firewall_info`, `get_ha_status`, etc.) accept `verbose` and `fields`.

### Measured savings

Token counts below were measured against synthetic fixtures sized to mid-range deployments (100 security rules, 500 address objects). Encoding: `cl100k_base` (GPT-4 / Claude approximation). Every scenario is a regression-guarded test in `tests/benchmark/` — the build fails if a future change widens a response shape past the budget.

| Scenario | Naive approach | Tokens | Better approach | Tokens | Saving |
| --- | --- | ---: | --- | ---: | ---: |
| Find an address object by IP | `get_address_objects` (dump 500) | 21,207 | `find_address_object("10.0.0.5")` | 88 | **241× smaller** |
| Inventory snapshot | `get_address_objects` (dump 500) | 21,207 | `count_objects_by_type` | 27 | **785× smaller** |
| Audit security policy | `get_security_rules` (verbose, 100 rules) | 19,627 | `summarize_security_policy` | 58 | **338× smaller** |
| Audit security policy | `get_security_rules` (lean, 100 rules) | 5,095 | `summarize_security_policy` | 58 | **88× smaller** |
| Find rules matching a pattern | `get_security_rules` (lean dump) | 5,095 | `find_rule_by_name("rule-001")` | 249 | **20× smaller** |
| Filter rules by pattern | `get_security_rules` (no filter) | 5,095 | `get_security_rules(pattern="rule-00")` | 523 | **10× smaller** |
| Lean default vs verbose | `get_security_rules(verbose=true)` | 19,627 | `get_security_rules` (default) | 5,095 | **74% reduction** |

Run the benchmark suite yourself:

```bash
pytest -v -s -m benchmark
```

The headline number: **a policy audit that would cost ~20K tokens via the naive "dump everything" approach costs ~60 tokens via `summarize_security_policy`.**

## Audit logging

Every tool invocation can be captured to a JSON-lines file:

```bash
export PANOS_AUDIT_LOG=/var/log/panos-mcp/audit.jsonl
```

Each line is one event:

```json
{"ts":"2026-05-07T14:32:11.483Z","tool":"add_security_rule","mode":"full","firewall":"hq-fw","args":{"name":"block-tor","action":"deny"},"success":true,"duration_ms":412}
```

Sensitive keys (`api_key`, `password`, `secret`, `token`, `auth`) are redacted at any depth before logging. Strings over 2 KB are elided. Write failures warn once and never block tool execution.

The audit log is the only after-the-fact record for admin-mode operations (`commit`, `set_config`, `run_op_command`). Treat it accordingly.

## TLS verification

Connections to PAN-OS are TLS-verified by default. For environments where the firewall presents a self-signed certificate or one signed by an internal CA:

```bash
# Option A: trust a custom CA bundle
export PANOS_TLS_CA=/etc/pki/internal-ca.pem

# Option B: disable verification (emits a startup warning)
export PANOS_TLS_VERIFY=false
```

The startup log reports the active posture:

```
PanOS TLS:   verify=ON (system trust store)
PanOS TLS:   verify=ON (custom CA via PANOS_TLS_CA)
PanOS TLS:   verify=OFF (PANOS_TLS_VERIFY=false)
```

## Multi-firewall configuration

For environments managing multiple firewalls or Panorama instances, define them in `~/.config/panos-mcp/firewalls.json` (override with `PANOS_FIREWALLS_CONFIG=/path`):

```json
{
  "firewalls": [
    { "name": "hq",       "host": "hq.example.com" },
    { "name": "branch",   "host": "branch.example.com" },
    { "name": "panorama", "host": "panorama.example.com" }
  ]
}
```

API keys live in the OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service via `keyring`). On first run, plaintext `api_key` fields in older configs auto-migrate to the keychain and the JSON is rewritten without them.

**Linux headless servers** without a keychain backend fall back to plaintext storage. The file is written with mode `0o600` (owner read/write only). The startup log emits a warning.

In multi-firewall mode, every tool that talks to a firewall requires an explicit `firewall: <name>` argument — no default is picked. Call `list_firewalls` to see configured targets and whether `firewall` is required.

## Tools by category

Each cell shows how many tools that category contributes at that RBAC tier. A `—` means none. The `Total` column sums across tiers; the bottom row matches the tool counts reported by `server_status`.

| Category          | Read | Standard | Full | Admin | Total |
| :---------------- | ---: | -------: | ---: | ----: | ----: |
| System            |    4 |        — |    — |     — |     4 |
| Diagnostics       |    5 |        — |    — |     — |     5 |
| Network           |    8 |        — |    2 |     — |    10 |
| Security          |    6 |        — |   12 |     — |    18 |
| Objects & tags    |    6 |       10 |    — |     — |    16 |
| NAT               |    1 |        — |    4 |     — |     5 |
| User-ID           |    3 |        — |    — |     — |     3 |
| Administrators    |    3 |        — |    — |     — |     3 |
| VPN               |    3 |        — |    — |     — |     3 |
| Panorama          |   22 |        — |    9 |     — |    31 |
| Logs              |    5 |        — |    — |     — |     5 |
| Threat & content  |    4 |        — |    — |     — |     4 |
| Certificates      |    3 |        — |    4 |     — |     7 |
| Licenses          |    2 |        — |    — |     — |     2 |
| Utility           |    4 |        — |    — |     1 |     5 |
| Server diagnostic |    2 |        — |    — |     — |     2 |
| Aggregations      |    8 |        — |    — |     — |     8 |
| Commit & raw API  |    — |        — |    — |     6 |     6 |
| **Total**         | **89** | **+10**  | **+31** | **+7** | **137** |

A few entries worth calling out by name:

- **Diagnostics** (read) — `test_security_policy_match`, `test_nat_policy_match`, `test_routing_fib_lookup`, `get_sessions`, `get_session_detail`. Live troubleshooting via read-only `<test>` and session ops.
- **Utility** — `get_config_xpath`, `run_show_command`, `get_pending_changes`, `get_config_diff` (read) for ad-hoc inspection and reviewing staged changes; `run_op_command` (admin) for the operational-command escape hatch.
- **Aggregations** (read) — includes `get_rule_hit_counts` and `unused_rules` for usage-based rule cleanup.
- **Commit & raw API** (admin) — `commit`, `panorama_commit`, `panorama_push_to_devices`, `revert_config`, `set_config`, `delete_config`.
- **Server diagnostic** — `server_status` and `list_firewalls`. Always available, even in `read` mode.

Call `server_status` from your MCP client at runtime to see the active mode, live tool counts, TLS posture, audit destination, and the configured firewall registry.

## Aggregation tools

Purpose-built summarization tools that replace "dump everything and grep" patterns with focused queries. Returning a structured answer is 10-20x cheaper in tokens than returning the full inventory and letting the LLM compute the same thing — the largest single source of token savings in the project.

| Tool | What it answers |
|---|---|
| `find_rule_by_name(pattern, rule_types=None)` | Search rule names across security, NAT, PBF, and decryption rulebases. Case-insensitive substring match; each result is tagged with its rule type. |
| `find_address_object(query)` | Find address objects by name substring **or** by IP membership. If the query parses as an IP/CIDR, returns objects whose ip-netmask, ip-range, or ip-wildcard value overlaps the query. Otherwise matches names. |
| `count_rules_by_action()` | `{"allow": 142, "deny": 38, "drop": 12}` plus enabled/disabled counts. Replaces a 10K-token rule dump with a ~200-token snapshot. |
| `summarize_security_policy()` | One-call policy audit: rule counts, action breakdown, any-any-any rules (worth reviewing), rules with logging disabled, rules without a profile group, and the set of zones referenced. |
| `count_objects_by_type()` | Inventory snapshot: address objects by type (ip-netmask vs fqdn vs range), address groups (static vs dynamic), service objects by protocol, plus tag and application-filter totals. |
| `get_rule_hit_counts(rule_base, vsys)` | Per-rule hit counts and last-hit timestamps (`show rule-hit-count`) — which rules actually carry traffic. |
| `unused_rules(vsys, include_disabled=False)` | Security rules with zero hits, joined with the rulebase for disabled status — a ready cleanup worklist. |

All are gated at `Mode.READ` — same tier as the underlying inventory reads.

## Troubleshooting

| Symptom | Cause | Fix |
|---------|-------|-----|
| `No module named panos_mcp` | Package not installed | Use the `uvx` config (installs automatically), or run `pip install node804-panos-mcp`. |
| Server starts but tools return an auth error | Invalid or expired API key | Regenerate the key (see [Getting a PAN-OS API key](#getting-a-pan-os-api-key)) and update your config. The key is invalidated if the admin's password changes. |
| TLS / certificate verification errors on connect | Firewall presents a self-signed or internal-CA cert | Set `PANOS_TLS_CA` to your CA bundle, or `PANOS_TLS_VERIFY=false` to disable verification (emits a startup warning). |
| Tools you expect are missing | Mode is too restrictive | Check `server_status` for the active mode, then raise `PANOS_MODE` (`read` → `standard` → `full` → `admin`). |
| Writes succeed but nothing changes on the firewall | Changes are staged in candidate config | Call `commit` (admin mode). Use `get_pending_changes` / `get_config_diff` to review what's staged first. |
| "firewall is required" errors | Multi-firewall mode with no target | Pass `firewall: <name>`; run `list_firewalls` to see configured targets. |
| `set_config` / `run_op_command` not available in admin mode | Raw escape hatches are off by default | Set `PANOS_ENABLE_RAW_CONFIG=true` / `PANOS_ENABLE_RAW_OPS=true` *and* `PANOS_MODE=admin`. |
| A vsys-scoped query returns the wrong vsys's data | Tools default to `vsys1` | This release targets `vsys1`; for other virtual systems use `get_config_xpath` with an explicit vsys path until a `vsys` parameter lands. |

## Development

```bash
git clone https://github.com/Node804/node804-panos-mcp.git
cd node804-panos-mcp
pip install -e ".[dev]"          # or: uv sync --extra dev

pytest -v                                        # full test suite
pytest -v -m "not integration and not benchmark" # CI-equivalent
ruff check . && ruff format --check .            # lint + format
mypy src                                         # type check
node804-panos-mcp                                # boot the server
```

`node804-mcp-toolkit` installs automatically as a dependency. Until it's on PyPI, install it from source first (see [Install](#install)).

## Acknowledgments

Built on [`pan-os-python`](https://github.com/PaloAltoNetworks/pan-os-python) — Palo Alto Networks' official Python SDK. The PAN-OS object model and API client behavior come from there; this project provides the MCP wrapper, response shaping, RBAC, and audit infrastructure on top.

## AI-Assisted Development

Portions of this project — including code, tests, and documentation — were developed with the assistance of generative AI (Anthropic's Claude, ChatGPT). All changes are human-reviewed before merge and exercised by the automated test suite, but as with any software, review the code and test against a non-production firewall or Panorama before trusting it with write access (`standard` mode or above) to a live device.

## License

MIT
