mcaxl/docs/query-patterns/sip-trunk-report.md
Ryan Malloy 0691ba8c46 2026.04.27.1: same-day post-release PII scrub
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.
2026-04-27 13:07:38 -06:00

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 for device.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 both t; PSTN-facing trunks usually f.

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:

  • address is VARCHAR(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.
  • addressipv6 exists on the same table but is empty on most clusters.
  • port is INTEGER — 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

  1. routelistdetail doesn'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.
  2. securityprofile is sipsecurityprofile for SIP trunks (not the generic phonesecurityprofile). If you add the security profile join, use the SIP-specific table.
  3. tkclass filters by class enum, not text — but typeclass.name provides the human-readable label. The query above filters on tc.name = 'Trunk' which matches all SIP and ICT trunks. To narrow to SIP-only, also require EXISTS (SELECT 1 FROM sipdevice sd WHERE sd.fkdevice = d.pkid) (or the inner JOIN sipdevice already does that).
  4. Trunks without a primary CSS are valid — Expressway-C trunks on this cluster have fkcallingsearchspace = NULL. Use LEFT JOIN and 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:

  1. 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.
  2. route_inspect_pattern(pattern, partition) — for each route pattern that targets a trunk-bearing route list, walk the call path.
  3. 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 Profile for 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.


  • 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-docs MCP for "SIPDevice", "SIPTrunkDestination", "Device" tables