docs: scaffold Starlight site at docs/
17-page Astro/Starlight site mirroring the bingham/cucx conventions
(telemetry off, devToolbar off, astro-icon + lucide, separate
custom.css, Diátaxis-structured sidebar with autogenerate per
directory). Green accent palette differentiates from bingham/cucx's
teal.
Pages by Diátaxis quadrant:
- Getting Started (3): installation, configuration, first-audit
- How-To (4): sip-trunk-report (port from docs/query-patterns/),
route-plan-overview, investigate-pattern (mermaid flowchart),
find-orphan-resources
- Reference (4): tools (all 19), prompts (all 10), env-vars,
cucm-schema-cheatsheet
- Explanation (4): read-only-by-structure, cluster-isolated-cache,
hamilton-review-patterns, pypi-yank-lesson
Build-verified clean (npm run build → 17 pages in 7.88s, pagefind
search index built across all pages, zero errors).
Legacy docs/query-patterns/sip-trunk-report.md kept in place — that
file ships in the published Python sdist's docs/ tree, deletion would
be a package change not just a docs-site change. The new how-to
version is a near-verbatim port.
Content gaps for follow-up: real cluster-output examples in tool/
prompt reference pages, verified CUCM 15 SQL in
find-orphan-resources.md, optional favicon.
Not yet wired for deployment (Caddyfile/Dockerfile out of scope for
v1). Local preview: cd docs && npm run dev.
This commit is contained in:
parent
0691ba8c46
commit
f060170e90
8
docs/.gitignore
vendored
Normal file
8
docs/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Astro / build output
|
||||||
|
.astro/
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Editor / OS
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
133
docs/astro.config.mjs
Normal file
133
docs/astro.config.mjs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import starlight from '@astrojs/starlight';
|
||||||
|
import icon from 'astro-icon';
|
||||||
|
import mermaid from 'astro-mermaid';
|
||||||
|
import starlightPageActions from 'starlight-page-actions';
|
||||||
|
|
||||||
|
// Reverse-proxy / HMR awareness. `DOMAIN` is injected by docker-compose
|
||||||
|
// (DEV_DOMAIN for the dev service, DOMAIN for prod). When unset, we
|
||||||
|
// assume plain local dev on http://localhost:4321.
|
||||||
|
const DOMAIN = process.env.DOMAIN || 'localhost';
|
||||||
|
const IS_BEHIND_PROXY = DOMAIN !== 'localhost';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
// Disabled per workspace convention.
|
||||||
|
telemetry: false,
|
||||||
|
devToolbar: { enabled: false },
|
||||||
|
|
||||||
|
site: IS_BEHIND_PROXY ? `https://${DOMAIN}` : undefined,
|
||||||
|
|
||||||
|
// Shiki doesn't ship a Cisco IOS grammar. Map `cisco` → `ini` (closest
|
||||||
|
// visual match — `!` line comments, "keyword value" shape). Silences
|
||||||
|
// build-time warnings for any Cisco code fences in narrative pages.
|
||||||
|
// `env` (dotenv files) → `bash` so the KEY=value syntax highlights.
|
||||||
|
markdown: {
|
||||||
|
shikiConfig: {
|
||||||
|
langAlias: {
|
||||||
|
cisco: 'ini',
|
||||||
|
env: 'bash',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
integrations: [
|
||||||
|
// Mermaid must come BEFORE starlight — its remark/rehype plugin has
|
||||||
|
// to register before Starlight sets up its MDX pipeline. Renders
|
||||||
|
// client-side (no build-time SVG generation).
|
||||||
|
mermaid({
|
||||||
|
theme: 'default',
|
||||||
|
autoTheme: true, // follows light/dark mode toggle
|
||||||
|
}),
|
||||||
|
icon({
|
||||||
|
include: {
|
||||||
|
lucide: ['*'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
starlight({
|
||||||
|
title: 'mcaxl docs',
|
||||||
|
description:
|
||||||
|
'Read-only MCP server for Cisco Unified Communications Manager (CUCM) — AXL SOAP API + RisPort70 audit. Documentation.',
|
||||||
|
logo: {
|
||||||
|
src: './src/assets/logo.svg',
|
||||||
|
replacesTitle: false,
|
||||||
|
},
|
||||||
|
customCss: ['./src/styles/custom.css'],
|
||||||
|
|
||||||
|
// Per-page action buttons: "Copy Markdown" + "View in Markdown"
|
||||||
|
// (.md route) + "Share" menus. Open-in-AI actions disabled because
|
||||||
|
// they assume the third-party AI can fetch the URL — fine here for
|
||||||
|
// a public site, but kept off to match the operator preference for
|
||||||
|
// copy-and-paste handoffs.
|
||||||
|
plugins: [
|
||||||
|
starlightPageActions({
|
||||||
|
actions: {
|
||||||
|
chatgpt: false,
|
||||||
|
claude: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
lastUpdated: true,
|
||||||
|
pagination: true,
|
||||||
|
social: [
|
||||||
|
{
|
||||||
|
icon: 'external',
|
||||||
|
label: 'PyPI',
|
||||||
|
href: 'https://pypi.org/project/mcaxl/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'external',
|
||||||
|
label: 'Gitea repo',
|
||||||
|
href: 'https://git.supported.systems/mcp/mcaxl',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
editLink: {
|
||||||
|
baseUrl: 'https://git.supported.systems/mcp/mcaxl/_edit/main/docs/',
|
||||||
|
},
|
||||||
|
sidebar: [
|
||||||
|
{
|
||||||
|
label: 'Overview',
|
||||||
|
items: [
|
||||||
|
{ label: 'Home', link: '/' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Getting started',
|
||||||
|
collapsed: false,
|
||||||
|
autogenerate: { directory: 'getting-started' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'How-to guides',
|
||||||
|
collapsed: false,
|
||||||
|
autogenerate: { directory: 'how-to' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Reference',
|
||||||
|
collapsed: false,
|
||||||
|
autogenerate: { directory: 'reference' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Explanation',
|
||||||
|
collapsed: true,
|
||||||
|
autogenerate: { directory: 'explanation' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
vite: {
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
// HMR survives behind a Caddy-terminated TLS reverse proxy when
|
||||||
|
// configured this way. Leaves localhost dev untouched.
|
||||||
|
hmr: IS_BEHIND_PROXY
|
||||||
|
? {
|
||||||
|
host: DOMAIN,
|
||||||
|
protocol: 'wss',
|
||||||
|
clientPort: 443,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
8944
docs/package-lock.json
generated
Normal file
8944
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
docs/package.json
Normal file
27
docs/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "mcaxl-docs",
|
||||||
|
"version": "2026.04.28",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"description": "mcaxl — read-only CUCM/AXL audit MCP server documentation site",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro",
|
||||||
|
"check": "astro check"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/starlight": "^0.38.3",
|
||||||
|
"@iconify-json/lucide": "^1.2.102",
|
||||||
|
"astro": "^6.1.8",
|
||||||
|
"astro-icon": "^1.1.5",
|
||||||
|
"astro-mermaid": "^2.0.1",
|
||||||
|
"sharp": "^0.33.5",
|
||||||
|
"starlight-page-actions": "^0.6.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
docs/src/assets/logo.svg
Normal file
6
docs/src/assets/logo.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" aria-hidden="true">
|
||||||
|
<rect x="2" y="2" width="36" height="36" rx="8" fill="#1f6f5c"/>
|
||||||
|
<path d="M10 12 L20 28 L30 12" stroke="#f3efe6" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
<circle cx="20" cy="28" r="2.4" fill="#f3efe6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 372 B |
28
docs/src/components/CardGrid.astro
Normal file
28
docs/src/components/CardGrid.astro
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
type Card = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
href: string;
|
||||||
|
icon: string; // lucide icon name, e.g. "book-open", "wrench"
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
cards: Card[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cards } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="mc-card-grid">
|
||||||
|
{
|
||||||
|
cards.map((card) => (
|
||||||
|
<a class="mc-card" href={card.href}>
|
||||||
|
<Icon name={`lucide:${card.icon}`} class="mc-card-icon" />
|
||||||
|
<h3>{card.title}</h3>
|
||||||
|
<p>{card.description}</p>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
46
docs/src/components/Diataxis.astro
Normal file
46
docs/src/components/Diataxis.astro
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
// Compact "what kind of page is this?" explainer for the landing page.
|
||||||
|
// Diátaxis quadrants: tutorials, how-to guides, reference, explanation.
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
const rows = [
|
||||||
|
{
|
||||||
|
icon: 'graduation-cap',
|
||||||
|
title: 'Tutorials',
|
||||||
|
body: 'Step-by-step lessons. Start here if mcaxl is new to you.',
|
||||||
|
href: '/getting-started/installation/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'wrench',
|
||||||
|
title: 'How-to guides',
|
||||||
|
body: 'Recipes for specific audit jobs — SIP trunk inventory, finding orphan resources.',
|
||||||
|
href: '/how-to/sip-trunk-report/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'book-open',
|
||||||
|
title: 'Reference',
|
||||||
|
body: 'Tool surface, prompt list, environment variables, schema cheat-sheet.',
|
||||||
|
href: '/reference/tools/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'lightbulb',
|
||||||
|
title: 'Explanation',
|
||||||
|
body: 'Why mcaxl is structured the way it is — design rationale and lessons learned.',
|
||||||
|
href: '/explanation/read-only-by-structure/',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="mc-diataxis">
|
||||||
|
{
|
||||||
|
rows.map((row) => (
|
||||||
|
<a class="mc-diataxis-row" href={row.href}>
|
||||||
|
<Icon name={`lucide:${row.icon}`} class="mc-card-icon" />
|
||||||
|
<div>
|
||||||
|
<strong>{row.title}</strong>
|
||||||
|
<p>{row.body}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
13
docs/src/content.config.ts
Normal file
13
docs/src/content.config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { defineCollection } from 'astro:content';
|
||||||
|
import { docsLoader } from '@astrojs/starlight/loaders';
|
||||||
|
import { docsSchema } from '@astrojs/starlight/schema';
|
||||||
|
|
||||||
|
// Single Starlight docs collection. Schema is the stock Starlight one;
|
||||||
|
// no per-page custom frontmatter fields needed for v1. Add z.object({...})
|
||||||
|
// extensions here later if pages need typed metadata.
|
||||||
|
export const collections = {
|
||||||
|
docs: defineCollection({
|
||||||
|
loader: docsLoader(),
|
||||||
|
schema: docsSchema(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
121
docs/src/content/docs/explanation/cluster-isolated-cache.md
Normal file
121
docs/src/content/docs/explanation/cluster-isolated-cache.md
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
---
|
||||||
|
title: Cluster-isolated cache
|
||||||
|
description: Why the response cache is keyed by SHA-256 of AXL_URL, and what that protects against.
|
||||||
|
sidebar:
|
||||||
|
order: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
`mcaxl` caches every tool's response to SQLite at
|
||||||
|
`~/.cache/mcaxl/responses/axl_responses.sqlite`. The cache key includes
|
||||||
|
a **cluster identifier** that's the SHA-256 hex digest of `AXL_URL`. This
|
||||||
|
is intentional, and it's the difference between a useful cache and a
|
||||||
|
landmine.
|
||||||
|
|
||||||
|
## The failure mode it prevents
|
||||||
|
|
||||||
|
Imagine the cache was keyed only by tool name and arguments — a
|
||||||
|
straightforward LRU. Now consider this workflow:
|
||||||
|
|
||||||
|
1. Operator works on **Cluster A** for a few hours. Cache fills with
|
||||||
|
`route_partitions()` results from Cluster A, `route_patterns()`,
|
||||||
|
`route_devices_using_css()` for various CSS names, etc.
|
||||||
|
2. Operator updates `.env` to point at **Cluster B**. Restarts the MCP server.
|
||||||
|
3. LLM calls `route_partitions()` against Cluster B.
|
||||||
|
4. Cache returns Cluster A's data because `(tool="route_partitions", args={})`
|
||||||
|
matches.
|
||||||
|
|
||||||
|
The LLM now has Cluster A's partition list while believing it's
|
||||||
|
auditing Cluster B. Findings are wrong; recommendations are wrong;
|
||||||
|
worst case, an operator acts on those findings against the wrong
|
||||||
|
cluster.
|
||||||
|
|
||||||
|
## How the isolation works
|
||||||
|
|
||||||
|
The cache key is `(cluster_id, method, args_hash)`, where:
|
||||||
|
|
||||||
|
- `cluster_id = sha256(AXL_URL).hexdigest()[:16]`
|
||||||
|
- `method` is the AXL operation name (e.g. `executeSQLQuery`)
|
||||||
|
- `args_hash` is a SHA-256 of the canonicalized arguments JSON
|
||||||
|
|
||||||
|
When the server starts, it computes `cluster_id` once and binds it to
|
||||||
|
the cache instance. Every read and every write filters by that
|
||||||
|
cluster_id. Pointing at a different cluster computes a different
|
||||||
|
hash — the new server instance literally cannot see the old cluster's
|
||||||
|
rows.
|
||||||
|
|
||||||
|
## What you see in `health()`
|
||||||
|
|
||||||
|
The `health` tool surfaces the current `cluster_id` so an operator can
|
||||||
|
verify which cluster's cache they're hitting:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cache": true,
|
||||||
|
"axl": true,
|
||||||
|
"cache_cluster_id": "8f2a9b1c4e7d6309"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you change `AXL_URL` and the `cache_cluster_id` doesn't change after
|
||||||
|
restart, the env var didn't take effect — check whether `.env` was
|
||||||
|
actually re-read or whether a shell-exported value is overriding.
|
||||||
|
|
||||||
|
## Why hash, not literal URL
|
||||||
|
|
||||||
|
Two reasons:
|
||||||
|
|
||||||
|
1. **Privacy in shared cache locations** — `~/.cache/mcaxl/` might be on
|
||||||
|
a multi-user system. The hash doesn't reveal cluster hostnames in a
|
||||||
|
directory listing the way a literal URL would.
|
||||||
|
2. **Stable across cosmetic URL changes** — except it's *not* stable,
|
||||||
|
and that's a feature: trailing slash differences, port-number
|
||||||
|
differences, IP-vs-FQDN — these all change the hash and force a
|
||||||
|
fresh cache. If two URLs *happen* to point at the same cluster,
|
||||||
|
that's a configuration ambiguity that's better surfaced as
|
||||||
|
different caches than papered over.
|
||||||
|
|
||||||
|
## TTL and invalidation
|
||||||
|
|
||||||
|
`AXL_CACHE_TTL=3600` (default) means rows expire 1 hour after write.
|
||||||
|
For audit work this is usually fine — CUCM config doesn't change
|
||||||
|
second-to-second, and a 1-hour-old `route_partitions()` result is
|
||||||
|
indistinguishable from a fresh one.
|
||||||
|
|
||||||
|
When you've made a known config change and want to force fresh queries:
|
||||||
|
|
||||||
|
```
|
||||||
|
cache_clear(method_pattern="route_%")
|
||||||
|
```
|
||||||
|
|
||||||
|
Cluster-scoped: only this cluster's `route_*` rows are cleared. Other
|
||||||
|
clusters' caches are untouched.
|
||||||
|
|
||||||
|
To clear everything for the current cluster:
|
||||||
|
|
||||||
|
```
|
||||||
|
cache_clear()
|
||||||
|
```
|
||||||
|
|
||||||
|
To wipe the entire SQLite file (all clusters): just delete it from disk.
|
||||||
|
The next server start will recreate it.
|
||||||
|
|
||||||
|
## What the cache stores
|
||||||
|
|
||||||
|
Every `axl_*` and `route_*` tool's response. RisPort70 results are
|
||||||
|
**not cached** — they're explicitly real-time state, and the whole point
|
||||||
|
of `device_registration_status` is that it changes minute to minute.
|
||||||
|
The `_cache: hit | miss` field on every cacheable response surfaces
|
||||||
|
whether the data is fresh or cached.
|
||||||
|
|
||||||
|
## Survives restarts
|
||||||
|
|
||||||
|
Because it's SQLite on disk, the cache survives MCP server restarts
|
||||||
|
and host reboots. This is intentional — most audit sessions span days
|
||||||
|
of intermittent work. Re-paying the AXL roundtrip cost on every fresh
|
||||||
|
session would burn budget for no benefit.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [`cache_stats`](/reference/tools/#cache_stats) and [`cache_clear`](/reference/tools/#cache_clear) — runtime cache control
|
||||||
|
- [Environment variables](/reference/env-vars/#axl_cache_ttl) — TTL knob
|
||||||
|
- [`health`](/reference/tools/#health) — surfaces `cache_cluster_id` for verification
|
||||||
145
docs/src/content/docs/explanation/hamilton-review-patterns.md
Normal file
145
docs/src/content/docs/explanation/hamilton-review-patterns.md
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
---
|
||||||
|
title: Hamilton review patterns
|
||||||
|
description: Why mcaxl shipped with seven hardening fixes and a schema-drift regression test.
|
||||||
|
sidebar:
|
||||||
|
order: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
Before the public release, `mcaxl` went through a Hamilton-style design
|
||||||
|
review — a deliberate adversarial pass that surfaced seven distinct
|
||||||
|
issues across two Critical, three Major, and two Minor severities. Each
|
||||||
|
finding became a patched code path *and* a regression test. This page
|
||||||
|
catalogs the patterns that came out of that review because they shape
|
||||||
|
how new tools should be written going forward.
|
||||||
|
|
||||||
|
## The seven findings
|
||||||
|
|
||||||
|
(Headlines paraphrased from the review notes; specifics live in the
|
||||||
|
`tests/` directory's regression-test names.)
|
||||||
|
|
||||||
|
### Critical #1 — Cache poisoning across clusters
|
||||||
|
|
||||||
|
**Finding:** the original cache schema had no cluster identifier. An
|
||||||
|
operator who switched `AXL_URL` between sessions would see Cluster A's
|
||||||
|
data when querying Cluster B.
|
||||||
|
|
||||||
|
**Fix:** SHA-256 of `AXL_URL` becomes part of every cache row's primary
|
||||||
|
key. See [Cluster-isolated cache](/explanation/cluster-isolated-cache/)
|
||||||
|
for the full design.
|
||||||
|
|
||||||
|
**Regression test:** seeds two cluster_ids, writes rows under both,
|
||||||
|
asserts that lookups under one cluster_id never see the other's rows.
|
||||||
|
|
||||||
|
### Critical #2 — Incomplete CSS reference coverage
|
||||||
|
|
||||||
|
**Finding:** `route_devices_using_css` originally checked ~12
|
||||||
|
`fkcallingsearchspace_*` columns. The CUCM 15 schema has **71** such
|
||||||
|
columns spread across phones, gateways, trunks, translation patterns,
|
||||||
|
route patterns, hunt pilots, voicemail ports, MGCP endpoints, and
|
||||||
|
more. A CSS could be reported as "unreferenced" while actually used by
|
||||||
|
a hunt pilot or voicemail port the tool didn't check.
|
||||||
|
|
||||||
|
**Fix:** the tool now walks all 71 known columns and reports findings
|
||||||
|
per category.
|
||||||
|
|
||||||
|
**Regression test:** `test_complete_schema_coverage_against_known_columns`
|
||||||
|
asserts the tool's hardcoded column list matches the actual schema. If
|
||||||
|
a future CUCM version adds new CSS-bearing columns, the test fails red
|
||||||
|
and forces an explicit update — better to surface drift loudly than to
|
||||||
|
silently miss references.
|
||||||
|
|
||||||
|
### Major #1 — `route_translation_chain` ignores route filters
|
||||||
|
|
||||||
|
**Finding:** `@` patterns are constrained by route filters that match
|
||||||
|
specific dial-plan slices (international, toll, local, etc.).
|
||||||
|
`route_translation_chain` matched `@` patterns greedily, returning
|
||||||
|
matches that the route filter would actually exclude at call-time.
|
||||||
|
|
||||||
|
**Fix:** documented as a known limitation; the tool emits a warning when
|
||||||
|
it returns an `@`-pattern match. Modeling route filters in the matcher
|
||||||
|
is on the roadmap but non-trivial.
|
||||||
|
|
||||||
|
**Regression test:** asserts the warning fires for `@`-pattern matches.
|
||||||
|
|
||||||
|
### Major #2 — RisPort SOAP retry on transient failures
|
||||||
|
|
||||||
|
**Finding:** RisPort70 throttles aggressively under load (502, 503, 504
|
||||||
|
responses are common on a busy cluster). The original code didn't
|
||||||
|
retry; one transient blip aborted a multi-page enumeration.
|
||||||
|
|
||||||
|
**Fix:** exponential backoff retry, configurable via
|
||||||
|
`AXL_RATE_LIMIT_RETRIES`. Default 3 covers most cases.
|
||||||
|
|
||||||
|
**Regression test:** mocks a 503 response twice followed by 200 success;
|
||||||
|
asserts the tool returns the eventual success.
|
||||||
|
|
||||||
|
### Major #3 — Inconsistent error shapes between tools
|
||||||
|
|
||||||
|
**Finding:** some tools returned `{"error": "..."}` for failures while
|
||||||
|
others raised `RuntimeError`. LLMs had to handle two patterns.
|
||||||
|
|
||||||
|
**Fix:** all tool failures now raise `RuntimeError` uniformly. `_require_cache()`
|
||||||
|
matches `_client()`'s behavior.
|
||||||
|
|
||||||
|
**Regression test:** every tool's failure path is exercised; all raise
|
||||||
|
`RuntimeError`.
|
||||||
|
|
||||||
|
### Minor #1 — Trailing semicolon in `axl_sql` queries
|
||||||
|
|
||||||
|
**Finding:** SQL queries with trailing semicolons sent through to AXL
|
||||||
|
returned a cryptic error. LLMs reasonably include trailing semicolons.
|
||||||
|
|
||||||
|
**Fix:** the validator strips trailing semicolons before submission.
|
||||||
|
|
||||||
|
### Minor #2 — Cache stats counted expired entries
|
||||||
|
|
||||||
|
**Finding:** `cache_stats()` reported total rows in the SQLite table,
|
||||||
|
including expired rows that wouldn't be served. Misleading.
|
||||||
|
|
||||||
|
**Fix:** stats now report `total_entries` and `live_entries` separately.
|
||||||
|
|
||||||
|
## Patterns that emerged
|
||||||
|
|
||||||
|
### 1. Schema coverage tests guard against drift
|
||||||
|
|
||||||
|
The CSS-coverage finding generalized: any tool that walks a known
|
||||||
|
column list, table list, or enum list should have a regression test
|
||||||
|
that asserts the list matches the live schema. Drift is the silent
|
||||||
|
killer; loud test failures are the alternative.
|
||||||
|
|
||||||
|
### 2. Failures raise, never return error dicts
|
||||||
|
|
||||||
|
`raise RuntimeError` consistently. LLMs handle exceptions cleanly; they
|
||||||
|
get tangled by inspect-then-branch on `.get("error")` patterns. One
|
||||||
|
pattern, applied everywhere.
|
||||||
|
|
||||||
|
### 3. Cluster identity is part of every cache key
|
||||||
|
|
||||||
|
Anything that gets cached gets a cluster_id prefix. The pattern is
|
||||||
|
encoded in the cache layer so individual tools don't have to think
|
||||||
|
about it.
|
||||||
|
|
||||||
|
### 4. Known limitations are documented, not silently swallowed
|
||||||
|
|
||||||
|
The route-filter limitation in `route_translation_chain` is documented
|
||||||
|
in three places: the tool docstring, the README, and the
|
||||||
|
[How-to: Investigate a pattern](/how-to/investigate-pattern/) page.
|
||||||
|
Ambiguity that the tool doesn't fully resolve should be loud.
|
||||||
|
|
||||||
|
### 5. Defense-in-depth at the boundary
|
||||||
|
|
||||||
|
The structural read-only guarantee plus the SQL validator plus the
|
||||||
|
service-account role binding is three independent layers. Any one of
|
||||||
|
them should be sufficient; together they're robust.
|
||||||
|
|
||||||
|
## Total test count
|
||||||
|
|
||||||
|
The full test suite is **155 unit tests** with the schema-drift guard
|
||||||
|
included. Every Hamilton finding has at least one regression test
|
||||||
|
named after the finding.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [Read-only by structure](/explanation/read-only-by-structure/) — the structural guarantee Critical #2 builds on
|
||||||
|
- [Cluster-isolated cache](/explanation/cluster-isolated-cache/) — the design that addresses Critical #1
|
||||||
|
- [`route_devices_using_css`](/reference/tools/#route_devices_using_css) — the 71-column tool that Critical #2 produced
|
||||||
143
docs/src/content/docs/explanation/pypi-yank-lesson.md
Normal file
143
docs/src/content/docs/explanation/pypi-yank-lesson.md
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
---
|
||||||
|
title: The PyPI yank-and-republish lesson
|
||||||
|
description: How a PII leak in a published sdist taught us to audit the unpacked tarball, not just curated source paths.
|
||||||
|
sidebar:
|
||||||
|
order: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
The story behind version `2026.04.27.1` — what we shipped, what
|
||||||
|
leaked, what we did about it, and the durable change that came out of
|
||||||
|
it. This is here for the next person who reaches a "ready to publish"
|
||||||
|
moment thinking the audit is done.
|
||||||
|
|
||||||
|
## What happened
|
||||||
|
|
||||||
|
`mcaxl` v`2026.04.27` was the first public PyPI release. The
|
||||||
|
pre-publish PII audit ran against `src/`, `tests/`, `pyproject.toml`,
|
||||||
|
and `README.md` — the curated source paths. It returned empty. We
|
||||||
|
published.
|
||||||
|
|
||||||
|
A stricter post-publish audit, this time against the **unpacked sdist**,
|
||||||
|
returned hits. Two of them:
|
||||||
|
|
||||||
|
1. **`docs/query-patterns/sip-trunk-report.md`** had a "Live result
|
||||||
|
snapshot" section with the test cluster's actual SIP trunk
|
||||||
|
inventory. Real internal hostnames. Real internal IPs. The
|
||||||
|
query-pattern doc itself was fine to ship — operationally rich,
|
||||||
|
useful — but the snapshot section had been added during development
|
||||||
|
for "look at the real output" and never scrubbed before publish.
|
||||||
|
|
||||||
|
2. **`.mcp.json`** had a local filesystem path in it. Dev artifact, not
|
||||||
|
meant for distribution. Hatchling included it in the sdist because
|
||||||
|
nothing in `[tool.hatch.build.targets.sdist] exclude` told it not to.
|
||||||
|
|
||||||
|
Neither leak was catastrophic — the hostnames and paths weren't
|
||||||
|
credentials, weren't tokens, didn't grant access. But they fingerprinted
|
||||||
|
a specific cluster, and the principle that "PyPI is immutable per
|
||||||
|
version" means that fingerprint would have lived on the public index
|
||||||
|
permanently.
|
||||||
|
|
||||||
|
## The recovery
|
||||||
|
|
||||||
|
PyPI's web UI has a per-version Yank action that marks a version as
|
||||||
|
"do not install" without removing the file. We yanked v`2026.04.27`
|
||||||
|
within minutes of confirming the leak.
|
||||||
|
|
||||||
|
For genuine PII removal (versus just yank), email
|
||||||
|
`admin@pypi.org` — they will sometimes do full file deletion, but it
|
||||||
|
takes days, not minutes. We escalated for full deletion.
|
||||||
|
|
||||||
|
We bumped the version to `2026.04.27.1` (CalVer post-release suffix —
|
||||||
|
PEP 440 compatible), scrubbed the snapshot section out of the
|
||||||
|
sip-trunk doc, added `.mcp.json` to the sdist exclude list, **re-ran the
|
||||||
|
unpacked-sdist audit** until it returned clean, and republished.
|
||||||
|
|
||||||
|
Total time from "uh oh" to "fixed and republished": about an hour. Most
|
||||||
|
of that was waiting for confidence in the new audit, not the
|
||||||
|
mechanical work.
|
||||||
|
|
||||||
|
## What we changed permanently
|
||||||
|
|
||||||
|
### 1. The audit runs against the unpacked sdist
|
||||||
|
|
||||||
|
The pre-publish audit script now does:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf dist/ /tmp/sdist-audit
|
||||||
|
uv build
|
||||||
|
mkdir -p /tmp/sdist-audit
|
||||||
|
tar -xzf dist/*.tar.gz -C /tmp/sdist-audit
|
||||||
|
grep -rnEi 'site-token|10\.[0-9]+|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|customer-name|/home/' /tmp/sdist-audit/
|
||||||
|
```
|
||||||
|
|
||||||
|
The unpacked-sdist grep is **non-negotiable** because the sdist's blast
|
||||||
|
surface is larger than the curated source-path enumeration. Hatchling
|
||||||
|
pulls in `docs/`, top-level dotfiles like `.mcp.json` and `.gitignore`,
|
||||||
|
`uv.lock`, `CHANGELOG.md`, and auto-generated files like `PKG-INFO`.
|
||||||
|
|
||||||
|
Whatever the unpacked-sdist grep returns is what ships to PyPI
|
||||||
|
permanently. Empty result = safe to publish. Anything else = scrub the
|
||||||
|
source, rebuild, re-audit.
|
||||||
|
|
||||||
|
### 2. Structural defense in `pyproject.toml`
|
||||||
|
|
||||||
|
Belt-and-suspenders alongside the audit script:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[tool.hatch.build.targets.sdist]
|
||||||
|
exclude = [
|
||||||
|
"CLAUDE.md",
|
||||||
|
".env", ".env.local",
|
||||||
|
".mcp.json",
|
||||||
|
"axlsqltoolkit.zip",
|
||||||
|
"audits/",
|
||||||
|
"tests/",
|
||||||
|
".pytest_cache/", ".ruff_cache/",
|
||||||
|
"dist/", "build/",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
The exclude list prevents inclusion at build time; the audit verifies
|
||||||
|
after build. Both are necessary because new dev artifacts may appear
|
||||||
|
that the exclude list hasn't been updated for — the audit catches the
|
||||||
|
gap.
|
||||||
|
|
||||||
|
### 3. CalVer post-release for same-day fixes
|
||||||
|
|
||||||
|
PyPI is immutable per version. You cannot fix a version after publish.
|
||||||
|
The convention encoded going forward:
|
||||||
|
|
||||||
|
- Initial release: `version = "YYYY.MM.DD"`
|
||||||
|
- Same-day fix: bump to `YYYY.MM.DD.1`, `YYYY.MM.DD.2`, etc. (PEP 440
|
||||||
|
post-release suffix)
|
||||||
|
- Test thoroughly before *that* publish since the lesson is fresh
|
||||||
|
|
||||||
|
### 4. `uv publish` is a one-shot commit
|
||||||
|
|
||||||
|
Hard-won corollary: `uv publish` does not abort mid-upload. Once the
|
||||||
|
HTTP request is in flight, killing the local process (Ctrl-C, harness
|
||||||
|
interrupt) does **not** recall the file from PyPI — the upload likely
|
||||||
|
completed before the kill arrived. Treat any `uv publish` invocation
|
||||||
|
as committed unless you see a clean error from the server.
|
||||||
|
|
||||||
|
The "Request interrupted" message in our LLM harness only kills the
|
||||||
|
local process, NOT the in-flight HTTP request.
|
||||||
|
|
||||||
|
## Why this is in the explanation section
|
||||||
|
|
||||||
|
This is documentation, not a runbook. The runbook for "release a new
|
||||||
|
version" lives in the project repo's `RELEASING.md`. This page exists
|
||||||
|
because the *reasoning* — why the audit shape changed, what classes of
|
||||||
|
mistake it now catches — is more valuable than the mechanical script.
|
||||||
|
A new operator reading this should come away knowing:
|
||||||
|
|
||||||
|
1. Curated grep paths are necessary but not sufficient
|
||||||
|
2. The sdist's blast surface is larger than `src/` + `tests/`
|
||||||
|
3. PyPI is immutable, so audit *before* publish, not after
|
||||||
|
4. Yank-and-republish via post-release is the recovery, not a fix
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [`pyproject.toml`](https://git.supported.systems/mcp/mcaxl/src/branch/main/pyproject.toml) — current sdist exclude list
|
||||||
|
- [`CHANGELOG.md`](https://git.supported.systems/mcp/mcaxl/src/branch/main/CHANGELOG.md) — `2026.04.27.1` retrospective entry
|
||||||
|
- The python.md rule in operator-private notes that codifies this audit pattern across all our PyPI-published packages
|
||||||
95
docs/src/content/docs/explanation/read-only-by-structure.md
Normal file
95
docs/src/content/docs/explanation/read-only-by-structure.md
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
title: Read-only by structure
|
||||||
|
description: Why mcaxl is structurally incapable of mutating CUCM, not just policy-restricted.
|
||||||
|
sidebar:
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
`mcaxl` is read-only — and not by *policy*. It's read-only by *absence*.
|
||||||
|
The server **never registers** an AXL write method. There is no
|
||||||
|
`executeSQLUpdate` tool, no `add*` / `update*` / `remove*` / `apply*` /
|
||||||
|
`reset*` / `restart*` tool. The mutating side of AXL is structurally
|
||||||
|
absent from the tool surface that gets exposed to the LLM.
|
||||||
|
|
||||||
|
This matters because the alternative — *runtime sanitization* of an LLM's
|
||||||
|
chosen tool calls — is a moving target. Every model release shifts the
|
||||||
|
boundary of what an LLM might try. Sanitization in code becomes a
|
||||||
|
forever-game of catching new escape hatches.
|
||||||
|
|
||||||
|
Absence is timeless. If the tool isn't registered, the LLM can't
|
||||||
|
invoke it.
|
||||||
|
|
||||||
|
## What that looks like in code
|
||||||
|
|
||||||
|
`server.py` registers exactly the 19 tools listed in the
|
||||||
|
[Tool reference](/reference/tools/). Each is an `@mcp.tool` decorator on
|
||||||
|
a function whose body calls one of the read-only paths in `client.py`:
|
||||||
|
|
||||||
|
- `client.execute_sql_query()` — wraps AXL's `executeSQLQuery` (read-only)
|
||||||
|
- `client.list_informix_tables()` — wraps a `executeSQLQuery` against `systables`
|
||||||
|
- `client.describe_informix_table()` — same, against `syscolumns`
|
||||||
|
- `client.get_ccm_version()` — wraps `getCCMVersion` (read-only)
|
||||||
|
|
||||||
|
There is no `client.execute_sql_update()`, no `client.add_*()`. The
|
||||||
|
methods don't exist; the import would fail.
|
||||||
|
|
||||||
|
## Defense-in-depth: SQL validator
|
||||||
|
|
||||||
|
Even though writes can't reach AXL via this server, the SQL validator
|
||||||
|
in `sql_validator.py` rejects non-`SELECT` / `WITH` queries client-side
|
||||||
|
before they go anywhere. This is belt-and-suspenders — if a future
|
||||||
|
refactor accidentally exposed `executeSQLQuery` to a non-`SELECT`
|
||||||
|
string (e.g. a stored-procedure call hidden inside a CTE), the
|
||||||
|
validator would catch it before the request left the host.
|
||||||
|
|
||||||
|
The validator's rules:
|
||||||
|
|
||||||
|
1. After stripping comments and trimming whitespace, the query must
|
||||||
|
start with `SELECT` or `WITH` (case-insensitive)
|
||||||
|
2. The query must not contain any of: `INSERT`, `UPDATE`, `DELETE`,
|
||||||
|
`DROP`, `CREATE`, `ALTER`, `TRUNCATE`, `EXEC`, `EXECUTE`, `CALL`
|
||||||
|
3. Trailing semicolons are stripped before submission
|
||||||
|
|
||||||
|
This catches the obvious cases. It is not a SQL parser, and it doesn't
|
||||||
|
try to be — the structural read-only guarantee is the primary defense,
|
||||||
|
the validator is the secondary defense.
|
||||||
|
|
||||||
|
## Operational consequence: minimal-privilege service account
|
||||||
|
|
||||||
|
Because the server is structurally incapable of writes, the AXL service
|
||||||
|
account it uses can be granted **only** the
|
||||||
|
`Standard AXL Read Only API Access` role. Operators sometimes attach
|
||||||
|
the full `Standard AXL API Access` role (read-write) "for convenience";
|
||||||
|
`mcaxl` is incapable of using it.
|
||||||
|
|
||||||
|
If the cluster's existing AXL service account already has write roles
|
||||||
|
attached for other tooling, that's fine — `mcaxl` won't use them. But
|
||||||
|
when creating a *new* account specifically for `mcaxl`, give it the
|
||||||
|
read-only role only. The `whoami` prompt will surface a finding if the
|
||||||
|
account has write-capable roles.
|
||||||
|
|
||||||
|
## Why this matters more for LLM-driven tools
|
||||||
|
|
||||||
|
A human operator typing into a SQL prompt can be trusted not to
|
||||||
|
type `DROP TABLE device`. An LLM has read tens of thousands of tutorials
|
||||||
|
that include "and then to clean up, you can do DROP TABLE..." — the
|
||||||
|
prior is *much* higher that an unbounded SQL tool would, eventually,
|
||||||
|
under the right confluence of prompt context and reasoning chain,
|
||||||
|
issue a destructive query.
|
||||||
|
|
||||||
|
Structural read-only is the only durable answer. Whatever the LLM
|
||||||
|
*reasons* it should do, the *capability* to mutate is not present.
|
||||||
|
|
||||||
|
## The composability angle
|
||||||
|
|
||||||
|
Pairing `mcaxl` with `@calltelemetry/cisco-cucm-mcp` (which exposes
|
||||||
|
operational debugging — log collection, packet capture, perfmon) gives
|
||||||
|
an LLM session a much richer surface, but `cisco-cucm-mcp` also exposes
|
||||||
|
write paths (service restart, packet capture start/stop, etc.). Operators
|
||||||
|
who care about strict read-only audit isolation can run `mcaxl` alone;
|
||||||
|
operators who want compound findings can run both side-by-side and
|
||||||
|
configure `cisco-cucm-mcp` with appropriately scoped credentials.
|
||||||
|
|
||||||
|
The two servers' choices reflect their different scopes — `mcaxl`'s
|
||||||
|
scope is audit, so read-only by structure. `cisco-cucm-mcp`'s scope is
|
||||||
|
operations, where writes are part of the job.
|
||||||
100
docs/src/content/docs/getting-started/configuration.md
Normal file
100
docs/src/content/docs/getting-started/configuration.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
title: Configuration
|
||||||
|
description: Environment variables, the AXL service account role binding, and TLS notes.
|
||||||
|
sidebar:
|
||||||
|
order: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
`mcaxl` reads its configuration from environment variables. Most
|
||||||
|
operators keep a `.env` file in the working directory that the server
|
||||||
|
loads automatically (via `python-dotenv`). Anything in the actual
|
||||||
|
process environment overrides values from `.env`.
|
||||||
|
|
||||||
|
## Required variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
AXL_URL=https://cucm-pub.example.com:8443/axl/
|
||||||
|
AXL_USER=your-axl-service-account
|
||||||
|
AXL_PASS=your-password
|
||||||
|
```
|
||||||
|
|
||||||
|
- `AXL_URL` must point to the **publisher** node. The AXL service runs on
|
||||||
|
the publisher only — pointing at a subscriber returns a misleading
|
||||||
|
empty-result error rather than a connection error.
|
||||||
|
- `AXL_URL` must end in `/axl/` (trailing slash). The SOAP envelope is
|
||||||
|
POSTed to that exact path.
|
||||||
|
- The default port is `8443/tcp`. Custom ports are uncommon; set them
|
||||||
|
explicitly in the URL if your cluster differs.
|
||||||
|
|
||||||
|
## Optional variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
AXL_VERIFY_TLS=false # CUCM ships self-signed certs; default is off
|
||||||
|
AXL_CACHE_TTL=3600 # response cache TTL in seconds; 0 disables
|
||||||
|
AXL_RATE_LIMIT_RETRIES=3 # 502/503/504 retry count with backoff
|
||||||
|
AXL_WSDL_PATH= # explicit WSDL file location override
|
||||||
|
AXL_WSDL_ZIP= # explicit toolkit zip path override
|
||||||
|
CISCO_DOCS_INDEX_PATH= # for prompt enrichment; see below
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Environment variables reference](/reference/env-vars/) for the
|
||||||
|
exhaustive list with defaults, units, and edge cases.
|
||||||
|
|
||||||
|
## TLS verification
|
||||||
|
|
||||||
|
CUCM ships a self-signed CA cert on every fresh install. Most clusters
|
||||||
|
never get this replaced with a real cert, so `AXL_VERIFY_TLS=false` is
|
||||||
|
the practical default. If your cluster has a CA-signed cert installed,
|
||||||
|
set `AXL_VERIFY_TLS=true` and `mcaxl` will validate against the
|
||||||
|
system trust store.
|
||||||
|
|
||||||
|
There is no `AXL_CA_BUNDLE` knob yet — if you need to validate against a
|
||||||
|
private CA, install the CA cert into your OS trust store (`update-ca-certificates`,
|
||||||
|
`security add-trusted-cert`, etc.) and `AXL_VERIFY_TLS=true` will pick it up.
|
||||||
|
|
||||||
|
## AXL service account role binding
|
||||||
|
|
||||||
|
The AXL service account needs the **`Standard AXL Read Only API Access`**
|
||||||
|
role at minimum. It does not need the full `Standard AXL API Access`
|
||||||
|
role — `mcaxl` is structurally incapable of using write permissions
|
||||||
|
even if the account has them.
|
||||||
|
|
||||||
|
To create a minimal-privilege account:
|
||||||
|
|
||||||
|
1. **User Management -> Application User -> Add New**
|
||||||
|
2. Set `userid` (e.g. `mcaxl`) and a strong password
|
||||||
|
3. **User Management -> Access Control Group -> Add New**
|
||||||
|
- Name: `mcaxl-readonly`
|
||||||
|
- Roles: `Standard AXL Read Only API Access`
|
||||||
|
4. Add the application user to the new access control group
|
||||||
|
|
||||||
|
The `whoami` prompt with no arguments will display the role chain for
|
||||||
|
the configured `AXL_USER` so you can verify the binding without leaving
|
||||||
|
your LLM session.
|
||||||
|
|
||||||
|
## Optional: schema-grounded prompt enrichment
|
||||||
|
|
||||||
|
Set `CISCO_DOCS_INDEX_PATH` to a directory containing `chunks.jsonl` and
|
||||||
|
`index_meta.json` (produced by the `mcp-cisco-docs` indexer or any
|
||||||
|
compatible embedding pipeline) to have prompts pull relevant Cisco
|
||||||
|
documentation chunks inline. Without this, prompts gracefully degrade
|
||||||
|
to a fallback notice instructing the LLM to use the sibling
|
||||||
|
`cisco-docs` server's `search_docs` tool.
|
||||||
|
|
||||||
|
## Sample `.env`
|
||||||
|
|
||||||
|
```env
|
||||||
|
AXL_URL=https://cucm-pub.example.com:8443/axl/
|
||||||
|
AXL_USER=mcaxl
|
||||||
|
AXL_PASS=change-me
|
||||||
|
AXL_VERIFY_TLS=false
|
||||||
|
AXL_CACHE_TTL=3600
|
||||||
|
|
||||||
|
# Optional schema-doc enrichment
|
||||||
|
CISCO_DOCS_INDEX_PATH=/var/lib/cisco-docs-index/15.0/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next
|
||||||
|
|
||||||
|
- [Run your first audit](/getting-started/first-audit/) — orchestrate tools through the `route_plan_overview` prompt
|
||||||
|
- [Environment variables reference](/reference/env-vars/) — the exhaustive list
|
||||||
117
docs/src/content/docs/getting-started/first-audit.md
Normal file
117
docs/src/content/docs/getting-started/first-audit.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
title: Your first audit
|
||||||
|
description: A 10-minute tour. Run route_plan_overview against a live cluster and read the output.
|
||||||
|
sidebar:
|
||||||
|
order: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
This walkthrough assumes you've completed [Installation](/getting-started/installation/)
|
||||||
|
and [Configuration](/getting-started/configuration/) and have an
|
||||||
|
MCP-aware client connected to `mcaxl`. We'll use Claude Code in the
|
||||||
|
examples but any client works the same way.
|
||||||
|
|
||||||
|
## Step 1 — sanity check
|
||||||
|
|
||||||
|
In your LLM client, ask:
|
||||||
|
|
||||||
|
> Run the `health` and `axl_version` tools.
|
||||||
|
|
||||||
|
You should see:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cache": true,
|
||||||
|
"axl": true,
|
||||||
|
"docs": false,
|
||||||
|
"risport": true,
|
||||||
|
"axl_connection": "ok"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "15.0.1.12900-234",
|
||||||
|
"_cache": "miss"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If `docs` is `false` that's normal — it means `CISCO_DOCS_INDEX_PATH`
|
||||||
|
is unset (the docs indexer is optional). If `axl_connection` shows
|
||||||
|
`error`, re-check `AXL_URL` and credentials in your `.env`.
|
||||||
|
|
||||||
|
## Step 2 — invoke the route plan overview prompt
|
||||||
|
|
||||||
|
In Claude Code, type:
|
||||||
|
|
||||||
|
```
|
||||||
|
/mcp__cucm-axl__route_plan_overview
|
||||||
|
```
|
||||||
|
|
||||||
|
(The slash command name follows the pattern
|
||||||
|
`/mcp__<server-id>__<prompt-name>` — your `<server-id>` will match
|
||||||
|
whatever you used in `claude mcp add`.)
|
||||||
|
|
||||||
|
The prompt seeds the conversation with:
|
||||||
|
|
||||||
|
- A description of what to look for in the route plan
|
||||||
|
- A recommended sequence of tool calls (`route_partitions` ->
|
||||||
|
`route_calling_search_spaces` -> `route_patterns` -> follow-up
|
||||||
|
inspections on anything that looks anomalous)
|
||||||
|
- A findings template the LLM uses to structure observations
|
||||||
|
|
||||||
|
## Step 3 — let the LLM drive
|
||||||
|
|
||||||
|
The LLM will start by calling `route_partitions()` and
|
||||||
|
`route_calling_search_spaces()` to build a mental map of the cluster's
|
||||||
|
access-control structure. From there it will sample `route_patterns()`,
|
||||||
|
flag interesting transformations (calling-party masks, prefixes, digit
|
||||||
|
discards), and drill into specific patterns with `route_inspect_pattern`.
|
||||||
|
|
||||||
|
Don't interrupt unless you see a question — the prompt is designed to
|
||||||
|
orchestrate a multi-step audit autonomously. Typical run time on a
|
||||||
|
50-pattern cluster is 30-90 seconds.
|
||||||
|
|
||||||
|
## Step 4 — read the findings
|
||||||
|
|
||||||
|
The prompt asks the LLM to surface findings in a structured shape:
|
||||||
|
|
||||||
|
> **Finding 1 — Calling Search Space `Internal-Only` is unreferenced**
|
||||||
|
>
|
||||||
|
> Severity: Minor
|
||||||
|
> Evidence: `route_devices_using_css('Internal-Only')` returned 0 across all 71 known fkcallingsearchspace_* columns.
|
||||||
|
> Recommendation: Confirm intent; consider deletion to reduce config sprawl.
|
||||||
|
|
||||||
|
Findings are plain Markdown — copy them out, paste into a runbook,
|
||||||
|
or feed them to `whoami` / `route_inspect_pattern` for follow-up
|
||||||
|
verification.
|
||||||
|
|
||||||
|
## Step 5 — cross-reference with RisPort
|
||||||
|
|
||||||
|
If a finding mentions a phone, gateway, or trunk, follow up with:
|
||||||
|
|
||||||
|
```
|
||||||
|
Run device_registration_summary, then device_registration_status with
|
||||||
|
device_class=Phone for any model the summary flagged.
|
||||||
|
```
|
||||||
|
|
||||||
|
This pulls **live registration state** from RisPort70 — distinct from
|
||||||
|
the AXL configuration view. Compound findings like *"CSS X is
|
||||||
|
unreferenced AND zero phones currently registered against it"* require
|
||||||
|
both data sources.
|
||||||
|
|
||||||
|
## What to do next
|
||||||
|
|
||||||
|
- Try the `sip_trunk_report` prompt for a complete SIP trunk inventory
|
||||||
|
- Try the `whoami` prompt to verify your own service account's role chain
|
||||||
|
- Read the [SIP Trunk Report how-to](/how-to/sip-trunk-report/) for the deep version of that recipe
|
||||||
|
- Read [Hamilton review patterns](/explanation/hamilton-review-patterns/) to understand why the tool surface looks the way it does
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Likely cause |
|
||||||
|
|---|---|
|
||||||
|
| `axl_version` returns "WSDL not found" | `axlsqltoolkit.zip` not in cwd; see [WSDL bootstrap](/getting-started/installation/#wsdl-bootstrap) |
|
||||||
|
| `axl_connection: error` and `403 Forbidden` | Service account missing `Standard AXL Read Only API Access` role |
|
||||||
|
| `axl_connection: error` and `SSL: CERTIFICATE_VERIFY_FAILED` | Set `AXL_VERIFY_TLS=false` (CUCM self-signed cert) |
|
||||||
|
| Prompts return "no docs index configured" warnings | `CISCO_DOCS_INDEX_PATH` unset; this is fine, prompts still work |
|
||||||
|
| RisPort tools return rate-limit errors | Lower the page size; default `AXL_RATE_LIMIT_RETRIES=3` should auto-handle most 503s |
|
||||||
99
docs/src/content/docs/getting-started/installation.md
Normal file
99
docs/src/content/docs/getting-started/installation.md
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
title: Installation
|
||||||
|
description: Install mcaxl from PyPI and wire it into your MCP-aware client (Claude Code, Continue, Cline).
|
||||||
|
sidebar:
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
`mcaxl` is published on PyPI. The fastest path is `uvx`, which downloads
|
||||||
|
the package into a managed environment and runs the entry point in one
|
||||||
|
step — no virtualenv to manage, no editable install, no pinning to fight.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Python 3.11+** (3.12 / 3.13 work; `mcaxl` declares `requires-python = ">=3.11"`)
|
||||||
|
- **`uv`** — see [astral.sh/uv](https://docs.astral.sh/uv/) for install
|
||||||
|
- **Network reachability to CUCM** on port `8443/tcp` (AXL) and `8443/tcp` (RisPort70 — same port, different SOAP endpoint)
|
||||||
|
- **An AXL service account** on CUCM — see [Configuration](/getting-started/configuration/)
|
||||||
|
- **The Cisco AXL toolkit** — vendor-licensed, downloaded from your CUCM admin UI; see [WSDL bootstrap](#wsdl-bootstrap) below
|
||||||
|
|
||||||
|
## Install paths
|
||||||
|
|
||||||
|
### From PyPI via `uvx` (recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx mcaxl
|
||||||
|
```
|
||||||
|
|
||||||
|
`uvx` resolves the package, creates an ephemeral virtualenv, and runs
|
||||||
|
`mcaxl` from the entry point declared in `pyproject.toml`. No state on
|
||||||
|
disk except `~/.cache/uv/` and `~/.cache/mcaxl/`. Re-running picks up
|
||||||
|
new releases automatically.
|
||||||
|
|
||||||
|
### Pinned dev install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install mcaxl==2026.04.27.1
|
||||||
|
mcaxl
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this when you want to control upgrade timing — for example, in CI
|
||||||
|
or in a long-lived service account environment where reproducibility
|
||||||
|
matters more than auto-update.
|
||||||
|
|
||||||
|
### Via Claude Code's MCP registry
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add cucm-axl -- uvx mcaxl
|
||||||
|
```
|
||||||
|
|
||||||
|
This adds an entry to your `~/.claude/mcp.json` that Claude Code reads
|
||||||
|
on session start. The server runs as a child process of `claude`,
|
||||||
|
inherits your environment variables, and exits when Claude Code exits.
|
||||||
|
|
||||||
|
## WSDL bootstrap
|
||||||
|
|
||||||
|
CUCM's AXL toolkit is **Cisco-licensed and not redistributable**, so it
|
||||||
|
cannot be bundled with the package. Download it once from your CUCM admin UI:
|
||||||
|
|
||||||
|
> Application -> Plugins -> Find -> "Cisco AXL Toolkit" -> Download
|
||||||
|
|
||||||
|
Drop the resulting `axlsqltoolkit.zip` into your working directory. On
|
||||||
|
first launch, the server auto-extracts `schema/<version>/` (matching
|
||||||
|
your cluster) into `~/.cache/mcaxl/wsdl/<version>/`.
|
||||||
|
|
||||||
|
### Alternative resolution paths
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Explicit zip location (overrides cwd lookup)
|
||||||
|
export AXL_WSDL_ZIP=/path/to/axlsqltoolkit.zip
|
||||||
|
|
||||||
|
# Explicit WSDL file (skips zip extraction entirely)
|
||||||
|
export AXL_WSDL_PATH=/path/to/schema/15.0/AXLAPI.wsdl
|
||||||
|
|
||||||
|
# Or pre-populate the cache by hand
|
||||||
|
mkdir -p ~/.cache/mcaxl/wsdl/15.0/
|
||||||
|
cp /path/to/schema/15.0/* ~/.cache/mcaxl/wsdl/15.0/
|
||||||
|
```
|
||||||
|
|
||||||
|
Resolution order: `AXL_WSDL_PATH` -> `AXL_WSDL_ZIP` -> `./axlsqltoolkit.zip` -> `~/.cache/mcaxl/wsdl/<version>/`.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
After install, point an MCP client at the server and call the
|
||||||
|
`axl_version` tool. A successful round-trip looks like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "15.0.1.12900-234",
|
||||||
|
"_cache": "miss"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you get a connection error, see [Configuration](/getting-started/configuration/)
|
||||||
|
for the env-var checklist and TLS notes.
|
||||||
|
|
||||||
|
## Next
|
||||||
|
|
||||||
|
- [Configure your environment](/getting-started/configuration/) — env vars and the AXL service account role binding
|
||||||
|
- [Run your first audit](/getting-started/first-audit/) — walk the `route_plan_overview` prompt end to end
|
||||||
144
docs/src/content/docs/how-to/find-orphan-resources.md
Normal file
144
docs/src/content/docs/how-to/find-orphan-resources.md
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
---
|
||||||
|
title: Find orphan resources
|
||||||
|
description: Identify CSSs, partitions, route lists, and route groups that are configured but unreferenced.
|
||||||
|
sidebar:
|
||||||
|
order: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
CUCM clusters accumulate cruft over time. Test partitions from a
|
||||||
|
migration, CSSs cloned for a one-off project, route lists for a
|
||||||
|
decommissioned trunk — all sit in the database, all show up in the
|
||||||
|
admin UI, none are reachable from any actual call. Cleaning them up
|
||||||
|
reduces audit surface and removes the *"can I delete this?"*
|
||||||
|
ambiguity from future operators.
|
||||||
|
|
||||||
|
## The pattern
|
||||||
|
|
||||||
|
For any resource type, the question is the same:
|
||||||
|
|
||||||
|
> Is this resource referenced by anything that actually routes calls?
|
||||||
|
|
||||||
|
`mcaxl` doesn't have a single `find_orphans` tool because the answer
|
||||||
|
varies by resource type. Here are the recipes for each.
|
||||||
|
|
||||||
|
## Calling Search Spaces (CSS)
|
||||||
|
|
||||||
|
The canonical tool:
|
||||||
|
|
||||||
|
```
|
||||||
|
route_devices_using_css(css_name="<name>")
|
||||||
|
```
|
||||||
|
|
||||||
|
This walks **all 71 known fkcallingsearchspace_* columns** across the
|
||||||
|
schema (phones, gateways, trunks, translation patterns, route patterns,
|
||||||
|
hunt pilots, voicemail ports, MGCP endpoints, ...) and reports zero or
|
||||||
|
more references per category.
|
||||||
|
|
||||||
|
A CSS with zero references in every category is a confirmed orphan.
|
||||||
|
|
||||||
|
To enumerate CSSs and check each:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. route_calling_search_spaces() -> list of all CSS names
|
||||||
|
2. For each CSS: route_devices_using_css(css_name=<name>)
|
||||||
|
3. Filter for CSSs where every category returned 0
|
||||||
|
```
|
||||||
|
|
||||||
|
A small LLM-driven loop handles this elegantly — ask the LLM to
|
||||||
|
*"find every CSS with no references across any device or pattern type
|
||||||
|
and report them as orphans."*
|
||||||
|
|
||||||
|
## Partitions
|
||||||
|
|
||||||
|
Partitions are referenced through CSSs (which include them) and
|
||||||
|
patterns (which live in them). A truly orphan partition has:
|
||||||
|
|
||||||
|
- Zero CSSs include it
|
||||||
|
- Zero patterns assigned to it
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Orphan partitions: defined, never included in any CSS, contain no patterns
|
||||||
|
SELECT rp.name
|
||||||
|
FROM routepartition rp
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM callingsearchspacemember m
|
||||||
|
WHERE m.fkroutepartition = rp.pkid
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM numplan n WHERE n.fkroutepartition = rp.pkid
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Partitions that are in a CSS but contain no patterns are a softer
|
||||||
|
finding — they're not strictly orphan (CSSs reference them) but
|
||||||
|
they don't *do* anything.
|
||||||
|
|
||||||
|
## Route lists
|
||||||
|
|
||||||
|
Route lists are referenced by route patterns (`numplan.fkroutelist`)
|
||||||
|
and a few specialized features (Hunt Pilot, etc.).
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Orphan route lists: defined, never targeted by any pattern
|
||||||
|
SELECT rl.name
|
||||||
|
FROM routelist rl
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM numplan n WHERE n.fkroutelist = rl.pkid
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route groups
|
||||||
|
|
||||||
|
Route groups are referenced by route lists (via the route-list
|
||||||
|
membership table) and by device-pool Local Route Group mappings.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Orphan route groups: not in any route list, not any pool's local RG
|
||||||
|
SELECT rg.name
|
||||||
|
FROM routegroup rg
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM routelistmemberlist m WHERE m.fkroutegroup = rg.pkid
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM devicepool dp WHERE dp.fkroutegroup_local = rg.pkid
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
(The exact join-table name has shifted across CUCM versions —
|
||||||
|
`route_lists_and_groups()` is the version-aware abstraction. Confirm
|
||||||
|
the table with `axl_list_tables('route%')` if the SQL above errors.)
|
||||||
|
|
||||||
|
## Phones
|
||||||
|
|
||||||
|
A phone configured in CUCM but never registered is a different kind of
|
||||||
|
orphan — and AXL alone can't tell you (the database doesn't track
|
||||||
|
registration history). Cross-reference with RisPort70:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. axl_sql("SELECT name, description FROM device WHERE tkclass = (SELECT enum FROM typeclass WHERE name = 'Phone')")
|
||||||
|
2. device_registration_status(device_class="Phone", status="any")
|
||||||
|
3. Set-difference: phones in (1) but not in (2)
|
||||||
|
```
|
||||||
|
|
||||||
|
Phones that haven't registered in the lifetime of the current
|
||||||
|
unrestarted CUCM are confirmed orphans. Phones that *just* haven't
|
||||||
|
registered today might just be powered off — be cautious about acting
|
||||||
|
on a single snapshot.
|
||||||
|
|
||||||
|
## Wrap it in a prompt
|
||||||
|
|
||||||
|
The `audit_routing(focus="full")` prompt asks the LLM to surface orphan
|
||||||
|
resources as part of its findings. For a more targeted run, ask:
|
||||||
|
|
||||||
|
> Walk the cluster's CSSs, partitions, route lists, and route groups.
|
||||||
|
> Report any that have zero references. For each, state the evidence
|
||||||
|
> (which tool returned what) and a confidence level.
|
||||||
|
|
||||||
|
The LLM handles the loops; you focus on which findings to actually act
|
||||||
|
on.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [`route_devices_using_css`](/reference/tools/#route_devices_using_css) — the 71-column impact analysis
|
||||||
|
- [`audit_routing` prompt](/reference/prompts/#audit_routing) — surfaces orphans as part of a broader audit
|
||||||
|
- [Hamilton review patterns](/explanation/hamilton-review-patterns/) — why the schema-coverage test guards the 71-column list against drift
|
||||||
94
docs/src/content/docs/how-to/investigate-pattern.md
Normal file
94
docs/src/content/docs/how-to/investigate-pattern.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
title: Investigate a specific pattern
|
||||||
|
description: Walk a single route pattern from match through transformations to destination.
|
||||||
|
sidebar:
|
||||||
|
order: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
When you need to answer *"what happens when someone dials `9.@`?"* —
|
||||||
|
or a translation pattern is misbehaving and you need to see every
|
||||||
|
transformation step — `route_inspect_pattern` is the canonical tool.
|
||||||
|
|
||||||
|
## When to use it
|
||||||
|
|
||||||
|
- A user reports a call that routed unexpectedly and you have the dialed digits
|
||||||
|
- You're about to delete a pattern and want to know the full blast radius
|
||||||
|
- You're debugging a calling-party-number transformation
|
||||||
|
- A finding from `route_plan_overview` needs deeper inspection
|
||||||
|
|
||||||
|
## Invocation
|
||||||
|
|
||||||
|
Direct tool call:
|
||||||
|
|
||||||
|
```
|
||||||
|
route_inspect_pattern("9.@", partition="PSTN-PT")
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via the prompt for a guided narrative:
|
||||||
|
|
||||||
|
```
|
||||||
|
/mcp__cucm-axl__investigate_pattern pattern="9.@" partition="PSTN-PT"
|
||||||
|
```
|
||||||
|
|
||||||
|
The `partition` argument is optional but recommended — without it, the
|
||||||
|
tool searches all partitions and may return multiple rows if the same
|
||||||
|
pattern exists in different partitions.
|
||||||
|
|
||||||
|
## What it returns
|
||||||
|
|
||||||
|
A single dictionary with these keys:
|
||||||
|
|
||||||
|
| Key | Contents |
|
||||||
|
|---|---|
|
||||||
|
| `pattern` | The pattern row itself: all transformation masks, prefix digits, DDI assignment |
|
||||||
|
| `reverse_css_lookup` | Every CSS that includes this pattern's partition (in what order) |
|
||||||
|
| `route_filter` | If the pattern uses `@` and has a route filter, the filter clauses + member rules |
|
||||||
|
| `destination_chain` | If it's a route pattern: route list -> route group(s) -> gateway/trunk leaves |
|
||||||
|
|
||||||
|
The destination chain is the most operationally useful field — it's the
|
||||||
|
full call-routing topology that this one pattern reaches.
|
||||||
|
|
||||||
|
## Reading the output
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Pattern["9.@<br/>Partition: PSTN-PT"]
|
||||||
|
RL["Route List:<br/>RL_PSTN_Primary"]
|
||||||
|
RG1["Route Group:<br/>RG_PSTN_SiteA"]
|
||||||
|
RG2["Route Group:<br/>RG_PSTN_SiteB"]
|
||||||
|
Trunk1["SIP Trunk:<br/>Carrier-SiteA"]
|
||||||
|
Trunk2["SIP Trunk:<br/>Carrier-SiteB"]
|
||||||
|
|
||||||
|
Pattern --> RL
|
||||||
|
RL -->|priority 1| RG1
|
||||||
|
RL -->|priority 2| RG2
|
||||||
|
RG1 --> Trunk1
|
||||||
|
RG2 --> Trunk2
|
||||||
|
```
|
||||||
|
|
||||||
|
The chain stops at the leaf devices (gateways, SIP trunks). To go
|
||||||
|
*further* — into the actual carrier-side configuration — use the
|
||||||
|
`sipdevice` and `siptrunkdestination` queries from the
|
||||||
|
[SIP trunk report](/how-to/sip-trunk-report/) recipe.
|
||||||
|
|
||||||
|
## Common gotchas
|
||||||
|
|
||||||
|
- **`@` patterns** require route filters to be operationally useful, but
|
||||||
|
`route_inspect_pattern` does not yet model filter constraints when
|
||||||
|
building the destination chain. Use the `route_filter` field in the
|
||||||
|
output to check filter rules manually.
|
||||||
|
- **Local Route Groups** (route groups with no static device members)
|
||||||
|
resolve at call-time via the calling device's device-pool
|
||||||
|
`fkroutegroup_local` mapping. The destination chain shows them as
|
||||||
|
`local_route_group: true` with no static gateway list — follow up
|
||||||
|
with `route_device_pool_route_groups()` to enumerate per-pool resolution.
|
||||||
|
- **Translation patterns** (vs route patterns) don't have a destination
|
||||||
|
chain — they rewrite digits and re-enter digit analysis with the new
|
||||||
|
number. The output's `destination_chain` will be empty; look at the
|
||||||
|
transformation masks instead.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [`route_inspect_pattern` tool](/reference/tools/#route_inspect_pattern)
|
||||||
|
- [`investigate_pattern` prompt](/reference/prompts/#investigate_pattern)
|
||||||
|
- [`route_translation_chain`](/reference/tools/#route_translation_chain) — wildcard-aware matcher for "what does dialing X do?" questions
|
||||||
85
docs/src/content/docs/how-to/route-plan-overview.md
Normal file
85
docs/src/content/docs/how-to/route-plan-overview.md
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
title: Generate a route plan overview
|
||||||
|
description: Use the route_plan_overview prompt to seed a structured audit of the cluster dial plan.
|
||||||
|
sidebar:
|
||||||
|
order: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
The `route_plan_overview` prompt is the fastest path to a structured
|
||||||
|
audit of a CUCM dial plan. It's the right starting point for any of:
|
||||||
|
|
||||||
|
- A handoff document for a new operator
|
||||||
|
- A post-migration sanity check
|
||||||
|
- A pre-change impact assessment
|
||||||
|
- Just *understanding* an unfamiliar cluster
|
||||||
|
|
||||||
|
## When to use it
|
||||||
|
|
||||||
|
Use the prompt **at the start** of a fresh LLM session — it seeds the
|
||||||
|
conversation context with:
|
||||||
|
|
||||||
|
- A description of CUCM's dial-plan model (partitions, CSSs, patterns, transformations)
|
||||||
|
- A recommended tool-call sequence
|
||||||
|
- A findings template for structured output
|
||||||
|
|
||||||
|
Don't use it mid-session if the LLM already has plenty of cluster
|
||||||
|
context loaded — duplicating the orientation material wastes tokens.
|
||||||
|
|
||||||
|
## Invocation
|
||||||
|
|
||||||
|
In Claude Code:
|
||||||
|
|
||||||
|
```
|
||||||
|
/mcp__cucm-axl__route_plan_overview
|
||||||
|
```
|
||||||
|
|
||||||
|
In a generic MCP client, look for a "Prompts" menu and select
|
||||||
|
`route_plan_overview` (no arguments).
|
||||||
|
|
||||||
|
## What happens
|
||||||
|
|
||||||
|
The LLM will follow the prompt's recommended sequence:
|
||||||
|
|
||||||
|
1. **`route_partitions()`** — top-down map of access-control groupings
|
||||||
|
2. **`route_calling_search_spaces()`** — the CSSs that bind partitions into
|
||||||
|
ordered search lists
|
||||||
|
3. **`route_patterns(kind="route")`** then `kind="translation"` — the patterns
|
||||||
|
that actually route calls
|
||||||
|
4. **Sampling** — for any pattern that looks anomalous, follow up with
|
||||||
|
`route_inspect_pattern(pattern, partition)`
|
||||||
|
|
||||||
|
Total run time on a 50-pattern cluster: 30-90 seconds.
|
||||||
|
|
||||||
|
## What to expect in the output
|
||||||
|
|
||||||
|
Findings appear in a structured shape:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
**Finding N — <short title>**
|
||||||
|
|
||||||
|
Severity: Critical | Major | Minor
|
||||||
|
Evidence: <which tool calls returned what>
|
||||||
|
Recommendation: <what to do about it>
|
||||||
|
```
|
||||||
|
|
||||||
|
Common findings on a long-lived cluster:
|
||||||
|
|
||||||
|
- Unreferenced CSSs (configured but not bound to any device)
|
||||||
|
- Unreferenced partitions (configured but not in any CSS)
|
||||||
|
- Translation patterns that loop or shadow each other
|
||||||
|
- Route patterns with no destination (route list deleted but pattern remains)
|
||||||
|
- Wildcard collisions (two patterns with overlapping match space)
|
||||||
|
|
||||||
|
## Follow-up workflows
|
||||||
|
|
||||||
|
Once the overview is done, drill in with one of these recipes:
|
||||||
|
|
||||||
|
- [Investigate a specific pattern](/how-to/investigate-pattern/)
|
||||||
|
- [Find orphan resources](/how-to/find-orphan-resources/)
|
||||||
|
- [SIP trunk report](/how-to/sip-trunk-report/)
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [`route_plan_overview` prompt](/reference/prompts/#route_plan_overview)
|
||||||
|
- [`audit_routing` prompt](/reference/prompts/#audit_routing) — heavier-weight version with a checklist
|
||||||
|
- [CUCM schema cheat-sheet](/reference/cucm-schema-cheatsheet/)
|
||||||
210
docs/src/content/docs/how-to/sip-trunk-report.md
Normal file
210
docs/src/content/docs/how-to/sip-trunk-report.md
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
---
|
||||||
|
title: SIP trunk report
|
||||||
|
description: Produce a complete SIP trunk inventory with destinations, profiles, and downstream route-group membership.
|
||||||
|
sidebar:
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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, URI 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.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
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.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
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
|
||||||
|
often 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 the `sip_trunk_report` prompt runs, it asks 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).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [`route_lists_and_groups`](/reference/tools/#route_lists_and_groups) — the right way to traverse the trunk -> RG -> RL chain
|
||||||
|
- [`route_devices_using_css`](/reference/tools/#route_devices_using_css) — for follow-up blast-radius analysis on each trunk's CSS
|
||||||
|
- [`sip_trunk_report` prompt](/reference/prompts/#sip_trunk_report) — invokes the queries above with the findings template wired in
|
||||||
77
docs/src/content/docs/index.mdx
Normal file
77
docs/src/content/docs/index.mdx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
title: mcaxl
|
||||||
|
description: Read-only MCP server for Cisco Unified Communications Manager — AXL SOAP API + RisPort70 audit. Documentation home.
|
||||||
|
template: splash
|
||||||
|
hero:
|
||||||
|
tagline: Read-only audit of CUCM dial plan and configuration via the AXL SOAP API and RisPort70 — wired to your LLM through MCP.
|
||||||
|
actions:
|
||||||
|
- text: Install in 60 seconds
|
||||||
|
link: /getting-started/installation/
|
||||||
|
icon: right-arrow
|
||||||
|
variant: primary
|
||||||
|
- text: Tool reference
|
||||||
|
link: /reference/tools/
|
||||||
|
icon: external
|
||||||
|
variant: secondary
|
||||||
|
---
|
||||||
|
|
||||||
|
import CardGrid from '../../components/CardGrid.astro';
|
||||||
|
import Diataxis from '../../components/Diataxis.astro';
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
`mcaxl` is a Python MCP server that exposes Cisco Unified Communications
|
||||||
|
Manager configuration to LLM-aware clients. It's read-only by structure
|
||||||
|
(no AXL write methods are ever registered) and ships 19 tools plus 10
|
||||||
|
audit-narrative prompts that orchestrate those tools toward findings.
|
||||||
|
|
||||||
|
It pairs well with [`@calltelemetry/cisco-cucm-mcp`](https://github.com/calltelemetry/cisco-cucm-mcp)
|
||||||
|
for operational debugging — `mcaxl` answers *"what does the config say?"*,
|
||||||
|
that server answers *"what's happening right now?"*.
|
||||||
|
|
||||||
|
<CardGrid
|
||||||
|
cards={[
|
||||||
|
{
|
||||||
|
title: 'Install',
|
||||||
|
description: 'uvx mcaxl, an axlsqltoolkit.zip, and an .env. That is the whole bootstrap.',
|
||||||
|
href: '/getting-started/installation/',
|
||||||
|
icon: 'download',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'First audit',
|
||||||
|
description: 'Walk through the route_plan_overview prompt end to end on a live cluster.',
|
||||||
|
href: '/getting-started/first-audit/',
|
||||||
|
icon: 'compass',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'SIP trunk report',
|
||||||
|
description: 'A complete trunk inventory recipe — queries, joins, gotchas, follow-ups.',
|
||||||
|
href: '/how-to/sip-trunk-report/',
|
||||||
|
icon: 'cable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tool reference',
|
||||||
|
description: 'All 19 tools with arguments, return shape, and which audits they feed.',
|
||||||
|
href: '/reference/tools/',
|
||||||
|
icon: 'wrench',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Find your way around
|
||||||
|
|
||||||
|
This site follows the [Diátaxis](https://diataxis.fr/) framework — every
|
||||||
|
page is one of four kinds, and the sidebar groups them accordingly.
|
||||||
|
|
||||||
|
<Diataxis />
|
||||||
|
|
||||||
|
## Project status
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Latest version | `2026.04.27.1` (CalVer) |
|
||||||
|
| Tested against | CUCM 15.0(1) |
|
||||||
|
| Compatible with | CUCM 12.5+ |
|
||||||
|
| License | MIT |
|
||||||
|
| PyPI | [pypi.org/project/mcaxl](https://pypi.org/project/mcaxl/) |
|
||||||
|
| Source | [git.supported.systems/mcp/mcaxl](https://git.supported.systems/mcp/mcaxl) |
|
||||||
194
docs/src/content/docs/reference/cucm-schema-cheatsheet.md
Normal file
194
docs/src/content/docs/reference/cucm-schema-cheatsheet.md
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
---
|
||||||
|
title: CUCM schema cheat-sheet
|
||||||
|
description: The Informix tables and joins you'll actually use day-to-day, with the gotchas that bit us.
|
||||||
|
sidebar:
|
||||||
|
order: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
CUCM's Informix schema has hundreds of tables. This cheat-sheet
|
||||||
|
covers the ones that actually come up in routine audits, with
|
||||||
|
the join shapes and gotchas accumulated from real cluster work.
|
||||||
|
|
||||||
|
For exhaustive reference, query the `cisco-docs` MCP server's
|
||||||
|
`search_docs("data dictionary <table>")` or browse Cisco's published
|
||||||
|
data dictionary PDF.
|
||||||
|
|
||||||
|
## Naming conventions
|
||||||
|
|
||||||
|
| Prefix | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `tk*` | "Type Key" — enum FK column on a row, joins to `type*` lookup |
|
||||||
|
| `type*` | Enum lookup table (e.g. `typeclass`, `typesipcodec`, `typepatternusage`) |
|
||||||
|
| `fk*` | Foreign key to another table's `pkid` |
|
||||||
|
| `pkid` | Primary key (UUID, stored as VARCHAR) |
|
||||||
|
|
||||||
|
Most schema confusion comes from mistaking a `tk*` (enum column on the
|
||||||
|
fact row) for an `fk*` (FK to another fact row).
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
### `device`
|
||||||
|
|
||||||
|
The fact table for every "device" in CUCM: phones, gateways, trunks,
|
||||||
|
hunt lists, MOH, conference bridges. The `tkclass` column is the
|
||||||
|
discriminator.
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `pkid` | UUID | PK |
|
||||||
|
| `name` | VARCHAR | Device name (typically MAC for phones, hostname for gateways) |
|
||||||
|
| `description` | VARCHAR | Free-form operator annotation |
|
||||||
|
| `tkclass` | INT | enum -> `typeclass` |
|
||||||
|
| `fkdevicepool` | UUID | -> `devicepool` |
|
||||||
|
| `fkcallingsearchspace` | UUID | -> `callingsearchspace` |
|
||||||
|
| `fklocation` | UUID | -> `location` |
|
||||||
|
| `fksipprofile` | UUID | -> `sipprofile` (SIP devices only) |
|
||||||
|
| `fksecurityprofile` | UUID | -> `phonesecurityprofile` for phones, `sipsecurityprofile` for SIP trunks |
|
||||||
|
|
||||||
|
**Gotcha:** there's no single `securityprofile` table — the FK target
|
||||||
|
varies by `tkclass`. SIP trunks use `sipsecurityprofile`; phones use
|
||||||
|
`phonesecurityprofile`.
|
||||||
|
|
||||||
|
### `typeclass`
|
||||||
|
|
||||||
|
Discriminator lookup for `device.tkclass`.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT enum, name FROM typeclass ORDER BY enum;
|
||||||
|
```
|
||||||
|
|
||||||
|
Common values: `Phone`, `Gateway`, `H323 Gateway`, `H323 Phone`, `Trunk`,
|
||||||
|
`CTI Route Point`, `CTI Port`, `Conference Bridge`, `Music On Hold`,
|
||||||
|
`Hunt List`, `Voice Mail`.
|
||||||
|
|
||||||
|
## Dial plan
|
||||||
|
|
||||||
|
### `routepartition`
|
||||||
|
|
||||||
|
Partitions group patterns for access control.
|
||||||
|
|
||||||
|
| Column | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `pkid` | PK |
|
||||||
|
| `name` | Partition name |
|
||||||
|
| `description` | Free-form |
|
||||||
|
|
||||||
|
### `callingsearchspace`
|
||||||
|
|
||||||
|
Calling Search Spaces (CSSs).
|
||||||
|
|
||||||
|
| Column | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `pkid` | PK |
|
||||||
|
| `name` | CSS name |
|
||||||
|
|
||||||
|
CSS membership lives in `callingsearchspacemember`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- CSSs and their ordered partition lists
|
||||||
|
SELECT
|
||||||
|
css.name AS css_name,
|
||||||
|
rp.name AS partition_name,
|
||||||
|
m.sortorder
|
||||||
|
FROM callingsearchspace css
|
||||||
|
JOIN callingsearchspacemember m ON m.fkcallingsearchspace = css.pkid
|
||||||
|
JOIN routepartition rp ON m.fkroutepartition = rp.pkid
|
||||||
|
ORDER BY css.name, m.sortorder;
|
||||||
|
```
|
||||||
|
|
||||||
|
### `numplan`
|
||||||
|
|
||||||
|
The "everything pattern" table — DNs, route patterns, translations,
|
||||||
|
hunt pilots, voicemail pilots, conference, park codes — they all live
|
||||||
|
here, discriminated by `tkpatternusage`.
|
||||||
|
|
||||||
|
| Column | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `dnorpattern` | The pattern text |
|
||||||
|
| `fkroutepartition` | -> `routepartition` |
|
||||||
|
| `tkpatternusage` | enum -> `typepatternusage` |
|
||||||
|
| `prefixdigitsout` | Outbound prefix |
|
||||||
|
| `calledpartytransformationmask` | Called party transform |
|
||||||
|
| `callingpartytransformationmask` | Calling party transform |
|
||||||
|
| `tkdigitdiscardinst` | enum -> `digitdiscardinstruction` |
|
||||||
|
| `fkroutelist` | -> `routelist` (route patterns) |
|
||||||
|
|
||||||
|
### `routelist` / `routegroup`
|
||||||
|
|
||||||
|
Route lists are ordered groups of route groups. Route groups are
|
||||||
|
ordered groups of devices (gateways, trunks).
|
||||||
|
|
||||||
|
**Don't write the join SQL by hand** — the membership table name has
|
||||||
|
shifted across CUCM versions. Use the `route_lists_and_groups()` MCP
|
||||||
|
tool which handles version detection.
|
||||||
|
|
||||||
|
## Users and roles
|
||||||
|
|
||||||
|
### `applicationuser`
|
||||||
|
|
||||||
|
Application user accounts (service accounts, including the AXL one).
|
||||||
|
|
||||||
|
### `enduser`
|
||||||
|
|
||||||
|
End-user accounts (associated with phones, mailboxes).
|
||||||
|
|
||||||
|
### Role chain
|
||||||
|
|
||||||
|
The four-table join that the `whoami` prompt walks:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
au.userid,
|
||||||
|
dg.name AS access_control_group,
|
||||||
|
fr.name AS function_role
|
||||||
|
FROM applicationuser au
|
||||||
|
JOIN applicationuserdirgroupmap m ON m.fkapplicationuser = au.pkid
|
||||||
|
JOIN dirgroup dg ON m.fkdirgroup = dg.pkid
|
||||||
|
JOIN functionroledirgroupmap fdm ON fdm.fkdirgroup = dg.pkid
|
||||||
|
JOIN functionrole fr ON fdm.fkfunctionrole = fr.pkid
|
||||||
|
WHERE au.userid = 'mcaxl';
|
||||||
|
```
|
||||||
|
|
||||||
|
For end users, swap `applicationuser` -> `enduser` and
|
||||||
|
`applicationuserdirgroupmap` -> `enduserdirgroupmap`.
|
||||||
|
|
||||||
|
## SIP trunks
|
||||||
|
|
||||||
|
See [How-to: SIP trunk report](/how-to/sip-trunk-report/) for the full
|
||||||
|
recipe. Key tables:
|
||||||
|
|
||||||
|
- `sipdevice` — SIP-specific config (joined via `fkdevice -> device.pkid`)
|
||||||
|
- `siptrunkdestination` — destination IP/port (one-to-many)
|
||||||
|
- `sipprofile`, `sipsecurityprofile` — name lookups
|
||||||
|
- `typesipcodec` — codec name enum
|
||||||
|
|
||||||
|
## LVARCHAR(1) flag fields
|
||||||
|
|
||||||
|
Many "boolean" columns in CUCM are actually `LVARCHAR(1)` returning
|
||||||
|
`'t'` or `'f'` (lowercase, single character) — not Python booleans. If
|
||||||
|
you `SELECT` one through `axl_sql`, you'll get the string. Render
|
||||||
|
appropriately in any output.
|
||||||
|
|
||||||
|
Examples: `sipdevice.isanonymous`, `sipdevice.acceptinboundrdnis`,
|
||||||
|
`device.allowctianagement`, `enduser.enableupnauthentication`.
|
||||||
|
|
||||||
|
## Common gotchas
|
||||||
|
|
||||||
|
1. **`routelistdetail` doesn't exist** — the route-list-to-route-group
|
||||||
|
join table has a version-dependent name. Use `route_lists_and_groups()`.
|
||||||
|
2. **`securityprofile` is a category, not a table** — SIP trunks use
|
||||||
|
`sipsecurityprofile`, phones use `phonesecurityprofile`.
|
||||||
|
3. **`tkclass` filters by enum, not string** — always join to
|
||||||
|
`typeclass` for the human-readable label.
|
||||||
|
4. **NULL FKs are valid** — Expressway-C trunks legitimately have
|
||||||
|
`fkcallingsearchspace = NULL`. Use `LEFT JOIN` and render NULL
|
||||||
|
appropriately.
|
||||||
|
5. **Pattern partitions can be NULL** — patterns in the (no partition)
|
||||||
|
bucket have `fkroutepartition = NULL`. The default-CSS rules apply
|
||||||
|
to them.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [Tools reference](/reference/tools/) — the helpers that abstract over the gotchas above
|
||||||
|
- [SIP trunk report](/how-to/sip-trunk-report/) — worked example with all the joins
|
||||||
|
- Cisco's official CUCM data dictionary — search via `cisco-docs` MCP for "data dictionary"
|
||||||
167
docs/src/content/docs/reference/env-vars.md
Normal file
167
docs/src/content/docs/reference/env-vars.md
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
---
|
||||||
|
title: Environment variables
|
||||||
|
description: Every environment variable mcaxl reads, with defaults and edge cases.
|
||||||
|
sidebar:
|
||||||
|
order: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
`mcaxl` reads its configuration from environment variables. A `.env`
|
||||||
|
file in the working directory is loaded automatically (via
|
||||||
|
`python-dotenv`); anything in the actual process environment overrides
|
||||||
|
values from `.env`.
|
||||||
|
|
||||||
|
## Connection
|
||||||
|
|
||||||
|
### `AXL_URL`
|
||||||
|
|
||||||
|
**Required.** Full URL to the AXL endpoint on the CUCM publisher.
|
||||||
|
|
||||||
|
```env
|
||||||
|
AXL_URL=https://cucm-pub.example.com:8443/axl/
|
||||||
|
```
|
||||||
|
|
||||||
|
- Must point to the **publisher** node (AXL doesn't run on subscribers).
|
||||||
|
- Must end in `/axl/` (trailing slash).
|
||||||
|
- Default port `8443/tcp`.
|
||||||
|
|
||||||
|
### `AXL_USER`
|
||||||
|
|
||||||
|
**Required.** Application User account with `Standard AXL Read Only API
|
||||||
|
Access` role at minimum.
|
||||||
|
|
||||||
|
```env
|
||||||
|
AXL_USER=mcaxl
|
||||||
|
```
|
||||||
|
|
||||||
|
### `AXL_PASS`
|
||||||
|
|
||||||
|
**Required.** Password for `AXL_USER`. Quote if it contains shell
|
||||||
|
metacharacters.
|
||||||
|
|
||||||
|
```env
|
||||||
|
AXL_PASS=change-me
|
||||||
|
```
|
||||||
|
|
||||||
|
### `AXL_VERIFY_TLS`
|
||||||
|
|
||||||
|
Default: `false`.
|
||||||
|
|
||||||
|
CUCM ships self-signed certs; most clusters never replace them. Set
|
||||||
|
`true` if your cluster has a CA-signed cert that's trusted by the OS
|
||||||
|
trust store on the host running `mcaxl`.
|
||||||
|
|
||||||
|
```env
|
||||||
|
AXL_VERIFY_TLS=false
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no `AXL_CA_BUNDLE` knob yet — install your private CA into the
|
||||||
|
OS trust store if you need to validate against one.
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
### `AXL_CACHE_TTL`
|
||||||
|
|
||||||
|
Default: `3600` (1 hour). Units: seconds.
|
||||||
|
|
||||||
|
Set to `0` to disable caching entirely. Most operators leave the
|
||||||
|
default; CUCM config doesn't change second-to-second, and 1 hour
|
||||||
|
matches the typical audit-session duration.
|
||||||
|
|
||||||
|
```env
|
||||||
|
AXL_CACHE_TTL=3600
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resilience
|
||||||
|
|
||||||
|
### `AXL_RATE_LIMIT_RETRIES`
|
||||||
|
|
||||||
|
Default: `3`. Integer count.
|
||||||
|
|
||||||
|
When CUCM returns `502 / 503 / 504`, `mcaxl` retries with exponential
|
||||||
|
backoff. RisPort70 in particular can throttle aggressively under load;
|
||||||
|
the default handles most cases. Bump to `5` or `6` on a cluster with
|
||||||
|
chronic load issues.
|
||||||
|
|
||||||
|
```env
|
||||||
|
AXL_RATE_LIMIT_RETRIES=3
|
||||||
|
```
|
||||||
|
|
||||||
|
## WSDL resolution
|
||||||
|
|
||||||
|
### `AXL_WSDL_PATH`
|
||||||
|
|
||||||
|
Default: empty (unset).
|
||||||
|
|
||||||
|
Explicit path to an extracted `AXLAPI.wsdl`. Highest priority — if set,
|
||||||
|
no zip extraction is attempted.
|
||||||
|
|
||||||
|
```env
|
||||||
|
AXL_WSDL_PATH=/path/to/schema/15.0/AXLAPI.wsdl
|
||||||
|
```
|
||||||
|
|
||||||
|
### `AXL_WSDL_ZIP`
|
||||||
|
|
||||||
|
Default: empty (unset).
|
||||||
|
|
||||||
|
Explicit path to `axlsqltoolkit.zip`. Used if `AXL_WSDL_PATH` is unset.
|
||||||
|
|
||||||
|
```env
|
||||||
|
AXL_WSDL_ZIP=/path/to/axlsqltoolkit.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
Resolution order: `AXL_WSDL_PATH` -> `AXL_WSDL_ZIP` -> `./axlsqltoolkit.zip` -> `~/.cache/mcaxl/wsdl/<version>/`.
|
||||||
|
|
||||||
|
## Prompt enrichment
|
||||||
|
|
||||||
|
### `CISCO_DOCS_INDEX_PATH`
|
||||||
|
|
||||||
|
Default: empty (unset).
|
||||||
|
|
||||||
|
Path to a directory containing `chunks.jsonl` and `index_meta.json`
|
||||||
|
produced by the `mcp-cisco-docs` indexer (or any compatible embedding
|
||||||
|
pipeline). When set, prompts pull relevant Cisco documentation chunks
|
||||||
|
inline. When unset, prompts gracefully degrade to a fallback notice
|
||||||
|
asking the LLM to use a sibling `cisco-docs` server's `search_docs`
|
||||||
|
tool.
|
||||||
|
|
||||||
|
```env
|
||||||
|
CISCO_DOCS_INDEX_PATH=/var/lib/cisco-docs-index/15.0/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
After editing your `.env`, restart the MCP server (`claude mcp restart
|
||||||
|
cucm-axl` in Claude Code) and call the `health` tool. Every key should
|
||||||
|
be `true` except `docs` if you haven't configured `CISCO_DOCS_INDEX_PATH`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cache": true,
|
||||||
|
"axl": true,
|
||||||
|
"docs": false,
|
||||||
|
"risport": true,
|
||||||
|
"axl_connection": "ok",
|
||||||
|
"cache_cluster_id": "<sha256-hex-prefix>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sample full `.env`
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Connection
|
||||||
|
AXL_URL=https://cucm-pub.example.com:8443/axl/
|
||||||
|
AXL_USER=mcaxl
|
||||||
|
AXL_PASS=change-me
|
||||||
|
|
||||||
|
# TLS
|
||||||
|
AXL_VERIFY_TLS=false
|
||||||
|
|
||||||
|
# Caching
|
||||||
|
AXL_CACHE_TTL=3600
|
||||||
|
|
||||||
|
# Resilience
|
||||||
|
AXL_RATE_LIMIT_RETRIES=3
|
||||||
|
|
||||||
|
# Optional schema-doc enrichment for prompts
|
||||||
|
CISCO_DOCS_INDEX_PATH=/var/lib/cisco-docs-index/15.0/
|
||||||
|
```
|
||||||
145
docs/src/content/docs/reference/prompts.md
Normal file
145
docs/src/content/docs/reference/prompts.md
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
---
|
||||||
|
title: Prompts (10)
|
||||||
|
description: Audit-narrative prompts that orchestrate multiple tool calls toward findings.
|
||||||
|
sidebar:
|
||||||
|
order: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
Each prompt orchestrates multiple tool calls toward a specific audit
|
||||||
|
narrative. Prompts are **schema-grounded**: when `CISCO_DOCS_INDEX_PATH`
|
||||||
|
is configured, they pull relevant Cisco documentation chunks inline so
|
||||||
|
the LLM has authoritative reference material at hand. Without the index,
|
||||||
|
they degrade gracefully with a fallback notice.
|
||||||
|
|
||||||
|
In Claude Code they appear under the slash menu as
|
||||||
|
`/mcp__<server-id>__<prompt-name>`.
|
||||||
|
|
||||||
|
## `route_plan_overview`
|
||||||
|
|
||||||
|
Fresh-conversation seed for a structured dial-plan audit.
|
||||||
|
|
||||||
|
**Arguments:** none
|
||||||
|
|
||||||
|
**Use when:** you're starting an audit on an unfamiliar cluster, or
|
||||||
|
preparing a handoff document.
|
||||||
|
|
||||||
|
**See also:** [How-to: Generate a route plan overview](/how-to/route-plan-overview/)
|
||||||
|
|
||||||
|
## `investigate_pattern`
|
||||||
|
|
||||||
|
Single-pattern deep dive — match logic, transformations, destination
|
||||||
|
chain, blast radius.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `pattern` | `str` | The pattern text (e.g. `"9.@"`). |
|
||||||
|
| `partition` | `str \| None` | Recommended — disambiguates same pattern in different partitions. |
|
||||||
|
|
||||||
|
**See also:** [How-to: Investigate a specific pattern](/how-to/investigate-pattern/)
|
||||||
|
|
||||||
|
## `audit_routing`
|
||||||
|
|
||||||
|
Comprehensive walkthrough with checklist. Heavier than
|
||||||
|
`route_plan_overview` — this one walks every category and asks the LLM
|
||||||
|
to populate a checklist of common findings.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `focus` | `str` | `full` (default), `partitions`, `css`, `patterns`, `transformations`, `filters`. |
|
||||||
|
|
||||||
|
## `cucm_sql_help`
|
||||||
|
|
||||||
|
Catch-all SQL helper. Pass a natural-language question; the prompt
|
||||||
|
seeds context about the Informix data dictionary, common joins, and
|
||||||
|
LVARCHAR(1) flag fields, then asks the LLM to produce and execute the
|
||||||
|
SQL.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `question` | `str` | Natural-language question. |
|
||||||
|
|
||||||
|
## `sip_trunk_report`
|
||||||
|
|
||||||
|
SIP trunk inventory + findings template. Embeds the Query 1 + Query 2
|
||||||
|
SQL plus the recommended follow-up sequence.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `name_filter` | `str \| None` | Optional substring match against trunk name. |
|
||||||
|
|
||||||
|
**See also:** [How-to: SIP trunk report](/how-to/sip-trunk-report/)
|
||||||
|
|
||||||
|
## `phone_inventory_report`
|
||||||
|
|
||||||
|
Phone fleet aggregates with anomaly findings. **Cross-references
|
||||||
|
RisPort70** for registration state, so findings can compound (e.g.
|
||||||
|
*"this phone model is configured but zero of them are currently
|
||||||
|
registered"*).
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `filter` | `str \| None` | Optional substring against phone name. |
|
||||||
|
|
||||||
|
## `user_audit`
|
||||||
|
|
||||||
|
End users + application users + role assignments. Surfaces:
|
||||||
|
|
||||||
|
- Application users with write-capable roles
|
||||||
|
- End users who haven't logged in recently (where the cluster tracks this)
|
||||||
|
- Role assignments that look misnamed ("Read-Only-X" group containing write-capable roles — the case that started this whole project; see [whoami example](/) on the home page)
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `focus` | `str` | `full` (default), `app_users`, `end_users`, `roles`. |
|
||||||
|
|
||||||
|
## `inbound_did_audit`
|
||||||
|
|
||||||
|
XFORM-Inbound-DNIS inventory + screening pipeline. Walks every inbound
|
||||||
|
translation pattern and reports the digit-manipulation chain from
|
||||||
|
PSTN-received DNIS to internal extension.
|
||||||
|
|
||||||
|
**Arguments:** none
|
||||||
|
|
||||||
|
## `hunt_pilot_audit`
|
||||||
|
|
||||||
|
Hunt pilots, queue settings, line group membership. Surfaces:
|
||||||
|
|
||||||
|
- Hunt pilots with empty line groups
|
||||||
|
- Line groups with all members offline (cross-references RisPort)
|
||||||
|
- Queue depth / overflow misconfiguration
|
||||||
|
|
||||||
|
**Arguments:** none
|
||||||
|
|
||||||
|
## `whoami`
|
||||||
|
|
||||||
|
Single-user role chain. Walks the four-table join `applicationuser ->
|
||||||
|
applicationuserdirgroupmap -> dirgroup -> functionroledirgroupmap ->
|
||||||
|
functionrole` and produces an audit-style finding.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `userid` | `str \| None` | If omitted, defaults to the AXL service account from `AXL_USER`. |
|
||||||
|
|
||||||
|
This is the prompt featured on the home page — runs against the
|
||||||
|
configured service account by default and surfaces *"this group's name
|
||||||
|
implies read-only intent but it has write-capable roles"* as a finding
|
||||||
|
without any operator effort.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [Tools](/reference/tools/) — the underlying tool surface each prompt orchestrates
|
||||||
|
- [Environment variables](/reference/env-vars/) — `CISCO_DOCS_INDEX_PATH` for prompt enrichment
|
||||||
254
docs/src/content/docs/reference/tools.md
Normal file
254
docs/src/content/docs/reference/tools.md
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
---
|
||||||
|
title: Tools (19)
|
||||||
|
description: Every MCP tool mcaxl exposes — arguments, return shape, caching behavior.
|
||||||
|
sidebar:
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
`mcaxl` registers 19 tools across three groups: **foundational**
|
||||||
|
(version, raw SQL, table introspection, cache, health), **route plan**
|
||||||
|
(partitions, CSSs, patterns, lists, groups, transformations, filters),
|
||||||
|
and **real-time registration** (RisPort70 device state).
|
||||||
|
|
||||||
|
Every tool is read-only. The server never registers AXL write methods —
|
||||||
|
no `executeSQLUpdate`, no `add*` / `update*` / `remove*` / `apply*` /
|
||||||
|
`reset*` / `restart*`. See [Read-only by structure](/explanation/read-only-by-structure/)
|
||||||
|
for the rationale.
|
||||||
|
|
||||||
|
## Foundational
|
||||||
|
|
||||||
|
### `axl_version`
|
||||||
|
|
||||||
|
Cluster version sanity check. Cached for 1 hour — version doesn't
|
||||||
|
change between cluster upgrades.
|
||||||
|
|
||||||
|
**Arguments:** none
|
||||||
|
|
||||||
|
**Returns:** `{ version: str, _cache: "hit" | "miss" }`
|
||||||
|
|
||||||
|
### `axl_sql`
|
||||||
|
|
||||||
|
Execute a `SELECT` (or `WITH` CTE) against the CUCM Informix data
|
||||||
|
dictionary. Read-only by structural guarantee plus client-side
|
||||||
|
validation that rejects non-`SELECT`/`WITH` queries as
|
||||||
|
defense-in-depth.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `query` | `str` | A single SQL SELECT statement. Trailing semicolon optional. |
|
||||||
|
|
||||||
|
**Returns:** `{ row_count: int, rows: list[dict], query_sent: str, _cache: "hit" | "miss" }`
|
||||||
|
|
||||||
|
### `axl_list_tables`
|
||||||
|
|
||||||
|
List Informix tables in the CUCM database.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `pattern` | `str \| None` | Optional LIKE pattern (`%` wildcards). e.g. `"route%"` finds `routelist`, `routegroup`, `routepartition`, `routefilter`. |
|
||||||
|
|
||||||
|
### `axl_describe_table`
|
||||||
|
|
||||||
|
Describe an Informix table's columns: name, type, length, nullability.
|
||||||
|
Use this **before** writing `axl_sql` queries against an unfamiliar
|
||||||
|
table.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `table_name` | `str` | Exact Informix table name. |
|
||||||
|
|
||||||
|
### `cache_stats`
|
||||||
|
|
||||||
|
Cache statistics: total entries, live entries, breakdown by method.
|
||||||
|
|
||||||
|
**Arguments:** none
|
||||||
|
|
||||||
|
### `cache_clear`
|
||||||
|
|
||||||
|
Clear cache entries for the current cluster.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `method_pattern` | `str \| None` | Optional method-name pattern (`%` wildcards). If omitted, clears the entire cache. |
|
||||||
|
|
||||||
|
### `health`
|
||||||
|
|
||||||
|
Server-state self-check: which globals are initialized, AXL connection
|
||||||
|
state from the most recent attempt.
|
||||||
|
|
||||||
|
**Returns:** `{ cache: bool, axl: bool, docs: bool, risport: bool, axl_connection: str, cache_cluster_id: str }`
|
||||||
|
|
||||||
|
## Route plan
|
||||||
|
|
||||||
|
### `route_partitions`
|
||||||
|
|
||||||
|
All route partitions, with pattern count and CSS member count per
|
||||||
|
partition.
|
||||||
|
|
||||||
|
**Arguments:** none
|
||||||
|
|
||||||
|
### `route_calling_search_spaces`
|
||||||
|
|
||||||
|
Calling Search Spaces with their ordered partition lists.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `name` | `str \| None` | Optional CSS name to fetch one specific CSS. If `None`, returns all. |
|
||||||
|
|
||||||
|
### `route_patterns`
|
||||||
|
|
||||||
|
The Route Plan Report — patterns with their transformations.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `kind` | `str \| None` | Pattern kind filter. One of: `directory_number`, `translation`, `route`, `conference`, `voicemail`, `hunt_pilot`, `call_pickup_group`, `park_code`, `directed_pickup`, `message_waiting`, `device_template`. Default: all kinds. |
|
||||||
|
| `partition` | `str \| None` | Filter by partition name (exact match). |
|
||||||
|
| `filter` | `str \| None` | Substring match against the pattern text. |
|
||||||
|
| `limit` | `int` | Max rows. Default 500. |
|
||||||
|
|
||||||
|
### `route_inspect_pattern`
|
||||||
|
|
||||||
|
Deep dive on a single pattern. Returns the pattern row, the route
|
||||||
|
filter (if any), every CSS that includes the pattern's partition, and
|
||||||
|
the full destination chain (route list -> route groups -> gateways).
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `pattern` | `str` | The pattern text (e.g. `"9.@"`, `"\\+1[2-9]XX[2-9]XXXXXX"`). |
|
||||||
|
| `partition` | `str \| None` | Optional partition name. Recommended — disambiguates same pattern in different partitions. |
|
||||||
|
|
||||||
|
### `route_lists_and_groups`
|
||||||
|
|
||||||
|
Route list -> route group -> gateway/trunk chain. The right way to
|
||||||
|
traverse the routing topology — handles version-shifting join-table
|
||||||
|
names internally.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `name` | `str \| None` | Optional route list name. If `None`, returns all. |
|
||||||
|
|
||||||
|
### `route_translation_chain`
|
||||||
|
|
||||||
|
Wildcard-aware pattern matcher. Given a number and an optional CSS,
|
||||||
|
returns the chain of patterns that would match.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `number` | `str` | The dialed digits. |
|
||||||
|
| `css_name` | `str \| None` | Optional CSS context. If `None`, searches all partitions. |
|
||||||
|
|
||||||
|
**Caveat:** Evaluates CUCM wildcards (`X`, `!`, `[0-9]`, `@`, `\+`) but
|
||||||
|
does *not* model route-filter constraints on `@` patterns. Use as
|
||||||
|
guidance, not authoritative.
|
||||||
|
|
||||||
|
### `route_digit_discard_instructions`
|
||||||
|
|
||||||
|
Catalog of all Digit Discard Instructions (DDIs) defined on the
|
||||||
|
cluster.
|
||||||
|
|
||||||
|
**Arguments:** none
|
||||||
|
|
||||||
|
### `route_device_pool_route_groups`
|
||||||
|
|
||||||
|
Local Route Group resolution per device pool. For pools with a
|
||||||
|
`fkroutegroup_local` mapping, returns the route group that the pool's
|
||||||
|
calling devices resolve "Standard Local Route Group" to.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `device_pool_name` | `str \| None` | Optional pool name filter. |
|
||||||
|
|
||||||
|
### `route_devices_using_css`
|
||||||
|
|
||||||
|
Impact analysis: which devices, patterns, and templates reference the
|
||||||
|
given CSS. Walks all 71 known `fkcallingsearchspace_*` columns across
|
||||||
|
the schema.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `css_name` | `str` | Exact CSS name. |
|
||||||
|
| `max_per_category` | `int` | Limit per category (default 50). |
|
||||||
|
|
||||||
|
The 71-column coverage is regression-tested by
|
||||||
|
`test_complete_schema_coverage_against_known_columns` — adding a new
|
||||||
|
CUCM version with new CSS-bearing columns will fail red until the tool
|
||||||
|
is updated. See [Hamilton review patterns](/explanation/hamilton-review-patterns/).
|
||||||
|
|
||||||
|
### `route_filters`
|
||||||
|
|
||||||
|
Route filter clauses + member rules. Route filters constrain `@`
|
||||||
|
patterns to a subset of the international dial plan.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `name` | `str \| None` | Optional filter name. |
|
||||||
|
| `include_members` | `bool` | If `True`, include member rules. Default `False`. |
|
||||||
|
|
||||||
|
## Real-time registration (RisPort70)
|
||||||
|
|
||||||
|
These tools query CUCM's RisPort70 SOAP service rather than AXL — they
|
||||||
|
return **live registration state**, not configuration. They share the
|
||||||
|
same auth credentials as AXL.
|
||||||
|
|
||||||
|
### `device_registration_status`
|
||||||
|
|
||||||
|
Page through CUCM's RisPort `selectCmDevice` for live registration
|
||||||
|
state.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `device_class` | `str` | One of `Phone`, `Gateway`, `H323`, `Cti`, `VoiceMail`, `MediaResources`, `HuntList`, `SIPTrunk`, `Any`. |
|
||||||
|
| `status` | `str` | `Registered`, `UnRegistered`, `Rejected`, `PartiallyRegistered`, `Unknown`, `Any`. |
|
||||||
|
| `name_filter` | `str` | Substring match against device name. |
|
||||||
|
| `page_size` | `int` | Default 200. RisPort caps at 1000. |
|
||||||
|
|
||||||
|
### `device_registration_summary`
|
||||||
|
|
||||||
|
Cluster-wide breakdown across `Phone`, `Gateway`, `SIPTrunk`,
|
||||||
|
`HuntList`, etc. — registered / unregistered / rejected counts per
|
||||||
|
class.
|
||||||
|
|
||||||
|
**Arguments:** none
|
||||||
|
|
||||||
|
## Caching behavior
|
||||||
|
|
||||||
|
Every tool's response is cached in SQLite at
|
||||||
|
`~/.cache/mcaxl/responses/axl_responses.sqlite`. Cache is
|
||||||
|
**cluster-isolated** by SHA-256 of `AXL_URL` — pointing at a different
|
||||||
|
cluster never serves stale data from a previous one. See
|
||||||
|
[Cluster-isolated cache](/explanation/cluster-isolated-cache/).
|
||||||
|
|
||||||
|
Default TTL: 1 hour (`AXL_CACHE_TTL=3600`). Set to `0` to disable.
|
||||||
|
Force-refresh a specific method with `cache_clear(method_pattern="route_patterns")`.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [Prompts](/reference/prompts/) — orchestrated audit narratives that call these tools
|
||||||
|
- [Environment variables](/reference/env-vars/)
|
||||||
|
- [CUCM schema cheat-sheet](/reference/cucm-schema-cheatsheet/)
|
||||||
1
docs/src/env.d.ts
vendored
Normal file
1
docs/src/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
165
docs/src/styles/custom.css
Normal file
165
docs/src/styles/custom.css
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
* mcaxl docs — theme overrides
|
||||||
|
*
|
||||||
|
* Palette: muted forest-green accent, warm slate neutrals.
|
||||||
|
* No purple, no aggressive gradients. Both light and dark modes
|
||||||
|
* get legibility-first treatment.
|
||||||
|
*
|
||||||
|
* Swatches:
|
||||||
|
* green-50 #eef5f1
|
||||||
|
* green-100 #d4e7dc
|
||||||
|
* green-200 #a8cdb8
|
||||||
|
* green-400 #4f9a78
|
||||||
|
* green-500 #2f7d5e (primary accent — dark mode)
|
||||||
|
* green-600 #1f6f5c
|
||||||
|
* green-700 #185647
|
||||||
|
* green-800 #103e34
|
||||||
|
* green-900 #0a2620
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--sl-color-accent-low: #103e34;
|
||||||
|
--sl-color-accent: #2f7d5e;
|
||||||
|
--sl-color-accent-high: #a8cdb8;
|
||||||
|
|
||||||
|
/* Warm neutral grey scale (dark mode; Starlight inverts for light) */
|
||||||
|
--sl-color-white: #f3efe6;
|
||||||
|
--sl-color-gray-1: #e1ddd4;
|
||||||
|
--sl-color-gray-2: #b6b1a7;
|
||||||
|
--sl-color-gray-3: #888278;
|
||||||
|
--sl-color-gray-4: #555049;
|
||||||
|
--sl-color-gray-5: #38352f;
|
||||||
|
--sl-color-gray-6: #28251f;
|
||||||
|
--sl-color-black: #1a1814;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme='light'] {
|
||||||
|
--sl-color-accent-low: #d4e7dc;
|
||||||
|
--sl-color-accent: #1f6f5c;
|
||||||
|
--sl-color-accent-high: #0a2620;
|
||||||
|
|
||||||
|
--sl-color-white: #1a1814;
|
||||||
|
--sl-color-gray-1: #28251f;
|
||||||
|
--sl-color-gray-2: #38352f;
|
||||||
|
--sl-color-gray-3: #555049;
|
||||||
|
--sl-color-gray-4: #888278;
|
||||||
|
--sl-color-gray-5: #d2cec4;
|
||||||
|
--sl-color-gray-6: #ebe7dd;
|
||||||
|
--sl-color-gray-7: #f3efe6;
|
||||||
|
--sl-color-black: #fbf8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slightly tighter line height for code-heavy pages */
|
||||||
|
html {
|
||||||
|
font-feature-settings: 'ss01', 'cv11';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card grid for the landing page */
|
||||||
|
.mc-card-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
margin-block: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.mc-card-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
.mc-card-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
border: 1px solid var(--sl-color-gray-5);
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
background: var(--sl-color-black);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition:
|
||||||
|
border-color 150ms ease,
|
||||||
|
transform 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-card:hover {
|
||||||
|
border-color: var(--sl-color-accent);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-card h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
color: var(--sl-color-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-card p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--sl-color-gray-2);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-card-icon {
|
||||||
|
color: var(--sl-color-accent);
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Diátaxis "what kind of page is this?" badge row on the home page */
|
||||||
|
.mc-diataxis {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
margin-block: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 720px) {
|
||||||
|
.mc-diataxis {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-diataxis-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid var(--sl-color-gray-5);
|
||||||
|
border-left: 3px solid var(--sl-color-accent);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-diataxis-row strong {
|
||||||
|
color: var(--sl-color-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-diataxis-row p {
|
||||||
|
margin: 0.15rem 0 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--sl-color-gray-2);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline pills for the tool/prompt reference tables */
|
||||||
|
.mc-pill {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.05rem 0.45rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.mc-pill.read-only { color: #4f9a78; }
|
||||||
|
.mc-pill.cached { color: #b28943; }
|
||||||
|
.mc-pill.realtime { color: #6691b8; }
|
||||||
5
docs/tsconfig.json
Normal file
5
docs/tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
|
"exclude": ["dist"]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user