Ryan Malloy 90227ab391 Hamilton review fixes part 2: bounded regex, connection recovery, _to_int diagnostic, consistent error shapes
Closes the four remaining findings from the margaret-hamilton review.
13 new regression tests; all 100 pass; live cluster smoke verified.

MAJOR #4 — wildcard regex catastrophic backtracking + silent malformed.

Two changes to _wildcard_to_regex():

a) Bounded the `!` and `@` wildcards to \d{1,50} (was \d+). Adjacent
   `!` patterns previously compiled to (\d+)(\d+)... which has
   exponential backtracking on near-miss inputs. CUCM dial strings
   are practically capped well below 50 digits; the bound keeps
   complexity polynomial without losing real-world coverage.
   Verified: 10 adjacent `!` against a 30-digit near-miss now finishes
   in ~240ms (was unbounded; could have been minutes on real
   pathological cases).

b) Unclosed `[` now raises ValueError instead of silently treating the
   bracket as a literal. _pattern_matches_number catches the error
   and returns False so a single bad pattern doesn't crash
   translation_chain — but the bad pattern is no longer invisibly
   producing wrong matches. The previous silent fallback meant a
   pattern like `[0-9` (typo, missing `]`) would match input
   containing the literal characters `[` `0` `-` `9`.

3 new tests covering: bounded-regex shape (`\d{1,N}`), pathological
input completes quickly, unclosed bracket raises explicitly,
well-formed character class still works.

MAJOR #5 — distinguish config errors from operational errors.

Pre-fix: any first-time connection failure set `_connection_error`
and pinned it forever. A transient network blip or session timeout
required restarting the MCP server. Hamilton's framing: Apollo's
software was *designed* to recover from transient faults; pinning
forever is the antithesis of "design the error path first."

Fix: split into two state fields:
  _config_error  — permanent until restart (missing env vars only)
  _last_error    — last operational failure, NOT a pin

Operational failures (zeep Client construction, network, TLS, session)
clear from the next call's perspective: the next call attempts fresh.
Configuration errors (missing AXL_URL etc.) stay pinned because
they don't get better on retry.

Added _ConfigError as a private subclass to make the distinction
explicit at the raise site, and connection_status() to expose
connected/connected_at/config_error/last_error for diagnostic
transparency.

3 new tests: config errors pin, operational errors don't pin,
connection_status() reports state.

MINOR #6 — _to_int silent coercion of bad data.

Pre-fix: a non-numeric value from the cluster (data corruption,
schema drift across CUCM versions) silently became None, which
downstream sort logic defaulted to 0 — jumbling the failover order
in the displayed result with no warning.

Fix: still returns None on bad data (caller error path unchanged),
but logs the offending value to stderr so an operator notices
something's wrong at the data layer. None itself is silent
(legitimately-unset column).

2 new tests: real None is silent, bad string logs to stderr with
the offending value visible.

MINOR #7 — standardize tool failure shapes; add health() tool.

Pre-fix: cache_stats and cache_clear returned `{"error": "..."}`
when _cache was None, while AXL-touching tools raised RuntimeError.
LLM consumers had to handle two shapes.

Fix: _require_cache() helper raises RuntimeError consistently with
_client(). All tool failures now use the same exception shape.
Added health() tool that reports cache/axl/docs initialization
status plus the AXL connection_status — gives operators a
self-diagnostic when something fails at bootstrap.

3 new tests: cache_stats raises, cache_clear raises, health()
reports each subsystem.
2026-04-25 23:19:32 -06:00
2026-04-25 20:29:18 -06:00
2026-04-25 20:29:18 -06:00
2026-04-25 20:29:18 -06:00
2026-04-25 20:29:18 -06:00

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 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

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

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.
Description
Read-only MCP server for Cisco CUCM via AXL + RisPort70 — built for LLM-driven dial-plan and configuration auditing.
Readme MIT 867 KiB
Languages
Python 100%