Third cross-server prompt landing. Story D from
axl/agent-threads/cross-server-prompts/. Lives in mcaxl because the
dial plan is the first layer to investigate — "did the call make it
past CUCM at all?" determines the rest of the triage.
Composes:
- mcaxl (required) — route lookup for the dialed DID, identify
egress trunk and any translation patterns
- mcsiphon (recommended) — CDR record(s) for the failure window;
extract Q.850 cause codes, duration,
connect status, release direction,
device names + IPs
- mcdewey (optional) — Cisco's published troubleshooting guidance
for the specific cause code surfaced
Verdict layer names (literal vocabulary so downstream tooling can
pattern-match):
- cucm_dial_plan
- cucm_region_or_css
- cube_sip_trunk_negotiation
- far_end_sbc
- far_end_fax_server
- t38_negotiation_failure
- inconclusive
Cause-code-to-layer mapping table embedded in the prompt body covers
the high-frequency Q.850 codes (1, 16, 17, 19, 27, 31, 38, 47, 65,
79, 127). Plus releaseDirection (originator/destination/network)
guidance for narrowing.
Common patterns surfaced explicitly:
- Setup failures with dateTimeConnect=0 (signaling-layer)
- Mid-call drops with non-zero duration (often T.38 renegotiation)
- Cause-code mismatch between origCause and destCause (CUBE
translation layer)
- Receive-and-abandon at the route point (CSS doesn't reach
destination partition)
The prompt's specific high-leverage detail is the "T.38 renegotiation
mid-call" pattern — calls that connect on G.711, then fail when fax
tones trigger T.38 switchover. That shape is invisible from any single
MCP and is exactly what cucx-docs's planned-but-unwritten
runbooks/rightfax-failed-fax-investigation.mdx anticipated.
Tests: registration sentinel updated to 14 prompts. 238 tests passing.
Closes the planned cucx-docs runbook page — that becomes a thin
operational shim around this prompt rather than a from-scratch
troubleshooting tree.
First cross-server prompt landing. Story C from
axl/agent-threads/cross-server-prompts/. Closes a concrete cucx-docs
open finding (orphaned paging DNs 1302 / 1304 at /systems/paging/
carried as "confirm with operator").
A DN is "definitively dead" when:
1. It exists in numplan but has no devicenumplanmap entry —
no device claims it as a line (mcaxl, required)
2. It is not referenced as a UCCX trigger entry point
(mcuccx, strongly recommended)
3. It has no recent CDR activity (mcsiphon, optional)
Each layer narrows the candidate set; the intersection is "safe to
retire — verified across CUCM dial plan + UCCX contact center + CDR
activity."
The prompt embodies the architectural decisions confirmed in
cross-server-prompts/002:
- Per-primary-lens placement (Q1): lives in mcaxl because the
dial plan is its primary lens
- Graceful degradation with explicit gaps (Q2): the verdict
declares MCP availability up-front and adjusts confidence per
sibling; distinguishes connected-but-broken (include error) from
not-connected (note "unavailable")
- Normal naming, no cross_* prefix (Q3): just "dead_dn_finder"
Tier output: "definitively dead" / "likely dead, partial coverage" /
"structurally orphan, unconfirmed" / "active". The cucx-docs paging
DNs (1302, 1304) get explicit name-callouts in the verdict if they
appear, closing the loop back to /systems/paging/.
Tests: registration sentinel updated to 13 prompts. 238/238 passing.
Live-cluster smoke test pending — cucx-docs will run against the
Bingham cluster once they consume this thread direction.
Closes items 4-7 of cucx-docs's prompt-suggestions roadmap (see
axl/agent-threads/cucx-prompt-suggestions/ for the source thread).
did_block_overlap(block_pattern) — new prompt. LLM-orchestrated audit
that finds carveout patterns inside a DID block and surfaces silent
routing exceptions (e.g., 9498/9499 carved out of the 20878594XX block
to route to a different fax server). Composes the existing
route_patterns(filter=) tool with post-processing rather than
introducing a new tool — cucx-docs's #3 was originally pitched as a
tool, but the audit-narrative output is more naturally a prompt.
partition_summary(partition_name=None) — new prompt. "What is this
partition for?" orientation report composing route_partitions,
route_patterns, route_calling_search_spaces, and the new
route_patterns_targeting. No new SQL — this is pure orchestration.
Useful when walking into an unfamiliar cluster and seeing a partition
name like RTC-MGW-Inbound and needing to figure out its role before
touching anything.
cucm_sql_help — deepened with five schema-landmark sections that cost
real audit sessions 3-5 query attempts each to discover. Topics:
numplan↔device M:N via devicenumplanmap; non-existence of
sipdestination as a table; routelist (singular) ≠ numplan→RL;
LEFT-JOIN convention for type-decoder enum tables; CDR/CMR timestamp
localization (cluster-TZ-conditional). Also updated the docs-search
reference from "cisco-docs MCP" to "mcdewey MCP" to match yesterday's
rename.
cucm-schema-cheatsheet docs — appended a "Schema gotchas (from real
audit sessions)" section mirroring the cucm_sql_help content. Two
locations because they serve different consumers: the prompt is read
by an LLM at query time, the docs page is read by a human reviewing
the cluster offline.
Tests: registration sentinel updated to include the two new prompts
(catches the case where a new module is added without a server.py
shim — the prompt would otherwise be invisible to the LLM). Full
suite still 238 passing.
Q3 verification (CDR timestamp empirical) still pending — cluster TLS
intermittent this session. The schema-landmark text is conditional
on cluster TZ per cucx-docs's caveat, so even an unverified ship is
defensible.
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.