Sophos XGS MCP Server
by Leon69924
README.md
# Sophos XGS MCP Server
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server for the Sophos
XGS Firewall XML API. Gives AI assistants like Claude visibility into your firewall
configuration, and — only when you explicitly enable it — the ability to change it.
**Read-only by default.** The write tools are not registered unless
`SOPHOS_ENABLE_WRITE=1` is set. Without it this server cannot modify the appliance.
## Features
- **60+ read tools** covering firewall rules, NAT, VPN, web filtering, network config,
routing, DHCP, DNS, IPS, SSL/TLS inspection, and admin settings
- **Opt-in write tools** — create, update, delete and preview any entity
- **Read-modify-write updates** — updates never send a bare delta (see below)
- **Secret redaction** — passwords, hashes and pre-shared keys never reach the client
- **XML injection protection** — tag validation and escaping on every value; no tool
accepts raw XML
- **Credential isolation** — credentials come from protected config files
- **Scoped TLS handling** — the self-signed cert bypass is per-request, not global
## Tools
| Category | Tools |
|----------|-------|
| Profiles | `get_active_profile`, `list_profiles`, `switch_profile` — target multiple firewalls from one server |
| Firewall | `list_firewall_rules`, `list_firewall_rule_groups`, `get_firewall_rule`, `search_firewall_rules` |
| Network Objects | `list_ip_hosts`, `list_ip_host_groups`, `list_fqdn_hosts`, `list_fqdn_host_groups`, `list_mac_hosts`, `get_ip_host`, `search_ip_hosts` |
| Services | `list_services`, `list_service_groups`, `get_service` |
| Zones & Interfaces | `list_zones`, `list_interfaces`, `list_vlans`, `list_interface_aliases`, `list_lag_interfaces`, `get_zone`, `get_interface` |
| NAT | `list_nat_rules` |
| VPN | `list_sslvpn_policies`, `list_vpn_ipsec_connections` |
| Web Filtering | `list_web_filter_policies`, `list_url_groups`, `list_web_filter_exceptions` |
| SSL/TLS Inspection | `list_ssl_tls_inspection_rules`, `get_ssl_tls_inspection_settings`, `list_decryption_profiles` |
| IPS | `list_ips_policies` |
| Routing & SD-WAN | `list_static_routes`, `list_sdwan_policy_routes`, `list_sdwan_profiles`, `list_gateway_hosts`, `list_gateway_configurations` |
| DHCP & DNS | `list_dhcp_servers`, `list_dns_forwarders`, `list_dns_request_routes`, `list_dns_host_entries` |
| Users & Admin | `list_users`, `list_admin_profiles`, `get_admin_settings`, `get_admin_authen`, `get_user_activity` |
| System | `get_backup_restore_settings`, `get_time_settings`, `get_reports_retention`, `list_syslog_servers`, `list_notifications`, `list_snmpv3_users`, `get_pim_settings`, `get_captive_portal` |
| Email | `list_smtp_policies` |
| Access Control | `list_local_service_acl`, `list_schedules`, `list_access_time_policies`, `list_surfing_quota_policies`, `list_data_transfer_policies` |
| IPv6 | `list_router_advertisements` |
| Advanced | `raw_api_query` — query any entity with optional filters |
| **Write** (opt-in) | `create_entity`, `update_entity`, `delete_entity`, `preview_write` |
### Appliance quirks these tools cannot hide
- **`list_schedules` does not return the SFOS built-ins.** `All The Time` is missing
from the result *and* from an exact-name filter, yet it is the most-used schedule in
a typical ruleset. Absence from this list is not evidence that a schedule does not
exist — check the rules that reference it.
- **Some entity names are plural where the object is singular.** Service definitions
live under `Services`; a `Get` on `Service` fails with `529 Input request module is
Invalid`. That code means *unknown entity*, not *no such object* — if `raw_api_query`
returns it, try the other spelling before concluding the object is gone.
## Prerequisites
- Node.js 18+
- Sophos XGS Firewall with the XML API enabled (port 4444 by default)
- An API admin account. For read-only use, a restricted profile is strongly
recommended; see [Write access](#write-access).
### Enabling the Sophos XML API
1. Log into your Sophos XGS web admin
2. Go to **Backup & firmware** > **API**
3. Enable the API and allow access from the host running the MCP server
## Setup
### 1. Install
```bash
git clone https://github.com/Leon69924/sophos-xgs-mcp.git
cd sophos-xgs-mcp
pnpm install
pnpm build
```
### 2. Configure credentials
```bash
mkdir -p ~/.config/sophos-xgs
chmod 700 ~/.config/sophos-xgs
```
Create `~/.config/sophos-xgs/config.json` with one profile per firewall.
`activeProfile` selects which firewall the tools target; switch at runtime with the
`switch_profile` tool (the switch is persisted to this file):
```json
{
"activeProfile": "main",
"profiles": {
"main": {
"host": "your-sophos-hostname-or-ip",
"port": "4444",
"username": "your-api-user",
"description": "optional free-text note"
}
}
}
```
Create `~/.config/sophos-xgs/credentials-<profile>` with just the password, one file
per profile:
```bash
echo -n "your-password" > ~/.config/sophos-xgs/credentials-main
chmod 600 ~/.config/sophos-xgs/config.json ~/.config/sophos-xgs/credentials-main
```
Alternatively, use environment variables — they override the config file entirely
(profile switching is disabled while they are set):
```bash
export SOPHOS_HOST=your-sophos-hostname-or-ip
export SOPHOS_PORT=4444
export SOPHOS_USERNAME=your-api-user
export SOPHOS_PASSWORD=your-password
```
### 3. Add to your MCP client
For Claude Code, add to `~/.mcp.json`:
```json
{
"mcpServers": {
"sophos-xgs": {
"type": "stdio",
"command": "node",
"args": ["/path/to/sophos-xgs-mcp/dist/index.js"]
}
}
}
```
## Write access
Write tools are registered only when `SOPHOS_ENABLE_WRITE=1` is present in the
server's environment:
```json
{
"mcpServers": {
"sophos-xgs": {
"type": "stdio",
"command": "node",
"args": ["/path/to/sophos-xgs-mcp/dist/index.js"],
"env": { "SOPHOS_ENABLE_WRITE": "1" }
}
}
}
```
**Back this with a restricted API user.** The env flag governs what this server
offers; it does not limit what the credentials may do. Sophos authenticates the API
with ordinary administrator accounts, and their rights come from the assigned
administration profile, where every module can be set to `None`, `Read-Only` or
`Read-Write`. Use a read-only profile for read-only deployments, and give a write
deployment only the modules it actually needs. That boundary holds regardless of
bugs in this server.
### Updates are read-modify-write
`Set operation="update"` **replaces** the entity definition on the appliance. Fields
omitted from the request fall back to defaults — they do not stay unchanged. An update
meant to flip one flag can silently empty a firewall rule's source and destination
networks.
`update_entity` therefore always fetches the current object, merges your fields onto
it, and sends the complete definition. It aborts rather than continuing if:
- the current state cannot be loaded, or the name does not exist
- more than one object matches the identifier
- **the object that came back does not carry the field it was addressed by** (see below)
- any field present before the update would be missing from what is about to be sent
- after the write, a field changed that was not part of your update
That third condition exists because a filtered `Get` does not always answer "no such
object" by returning nothing. Four entities on an XGS 2100 — `SSLVPNPolicy`,
`VPNIPSecConnection`, `AdminSettings`, `GatewayConfiguration` — answer a `Name` filter
with the element and nothing inside it, because they hold their contents in one record
with no top-level `Name` to filter on. `SSLVPNPolicy` filtered by name returns literally
`[{}]`.
An empty result is not `null`, so the "does not exist" check passes; merging onto it
yields little more than your delta; and the field-loss check has nothing to compare,
because the fields were never there. What would reach the appliance is a *partial*
update — against an API that replaces what it updates. For `SSLVPNPolicy` that is every
tunnel policy on the box reduced to a name.
`create_entity` aborts if the name already exists, `delete_entity` fetches the object
first so you can see what disappears, and `preview_write` shows the exact XML and
field-level diff without sending anything.
To clear a field on purpose, pass `null`. Omitting it means "leave unchanged".
### Confirming every write
This server does not prompt — MCP gives it no mechanism to. In Claude Code you can get
a confirmation per write with a `PreToolUse` hook matching `mcp__sophos-xgs__.*` that
returns `permissionDecision: "ask"` for the write tools. `ask` outranks the permission
allowlist, so it keeps asking even after "don't ask again".
Two things such a guard should do, both learned the hard way:
- **Name the target appliance in the dialog.** `switch_profile` persists across
sessions, so without the hostname a change gets confirmed for one firewall and
applied to another.
- **Allowlist the read tools rather than denylisting the write tools.** A tool added
to a later version then lands on the safe side by default.
The design, including the guard's decision matrix, is written up in
[`docs/superpowers/specs/2026-08-04-write-support-design.md`](docs/superpowers/specs/2026-08-04-write-support-design.md).
## Security
- **Read-only by default** — write tools are absent without `SOPHOS_ENABLE_WRITE=1`
- **Secrets are redacted** — `Password`, `PasswordHash`, `PreSharedKey` and similar
fields are masked before results leave the server, and writing a redaction
placeholder back to the appliance is refused
- **No raw XML from callers** — tag names are validated against
`^[A-Za-z][A-Za-z0-9_]*$` and every value is escaped
- **No hardcoded credentials** — loaded from `~/.config/sophos-xgs/` or env vars
- **Error sanitization** — credentials are stripped from error messages
- **TLS** — all connections use HTTPS; the self-signed cert bypass is scoped to the
Sophos connection only
## Development
```bash
pnpm build # tsc
pnpm test # node --test
pnpm dev # run from source
```
## License
AGPL-3.0-only — see [LICENSE](LICENSE)
TDQS
C2.9/5.0
Scored across 63 tools
Disambiguation4/5
Most tools have distinct purposes, but list_acl_rules and list_local_service_acl overlap, both describing local service ACL rules, which could cause confusion.
Naming Consistency5/5
All tools follow a consistent verb_noun pattern (e.g., list, get, search), with no mixed conventions.
Tool Count2/5
63 tools is excessive for a single server, even for a comprehensive firewall system, making it difficult to navigate.
Completeness2/5
The server only provides read operations (list/get/search). The lack of create, update, or delete tools is a significant gap for any management purpose.
Maintenance
ActivitySlowing
ResponsivenessNo issues