If you’ve been following the AI space lately, you’ve probably heard the term MCP thrown around a lot. Model Context Protocol. It sounds complicated and enterprise-y, like something that requires a team of developers and a Jira board to set up. It doesn’t. And if you’re already doing network automation with Python, Ansible, and NetBox, I’ve got news for you: you’re already halfway there.
In this post, I’m going to walk you through building your own MCP server using FastMCP that exposes real network tools directly to an AI like Claude. By the end, you’ll have an AI that can query NetBox, pull device configs, and trigger AWX job templates, just by asking it to.
No hand-wavy demos. No “it’s my lab so I’ll skip best practices.” Let’s build something real.
What Even Is MCP?
Before we write a single line of code, let me explain MCP in plain English, because most explanations make it way more complicated than it needs to be.
MCP (Model Context Protocol) is an open standard that lets AI models like Claude talk to external tools and data sources in a structured, secure way. Think of it as a plugin system for AI. Instead of copying and pasting device configs into a chat window and asking Claude to analyze them, your MCP server exposes a get_device_config() function that Claude can call directly, on demand, with real-time data.
The big deal here is standardization. Without MCP, every AI tool has its own way of connecting to external services. MCP gives you one protocol, one server, and any MCP-compatible AI client can use your tools, Claude Desktop, VS Code extensions, custom agents, all of it.
FastMCP is a Python library that strips away the boilerplate and lets you build an MCP server in a fraction of the time. If you’ve used FastAPI before, you’ll feel right at home. If you haven’t, don’t worry, I’ll walk you through it.
Prerequisites
Before we get started, make sure you have the following:
- Python 3.10+ installed
- A virtual environment set up (you are using venvs, right?)
- NetBox instance with API access and a valid API token
- AWX or Ansible Automation Platform with an API token
- A network device reachable via SSH for the Netmiko example
- Claude Desktop installed (free), download here
- Ansible Vault or another secrets manager for your credentials, we are not hardcoding passwords (yes, I’m looking at you,go read this post if you haven’t already)
Install the required packages:
pip install fastmcp netmiko requests python-dotenv
Create a .env file for your secrets (we’ll load these with python-dotenv):
NETBOX_URL=https://your-netbox-instance.com
NETBOX_TOKEN=your_netbox_api_token
AWX_URL=https://your-awx-instance.com
AWX_TOKEN=your_awx_api_token
DEVICE_USERNAME=your_device_username
DEVICE_PASSWORD=your_device_password
⚠️ Add
.envto your.gitignore. Right now. Before you forget.
Project Structure
Keep it clean:
network-mcp/
├── server.py
├── tools/
│ ├── __init__.py
│ ├── netbox.py
│ ├── device_config.py
│ └── awx.py
├── .env
├── .gitignore
└── requirements.txt
Step 1: Create the FastMCP Server
This is your entry point. server.py is where you initialize FastMCP and register all your tools.
# server.py
from fastmcp import FastMCP
from tools.netbox import query_netbox
from tools.device_config import get_device_config
from tools.awx import run_awx_job
mcp = FastMCP(
name="Network Automation MCP",
instructions="""
You are a network automation assistant with access to real network tools.
Use get_device_config to retrieve live device configurations.
Use query_netbox to look up inventory, IP addresses, VLANs, and device data.
Use run_awx_job to trigger Ansible playbooks via AWX.
Always confirm destructive actions before executing them.
"""
)
mcp.tool()(query_netbox)
mcp.tool()(get_device_config)
mcp.tool()(run_awx_job)
if __name__ == "__main__":
mcp.run()
That’s it for the server file. FastMCP handles the protocol layer. You focus on writing the tools.
Step 2: Tool 1, Query NetBox
If you’ve read my post on using NetBox as an Ansible inventory, you already know how the NetBox API works. Here we’re wrapping that same logic into an MCP tool.
# tools/netbox.py
import os
import requests
from dotenv import load_dotenv
load_dotenv()
NETBOX_URL = os.getenv("NETBOX_URL")
NETBOX_TOKEN = os.getenv("NETBOX_TOKEN")
HEADERS = {
"Authorization": f"Token {NETBOX_TOKEN}",
"Content-Type": "application/json",
}
def query_netbox(endpoint: str, params: dict = {}) -> dict:
"""
Query the NetBox API.
Args:
endpoint: The NetBox API endpoint to query.
Examples: 'dcim/devices', 'ipam/ip-addresses', 'ipam/vlans'
params: Optional query parameters as a dictionary.
Example: {"site": "nyc-dc1", "status": "active"}
Returns:
A dictionary containing the API response from NetBox.
"""
url = f"{NETBOX_URL}/api/{endpoint}/"
try:
response = requests.get(url, headers=HEADERS, params=params, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
return {"error": str(e)}
Now Claude can answer questions like “What devices do we have in the NYC data center?” or “What VLANs are configured in NetBox?”, with live data, not whatever you pasted into the chat window last Tuesday.
Step 3: Tool 2, Pull a Device Config
This one uses Netmiko to SSH into a device and grab the running config. Same pattern you’d use in any network automation script, we’re just wrapping it as an MCP tool.
# tools/device_config.py
import os
from netmiko import ConnectHandler, NetmikoTimeoutException, NetmikoAuthenticationException
from dotenv import load_dotenv
load_dotenv()
def get_device_config(hostname: str, device_type: str = "cisco_ios") -> str:
"""
Retrieve the running configuration from a network device via SSH.
Args:
hostname: The hostname or IP address of the device.
device_type: The Netmiko device type. Defaults to 'cisco_ios'.
Other options: 'cisco_nxos', 'juniper_junos', 'cisco_asa'
Returns:
The running configuration as a string, or an error message.
"""
device = {
"device_type": device_type,
"host": hostname,
"username": os.getenv("DEVICE_USERNAME"),
"password": os.getenv("DEVICE_PASSWORD"),
"timeout": 15,
}
try:
with ConnectHandler(**device) as conn:
config = conn.send_command("show running-config")
return config
except NetmikoAuthenticationException:
return f"Error: Authentication failed for {hostname}. Check your credentials."
except NetmikoTimeoutException:
return f"Error: Connection to {hostname} timed out. Is the device reachable?"
except Exception as e:
return f"Error: {str(e)}"
A couple of things worth calling out here:
- We’re catching specific Netmiko exceptions and returning clean error messages. Claude will read these errors and tell you what went wrong in plain English rather than crashing silently.
- Credentials come from environment variables. Not hardcoded. Not in the source. Not in a comment. Never.
Step 4: Tool 3, Trigger an AWX Job Template
This is where it gets interesting. You can tell Claude “run the certificate renewal playbook on core-fw-01” and it will call this tool, hit the AWX API, and kick off the job, no GUI required.
# tools/awx.py
import os
import requests
from dotenv import load_dotenv
load_dotenv()
AWX_URL = os.getenv("AWX_URL")
AWX_TOKEN = os.getenv("AWX_TOKEN")
HEADERS = {
"Authorization": f"Bearer {AWX_TOKEN}",
"Content-Type": "application/json",
}
def run_awx_job(job_template_id: int, extra_vars: dict = {}) -> dict:
"""
Trigger an AWX/AAP job template via the API.
Args:
job_template_id: The numeric ID of the AWX job template to run.
Find this in the AWX UI under Templates.
extra_vars: Optional dictionary of extra variables to pass
to the job template at runtime.
Example: {"target_host": "core-sw-01", "vlan_id": 200}
Returns:
A dictionary with the job ID and status URL, or an error message.
"""
url = f"{AWX_URL}/api/v2/job_templates/{job_template_id}/launch/"
payload = {}
if extra_vars:
payload["extra_vars"] = extra_vars
try:
response = requests.post(url, headers=HEADERS, json=payload, timeout=10)
response.raise_for_status()
data = response.json()
return {
"job_id": data.get("id"),
"status": data.get("status"),
"url": f"{AWX_URL}/#/jobs/{data.get('id')}/output"
}
except requests.exceptions.RequestException as e:
return {"error": str(e)}
💡 Gotcha: Make sure the AWX user tied to your API token has Execute permission on the job template, not just Read. This one will bite you and the error message isn’t always obvious.
Step 5: Connect to Claude Desktop
This is where it all comes together. Claude Desktop supports MCP natively, and connecting your server takes about 60 seconds.
Open your Claude Desktop config file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Add your server:
{
"mcpServers": {
"network-automation": {
"command": "python",
"args": ["/full/path/to/your/network-mcp/server.py"],
"env": {
"PYTHONPATH": "/full/path/to/your/network-mcp"
}
}
}
}
Restart Claude Desktop. You should see a small 🔌 tools icon in the chat window. Click it, your three tools should be listed there. If they’re not, check the MCP logs under Claude > Settings > Developer.
Now ask Claude something like:
“Query NetBox for all active devices at site nyc-dc1”
Watch it call your tool, hit your NetBox instance, and return real data. That’s your infrastructure, talking to an AI, through code you wrote.
Security Considerations
Before you start connecting this to production, let’s talk about a few things:
Scope your API tokens. Your NetBox token should have read-only access unless you’re building write operations. Your AWX token should be scoped to specific job templates. Least privilege, same as always.
Be careful with get_device_config. Running configs can contain sensitive data, pre-shared keys, SNMP community strings, local usernames. Think carefully before exposing this tool broadly. Consider filtering the output before returning it.
You control what Claude can do. Claude will only call the tools you give it. It cannot reach out to the internet, modify your infrastructure, or do anything outside of the three functions you’ve defined. The blast radius is exactly what you design it to be.
Don’t expose your MCP server to the internet. For now, this runs locally. Keep it that way until you’ve thought through authentication properly.
What’s Next?
You now have a working MCP server that gives an AI real, live access to your network automation stack. This is the foundation. A few directions you can take it from here:
- Add a
push_device_config()tool with a dry-run mode (covered in an upcoming post on deterministic agents) - Expose Terraform state queries so Claude can reason about your infrastructure as code
- Build a multi-tool troubleshooting workflow: Claude queries NetBox → pulls config → analyzes BGP state → recommends a fix
The pattern is the same every time: take something you already know how to automate in Python, wrap it in a well-documented function, and register it with FastMCP. That’s it.
Your existing automation tooling is already halfway there. FastMCP gets you the rest of the way.
Have questions or ran into a gotcha I didn’t cover? Drop a comment below or find me on LinkedIn.
— David Henderson | Network Doodles,Decoding Tech, One Doodle at a Time