MCP: Giving AI Agents the Keys to the (Carefully Segmented) Kingdom

mcpjuniperopnsensearubasynologyclaude-code

The Model Context Protocol (MCP) is Anthropic’s answer to the question: how do you give an AI agent structured, safe access to real systems without either hardcoding a thousand tool definitions or letting it loose with unrestricted API access?

The answer is a standardised client-server protocol where each “MCP server” exposes a set of typed tools, the AI client calls those tools through a defined interface, and you get auditability, composability, and the ability to revoke access by stopping a process.

The lab has four. One for each piece of major infrastructure.

Why Custom MCP Servers

Pre-built MCP servers exist for many common services (GitHub, Slack, various databases). Pre-built servers for a Juniper EX3300 running JunOS 21.x, a OPNsense firewall with a custom REST API, an ArubaOS 8.x wireless controller, and a Synology NAS do not exist in any form that was useful without significant modification. So: custom servers.

All four use stdio transport rather than SSE (Server-Sent Events). This is a constitution-level requirement: SSE has reliability issues in Docker environments and behind reverse proxies, while stdio transport is a simple pipe that works reliably anywhere Python runs. Each server is a Python process that reads JSON-RPC requests from stdin and writes responses to stdout.

Juniper MCP: Network Switch Management

The Juniper MCP server uses junos-eznc — PyEZ, Juniper’s Python library for NETCONF-based device management. It provides eleven tools:

  • execute_junos_command — Run arbitrary CLI commands (show interfaces, show route, etc.)
  • get_junos_config — Retrieve full device configuration
  • junos_config_diff — Compare configuration versions (rollback comparison)
  • gather_device_facts — Collect device model, software version, uptime
  • get_router_list — List configured devices from devices.json
  • load_and_commit_config — Apply configuration changes (set/text/XML formats)
  • render_and_apply_j2_template — Apply Jinja2 configuration templates
  • add_device / reload_devices — Manage the device list at runtime

The devices.json config maps friendly names to connection parameters:

{
  "core-switch": {
    "ip": "172.16.100.254",
    "port": 22,
    "username": "<ai-service-account>",
    "auth": { "type": "ssh_key", "private_key_path": "~/.ssh/id_ed25519" }
  }
}

SSH key authentication only. No passwords for network device access — that’s a requirement, not a preference. The network device service account is a named account in Authentik that has Juniper CLI access but no write permissions by default. Load-and-commit operations require explicit tool invocation with a configuration payload, which means the agent has to deliberately construct and submit a config change — it can’t accidentally commit one as a side effect of a query.

OPNsense MCP: Firewall Management

The OPNsense server uses FastMCP and the OPNsense REST API. OPNsense’s built-in API exposes firewall rules, aliases, DHCP leases, interfaces, routing tables, gateway status, WireGuard tunnels, and more via authenticated REST calls.

Key capabilities:

  • Query firewall rules and aliases by interface and direction
  • View DHCP leases and correlate them with Nmap discovery data
  • Check routing table entries and gateway health (useful for debugging the WireGuard VPN routes)
  • View interface statistics, WireGuard tunnel state, and live firewall state counts

The MCP server is launched directly from its Python package; credentials come from a .env file populated by the VaultWarden CLI at start time, with the secret entry named after the appliance:

source <(vault-get-secret.sh "MCP - OPNsense API Key" notes)
export OPNSENSE_URL="https://opnsense.sdx.local"
exec /usr/bin/python3 -m src.main

Aruba MCP: Wireless Controller

The Aruba server interacts with ArubaOS 8.x via its REST API. The scope is deliberately read-only — show commands only. Making configuration changes to the wireless controller via an AI agent is a well-intentioned idea right up until it causes a broadcast storm, at which point it becomes a very good reason for spending a Saturday with a console cable.

Available operations:

  • List access points and their status (up/down, clients connected, channel utilisation)
  • Show associated client devices (MAC, IP, SSID, signal strength)
  • Execute arbitrary show commands (output parsed and returned as text)
  • View controller health and system statistics

In practice, the Aruba MCP is most useful for answering “why is WiFi slow in the back bedroom” type queries — Claude can check which AP the device is associated with, what channel it’s on, and whether there’s interference, without anyone having to log into the controller web UI.

Synology MCP: NAS Management

The Synology server is the most functionally broad, covering five API surface areas:

  • FileStation — Browse files, create/delete directories, move files
  • DownloadStation — Manage downloads (add, pause, resume, status)
  • NFS — View and manage NFS mounts and exports
  • Health — Storage pool status, volume health, SMART data
  • User Management — List and manage NAS user accounts

The health endpoint is the one used most frequently: a quick query confirms that all storage pools are healthy, all disks are within normal temperature ranges, and no SMART errors are accumulating. The alternative is logging into the Synology web UI, which requires remembering the URL, the credentials, and whether you’re on the right VLAN.

Claude Code Integration

All four servers are configured in F:\Projects\.claude\settings.json:

{
  "mcpServers": {
    "juniper": {
      "command": "wsl",
      "args": ["-d", "Ubuntu", "--", "bash", "-c",
               "cd ~/mcp-servers/juniper && python3 jmcp.py -f devices.json -t stdio"],
      "type": "stdio"
    },
    "opnsense": {
      "command": "wsl",
      "args": ["-d", "Ubuntu", "--", "bash", "-c",
               "cd ~/mcp-servers/opnsense && python3 -m src.main"],
      "type": "stdio"
    }
  }
}

From Windows, Claude Code spawns WSL2 processes to launch the servers. From WSL2-native contexts (LangGraph agents running inside WSL2), the servers are launched directly without the wsl wrapper.

The Use Case: Conversational Infrastructure Management

The before state: checking firewall rules required logging into OPNsense, navigating to the firewall rules page, filtering by interface, and reading a table of rules.

The after state:

“Claude, show me all firewall rules that allow traffic from the VM VLAN to the infrastructure VLAN.”

Claude calls the OPNsense MCP, retrieves the relevant rules, and presents them in a readable format with a brief explanation of what each rule permits. The same query that took four browser navigation steps now takes one conversational turn.

More usefully: when debugging why a new VM couldn’t reach the Synology NAS, Claude was able to simultaneously query the OPNsense firewall rules, the Juniper switch port configuration, and the Synology NFS exports — correlating three separate data sources in a single response. That kind of multi-source correlation is where conversational infrastructure management earns its keep.

The servers have been running stably since May 8, 2026. The Juniper MCP in particular has become a first-line tool for switch configuration verification — it’s faster to ask Claude to diff the running config against a saved baseline than to SSH in and run the comparison manually.


Update — August 2026

All four servers are still running, still stdio, and the design has held. The details around them have moved.

The Juniper server now targets one device, not two. The EX3300 core and office switch were replaced on 6 June 2026 by a two-member EX3400 virtual chassis, which presents as a single logical device — so devices.json collapsed to one entry pointing at 172.16.100.254. Junos went from 12.3 to 23.4, which meant an ELS configuration rewrite, but nothing in the MCP server changed.

Where the configuration lives was wrong in this post. MCP servers are registered in .claude.json, not settings.json — the latter holds the model and proxy environment that the mode-switch scripts rewrite. Putting server definitions there means they get clobbered on the next backend switch.

Credentials come from per-server .env files now. VaultWarden was decommissioned in June, so the vault-get-secret.sh pattern above is gone. The replacement is less elegant and more reliable, and it’s forced by a constraint worth knowing: environment variables set by the MCP client do not survive the wsl -e boundary. A server launched from Windows into WSL2 sees none of them. The credentials have to be loaded from a file on the Linux side at import time, or the server starts and fails its first authenticated call.

There’s a matching trap with SSH keys. The Juniper server’s key has to live on the WSL2 ext4 filesystem, not on the mounted Windows drive, because everything under /mnt/f reports as mode 0777 and OpenSSH refuses to use a world-readable private key. The key is in the repo for backup; the copy that works is the one at ~/.ssh.

What each server turned out to need

Juniper — the read-only account uses a custom login class, not the built-in read-only one, which lacks configuration-view permission and therefore can’t do the single most useful thing on the list. It also genuinely cannot commit: when a firewall filter change needed reverting in July, the MCP account correctly refused and the change had to go through a break-glass session. That’s the separation this post described working exactly as intended, at the one moment it mattered.

OPNsense — grew to 28 tools, and one of them exposed something worth knowing about the platform: OPNsense keeps firewall rules in two separate stores. Legacy rules live in config.xml and appear under Firewall → Rules. Automation rules live elsewhere and are the only ones the REST API returns. Query the API and you see half the policy. The server now merges both and tags each rule with its source, which is the sort of fix that only exists because an audit produced a rule count that didn’t match the screenshot.

Aruba — ten read-only tools, and every single call pays a twenty-second penalty. The controller’s management authentication points at a RADIUS server that never accepts, so each login times out through its retransmits before falling back to the local account. TCP and TLS are instant; it’s purely the authentication step. It silently broke the weekly configuration backup in July until that script’s timeout was raised to 90 seconds.

Synology — moved to HTTPS on nas.sdx.local:5001. Its service account was briefly granted full NAS administrator rights to make the health and SMART tools work, which a security audit correctly flagged; the account was dropped back to non-admin, and some of the admin-scoped tools now return permission errors. That’s the right trade — an MCP server that can read disk temperatures does not need an account that can create users.

And Claude Code isn’t the only client any more. Homer — the Hermes agent that replaced Clawdia in June — reaches six lab MCP servers, these four plus Home Assistant, through stdio wrappers on the assistant VM. Same servers, same transport, different client. Building them for one AI client and having them work unchanged for another was the payoff of the protocol, and it’s the part of this post that needs no correction at all.