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).
7.5 KiB
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_PTpartition, 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 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):
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):
# 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
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 seedinvestigate_pattern(pattern, partition=None)— deep-dive a specific patternaudit_routing(focus="full")— comprehensive audit walkthroughcucm_sql_help(question)— catch-all for arbitrary SQL questionssip_trunk_report(name_filter=None)— SIP trunk inventory + findingsphone_inventory_report(filter=None)— phone fleet aggregates with anomaly findings; cross-references RisPort registration stateuser_audit(focus="full")— end users + application users + role assignmentsinbound_did_audit()— XFORM-Inbound-DNIS inventory + screening pipelinehunt_pilot_audit()— hunt pilots, queue settings, line group membershipwhoami(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
alongside this server:
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_chaindoes 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 byroute_patterns(kind=...)are stable across CUCM versions but enumerated against thetypepatternusagetable at query time, so any cluster-specific custom types still work.