> **Draft — edit before publishing**

---

# Building an MCP Server for Central

> *"Which of my sites have active critical alerts right now?"*

Claude called two tools, correlated the results, and answered in under 4 seconds:

> **3 sites have critical alerts:** HQ (4 critical, WLAN category), Branch-East (2 critical, Routing), DC-Core (1 critical, System).

No API docs. No filter syntax. No pagination. Just a question.

That's `central-mcp-server` — an open-source MCP server that connects your AI assistant directly to Central. This post covers what it can do, the technical decisions behind it, and why MCP is the right model for this kind of problem.

![Architecture overview: AI clients connect to central-mcp-server over MCP stdio, which calls Central REST APIs via pycentral](./images/architecture.svg)

---

## The Problem

Central has a genuinely good REST API. Site health scores, device inventory, client connectivity, alerts, events — the data is there. The gap is between "the data exists" and "I can ask a question and get an answer." Endpoint knowledge, filter syntax, pagination, result correlation — it's friction that compounds on every ad-hoc investigation.

I want to be clear about what this project is and isn't. HPE already ships [Networking CoPilot](https://www.arubanetworks.com/products/network-management-operations/ai-ops/), a capable AI built into Central that handles many of these workflows natively. This project doesn't try to replace it. `central-mcp-server` is for a specific set of use cases:

- You want to connect your own AI assistant (Claude, GitHub Copilot, Cursor) to your Central instance
- You want to query live monitoring data through conversation in the same environment where you're already working
- You want to extend or customize the tooling yourself — add tools, modify prompts, wire in other systems

If CoPilot covers your needs, use it. If you want an open, self-hosted, developer-extensible bridge, that's what this is for.

## Why MCP?

[Model Context Protocol](https://modelcontextprotocol.io) is an open standard for giving AI models structured access to tools and data sources. The key property: the AI decides *when* and *how* to call a tool based on the conversation. You describe tools well and the model reasons about sequencing — you don't wire up rigid workflows.

Here's what a single "which sites have poor health?" question looks like under the hood:

![Sequence diagram showing a site health investigation: two tool calls, one Central API, one synthesized answer](./images/sequence-site-health.svg)

Two targeted API calls, correlated by the AI. The user just asked a question.

---

## What It Can Do

### Tools

The server exposes 9 tools across 5 domains. All are read-only.

| Tool | What it does | When to use it |
|------|-------------|----------------|
| `central_get_site_name_id_mapping` | Lightweight index of all sites: name, ID, health score, alert count | First call in almost every workflow — resolves names to IDs cheaply |
| `central_get_sites` | Full metrics for one or more sites | After mapping reveals which sites need investigation |
| `central_get_devices` | Filtered device inventory (type, site, model, serial, provisioning) | Inventory checks, per-site device health |
| `central_find_device` | Single device by serial number or name | Targeted lookups when you have an identifier |
| `central_get_clients` | Filtered client list (status, connection type, VLAN, WLAN, time window) | Failed client analysis, connectivity audits |
| `central_find_client` | Single client by MAC address | Troubleshooting a specific user |
| `central_get_alerts` | Filtered alerts for a site (severity, device type, category, status) | Active issue triage |
| `central_get_events` | Events for a site, device, or client within a time window | Timeline reconstruction, pre-failure analysis |
| `central_get_events_count` | Event count breakdown by type — no full event fetch | Quick signal check before deciding whether to pull events |

### Guided Prompts

The server ships 10 built-in prompts. A prompt isn't a script — it's an instruction to the AI about which tools to call, in what order, and how to synthesize the results. The user doesn't need to know the tools exist.

**`troubleshoot_site`** — deep-dive triage for a single site:

```python
@mcp.prompt
def troubleshoot_site(site_name: str) -> str:
    return f"""
Troubleshoot site "{site_name}":

1. Call central_get_site_name_id_mapping to verify the site and get its site_id.
2. Call central_get_sites with site_names=["{site_name}"] for detailed metrics.
3. Call central_get_alerts with site_id and status="Active". Prioritize by severity.
4. Call central_get_devices with the site_id to list all devices.
5. Summarize: health, alert breakdown by category/severity, device overview.
    """.strip()
```

The user types "troubleshoot HQ." The AI runs the sequence and delivers:

```
## HQ — Site Health Report

**Health score:** 45/100

**Active alerts (12 total)**
| Severity | Count | Categories |
|----------|-------|------------|
| Critical | 4     | WLAN (3), System (1) |
| High     | 6     | LAN (4), Clients (2) |
| Medium   | 2     | WLAN (2) |

**Devices (18 total)**
| State | Count |
|-------|-------|
| Good  | 14    |
| Fair  | 3     |
| Poor  | 1     |
```

**`critical_alerts_review`** — network-wide critical alert sweep:

> 1. Get all sites and their alert counts.
> 2. For each site with alerts, fetch active alerts.
> 3. Filter to Critical severity. Group by site and category.
> 4. Summarize: total critical alerts across the network, top affected sites, most common alert names.

No site names needed. No IDs. Just "review my critical alerts."

<!-- TODO: Record GIF of troubleshoot_site prompt running in Claude Desktop — tool calls visible in sidebar, final summary in chat -->
![Troubleshoot site workflow in action](./images/demo-troubleshoot-site.gif)

The full prompt library: network health overview, site troubleshooting, client connectivity check, device event investigation, failed clients investigation, site event summary, client overview, device type health check, critical alerts review, and multi-site health comparison.

---

## Technical Decisions

### Docstrings Are Prompts

FastMCP surfaces function docstrings to the AI model as tool descriptions. The docstring isn't for a human reading the source — it's an instruction that tells the AI when and how to use the tool.

```python
@mcp.tool(annotations=READ_ONLY)
async def central_get_sites(
    ctx: Context,
    site_names: list[str] | None = None,
) -> List[SiteData]:
    """
    Returns detailed metrics for one or more sites.

    Prefer calling with a site_names filter targeting only the sites you care about.
    Do NOT call without a filter unless the user explicitly requests data for all
    sites — returning all sites is expensive and consumes significant context.
    # ↑ Steers the AI away from expensive unfiltered calls

    Recommended workflow: Call central_get_site_name_id_mapping first to get a
    lightweight overview of all sites (names, IDs, health scores). Use health
    scores and alert counts to decide which sites warrant further investigation,
    then call this tool with those specific site names.
    # ↑ Encodes the two-step pattern directly in the tool description

    Parameters:
    - site_names: One or more site names to filter by. Supports exact matches.
      If omitted, all sites are returned (use sparingly or when explicitly requested).
    """
```

**State dependencies explicitly.** Any tool that needs a `site_id` says to call `central_get_site_name_id_mapping` first. The model follows it.

**Discourage expensive calls.** An AI that dumps every site's full metrics into context on every health-check query burns tokens and slows the conversation. The docstring is the only enforcement mechanism — it has to be direct.

### The Lightweight Mapping Pattern

`central_get_site_name_id_mapping` doesn't exist in Central's API. It was built because designing tools for an AI client (rather than an HTTP client) forces a different question: *what does the AI actually need to navigate this data?*

An HTTP client paginating the sites list can extract IDs and filter on the client side. An AI needs a tool that answers "what sites exist, which are degraded, and what are their IDs?" in a single cheap call — so it can decide what to fetch next rather than pulling everything upfront.

![Two-step mapping pattern: call the index first, then fetch full data only for degraded sites](./images/mapping-pattern.svg)

```python
# Step 1 — cheap, fast (~1 API call)
central_get_site_name_id_mapping()
# → { "HQ": { site_id: "abc123", health: 45, total_alerts: 12 },
#     "Branch1": { site_id: "def456", health: 88, total_alerts: 1 } }

# Step 2 — targeted, only for degraded sites
central_get_sites(site_names=["HQ"])
# → full device/client/health breakdown for HQ only
```

Full data only flows for sites that warrant it.

### OData Filters as Typed Parameters

Central's v2 APIs use OData v4.0 filter syntax. Exposing raw OData to an AI is a bad idea — malformed strings fail silently at the API layer.

Instead, each tool declares its filterable fields as typed Python parameters with `Literal` annotations:

```python
async def central_get_devices(
    ctx: Context,
    device_type: Optional[Literal["ACCESS_POINT", "SWITCH", "GATEWAY"]] = None,
    site_id: Optional[str] = None,
    model: Optional[str] = None,
    is_provisioned: Optional[bool] = None,
    ...
) -> List[Device] | str:
```

Pydantic validates `Literal` values before the tool body runs. A utility function translates the validated parameters into the OData string:

```python
filter_str = build_odata_filter([
    (DEVICE_FILTER_FIELDS["device_type"], device_type),
    (DEVICE_FILTER_FIELDS["site_id"], site_id),
])
# → "$filter=deviceType eq 'ACCESS_POINT' and siteId eq 'abc123'"
```

Invalid enum values are rejected before they reach Central.

### Read-Only Annotations

MCP 1.5 added tool annotations — metadata the client and model can inspect. All tools carry `readOnlyHint: True`:

```python
READ_ONLY = {"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True}

@mcp.tool(annotations=READ_ONLY)
async def central_get_alerts(ctx: Context, site_id: str, ...):
    ...
```

Nothing in this server writes to Central. Making that explicit in the tool metadata — not just the README — means clients can surface it and the AI doesn't need to confirm before calling.

### Pagination and Retry Without Leaking Complexity

Central's APIs use two pagination styles: cursor-based and offset-based. A generic `paginated_fetch()` utility handles both. Retry logic (5 attempts, exponential backoff on 429/5xx, immediate raise on 4xx) lives in a separate utility. Tools get the final result — they don't deal with either directly.

---

## The Stack

The server is built on [FastMCP](https://gofastmcp.com). Each tool is a Python function decorated with `@mcp.tool` — the function signature becomes the schema, the docstring becomes the description. The default transport is stdio, which means it works with every MCP client that matters (Claude Desktop, Claude Code, Cursor, GitHub Copilot, Windsurf) and installs as a plain `uvx` subprocess. No HTTP server to manage.

```python
# server.py
from fastmcp import FastMCP
import tools.sites, tools.devices, tools.clients, tools.alerts, tools.events, tools.prompts

mcp = FastMCP("Central MCP")

tools.sites.register(mcp)
tools.devices.register(mcp)
tools.clients.register(mcp)
tools.alerts.register(mcp)
tools.events.register(mcp)
tools.prompts.register(mcp)
```

```
INFO     FastMCP server 'Central MCP' starting...
INFO     Registered 9 tools: central_get_sites, central_get_site_name_id_mapping,
         central_get_devices, central_find_device, central_get_clients,
         central_find_client, central_get_alerts, central_get_events,
         central_get_events_count
INFO     Registered 10 prompts: network_health_overview, troubleshoot_site, ...
INFO     Transport: stdio
```

---

## Trade-offs and Limitations

**No writes.** This server is intentionally read-only. Central supports configuration operations — pushing config, provisioning devices. None of that is exposed. Read-only access is easier to audit and significantly reduces the risk surface.

**Static credentials.** The server takes credentials at startup via environment variables and holds a single connection instance. No per-request auth, no multi-tenant support. Fine for one engineer, one Central instance. Not designed for shared or multi-org deployments.

**Heavy queries are slow.** Central's APIs aren't instant. A full client list for a large site takes a few seconds. The docstrings steer the AI away from unfiltered queries, but it's guidance, not enforcement.

**Community project.** This is not an HPE product. It depends on [pycentral](https://github.com/aruba/pycentral), HPE's Python SDK, pinned to a specific branch. Breaking changes in the upstream SDK require updates here.

---

## Getting Started

```bash
uv tool install central-mcp-server
```

```
Resolved 12 packages in 230ms
Installed central-mcp-server
```

Then add credentials to your MCP client config. For Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):

```json
{
  "mcpServers": {
    "central-mcp": {
      "command": "uvx",
      "args": ["central-mcp-server"],
      "env": {
        "CENTRAL_BASE_URL": "https://us5.api.central.arubanetworks.com",
        "CENTRAL_CLIENT_ID": "your-client-id",
        "CENTRAL_CLIENT_SECRET": "your-client-secret"
      }
    }
  }
}
```

<!-- TODO: Record GIF of install + first query ("which sites have poor health?") in Claude Desktop -->
![Install and first query](./images/demo-first-query.gif)

Full setup instructions:

- [Overview](../docs/guide-1-overview.md) — what it does and what you can ask
- [Setup](../docs/guide-2-setup.md) — credentials, installation, and client configuration
- [Guided Prompts](../docs/guide-3-guided-prompts.md) — how to invoke the built-in workflows
- [In Action](../docs/guide-4-in-action.md) — examples of real queries and tool calls

---

## Further Reading

- [Model Context Protocol specification](https://modelcontextprotocol.io)
- [FastMCP documentation](https://gofastmcp.com)
- [Central REST API docs](https://developer.arubanetworks.com/new-central/docs)
- [pycentral SDK](https://github.com/aruba/pycentral)
- [HPE Networking CoPilot](https://www.arubanetworks.com/products/network-management-operations/ai-ops/) — the native AI in Central (not this project)
