Limitation surfaced by the live Bingham smoke-test (cti-audit-prompts/004):
the canonical 912-CTI-RP finding got the broken-forward flag correct,
but the suggested-fix message couldn't name CER911-PT (where pattern
'10.911' lives) because the exact-literal lookup
`WHERE np.dnorpattern = '10911'` doesn't match the dot-form `10.911`.
The CUCM separator-dot in patterns is purely visual — represents
access-code boundary, not a digit. A destination string `10911`
should match a configured pattern `10.911` since both represent the
same dialed digits.
Two-stage match in _suggest_failsafe_fix:
1. Exact-literal: WHERE np.dnorpattern = '<dest>' (current behavior)
2. Dot-stripped: pull all patterns with `.` in them, filter
Python-side by `pattern.replace('.', '') == dest`
Stage 2 only runs when stage 1 returns no partitions, so the common
case (exact-literal hit) takes the fast path. Falls back to the
wildcard-investigation generic message only when neither stage finds
a match.
The fix message also distinguishes the two cases:
- Exact-literal hit → "Pattern '10911' lives in partition X..."
- Dot-stripped hit → "Pattern '10.911' (matches destination '10911')
lives in partition X..."
Naming both the pattern form and the destination keeps the operator
oriented when the dialed digits and the configured pattern look
different.
Tests: +5 in TestDotStrippedFixSuggestion exercising:
- dot-stripped match cites the dotted pattern form
- exact-literal takes precedence over dotted match
- multi-partition dotted match
- no-exact-no-dotted falls back to generic
- irrelevant dot-positions correctly excluded from match
One existing assertion updated from "no exact-literal pattern" to
"no exact-literal or dot-stripped pattern" (more accurate after the
patch).
Full mcaxl suite: 264 → 269 passing (+5 dot-stripped tests).
The 1 unrelated test_wildcard.py timing flake is pre-existing
(regex-backtracking timing assertion fails by 36ms under load).
Cross-references:
- Live smoke-test findings: agent-threads/cti-audit-prompts/004
- Original tool: agent-threads/cti-audit-prompts/002, commit d33cd7c
mcaxl
Read-only MCP server for Cisco Unified Communications Manager (CUCM) — exposes the AXL SOAP API and RisPort70 real-time registration state to LLMs for dial-plan analysis, configuration auditing, and impact analysis.
Tested against CUCM 15.0(1). Should work on any CUCM 12.5+.
What it looks like
Invoke the whoami prompt in any MCP-aware LLM client. With no
arguments, it defaults to the AXL service account from your .env:
Account:
axl-readonly(applicationuser) Access control groups: 1 —Read-Only-AXLEffective roles (5):
- Standard CCM Admin Users ← write access
- Standard AXL API Access ← full read-write AXL
- Standard AXL Read Only API Access
- Standard Packet Sniffing
- Standard RealtimeAndTraceCollection
Finding: the group
Read-Only-AXLcontains two write-capable roles. The name implies read-only intent but the membership grants full administrative write access. Consider renaming the group OR removing the write-capable roles from its membership.
One tool call. One SQL join across four tables (applicationuser → applicationuserdirgroupmap → dirgroup → functionroledirgroupmap → functionrole). One audit finding with severity and remediation —
not a raw query result the operator has to interpret on their own.
That's the shape of every prompt mcaxl ships with: orchestrated
queries, structured findings, ready-to-act recommendations.
Scope and complement
mcaxl is intentionally narrow — read-only audit of CUCM
configuration via AXL, with RisPort70 cross-reference for live
registration state. It does not cover operational debugging:
log collection, packet capture, perfmon counters, service control.
For those, install
@calltelemetry/cisco-cucm-mcp
alongside this server:
claude mcp add cucm-ops -- npx -y @calltelemetry/cisco-cucm-mcp@latest
The two are designed to compose. mcaxl answers "what does the
config say?"; cisco-cucm-mcp answers "what's happening right
now?". An LLM session with both connected can produce compound
findings like "audit found CSS X has 0 references AND RisPort
confirms zero phones currently registered against any device pool
that inherits it → confirmed safe to delete."
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 our PSTN carrier."
- "Are there partitions defined but unreachable from any CSS?"
- "Which phones are configured but not currently registered?"
mcaxl gives an LLM SQL access to CUCM's Informix data dictionary,
schema-aware joins for common audit questions, and RisPort70
cross-reference for live registration state. Then a set of curated
prompts orchestrates the tools toward audit findings, not just data.
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.
This means the AXL service account mcaxl uses can be granted only
the Standard AXL Read Only API Access role. Even if it had write
roles attached (and operators sometimes do this for convenience),
mcaxl is structurally incapable of using them.
Install
# Run directly from PyPI:
uvx mcaxl
# Or as a pinned dev install:
pip install mcaxl
# Or via Claude Code's MCP registry:
claude mcp add cucm-axl -- uvx mcaxl
Configure
Set these env vars (most operators use a .env file in the working directory):
AXL_URL=https://cucm-pub.example.com:8443/axl/
AXL_USER=your-axl-service-account
AXL_PASS=your-password
# Optional:
AXL_VERIFY_TLS=false # CUCM ships self-signed certs; default off
AXL_CACHE_TTL=3600 # response cache TTL in seconds; 0 disables
AXL_RATE_LIMIT_RETRIES=3 # 502/503/504 retry count with backoff
AXL_WSDL_PATH= # explicit WSDL location override
AXL_WSDL_ZIP= # explicit toolkit zip path
CISCO_DOCS_INDEX_PATH= # for prompt enrichment (see Prompts section)
The AXL service account needs the Standard AXL Read Only API Access
role at minimum. It does not need the full Standard AXL API Access
role (read-write) — mcaxl is structurally incapable of using write
permissions even if granted.
AXL WSDL bootstrap
CUCM's AXL toolkit is Cisco-licensed and not redistributable, so it's not bundled. Download from your CUCM admin UI:
Application → Plugins → Find → "Cisco AXL Toolkit" → Download
Drop the resulting axlsqltoolkit.zip into your working directory. On
first launch, the server auto-extracts schema/15.0/ (or whichever
version matches your cluster) into ~/.cache/mcaxl/wsdl/15.0/.
Alternative resolution paths (in order):
export AXL_WSDL_ZIP=/path/to/axlsqltoolkit.zip # explicit zip
export AXL_WSDL_PATH=/path/to/schema/15.0/AXLAPI.wsdl # explicit WSDL
# Or pre-populate the cache:
mkdir -p ~/.cache/mcaxl/wsdl/15.0/
cp /path/to/schema/15.0/* ~/.cache/mcaxl/wsdl/15.0/
Tool surface (19 total)
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) |
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 |
route_translation_chain(number, css_name=None) |
Wildcard-aware pattern matcher |
route_digit_discard_instructions() |
DDI catalog |
route_device_pool_route_groups(device_pool_name=None) |
Local Route Group resolution |
route_devices_using_css(css_name) |
Impact analysis across 71 known fk-CSS columns |
route_filters(name=None, include_members=False) |
Route filter clauses + member rules |
Real-time device registration (RisPort70)
| 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 across Phone, Gateway, SIPTrunk, HuntList, etc. |
Prompts (10 total)
Each prompt orchestrates multiple tool calls toward a specific
audit narrative. They appear in Claude Code's slash menu under
/mcp__cucm-axl__<name>:
route_plan_overview— fresh audit conversation seedinvestigate_pattern(pattern, partition=None)— single-pattern deep diveaudit_routing(focus="full")— comprehensive walkthrough with checklistcucm_sql_help(question)— catch-all SQL helpersip_trunk_report(name_filter=None)— SIP trunk inventory + findingsphone_inventory_report(filter=None)— phone fleet aggregates with anomaly findings (cross-references RisPort)user_audit(focus="full")— end users + app 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)
Optional: schema-grounded prompt enrichment
Set CISCO_DOCS_INDEX_PATH to a directory containing chunks.jsonl
and index_meta.json (produced by the
mcdewey indexer or any compatible
embedding pipeline) to have prompts pull relevant Cisco documentation
chunks inline. Without this, prompts gracefully degrade to a fallback
notice instructing the LLM to use the sibling docs server's
search_docs tool.
Cache
Responses are cached in SQLite at
~/.cache/mcaxl/responses/axl_responses.sqlite. The cache is
cluster-isolated by SHA-256 of AXL_URL — pointing the server
at a different cluster never serves stale data from a previous one.
Cache survives restarts. Clear with cache_clear() after a known
config change.
Caveats
route_translation_chainevaluates CUCM wildcards (X,!,[0-9],@,\+) but does not model route-filter constraints on@patterns. Use as guidance, not authoritative.- The package's
recordingprofile/usageprofile/vipre164transformationreference categories were schema-verified against CUCM 15. If a future CUCM version adds newfkcallingsearchspace_*columns,route_devices_using_css's coverage will lag until the package is updated. Thetest_complete_schema_coverage_against_known_columnstest enforces the current snapshot — failing red surfaces the drift loudly.
License
MIT. See LICENSE.
Source
- Repo: git.supported.systems/mcp/mcaxl
- Issues: git.supported.systems/mcp/mcaxl/issues
- Changelog:
CHANGELOG.md