Skip to main content
Glama
README.md
# ACI MCP Server

A Model Context Protocol (MCP) server that gives Claude (or any
MCP-compatible AI client) natural-language tools to read and configure a
live Cisco ACI fabric through the APIC REST API — with confirmation
required before any state-changing action, and structural safety checks
that can't be bypassed by confirmation alone.

Built and tested against Cisco's free DevNet "ACI Simulator Always-On"
sandbox. Full build write-up: [How to Connect Claude AI to Cisco ACI Using
MCP: A Step-by-Step Guide](https://hackernoon.com/how-to-connect-claude-ai-to-cisco-aci-using-mcp-a-step-by-step-guide)
on HackerNoon.

## Why this exists

Most AI assistants can explain Cisco ACI's object model. This gives Claude
the ability to actually *operate* one — read fabric state, build a real
Tenant → Application Profile → EPG → Bridge Domain → VRF hierarchy, apply
contracts between EPGs, and audit the result — through conversation, with
every write gated behind explicit confirmation.

## Features

- **22 tools** spanning fabric visibility, tenant/application management,
  the full ACI object hierarchy, fabric access policy (VLAN pools, domains,
  AAEPs, port bindings), and policy enforcement via contracts
- **Dry-run by default** — every state-changing tool describes what it
  would do and requires a second call with `confirm=True` to actually
  apply it
- **Unconditional guardrails where they matter** — `delete_tenant` refuses
  to touch `common`, `infra`, or `mgmt` regardless of `confirm`
- **Pre-flight dependency checks** — `create_bridge_domain` and
  `apply_contract` verify their target VRF/contract actually exists before
  creating a reference to it, rather than silently accepting a dangling
  one (see [CHANGELOG.md](CHANGELOG.md) for the real bug this fixed)
- **A structural audit tool**, not just a lister — `get_tenant_detail`
  walks an entire tenant's object tree in one call and flags anything
  incomplete

## Tool reference

| Tool | Type | Description |
|---|---|---|
| `list_tenants` | Read | List every tenant on the fabric |
| `get_fabric_health` | Read | Fabric-wide health score |
| `get_faults` | Read | Active faults filtered by severity |
| `get_port_config` | Read | EPG static binding + AAEP for a pod/node/interface |
| `get_tenant_detail` | Read | Full-subtree audit of a tenant, flags incomplete objects |
| `create_tenant` | Write | Create a tenant |
| `delete_tenant` | Write | Delete a tenant — hard-refuses `common`/`infra`/`mgmt` |
| `create_application_profile` | Write | Create an AP inside a tenant |
| `create_epg` | Write | Create an EPG inside an AP, optional BD binding |
| `create_bridge_domain` | Write | Create a BD with a VRF (pre-flight checked) |
| `associate_epg_bridge_domain` | Write | Bind an existing EPG to an existing BD |
| `create_vrf` | Write | Create a VRF (Context) inside a tenant |
| `create_contract` | Write | Create a contract + filter, optional port restriction |
| `apply_contract` | Write | Apply a contract between provider and consumer EPGs (pre-flight checked) |
| `add_static_path_binding` | Write | Bind an EPG to a physical port with a VLAN |
| `remove_static_path_binding` | Write | Undo one EPG-to-port binding (precise, doesn't touch shared infra objects) |
| `add_subnet` | Write | Give a Bridge Domain a Layer 3 gateway |
| `create_vlan_pool` | Write | Create a fabric-wide static-allocation VLAN pool |
| `create_physical_domain` | Write | Create a domain, tied to a VLAN pool (pre-flight checked) |
| `create_aaep` | Write | Create an AAEP, tied to a domain (pre-flight checked) |
| `associate_epg_domain` | Write | Tie an EPG to a domain (pre-flight checked) |
| `bind_port_to_aaep` | Write | Chain a port to an AAEP via Interface Policy Group + Leaf/Interface profiles — reuses an existing policy group by name instead of duplicating it |

All **Write** tools require `confirm=True` to actually execute; called
without it, they return a description of the intended change instead.

## Requirements

- Python 3.10+ (the `mcp` SDK requires it — 3.9 will fail to install it)
- Access to an APIC controller (a free DevNet sandbox works fine — get
  current credentials at [devnetsandbox.cisco.com](https://devnetsandbox.cisco.com),
  search "ACI Simulator")
- An MCP-compatible client (Claude Code, Claude Desktop, etc.)

## Setup

```bash
git clone https://github.com/realpaul3907/aci-mcp-server.git
cd aci-mcp-server
pip install -r requirements.txt
cp .env.example .env
# edit .env with your real APIC_URL / APIC_USER / APIC_PASSWORD
```

**Sanity check before wiring in an AI client** — confirm the server starts
cleanly on its own:

```bash
python3 aci_mcp_server.py
```

It should hang silently (waiting on stdio) with no traceback — that's
success. `Ctrl+C` to stop.

## Registering with Claude Code

```bash
claude mcp add aci-lab --scope user -- python3 /full/path/to/aci_mcp_server.py
```

If your APIC/environment is only reachable from a remote host (e.g. behind
SSH, or inside a container), pass credentials explicitly and route through
that instead:

```bash
claude mcp add aci-lab --scope user -- ssh user@host docker exec -i \
  -e APIC_URL=https://your-apic \
  -e APIC_USER=admin \
  -e "APIC_PASSWORD=your-password" \
  container-name python3 /path/in/container/aci_mcp_server.py
```

Start a fresh Claude Code session, run `/mcp` to confirm `aci-lab` shows
connected with 22 tools, then try:

> "List all tenants"
>
> "Check fabric health"

## A worked example

[`docs/testcase_app_to_db.md`](docs/testcase_app_to_db.md) walks through a
complete test case: building a fresh tenant from nothing — VRF, two Bridge
Domains, an Application Profile, an App-tier EPG and a DB-tier EPG — then
applying a contract that permits only TCP/5432 between them, with the DB
tier correctly configured as the provider and the App tier as the
consumer.

## Design notes

- **MCP is a protocol, not an AI feature.** This server works unmodified
  with any MCP-compatible client — Claude, ChatGPT via its Agents SDK,
  Gemini, or a local model — since nothing in the code is Claude-specific.
- **The dry-run/confirm pattern is enforced in code, not by the model's
  judgment.** A write tool called without `confirm=True` cannot reach
  APIC — the check happens before any network call, not after.
- **Not every guardrail is the same strength on purpose.** Most writes are
  soft-gated (confirm and proceed); deleting a system tenant is hard-gated
  (no confirmation path exists at all). See `delete_tenant`.

## Security

- `.env` is git-ignored — never commit real credentials.
- This is built and tested against a free, shared DevNet sandbox. If you
  point this at a production APIC, review the credential-handling pattern
  first — storing a real password as a plaintext environment variable
  passed on a command line (as shown in the SSH example above) is
  acceptable for a lab, not for production. Use a proper secrets manager
  instead.

## License

[MIT](LICENSE)

## Author

Built by [Real Paul](https://www.linkedin.com/in/realpaul67) — network
automation engineer, NetDevOps. More at
[GitHub](https://github.com/realpaul3907) and
[HackerNoon](https://hackernoon.com/u/RealPaul_2komqcs8).