7 Commits

Author SHA1 Message Date
91bd3a0705 prompts: add failed_fax_investigation (cross-server, story D)
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.
2026-05-05 20:39:42 -06:00
ee1e058559 prompts: add dead_dn_finder (cross-server, story C from threads)
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.
2026-05-05 19:24:23 -06:00
a07f8c7291 prompts + docs: did_block_overlap, partition_summary, schema landmarks
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.
2026-05-05 17:52:44 -06:00
ca6956e826 Rename to mcaxl + scrub for public PyPI release
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
2026-04-27 12:53:54 -06:00
8815db06d8 Add whoami prompt — single-user role chain with AXL service-account default
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.
2026-04-26 00:05:31 -06:00
8aaeb04417 Add 4 audit prompts: phone_inventory, user_audit, inbound_did_audit, hunt_pilot_audit
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.
2026-04-25 23:57:01 -06:00
e6aa075793 Extract prompts into a package + add sip_trunk_report
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.
2026-04-25 23:29:05 -06:00