Two ideas borrowed from cisco-cucm-mcp (calltelemetry/cisco-cucm-mcp,
MIT licensed): real-time device registration via RisPort70, and
exponential-backoff retry on transient HTTP 5xx errors. Both are
purpose-built for the audit use case rather than general-purpose
ports — RisPort tools exist to inform audit findings, not as a
standalone "look at my devices" interface.
Rate limit / 503 backoff (~30 lines + 3 tests):
AxlClient now mounts an HTTPAdapter with a urllib3 Retry policy
(3 retries, exponential backoff, status_forcelist=[502,503,504]).
Configurable via AXL_RATE_LIMIT_RETRIES (default 3, 0 disables).
Surfaces in connection_status() so operators can see the policy.
Closes a real reliability gap: CUCM SOAP rate-limits under load
during change windows or with multiple concurrent admins; pre-fix
any 503 was a hard failure.
RisPort70 (new src/risport.py + 2 tools + prompt update):
Hand-coded SOAP client for /realtimeservice2/services/RISService70
(avoids dragging in another zeep instance for one operation).
Reuses AXL_URL/USER/PASS env vars — RisPort lives on the same host.
New tools:
device_registration_status(device_class, status, name_filter, page_size)
device_registration_summary() — cluster-wide breakdown by class
Live-cluster verification (cucm-pub.binghammemorial.org):
Phone: 803 registered=679 unregistered=123 rejected=1
Gateway: 85 registered=41 rejected=44 ← real audit finding
SIPTrunk: 22 registered=18 unregistered=4
HuntList: 28 registered=28
H323/CTI: 0 (cluster doesn't use these)
Discovered while live-verifying: CUCM 15 wraps the RisPort response
in an extra <SelectCmDeviceResult> element inside <selectCmDeviceReturn>.
Older CUCM versions exposed the fields directly. The parser falls
back to either shape; tests cover both (test_legacy_response_shape_still_parses
asserts the older shape still works).
phone_inventory_report prompt updated:
New Step 3 — "Cross-reference with real-time registration" — recommends
device_registration_summary() + device_registration_status(status="UnRegistered")
to surface configured-but-never-registered phones (strongest orphan signal),
PartiallyRegistered phones (firewall/cert/version mismatch indicator),
and registration-state vs config-state mismatches.
Tooling delta worth noting:
AXL device count: 1,377 phones
RisPort device count: 803 phones
Delta (~574) likely templates, hidden phones, or stale config —
itself an audit finding the new tool will surface
to anyone running phone_inventory_report.
README updated:
- Added health(), device_registration_status, device_registration_summary
- Added "Scope and complement" section recommending @calltelemetry/cisco-cucm-mcp
alongside for operational debugging (logs, perfmon, packet capture,
service control). The two servers answer different questions; the LLM
with both can compose audit findings with operational state.
- Listed all 10 prompts (was 4 outdated entries).
Tests: 134 → 155 (+21).
174 lines
7.5 KiB
Markdown
174 lines
7.5 KiB
Markdown
# mcp-cucm-axl
|
|
|
|
Read-only MCP server for **Cisco Unified CM 15** AXL — built for LLM-driven
|
|
cluster auditing, with a particular focus on the **Route Plan Report**:
|
|
partitions, calling search spaces, route patterns, translation patterns,
|
|
called/calling party transformations, and digit-discard instructions.
|
|
|
|
## Why this exists
|
|
|
|
CUCM's admin UI is great for one-config-at-a-time work but painful for
|
|
audit/discovery questions like:
|
|
|
|
- "Which translation patterns rewrite the calling party number, and why?"
|
|
- "Which CSSs include the `Internal_PT` partition, in what order?"
|
|
- "Show me every route pattern targeting the SIP trunk to the carrier."
|
|
- "Are there partitions defined but unreachable from any CSS?"
|
|
|
|
This server gives an LLM SQL access to CUCM's Informix data dictionary,
|
|
plus focused tools that bake in the right joins for routing-audit work.
|
|
Pair it with the sibling [`mcp-cisco-docs`](../docs/) server and the LLM
|
|
gets vendor documentation alongside live cluster state — answering
|
|
"is our config consistent with Cisco's recommended baseline?" in a single
|
|
conversation.
|
|
|
|
## Read-only by structural guarantee
|
|
|
|
The server **never registers** AXL write methods. There is no
|
|
`executeSQLUpdate`, no `add*`/`update*`/`remove*`/`apply*`/`reset*`/
|
|
`restart*` tool. Read-only is enforced by *absence* of write operations,
|
|
not by runtime sanitization. Defense-in-depth: SQL queries are also
|
|
client-side validated to begin with `SELECT` or `WITH`.
|
|
|
|
## Setup
|
|
|
|
### 1. Configure environment
|
|
|
|
Edit `.env` (already gitignored):
|
|
|
|
```env
|
|
AXL_URL=https://cucm-pub:8443/axl
|
|
AXL_USER=AxlUser
|
|
AXL_PASS=...
|
|
AXL_VERIFY_TLS=false # CUCM ships self-signed certs; default off
|
|
AXL_CACHE_TTL=3600 # 1 hour; 0 disables caching
|
|
AXL_WSDL_PATH= # optional explicit WSDL location
|
|
CISCO_DOCS_INDEX_PATH= # optional override for prompt enrichment
|
|
```
|
|
|
|
### 2. Bootstrap the AXL WSDL
|
|
|
|
Download the **Cisco AXL Toolkit** from your CUCM admin UI:
|
|
|
|
> Application → Plugins → Find → "Cisco AXL Toolkit" → Download
|
|
|
|
Drop the resulting `axlsqltoolkit.zip` into the project directory. On first
|
|
launch, the server auto-extracts `schema/15.0/` into `~/.cache/mcp-cucm-axl/wsdl/15.0/`.
|
|
The zip is gitignored (Cisco-licensed; not redistributable).
|
|
|
|
Alternatives (in resolution order):
|
|
|
|
```bash
|
|
# A: explicit zip elsewhere
|
|
export AXL_WSDL_ZIP=/path/to/axlsqltoolkit.zip
|
|
|
|
# B: explicit WSDL file
|
|
export AXL_WSDL_PATH=/path/to/schema/15.0/AXLAPI.wsdl
|
|
|
|
# C: pre-populated cache directory
|
|
mkdir -p ~/.cache/mcp-cucm-axl/wsdl/15.0/
|
|
cp /path/to/schema/15.0/* ~/.cache/mcp-cucm-axl/wsdl/15.0/
|
|
```
|
|
|
|
### 3. Install + run
|
|
|
|
```bash
|
|
uv sync
|
|
uv run mcp-cucm-axl
|
|
```
|
|
|
|
Or via the bundled `.mcp.json`, automatically registered when Claude Code
|
|
opens this directory.
|
|
|
|
## Tool surface
|
|
|
|
### Foundational
|
|
|
|
| Tool | Purpose |
|
|
|---|---|
|
|
| `axl_version()` | Cluster version sanity check |
|
|
| `axl_sql(query)` | Execute a SELECT against Informix data dictionary |
|
|
| `axl_list_tables(pattern=None)` | Discover Informix tables |
|
|
| `axl_describe_table(name)` | Column metadata for one table |
|
|
| `cache_stats()`, `cache_clear(pattern=None)` | Cache plumbing |
|
|
| `health()` | Subsystem self-check (cache / AXL / docs / RisPort init state) |
|
|
|
|
### Real-time device registration (RisPort70)
|
|
|
|
Complementary to AXL — AXL tells you what's *configured*, RisPort tells you
|
|
what's *currently registered*. The audit-relevant cross-reference is
|
|
"configured but unregistered" (orphan signal).
|
|
|
|
| Tool | Purpose |
|
|
|---|---|
|
|
| `device_registration_status(device_class, status, name_filter, page_size)` | Page through CUCM's RisPort `selectCmDevice` for live registration state |
|
|
| `device_registration_summary()` | Cluster-wide breakdown: registered / unregistered / rejected counts across Phone, Gateway, SIPTrunk, HuntList, etc. |
|
|
|
|
### Route plan
|
|
|
|
| Tool | Purpose |
|
|
|---|---|
|
|
| `route_partitions()` | All partitions with pattern + CSS-member counts |
|
|
| `route_calling_search_spaces(name=None)` | CSS list with ordered partitions |
|
|
| `route_patterns(kind=None, partition=None, filter=None)` | Route Plan Report — patterns + transformations |
|
|
| `route_inspect_pattern(pattern, partition=None)` | Deep dive: transforms, route filter, reachable-from CSS, full destination chain (route list → groups → gateways) |
|
|
| `route_lists_and_groups(name=None)` | Route list → route group → gateway chain (annotates Local Route Group placeholders) |
|
|
| `route_translation_chain(number, css_name=None)` | Wildcard-aware pattern matcher: evaluates X / ! / [0-9] / @ / \\+ against the number and returns matches sorted by specificity |
|
|
| `route_digit_discard_instructions()` | DDI catalog |
|
|
| `route_device_pool_route_groups(device_pool_name=None)` | How each device pool resolves Local Route Group placeholders to actual gateway-bearing groups |
|
|
| `route_devices_using_css(css_name)` | Impact analysis: every reference to a CSS across line CFA/CFB/CFNA/CFUR/translation/MWI/shared, device-level CSSs, voicemail pilots, route lists |
|
|
| `route_filters(name=None)` | Route filter clauses + member rules (composed with @-pattern routes) |
|
|
|
|
## Prompts
|
|
|
|
Schema-grounded conversation seeds. They pull relevant chunks from the
|
|
sibling `cisco-docs` index and embed them inline:
|
|
|
|
- `route_plan_overview` — fresh audit conversation seed
|
|
- `investigate_pattern(pattern, partition=None)` — deep-dive a specific pattern
|
|
- `audit_routing(focus="full")` — comprehensive audit walkthrough
|
|
- `cucm_sql_help(question)` — catch-all for arbitrary SQL questions
|
|
- `sip_trunk_report(name_filter=None)` — SIP trunk inventory + findings
|
|
- `phone_inventory_report(filter=None)` — phone fleet aggregates with anomaly findings; cross-references RisPort registration state
|
|
- `user_audit(focus="full")` — end users + application users + role assignments
|
|
- `inbound_did_audit()` — XFORM-Inbound-DNIS inventory + screening pipeline
|
|
- `hunt_pilot_audit()` — hunt pilots, queue settings, line group membership
|
|
- `whoami(userid=None)` — single-user role chain (defaults to AXL service account)
|
|
|
|
## Scope and complement
|
|
|
|
This server is **audit-focused**: read-only queries against AXL plus
|
|
RisPort cross-reference for registration state. It does *not* cover
|
|
operational debugging (logs, packet capture, perfmon counters,
|
|
service control, certificates, backups).
|
|
|
|
For those, install [`@calltelemetry/cisco-cucm-mcp`](https://github.com/calltelemetry/cisco-cucm-mcp)
|
|
alongside this server:
|
|
|
|
```bash
|
|
claude mcp add cucm-ops -- npx -y @calltelemetry/cisco-cucm-mcp@latest
|
|
```
|
|
|
|
The two servers are **complementary**, not competing — they answer
|
|
different questions and use different CUCM APIs (AXL + RisPort here;
|
|
DIME + RisPort + PerfMon + ControlCenter + SSH there). An LLM with
|
|
both servers can compose audit findings (this server) with operational
|
|
state (theirs) — e.g., *"audit found CSS X has 0 references AND
|
|
RisPort shows zero phones currently registered against any device pool
|
|
that inherits it → confirmed safe to delete."*
|
|
|
|
## Cache
|
|
|
|
Responses are cached in SQLite at `~/.cache/mcp-cucm-axl/responses/axl_responses.sqlite`.
|
|
Cache survives restarts. Clear with `cache_clear()` after a known config change.
|
|
|
|
## Notes
|
|
|
|
- `route_translation_chain` does literal/prefix matching only. CUCM's actual
|
|
matcher evaluates wildcards (`X`, `!`, `[0-9]`, etc.) and selects the
|
|
longest match. Treat results as "patterns to investigate" rather than
|
|
"definitive route."
|
|
- Pattern type codes (`tkpatternusage`) used by `route_patterns(kind=...)` are
|
|
stable across CUCM versions but enumerated against the `typepatternusage`
|
|
table at query time, so any cluster-specific custom types still work.
|