mcaxl/docs/query-patterns/sip-trunk-report.md
Ryan Malloy 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

12 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.0.1.12900-234 on 2026-04-25. 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.binghammemorial.org) 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).

Live result snapshot (Bingham, 2026-04-25)

11 SIP trunks. All on Main-Campus-Sub1-Pub-DP except exp-c-p-SIP-Trk (on Pub-Sub1-DP). All preferred codec is 711ulaw. All destinations on port 5060 (no TLS).

Trunk Destination SIP Profile CSS Location
Forward-Advantage-SIP-Trk 172.24.10.10 FWD Advantage SIP Profile National-CSS Main-Campus-LOC
PSTN-Router-SIP-Trk 172.20.6.222 PSTN Sparklite SIP Profile PSTN-Inbound-CSS Hub_None
RightFax-SIP-TRK 172.20.2.22 RightFax SIP Profile FAX-CSS Hub_None
Unity-Pub-SIP-TRK 172.20.6.104 Unity SIP Profile Internal-CSS Main-Campus-LOC
Unity-Sub-SIP-TRK 172.20.6.105 Unity SIP Profile Internal-CSS Main-Campus-LOC
VG450-SIP-TRK 172.20.6.99 RightFax SIP Profile National-CSS Hub_None
Verba-SIP-TRK 172.20.6.120 Verba Profile Internal-CSS Hub_None
ZetaFax-SIP-TRK 172.20.14.105 ZetaFax SIP Profile FAX-CSS Hub_None
exp-c-p-SIP-Trk exp-c-p.binghammemorial.org Expressway SIP Profile (none) Main-Campus-LOC
exp-c-s-SIP-Trk exp-c-s.binghammemorial.org Expressway SIP Profile (none) Main-Campus-LOC
singlewireFusion-SIP-TRK 172.20.6.114 singlewire SIP Profile (none) Hub_None

Observations from this snapshot (templates for what the prompt should flag):

  • VG450-SIP-TRK (analog voice gateway) shares the RightFax SIP Profile with RightFax-SIP-TRKprobably intentional (both terminate at fax endpoints) but worth confirming with the operator.
  • The 3 trunks with calling_search_space = NULL (Expressway-C primary, Expressway-C secondary, singlewireFusion) all serve specific device-only paths — they don't originate generic outbound routing. Not a finding, but a useful invariant to call out.
  • PSTN-Router-SIP-Trk is the only trunk with Hub_None location and PSTN-Inbound-CSS and a stripped-down CSS — consistent with its role as the carrier-facing trunk (and as the H1 SPOF in the route-plan audit).

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