Panorama Read-Only
A Model Context Protocol server enabling your AI client (Claude, Gemini, Codex …) to query Palo Alto Networks Panorama management servers in read-only mode using the PAN-OS XML API, running inside a Docker or Podman container.
What This Server Does
Panorama Read-Only exposes a curated set of read-only
tools that let an MCP-capable assistant inspect, but never modify, a Palo Alto
Networks Panorama deployment. It talks to Panorama over the PAN-OS XML API and
runs inside a Docker or Podman container.
The goal: give network and security teams a safe, audit-friendly way to put natural-language questions to their firewall management plane. Inspect a security policy, ask why a NAT rule exists, pull HA status, or grep through configuration without touching production state.
Every tool in this server is read-only. There are no write paths, no config
push, no commit. If a question would require changing Panorama state, the answer
is no.
What to Ask, in Plain English
A handful of real questions you can put to your AI client once this server is wired in. The model figures out which tools to call, you describe the outcome.
- "Show me all security rules that allow inbound RDP from any zone."
- "Which device groups have rules with threat scanning disabled?"
- "Pull the top-applications report for the last 7 days."
- "Are there uncommitted changes anywhere across Panorama right now?"
- "Which firewalls in the 'branch-offices' device group are out of sync?"
- "Run
show high-availability stateon firewall serial ABC123."
Before You Start
- Docker Desktop with the MCP Toolkit extension, or Podman 4.4+ (rootless). Pick one; the step-by-step for each is below.
- Palo Alto Networks Panorama running PAN-OS 11.1 or newer.
- Pre-generated PAN-OS API key, created via Panorama's API key generation endpoint.
- Read-only admin role bound to the API user. The server enforces this at the protocol level, but the role provides defense-in-depth.
23 Read-Only Tools
show commands and return results.Build & Configure
Two supported runtimes, pick the one you already run. Both build the same image
and keep your Panorama credentials out of the AI client's config: in the runtime's
secret store (recommended) or a local .env file you control. Generate
the API key first (it's identical for either path), then expand your runtime below.
$ curl -X POST 'https://<panorama-host>/api/?type=keygen' \ --data-urlencode 'user=<admin-username>' \ --data-urlencode 'password=<admin-password>'
Copy the <key> value from the response. Don't paste it into a chat, it goes into a runtime secret (or your local .env) in the steps below.
Docker Docker Desktop · MCP Gateway ›
Clone the repository so the Dockerfile and server files sit in your working directory.
$ git clone https://github.com/rosarion97/panorama-readonly-mcp-server-public.git $ cd panorama-readonly-mcp-server-public
$ docker build -t panorama-readonly-mcp-server .
Two ways to hand the container its credentials. Option A keeps your key in Docker's secret store and is recommended; Option B writes a plaintext .env for quick local testing.
$ docker mcp secret set PANORAMA_HOST="panorama.example.com" $ docker mcp secret set PANORAMA_API_KEY="LUFRPT1xxxxxxxxxxxxxxxxxxxxxxxxxx==" $ docker mcp secret set PANORAMA_VERIFY_SSL="yes"
With Option A, continue through Steps 4–7 below.
chmod 600 .env, never commit it. This path skips
the gateway: skip Steps 4–6, point your client at docker run --env-file
(below), then jump to Step 7.
$ cp .env.example .env $ chmod 600 .env # then edit .env: PANORAMA_HOST / PANORAMA_API_KEY / PANORAMA_VERIFY_SSL
{
"mcpServers": {
"panorama-readonly": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"--env-file", "/absolute/path/to/.env",
"panorama-readonly-mcp-server:latest"
]
}
}
}
Steps 4–6 are the Option A (secret store) path. Used Option B? Skip to Step 7.
$ mkdir -p ~/.docker/mcp/catalogs $ cp custom-catalog.yaml ~/.docker/mcp/catalogs/custom.yaml
Add the entry under the single top-level registry: key in ~/.docker/mcp/registry.yaml. Don't overwrite the file if it already exists.
registry:
panorama-readonly:
catalog: custom
enabled: true
Add the gateway block to claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/claude_desktop_config.json). The gateway runs as a container that spawns the Panorama image on demand, reads your catalog and registry, and resolves your PANORAMA_* secrets at request time. Replace <your-username> with your macOS username (run whoami to check):
{
"mcpServers": {
"mcp-toolkit-gateway": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-v", "/var/run/docker.sock:/var/run/docker.sock",
"-v", "/Users/<your-username>/.docker/mcp:/mcp",
"-v", "/Users/<your-username>/Library/Caches/docker-secrets-engine/engine.sock:/root/.cache/docker-secrets-engine/engine.sock",
"docker/mcp-gateway:latest",
"--catalog=/mcp/catalogs/custom.yaml",
"--registry=/mcp/registry.yaml",
"--transport=stdio"
]
}
}
}
All three bind-mounts are required: the Docker socket lets the gateway spawn the server container; ~/.docker/mcp is where it reads your catalog and registry; the docker-secrets-engine socket is the resolver Docker Desktop exposes for the secret store, without it your PANORAMA_* URLs resolve to empty strings and the server never starts. On Linux Docker Desktop the path is ~/.docker/desktop/secrets-engine/engine.sock; find it with find ~ -name engine.sock.
claude mcp add -s user mcp-toolkit-gateway -- docker run …; Codex reads the same shape from ~/.codex/config.toml as [mcp_servers.mcp-toolkit-gateway]. Any MCP-capable client, Google's Gemini included, reads an mcpServers block from its own settings file; only the location changes, the entry is identical.
Then quit and reopen the client. claude_desktop_config.json never contains the API key, the gateway resolves it from Docker's secret store at request time. As a shortcut, docker mcp client connect claude-desktop (or MCP Toolkit → Clients in Docker Desktop) writes a similar block automatically; the explicit JSON above survives Docker Desktop updates that may rewrite the auto-managed entry.
$ docker mcp server list # panorama-readonly → enabled $ docker mcp tools list
Podman Rootless · Podman 4.4+ ›
Clone the repo and change into the podman/ directory, which holds the Containerfile and server files.
$ git clone https://github.com/rosarion97/panorama-readonly-mcp-server-public.git $ cd panorama-readonly-mcp-server-public/podman
The machine is a small VM that runs your containers. Linux users skip this step.
$ podman machine init $ podman machine start
$ podman build -t panorama-readonly-mcp-server:latest .
Two ways to hand the container its credentials. Option A keeps your key in Podman's encrypted secret store and is recommended; Option B writes a plaintext .env (also the path for Podman older than 4.4, which lacks --secret type=env).
$ printf '%s' 'panorama.example.com' | podman secret create PANORAMA_HOST - $ printf '%s' 'LUFRPT1xxxxxxxxxxxxxxxxxxxxxxxxxx==' | podman secret create PANORAMA_API_KEY - $ printf '%s' 'yes' | podman secret create PANORAMA_VERIFY_SSL -
printf '%s' (no trailing newline) avoids a stray newline that triggers "Could not connect" errors.
chmod 600 .env, never commit it.
$ cp .env.example .env $ chmod 600 .env # then edit .env with your values
Add the entry matching your Step 4 choice. Option A references secret names only, the values stay in Podman. Option B reads them from your .env at container start.
{
"mcpServers": {
"panorama-readonly": {
"command": "podman",
"args": [
"run", "-i", "--rm",
"--secret", "PANORAMA_HOST,type=env",
"--secret", "PANORAMA_API_KEY,type=env",
"--secret", "PANORAMA_VERIFY_SSL,type=env",
"panorama-readonly-mcp-server:latest"
]
}
}
}
{
"mcpServers": {
"panorama-readonly": {
"command": "podman",
"args": [
"run", "-i", "--rm",
"--env-file", "/absolute/path/to/.env",
"panorama-readonly-mcp-server:latest"
]
}
}
}
mcpServers block from its own settings.json,
Google's Gemini included. Drop this same entry into that client's
settings.json and the Panorama tools appear there too. Only the
file's name and location change; the server entry is identical.
If podman isn't on the client's PATH (common for macOS GUI apps), replace "podman" with its absolute path, run which podman to find it.
Quit fully and reopen. The Panorama server should appear in the MCP tools list.
Run the same command the client will run, it should start and wait on stdin for JSON-RPC. Press Ctrl+C to exit.
$ podman run --rm -i \
--secret PANORAMA_HOST,type=env \
--secret PANORAMA_API_KEY,type=env \
--secret PANORAMA_VERIFY_SSL,type=env \
panorama-readonly-mcp-server:latest