The original 2026.04.27 was published-then-deleted from PyPI within
hours after a stricter audit (against the unpacked sdist, not just
curated source paths) found cluster-fingerprint content that the
pre-publish grep had missed. This release supersedes the deleted one;
no functional differences.
Issues found in 2026.04.27 that this fixes:
1. docs/query-patterns/sip-trunk-report.md — "Live result snapshot"
section (38 lines) contained the live cluster's actual SIP trunk
inventory: real hostnames (exp-c-p.binghammemorial.org), real
internal IPs (172.20.6.99, .104, .105, .114, .120, .222, plus
172.20.2.22, 172.20.14.105, 172.24.10.10), real trunk-name +
description rows. Section removed entirely. The query-pattern doc
itself still ships — schema/SQL guidance is generic and useful.
One inline FQDN example (`exp-c-p.binghammemorial.org`) replaced
with `exp-c-p.example.com`. Status line that named the specific
maintenance release (`Validated against CUCM 15.0.1.12900-234 on
2026-04-25.`) genericized to `Validated against CUCM 15.`
2. .mcp.json shipping in sdist with `/home/rpm/bingham/axl` as the
`--directory` argument. Local filesystem path = hostname leak.
Added to `[tool.hatch.build.targets.sdist] exclude`. File stays
in the source repo for development; no longer ships.
3. pyproject.toml comment about the audit workflow ironically
contained the literal word "bingham" as the example grep token.
Rewritten to use "site-specific tokens" generically.
Audit verification (against the unpacked sdist this time):
tar -xzf dist/mcaxl-2026.4.27.1.tar.gz -C /tmp/sdist-inspect
grep -rnEi 'bingham|binghammemorial|10\.[0-9]+\.[0-9]+\.[0-9]+|
172\.(1[6-9]|2[0-9]|3[01])\.[0-9]+\.[0-9]+|
192\.168\.[0-9]+\.[0-9]+|SupportedSystems|CCX-AXL|
CER-AXL|CUC-AXL|TabSync|variphy|15\.0\.1\.12900|
production cluster|/home/rpm|cucm-pub\.bingham'
/tmp/sdist-inspect/
→ returns empty (verified)
Tests still 155/155.
Lesson encoded for next time: the pre-publish audit MUST run against
the unpacked sdist, not just the four explicitly-named paths in the
python.md rule (src/, tests/, README.md, pyproject.toml, .env.example).
The sdist also pulls in docs/, top-level dotfiles, and uv.lock.
CHANGELOG.md spells this out in the post-release note for next time.
9.7 KiB
SIP Trunk Report — Query Pattern
Goal: Produce a comprehensive inventory of every SIP trunk on a CUCM cluster, with destinations, profile assignments, and downstream route-group/route-list membership. Useful for handoff documentation, post-migration cleanup, and identifying single-points-of-failure on specific trunks.
Status: Validated against CUCM 15.
Empty prompts/ directory at src/mcaxl/prompts/ is the
intended home for extracting this into a @mcp.prompt function. For
now, prompts live inline in server.py (see route_plan_overview,
investigate_pattern, audit_routing).
Source-of-truth tables
| Table | Holds |
|---|---|
device |
Trunk row: name, description, FKs to profiles/CSS/pool/location |
sipdevice |
SIP-specific config: codec, calling-party selection, RDNIS handling, security, UR I domain |
siptrunkdestination |
One row per destination IP/port (a trunk can have multiple, ordered by sortorder) |
typeclass |
Device class enum — filter tc.name = 'Trunk' |
sipprofile |
SIP Profile name (joined via device.fksipprofile) |
callingsearchspace |
CSS name (joined via device.fkcallingsearchspace) |
devicepool |
Device Pool name (joined via device.fkdevicepool) |
location |
Location name for CAC/RSVP (joined via device.fklocation) |
typesipcodec |
Codec name enum (joined via sipdevice.tksipcodec) |
Not directly relevant but worth knowing:
sipsecurityprofile— name lookup fordevice.fksecurityprofile. Skipped in the query below because the security profile name is rarely informative on a routine trunk inventory; add the join if security posture matters for the use case.siptrunkoauth— additional auth config for OAuth-authenticated trunks.
Query 1 — Trunk inventory (one row per trunk)
Joins device + sipdevice and pulls the human-readable names of every FK
field that operators typically want when scanning trunks.
SELECT
d.name AS trunk_name,
d.description,
sp.name AS sip_profile,
css.name AS calling_search_space,
dp.name AS device_pool,
loc.name AS location,
tsc.name AS preferred_codec,
sd.requesturidomainname AS sip_domain,
sd.isanonymous AS anon_caller_id,
sd.preferrouteheaderdestination AS prefer_route_header,
sd.acceptinboundrdnis AS accept_inbound_rdnis,
sd.acceptoutboundrdnis AS accept_outbound_rdnis
FROM device d
JOIN typeclass tc ON d.tkclass = tc.enum
JOIN sipdevice sd ON sd.fkdevice = d.pkid
LEFT JOIN sipprofile sp ON d.fksipprofile = sp.pkid
LEFT JOIN callingsearchspace css ON d.fkcallingsearchspace = css.pkid
LEFT JOIN devicepool dp ON d.fkdevicepool = dp.pkid
LEFT JOIN location loc ON d.fklocation = loc.pkid
LEFT JOIN typesipcodec tsc ON sd.tksipcodec = tsc.enum
WHERE tc.name = 'Trunk'
ORDER BY d.name;
Why these specific columns:
description— operator's free-form annotation; almost always names the upstream device + IP, useful when the trunk name itself is opaque.sip_profile— drives transport (UDP/TCP/TLS), early offer, OPTIONS ping, 100rel, etc. Trunks sharing a SIP profile share all of those settings.calling_search_space— the CSS used when this trunk originates a call (typical for inbound from a SIP carrier hitting the CUCM trunk).device_pool+location— clustering and CAC/RSVP grouping. In a single-site cluster these are usually homogeneous.preferred_codec— the codec CUCM advertises first in SDP from this trunk.accept_inbound_rdnis/accept_outbound_rdnis— does the trunk pass RDNIS (Redirected Dialed Number Identification Service) on diversions/forwards? Voicemail trunks need botht; PSTN-facing trunks usuallyf.
LVARCHAR(1) flag fields (anon_caller_id, prefer_route_header,
accept_inbound_rdnis, accept_outbound_rdnis) return 't' or 'f' — not
booleans. Render appropriately in any output.
Query 2 — Destinations (one row per destination IP/port)
A trunk can have multiple destinations (active/active or active/standby — sortorder controls retry order). Separate query because of the one-to-many relationship.
SELECT
d.name AS trunk_name,
std.address,
std.port,
std.sortorder
FROM siptrunkdestination std
JOIN sipdevice sd ON std.fksipdevice = sd.pkid
JOIN device d ON sd.fkdevice = d.pkid
ORDER BY d.name, std.sortorder;
Notes:
addressisVARCHAR(255)— IP literal or DNS name. Expressway-C trunks often use FQDNs (e.g.,exp-c-p.example.com) so SRV resolution can shift the actual destination.addressipv6exists on the same table but is empty on most clusters.portisINTEGER— defaults to 5060 (SIP over UDP/TCP) or 5061 (TLS), but custom ports are common for non-standard integrations (RightFax, recording platforms).
Query 3 — Route-group / route-list membership
Don't write raw SQL for this — the relevant join table is
devicenumplanmap-adjacent and its name has shifted across CUCM versions.
Use the existing MCP tool:
route_lists_and_groups()
Filter the result for route_groups[].devices[].class == "Trunk" to get
the set of (trunk → route group → route list) triples. Note that some
route lists have route groups with no static device members —
those resolve to a Local Route Group via the calling phone's device-pool
fkroutegroup_local mapping at call-time (the CUCM Standard Local Route
Group feature). Trunks reachable only through Local Route Groups
won't appear in the static result and require a follow-up call to
route_device_pool_route_groups() to enumerate.
Common gotchas
routelistdetaildoesn't exist. I tried it; it fails. The actual table name varies, and the join logic for route-list → route-group → device is non-obvious. Use the MCP tool above.securityprofileissipsecurityprofilefor SIP trunks (not the genericphonesecurityprofile). If you add the security profile join, use the SIP-specific table.tkclassfilters by class enum, not text — buttypeclass.nameprovides the human-readable label. The query above filters ontc.name = 'Trunk'which matches all SIP and ICT trunks. To narrow to SIP-only, also requireEXISTS (SELECT 1 FROM sipdevice sd WHERE sd.fkdevice = d.pkid)(or the innerJOIN sipdevicealready does that).- Trunks without a primary CSS are valid — Expressway-C trunks on
this cluster have
fkcallingsearchspace = NULL. UseLEFT JOINand render NULL as "(none)" rather than treating it as a finding.
Suggested follow-up tool calls
After running Query 1+2 and route_lists_and_groups(), the audit
narrative usually wants:
route_devices_using_css(css_name=<each unique trunk CSS>)— see what else uses the same CSS as a particular trunk; helps identify shared blast-radius dependencies.route_inspect_pattern(pattern, partition)— for each route pattern that targets a trunk-bearing route list, walk the call path.axl_sql("SELECT name, description FROM sipprofile WHERE pkid IN (...)")— if multiple trunks share a SIP profile, look up the profile's full detail (transport, early-offer, ping, etc.) once.
Findings template (what to call out)
When this query is wrapped in a @mcp.prompt, the prompt should ask the
LLM to surface:
- Single-point-of-failure trunks: any route group with one trunk
member where that route group is the only path for a critical pattern
(911, voicemail, fax). Cross-reference with
route_lists_and_groups()device counts. - Profile sprawl vs. consolidation: are 11 trunks using 11 different SIP profiles, or do most share a small number? Sprawl = harder to audit transport/timing settings consistently.
- CSS asymmetry: are PSTN-facing inbound trunks using a restrictive CSS that prevents them from reaching internal extensions? Are internal-facing trunks (voicemail) using a permissive CSS? Mismatches can cause one-way audio or routing failures.
- Codec heterogeneity: most clusters standardize on G.711 µ-law. Trunks advertising G.722 or G.729 first warrant explanation.
- DNS-vs-IP destinations: trunks using FQDNs depend on cluster DNS; flag if the FQDN resolution path adds a SPOF the audit hadn't surfaced (e.g., single DNS server).
- Security posture: trunks using
Non Secure SIP Trunk Profilefor carrier-facing connections are a finding worth noting (typical for premise-equipment SIP carriers, but document the deliberate choice).
Proposed prompt name and signature
@mcp.prompt
def sip_trunk_report() -> str:
"""Comprehensive SIP trunk inventory: profiles, destinations,
downstream route-group membership, with findings template.
"""
...
Or with optional filtering:
@mcp.prompt
def sip_trunk_report(name_filter: str | None = None) -> str:
"""SIP trunk inventory. Pass `name_filter` to narrow to one trunk
(substring match against device.name)."""
...
The body should embed the queries above, the follow-up tool-call list, and
the findings template — same pattern as route_plan_overview.
Related
- Existing prompts (inline in
server.py):route_plan_overview,investigate_pattern,audit_routing - Existing tool:
route_lists_and_groups()— the right way to traverse the trunk → RG → RL chain - Existing tool:
route_devices_using_css(css_name)— for follow-up blast-radius analysis on each trunk's CSS - Cisco data dictionary for CUCM 15: search via
cisco-docsMCP for "SIPDevice", "SIPTrunkDestination", "Device" tables