Renames the package from `mcp-cucm-axl` to `mcaxl` to fit the
operator's mc<interface> naming convention (mcusb, mcaxl, …),
and scrubs Bingham-specific defaults so the package works for
anyone, anywhere.
Rename:
- pyproject.toml: name, scripts entry point, description
- src/mcp_cucm_axl/ → src/mcaxl/ (git mv preserves history)
- All Python imports updated via sed
- Cache directory: ~/.cache/mcp-cucm-axl/ → ~/.cache/mcaxl/
- Log prefix [mcp-cucm-axl] → [mcaxl]
- Package version lookup: importlib.metadata.version("mcaxl")
- .mcp.json command updated to invoke `mcaxl` script
- All 155 tests pass under the new name (verified)
Bingham-specific scrubs:
- docs_loader._DEFAULT_INDEX_DIR: hardcoded /home/rpm/bingham/...
path removed; defaults to None. Operators set CISCO_DOCS_INDEX_PATH
env var; without it, prompts gracefully degrade with a fallback
notice instructing the LLM to use the cisco-docs MCP search_docs
tool instead.
- prompts/_common.docs_or_empty_msg: removed the explicit
/home/rpm/bingham/... path from the fallback message text.
- server.py: removed dead-code copy of _docs_or_empty_msg() that
was leftover from before the prompts package extraction.
- README.md: completely rewritten as a public-facing readme. Lead
paragraph names CUCM as the target platform, install instructions
cover uvx / pip / Claude Code MCP add. Recommends cisco-cucm-mcp
as the operations counterpart.
PyPI metadata:
- Initial CalVer version: 2026.04.27
- License: MIT (LICENSE file added)
- Project URLs: Homepage / Source / Issues / Changelog all point
at git.supported.systems/mcp/mcaxl (newly-created Gitea repo
in the mcp/ org for PyPI releases)
- Classifiers: Beta / Telecommunications Industry / Topic:Telephony
- Keywords: mcp, cisco, cucm, axl, risport, voip, sip, audit
- sdist excludes: CLAUDE.md, .env*, axlsqltoolkit.zip, audits/,
tests/, pytest/ruff caches. Verified clean: wheel ships only the
mcaxl/ source tree + LICENSE + METADATA + entry_points.
CHANGELOG.md added with a 2026.04.27 initial-release entry,
documenting tool/prompt counts, structural read-only guarantees,
Hamilton review closure, live-cluster verification, and known
limitations.
Build verification:
- `uv build` produces clean wheel + sdist
- Wheel: 22 source files, 195KB total, no Bingham-specific files
- Sdist excludes verified: no CLAUDE.md, no axlsqltoolkit.zip
- Entry point: `mcaxl = mcaxl.server:main`
- Package installs as mcaxl==2026.4.27
Operator-suggested prompt: "what does my AXL account *actually* have
permission to do?" Resolves the user → access-control-group →
function-role chain for a single account, defaulting to the AXL service
account from AXL_USER env when no userid is given.
The prompt principle came in using table names from older Cisco
docs (`enduserauthgroupmap`, `dirgrouprolemap`) that don't exist on
CUCM 15. The shipped SQL uses the verified CUCM 15 names
(`enduserdirgroupmap`, `functionroledirgroupmap`); a regression test
asserts the deprecated names don't appear in the rendered SQL section,
so any future "fix" reverting to the older names fires red.
Live verification on cucm-pub.binghammemorial.org found the existing
AXL service account (`SupportedSystemsReadOnly`) has 4 roles via the
`ReadOnly-AXL` access control group:
- Standard AXL API Access (full RW — group misnamed)
- Standard AXL Read Only API Access (the genuinely-read-only one)
- Standard Packet Sniffing (PHI-relevant in healthcare)
- Standard RealtimeAndTraceCollection
The first finding is structural: the group `ReadOnly-AXL` contains
the FULL RW role `Standard AXL API Access` despite its name. The
MCP server's structural read-only enforcement (no write methods
registered) is what prevents this from mattering — but the account
itself is over-privileged relative to what the tool needs. The
prompt's findings template surfaces this kind of misnamed-group
case explicitly.
Also discovered (and documented in the prompt body): AXL auth is
case-insensitive for usernames, but SQL `WHERE name = 'X'` is
case-sensitive. Step 3 of the prompt handles the case-mismatch
fallback so a typo like `SupportedSYstemsReadOnly` (env) vs
`SupportedSystemsReadOnly` (cluster canonical) doesn't produce a
silently-empty result.
5 new tests:
- correct CUCM 15 table names embedded in SQL
- explicit userid threads through to the query
- default reads AXL_USER from env
- missing userid AND missing env → clear instruction
- SQL injection defense (single-quote escape)
123 → 128 tests; 9 → 10 prompts. Prompt registration smoke test
updated to assert the new shim is wired.
Builds on the prompts-package extraction. Each new prompt embeds
schema-verified SQL plus a findings template tuned to surface
audit-actionable issues (orphans, drift, capacity outliers, security
posture).
phone_inventory_report(filter=None):
Aggregates by model / device pool / CSS, then anomaly queries for
phones with no description, phones whose description echoes their
MAC-based name, phones with no owner, phones in non-default CSS.
Cross-references owner status (phones owned by inactive users
surface as findings).
user_audit(focus=full|admin|inactive|app_users):
End user + application user inventory, role/group assignments via
the enduserdirgroupmap → dirgroup → functionroledirgroupmap →
functionrole join chain. Security-critical findings: app users
with admin-grade role memberships, local-user accounts with admin
privileges, phones owned by inactive users.
inbound_did_audit():
Reusable form of today's cucm-inbound-did-inventory work. XFORM-
Inbound-DNIS curated list categorized (pass-through, block-trans,
specific renames, wildcards, catch-all hazard). Cross-checked
against Internal-PT route patterns and the operator-curated
PSTN-Screen-PT spam blocklist. Findings for orphan target
extensions and the silent !-catch-all risk.
hunt_pilot_audit():
Hunt pilot inventory with queue settings, line group membership,
and distribution algorithm decoding. Schema knowledge already
Hamilton-verified: huntpilotqueue joins via fknumplan_pilot, NOT
fknumplan (the test asserts the correct column appears in the
rendered prompt). Findings: queue misconfigurations (NULL
destinations, infinite max-wait), empty line groups, dead pilots
with no route-list destination.
Implementation notes:
- Each prompt's SQL was validated against the live cluster
(cucm-pub.binghammemorial.org, CUCM 15.0.1.12900-234).
- user_audit originally used UNION ALL with NULL-typed status
column for the headcounts query; Informix rejected it. Split
into two simpler queries (commented in the prompt body).
- phone_inventory_report uses a Hamilton-style SQL escape for
the optional name_filter (single quotes doubled).
- All four prompts gracefully degrade when the docs index isn't
loaded (verified by test_all_new_prompts_render_without_docs).
114 → 123 tests; 5 → 9 prompts. Full live-cluster verification:
- 12 phone models, 629 Cisco 7841 phones (largest model)
- 1,246 active end users, 25 application users
- Hunt pilots with named distribution algorithms (Broadcast, Top
Down, etc.) — confirms typedistributealgorithm join works
- Hamilton-fixed huntpilotqueue.fknumplan_pilot column verified
in the embedded SQL via dedicated regression test.
Refactor: the four existing inline prompts in server.py move into
individual modules under src/mcp_cucm_axl/prompts/. Server.py keeps
thin @mcp.prompt-decorated shims that delegate to the corresponding
render() function — FastMCP needs the shims because it introspects
their signatures to expose parameters to the LLM, but the prompt
*content* now lives one-prompt-per-file.
Why: server.py's prompt section had grown to ~200 lines of inline
markdown. As more query patterns get documented (see
docs/query-patterns/) this would only worsen. Per-module bodies are
easier to diff, review, and unit-test in isolation.
Layout:
src/mcp_cucm_axl/prompts/
__init__.py
_common.py — shared helpers, keyword sets, render_schema_block
route_plan_overview.py
investigate_pattern.py
audit_routing.py
cucm_sql_help.py
sip_trunk_report.py — NEW
Each prompt module exports a `render(docs, *args) -> str` function
that takes the DocsIndex as a parameter (no module globals). The
shim in server.py grabs the runtime `_docs` and passes it in. Pure
functions = trivially unit-testable.
NEW prompt: sip_trunk_report.
Implementation reference: docs/query-patterns/sip-trunk-report.md
(written separately as a query-pattern doc, validated against the
live cluster). The prompt embeds:
- Step 1: trunk inventory SQL (device + sipdevice + 5 LEFT JOINs)
- Step 2: per-destination SQL (siptrunkdestination)
- Step 3: pointer to existing route_lists_and_groups() tool
- Step 4: findings template (SPOF, profile sprawl, CSS asymmetry,
codec heterogeneity, DNS-vs-IP, security posture)
Optional `name_filter` parameter narrows the inventory via LIKE; the
filter value is escaped for SQL safety (single quotes doubled per
Informix convention).
Tests: 14 new in tests/test_prompts_package.py covering each
prompt's render() with and without docs, plus a registration smoke
test that confirms the FastMCP shim set matches the prompts package
exports (catches the case where a new module is added without its
shim).
Total: 100 → 114 tests; 5 prompts registered; live verification
against cucm-pub.binghammemorial.org confirms the embedded SQL
produces real inventory data. The four original prompts are
behaviorally identical to before — same content, just relocated.