Rename to mcaxl + scrub for public PyPI release
Renames the package from `mcp-cucm-axl` to `mcaxl` to fit the
operator's mc<interface> naming convention (mcusb, mcaxl, …),
and scrubs Bingham-specific defaults so the package works for
anyone, anywhere.
Rename:
- pyproject.toml: name, scripts entry point, description
- src/mcp_cucm_axl/ → src/mcaxl/ (git mv preserves history)
- All Python imports updated via sed
- Cache directory: ~/.cache/mcp-cucm-axl/ → ~/.cache/mcaxl/
- Log prefix [mcp-cucm-axl] → [mcaxl]
- Package version lookup: importlib.metadata.version("mcaxl")
- .mcp.json command updated to invoke `mcaxl` script
- All 155 tests pass under the new name (verified)
Bingham-specific scrubs:
- docs_loader._DEFAULT_INDEX_DIR: hardcoded /home/rpm/bingham/...
path removed; defaults to None. Operators set CISCO_DOCS_INDEX_PATH
env var; without it, prompts gracefully degrade with a fallback
notice instructing the LLM to use the cisco-docs MCP search_docs
tool instead.
- prompts/_common.docs_or_empty_msg: removed the explicit
/home/rpm/bingham/... path from the fallback message text.
- server.py: removed dead-code copy of _docs_or_empty_msg() that
was leftover from before the prompts package extraction.
- README.md: completely rewritten as a public-facing readme. Lead
paragraph names CUCM as the target platform, install instructions
cover uvx / pip / Claude Code MCP add. Recommends cisco-cucm-mcp
as the operations counterpart.
PyPI metadata:
- Initial CalVer version: 2026.04.27
- License: MIT (LICENSE file added)
- Project URLs: Homepage / Source / Issues / Changelog all point
at git.supported.systems/mcp/mcaxl (newly-created Gitea repo
in the mcp/ org for PyPI releases)
- Classifiers: Beta / Telecommunications Industry / Topic:Telephony
- Keywords: mcp, cisco, cucm, axl, risport, voip, sip, audit
- sdist excludes: CLAUDE.md, .env*, axlsqltoolkit.zip, audits/,
tests/, pytest/ruff caches. Verified clean: wheel ships only the
mcaxl/ source tree + LICENSE + METADATA + entry_points.
CHANGELOG.md added with a 2026.04.27 initial-release entry,
documenting tool/prompt counts, structural read-only guarantees,
Hamilton review closure, live-cluster verification, and known
limitations.
Build verification:
- `uv build` produces clean wheel + sdist
- Wheel: 22 source files, 195KB total, no Bingham-specific files
- Sdist excludes verified: no CLAUDE.md, no axlsqltoolkit.zip
- Entry point: `mcaxl = mcaxl.server:main`
- Package installs as mcaxl==2026.4.27
This commit is contained in:
parent
39d4b29392
commit
ca6956e826
@ -6,7 +6,7 @@
|
||||
"run",
|
||||
"--directory",
|
||||
"/home/rpm/bingham/axl",
|
||||
"mcp-cucm-axl"
|
||||
"mcaxl"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
67
CHANGELOG.md
Normal file
67
CHANGELOG.md
Normal file
@ -0,0 +1,67 @@
|
||||
# Changelog
|
||||
|
||||
This project uses [CalVer](https://calver.org/) — version numbers
|
||||
encode the date the package was tested against the upstream Cisco APIs
|
||||
and published. Format: `YYYY.MM.DD` with optional `.N` post-release
|
||||
suffix for same-day fixes.
|
||||
|
||||
## 2026.04.27 — initial public release
|
||||
|
||||
First public release on PyPI as `mcaxl`. Renamed from the internal
|
||||
working name `mcp-cucm-axl` to fit the operator's `mc<interface>`
|
||||
naming convention.
|
||||
|
||||
### Tools (19 total)
|
||||
|
||||
**Foundational**: `axl_version`, `axl_sql`, `axl_list_tables`,
|
||||
`axl_describe_table`, `cache_stats`, `cache_clear`, `health`.
|
||||
|
||||
**Route plan**: `route_partitions`, `route_calling_search_spaces`,
|
||||
`route_patterns`, `route_inspect_pattern`, `route_lists_and_groups`,
|
||||
`route_translation_chain`, `route_digit_discard_instructions`,
|
||||
`route_device_pool_route_groups`, `route_devices_using_css`,
|
||||
`route_filters`.
|
||||
|
||||
**Real-time registration (RisPort70)**: `device_registration_status`,
|
||||
`device_registration_summary`.
|
||||
|
||||
### Prompts (10 total)
|
||||
|
||||
Schema-grounded conversation seeds: `route_plan_overview`,
|
||||
`investigate_pattern`, `audit_routing`, `cucm_sql_help`,
|
||||
`sip_trunk_report`, `phone_inventory_report`, `user_audit`,
|
||||
`inbound_did_audit`, `hunt_pilot_audit`, `whoami`.
|
||||
|
||||
### Engineering rigor
|
||||
|
||||
- **Read-only by structural guarantee**: no AXL write methods are
|
||||
registered; the SQL validator rejects non-SELECT/WITH queries as
|
||||
defense-in-depth.
|
||||
- **Hamilton-style review closed**: 7 findings (2 Critical, 3 Major,
|
||||
2 Minor) addressed during pre-release hardening, each with a
|
||||
regression test.
|
||||
- **Live-cluster verified**: every tool path verified against a
|
||||
production CUCM 15.0.1.12900 cluster before release.
|
||||
- **155 unit tests**, schema drift guard for all 71 known
|
||||
`fkcallingsearchspace_*` columns (via
|
||||
`test_complete_schema_coverage_against_known_columns`).
|
||||
|
||||
### Known limitations
|
||||
|
||||
- `route_translation_chain` evaluates CUCM wildcards (`X`, `!`,
|
||||
`[0-9]`, `@`, `\\+`) but does not model route-filter constraints on
|
||||
`@` patterns — use as guidance, not authoritative.
|
||||
- AXL WSDL must be supplied externally (Cisco-licensed; not bundled).
|
||||
See `README.md` for bootstrap instructions.
|
||||
- RisPort `state_info` cursor pagination is implemented but not yet
|
||||
stress-tested on clusters with > 1000 devices in a single class.
|
||||
|
||||
### Acknowledgments
|
||||
|
||||
Borrowed two ideas from
|
||||
[`@calltelemetry/cisco-cucm-mcp`](https://github.com/calltelemetry/cisco-cucm-mcp)
|
||||
(MIT licensed): the RisPort70 SOAP envelope shape and the
|
||||
exponential-backoff retry policy on HTTP 503. Their tool covers
|
||||
operational debugging (logs, perfmon, packet capture) — install both
|
||||
side-by-side for compound questions like *"audit found CSS X
|
||||
unreferenced AND RisPort confirms zero phones registered against it."*
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Ryan Malloy <ryan@supported.systems>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
237
README.md
237
README.md
@ -1,86 +1,99 @@
|
||||
# mcp-cucm-axl
|
||||
# mcaxl
|
||||
|
||||
Read-only MCP server for **Cisco Unified CM 15** AXL — built for LLM-driven
|
||||
cluster auditing, with a particular focus on the **Route Plan Report**:
|
||||
partitions, calling search spaces, route patterns, translation patterns,
|
||||
called/calling party transformations, and digit-discard instructions.
|
||||
Read-only MCP server for **Cisco Unified Communications Manager (CUCM)** —
|
||||
exposes the AXL SOAP API and RisPort70 real-time registration state to
|
||||
LLMs for dial-plan analysis, configuration auditing, and impact analysis.
|
||||
|
||||
> Tested against CUCM 15.0.1.12900. Should work on any CUCM 12.5+.
|
||||
|
||||
## Why this exists
|
||||
|
||||
CUCM's admin UI is great for one-config-at-a-time work but painful for
|
||||
audit / discovery questions like:
|
||||
|
||||
- "Which translation patterns rewrite the calling party number, and why?"
|
||||
- "Which CSSs include the `Internal_PT` partition, in what order?"
|
||||
- "Show me every route pattern targeting the SIP trunk to the carrier."
|
||||
- "Are there partitions defined but unreachable from any CSS?"
|
||||
- *"Which translation patterns rewrite the calling party number, and why?"*
|
||||
- *"Which CSSs include the `Internal-PT` partition, in what order?"*
|
||||
- *"Show me every route pattern targeting our PSTN carrier."*
|
||||
- *"Are there partitions defined but unreachable from any CSS?"*
|
||||
- *"Which phones are configured but not currently registered?"*
|
||||
|
||||
This server gives an LLM SQL access to CUCM's Informix data dictionary,
|
||||
plus focused tools that bake in the right joins for routing-audit work.
|
||||
Pair it with the sibling [`mcp-cisco-docs`](../docs/) server and the LLM
|
||||
gets vendor documentation alongside live cluster state — answering
|
||||
"is our config consistent with Cisco's recommended baseline?" in a single
|
||||
conversation.
|
||||
`mcaxl` gives an LLM SQL access to CUCM's Informix data dictionary,
|
||||
schema-aware joins for common audit questions, and RisPort70
|
||||
cross-reference for live registration state. Then a set of curated
|
||||
prompts orchestrates the tools toward audit *findings*, not just data.
|
||||
|
||||
## Read-only by structural guarantee
|
||||
|
||||
The server **never registers** AXL write methods. There is no
|
||||
`executeSQLUpdate`, no `add*`/`update*`/`remove*`/`apply*`/`reset*`/
|
||||
`restart*` tool. Read-only is enforced by *absence* of write operations,
|
||||
not by runtime sanitization. Defense-in-depth: SQL queries are also
|
||||
client-side validated to begin with `SELECT` or `WITH`.
|
||||
`executeSQLUpdate`, no `add*` / `update*` / `remove*` / `apply*` /
|
||||
`reset*` / `restart*` tool. Read-only is enforced by *absence* of write
|
||||
operations, not by runtime sanitization. Defense-in-depth: SQL queries
|
||||
are also client-side validated to begin with `SELECT` or `WITH`.
|
||||
|
||||
## Setup
|
||||
For operations that require write access (service control, packet capture,
|
||||
log download, perfmon, etc.), install
|
||||
[`@calltelemetry/cisco-cucm-mcp`](https://github.com/calltelemetry/cisco-cucm-mcp)
|
||||
alongside this server. The two are complementary — `mcaxl` answers
|
||||
"what does the config say?", `cisco-cucm-mcp` answers "what's happening
|
||||
right now?".
|
||||
|
||||
### 1. Configure environment
|
||||
## Install
|
||||
|
||||
Edit `.env` (already gitignored):
|
||||
```bash
|
||||
# Run directly from PyPI:
|
||||
uvx mcaxl
|
||||
|
||||
```env
|
||||
AXL_URL=https://cucm-pub:8443/axl
|
||||
AXL_USER=AxlUser
|
||||
AXL_PASS=...
|
||||
AXL_VERIFY_TLS=false # CUCM ships self-signed certs; default off
|
||||
AXL_CACHE_TTL=3600 # 1 hour; 0 disables caching
|
||||
AXL_WSDL_PATH= # optional explicit WSDL location
|
||||
CISCO_DOCS_INDEX_PATH= # optional override for prompt enrichment
|
||||
# Or as a pinned dev install:
|
||||
pip install mcaxl
|
||||
|
||||
# Or via Claude Code's MCP registry:
|
||||
claude mcp add cucm-axl -- uvx mcaxl
|
||||
```
|
||||
|
||||
### 2. Bootstrap the AXL WSDL
|
||||
## Configure
|
||||
|
||||
Download the **Cisco AXL Toolkit** from your CUCM admin UI:
|
||||
Set these env vars (most operators use a `.env` file in the working directory):
|
||||
|
||||
```env
|
||||
AXL_URL=https://your-cucm-pub:8443/axl/
|
||||
AXL_USER=your-axl-service-account
|
||||
AXL_PASS=your-password
|
||||
|
||||
# Optional:
|
||||
AXL_VERIFY_TLS=false # CUCM ships self-signed certs; default 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 location override
|
||||
AXL_WSDL_ZIP= # explicit toolkit zip path
|
||||
CISCO_DOCS_INDEX_PATH= # for prompt enrichment (see Prompts section)
|
||||
```
|
||||
|
||||
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 (read-write) — `mcaxl` is structurally incapable of using write
|
||||
permissions even if granted.
|
||||
|
||||
## AXL WSDL bootstrap
|
||||
|
||||
CUCM's AXL toolkit is Cisco-licensed and not redistributable, so it's
|
||||
not bundled. Download from your CUCM admin UI:
|
||||
|
||||
> Application → Plugins → Find → "Cisco AXL Toolkit" → Download
|
||||
|
||||
Drop the resulting `axlsqltoolkit.zip` into the project directory. On first
|
||||
launch, the server auto-extracts `schema/15.0/` into `~/.cache/mcp-cucm-axl/wsdl/15.0/`.
|
||||
The zip is gitignored (Cisco-licensed; not redistributable).
|
||||
|
||||
Alternatives (in resolution order):
|
||||
Drop the resulting `axlsqltoolkit.zip` into your working directory. On
|
||||
first launch, the server auto-extracts `schema/15.0/` (or whichever
|
||||
version matches your cluster) into `~/.cache/mcaxl/wsdl/15.0/`.
|
||||
|
||||
Alternative resolution paths (in order):
|
||||
```bash
|
||||
# A: explicit zip elsewhere
|
||||
export AXL_WSDL_ZIP=/path/to/axlsqltoolkit.zip
|
||||
|
||||
# B: explicit WSDL file
|
||||
export AXL_WSDL_PATH=/path/to/schema/15.0/AXLAPI.wsdl
|
||||
|
||||
# C: pre-populated cache directory
|
||||
mkdir -p ~/.cache/mcp-cucm-axl/wsdl/15.0/
|
||||
cp /path/to/schema/15.0/* ~/.cache/mcp-cucm-axl/wsdl/15.0/
|
||||
export AXL_WSDL_ZIP=/path/to/axlsqltoolkit.zip # explicit zip
|
||||
export AXL_WSDL_PATH=/path/to/schema/15.0/AXLAPI.wsdl # explicit WSDL
|
||||
# Or pre-populate the cache:
|
||||
mkdir -p ~/.cache/mcaxl/wsdl/15.0/
|
||||
cp /path/to/schema/15.0/* ~/.cache/mcaxl/wsdl/15.0/
|
||||
```
|
||||
|
||||
### 3. Install + run
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
uv run mcp-cucm-axl
|
||||
```
|
||||
|
||||
Or via the bundled `.mcp.json`, automatically registered when Claude Code
|
||||
opens this directory.
|
||||
|
||||
## Tool surface
|
||||
## Tool surface (19 total)
|
||||
|
||||
### Foundational
|
||||
|
||||
@ -93,17 +106,6 @@ opens this directory.
|
||||
| `cache_stats()`, `cache_clear(pattern=None)` | Cache plumbing |
|
||||
| `health()` | Subsystem self-check (cache / AXL / docs / RisPort init state) |
|
||||
|
||||
### Real-time device registration (RisPort70)
|
||||
|
||||
Complementary to AXL — AXL tells you what's *configured*, RisPort tells you
|
||||
what's *currently registered*. The audit-relevant cross-reference is
|
||||
"configured but unregistered" (orphan signal).
|
||||
|
||||
| Tool | Purpose |
|
||||
|---|---|
|
||||
| `device_registration_status(device_class, status, name_filter, page_size)` | Page through CUCM's RisPort `selectCmDevice` for live registration state |
|
||||
| `device_registration_summary()` | Cluster-wide breakdown: registered / unregistered / rejected counts across Phone, Gateway, SIPTrunk, HuntList, etc. |
|
||||
|
||||
### Route plan
|
||||
|
||||
| Tool | Purpose |
|
||||
@ -112,62 +114,75 @@ what's *currently registered*. The audit-relevant cross-reference is
|
||||
| `route_calling_search_spaces(name=None)` | CSS list with ordered partitions |
|
||||
| `route_patterns(kind=None, partition=None, filter=None)` | Route Plan Report — patterns + transformations |
|
||||
| `route_inspect_pattern(pattern, partition=None)` | Deep dive: transforms, route filter, reachable-from CSS, full destination chain (route list → groups → gateways) |
|
||||
| `route_lists_and_groups(name=None)` | Route list → route group → gateway chain (annotates Local Route Group placeholders) |
|
||||
| `route_translation_chain(number, css_name=None)` | Wildcard-aware pattern matcher: evaluates X / ! / [0-9] / @ / \\+ against the number and returns matches sorted by specificity |
|
||||
| `route_lists_and_groups(name=None)` | Route list → route group → gateway chain |
|
||||
| `route_translation_chain(number, css_name=None)` | Wildcard-aware pattern matcher |
|
||||
| `route_digit_discard_instructions()` | DDI catalog |
|
||||
| `route_device_pool_route_groups(device_pool_name=None)` | How each device pool resolves Local Route Group placeholders to actual gateway-bearing groups |
|
||||
| `route_devices_using_css(css_name)` | Impact analysis: every reference to a CSS across line CFA/CFB/CFNA/CFUR/translation/MWI/shared, device-level CSSs, voicemail pilots, route lists |
|
||||
| `route_filters(name=None)` | Route filter clauses + member rules (composed with @-pattern routes) |
|
||||
| `route_device_pool_route_groups(device_pool_name=None)` | Local Route Group resolution |
|
||||
| `route_devices_using_css(css_name)` | Impact analysis across 71 known fk-CSS columns |
|
||||
| `route_filters(name=None, include_members=False)` | Route filter clauses + member rules |
|
||||
|
||||
## Prompts
|
||||
### Real-time device registration (RisPort70)
|
||||
|
||||
Schema-grounded conversation seeds. They pull relevant chunks from the
|
||||
sibling `cisco-docs` index and embed them inline:
|
||||
| Tool | Purpose |
|
||||
|---|---|
|
||||
| `device_registration_status(device_class, status, name_filter, page_size)` | Page through CUCM's RisPort `selectCmDevice` for live registration state |
|
||||
| `device_registration_summary()` | Cluster-wide breakdown across Phone, Gateway, SIPTrunk, HuntList, etc. |
|
||||
|
||||
## Prompts (10 total)
|
||||
|
||||
Each prompt orchestrates multiple tool calls toward a specific
|
||||
audit narrative. They appear in Claude Code's slash menu under
|
||||
`/mcp__cucm-axl__<name>`:
|
||||
|
||||
- `route_plan_overview` — fresh audit conversation seed
|
||||
- `investigate_pattern(pattern, partition=None)` — deep-dive a specific pattern
|
||||
- `audit_routing(focus="full")` — comprehensive audit walkthrough
|
||||
- `cucm_sql_help(question)` — catch-all for arbitrary SQL questions
|
||||
- `investigate_pattern(pattern, partition=None)` — single-pattern deep dive
|
||||
- `audit_routing(focus="full")` — comprehensive walkthrough with checklist
|
||||
- `cucm_sql_help(question)` — catch-all SQL helper
|
||||
- `sip_trunk_report(name_filter=None)` — SIP trunk inventory + findings
|
||||
- `phone_inventory_report(filter=None)` — phone fleet aggregates with anomaly findings; cross-references RisPort registration state
|
||||
- `user_audit(focus="full")` — end users + application users + role assignments
|
||||
- `phone_inventory_report(filter=None)` — phone fleet aggregates with anomaly findings (cross-references RisPort)
|
||||
- `user_audit(focus="full")` — end users + app users + role assignments
|
||||
- `inbound_did_audit()` — XFORM-Inbound-DNIS inventory + screening pipeline
|
||||
- `hunt_pilot_audit()` — hunt pilots, queue settings, line group membership
|
||||
- `whoami(userid=None)` — single-user role chain (defaults to AXL service account)
|
||||
|
||||
## Scope and complement
|
||||
### Optional: schema-grounded prompt enrichment
|
||||
|
||||
This server is **audit-focused**: read-only queries against AXL plus
|
||||
RisPort cross-reference for registration state. It does *not* cover
|
||||
operational debugging (logs, packet capture, perfmon counters,
|
||||
service control, certificates, backups).
|
||||
|
||||
For those, install [`@calltelemetry/cisco-cucm-mcp`](https://github.com/calltelemetry/cisco-cucm-mcp)
|
||||
alongside this server:
|
||||
|
||||
```bash
|
||||
claude mcp add cucm-ops -- npx -y @calltelemetry/cisco-cucm-mcp@latest
|
||||
```
|
||||
|
||||
The two servers are **complementary**, not competing — they answer
|
||||
different questions and use different CUCM APIs (AXL + RisPort here;
|
||||
DIME + RisPort + PerfMon + ControlCenter + SSH there). An LLM with
|
||||
both servers can compose audit findings (this server) with operational
|
||||
state (theirs) — e.g., *"audit found CSS X has 0 references AND
|
||||
RisPort shows zero phones currently registered against any device pool
|
||||
that inherits it → confirmed safe to delete."*
|
||||
Set `CISCO_DOCS_INDEX_PATH` to a directory containing `chunks.jsonl`
|
||||
and `index_meta.json` (produced by the
|
||||
[`mcp-cisco-docs`](https://github.com/...) 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.
|
||||
|
||||
## Cache
|
||||
|
||||
Responses are cached in SQLite at `~/.cache/mcp-cucm-axl/responses/axl_responses.sqlite`.
|
||||
Cache survives restarts. Clear with `cache_clear()` after a known config change.
|
||||
Responses are cached in SQLite at
|
||||
`~/.cache/mcaxl/responses/axl_responses.sqlite`. The cache is
|
||||
**cluster-isolated** by SHA-256 of `AXL_URL` — pointing the server
|
||||
at a different cluster never serves stale data from a previous one.
|
||||
Cache survives restarts. Clear with `cache_clear()` after a known
|
||||
config change.
|
||||
|
||||
## Notes
|
||||
## Caveats
|
||||
|
||||
- `route_translation_chain` does literal/prefix matching only. CUCM's actual
|
||||
matcher evaluates wildcards (`X`, `!`, `[0-9]`, etc.) and selects the
|
||||
longest match. Treat results as "patterns to investigate" rather than
|
||||
"definitive route."
|
||||
- Pattern type codes (`tkpatternusage`) used by `route_patterns(kind=...)` are
|
||||
stable across CUCM versions but enumerated against the `typepatternusage`
|
||||
table at query time, so any cluster-specific custom types still work.
|
||||
- `route_translation_chain` evaluates CUCM wildcards (`X`, `!`, `[0-9]`,
|
||||
`@`, `\+`) but does *not* model route-filter constraints on `@`
|
||||
patterns. Use as guidance, not authoritative.
|
||||
- The package's `recordingprofile` / `usageprofile` / `vipre164transformation`
|
||||
reference categories were schema-verified against CUCM 15. If a future
|
||||
CUCM version adds new `fkcallingsearchspace_*` columns,
|
||||
`route_devices_using_css`'s coverage will lag until the package is
|
||||
updated. The
|
||||
`test_complete_schema_coverage_against_known_columns` test enforces
|
||||
the current snapshot — failing red surfaces the drift loudly.
|
||||
|
||||
## License
|
||||
|
||||
MIT. See `LICENSE`.
|
||||
|
||||
## Source
|
||||
|
||||
- Repo: [git.supported.systems/mcp/mcaxl](https://git.supported.systems/mcp/mcaxl)
|
||||
- Issues: [git.supported.systems/mcp/mcaxl/issues](https://git.supported.systems/mcp/mcaxl/issues)
|
||||
- Changelog: [`CHANGELOG.md`](./CHANGELOG.md)
|
||||
|
||||
@ -7,7 +7,7 @@ post-migration cleanup, and identifying single-points-of-failure on
|
||||
specific trunks.
|
||||
|
||||
**Status:** Validated against CUCM 15.0.1.12900-234 on 2026-04-25.
|
||||
Empty `prompts/` directory at `src/mcp_cucm_axl/prompts/` is the
|
||||
Empty `prompts/` directory at `src/mcaxl/prompts/` is the
|
||||
intended home for extracting this into a `@mcp.prompt` function. For
|
||||
now, prompts live inline in `server.py` (see `route_plan_overview`,
|
||||
`investigate_pattern`, `audit_routing`).
|
||||
|
||||
@ -1,11 +1,29 @@
|
||||
[project]
|
||||
name = "mcp-cucm-axl"
|
||||
version = "0.1.0"
|
||||
description = "Read-only MCP server for CUCM 15 AXL — exposes executeSQLQuery + Informix data dictionary introspection, with schema-grounded prompts that pull from the sibling cisco-docs index. Built for LLM-driven cluster auditing."
|
||||
name = "mcaxl"
|
||||
version = "2026.04.27"
|
||||
description = "Read-only MCP server for Cisco Unified Communications Manager (CUCM) — AXL SOAP API + RisPort70 registration state — purpose-built for LLM-driven dial-plan and configuration auditing."
|
||||
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
requires-python = ">=3.11"
|
||||
keywords = [
|
||||
"mcp", "cisco", "cucm", "axl", "risport",
|
||||
"voip", "sip", "audit", "telephony",
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: System Administrators",
|
||||
"Intended Audience :: Telecommunications Industry",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Topic :: Communications :: Telephony",
|
||||
"Topic :: System :: Networking :: Monitoring",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"fastmcp>=3.2",
|
||||
@ -22,14 +40,36 @@ test = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
mcp-cucm-axl = "mcp_cucm_axl.server:main"
|
||||
mcaxl = "mcaxl.server:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://git.supported.systems/mcp/mcaxl"
|
||||
Source = "https://git.supported.systems/mcp/mcaxl"
|
||||
Issues = "https://git.supported.systems/mcp/mcaxl/issues"
|
||||
Changelog = "https://git.supported.systems/mcp/mcaxl/src/branch/main/CHANGELOG.md"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/mcp_cucm_axl"]
|
||||
packages = ["src/mcaxl"]
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
# Keep the published source distribution focused on what's needed to
|
||||
# build / install / run. Excluded files exist for local development only.
|
||||
exclude = [
|
||||
"CLAUDE.md", # operator-private project context for Claude Code
|
||||
".env", # never ship credentials
|
||||
".env.local",
|
||||
"axlsqltoolkit.zip", # Cisco-licensed; do not redistribute
|
||||
"audits/", # cluster-specific audit reports
|
||||
"tests/", # tests live in source repo, not the sdist
|
||||
".pytest_cache/",
|
||||
".ruff_cache/",
|
||||
"dist/",
|
||||
"build/",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
|
||||
5
src/mcaxl/__init__.py
Normal file
5
src/mcaxl/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""mcaxl — read-only MCP server for CUCM 15 AXL."""
|
||||
|
||||
from .server import main
|
||||
|
||||
__all__ = ["main"]
|
||||
@ -136,7 +136,7 @@ class AxlClient:
|
||||
# zeep's own WSDL cache (separate from our response cache) keeps
|
||||
# repeat startups fast — it parses the WSDL once and reuses
|
||||
from platformdirs import user_cache_dir
|
||||
zeep_cache_path = Path(user_cache_dir("mcp-cucm-axl")) / "zeep_wsdl.db"
|
||||
zeep_cache_path = Path(user_cache_dir("mcaxl")) / "zeep_wsdl.db"
|
||||
zeep_cache_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
transport = Transport(
|
||||
@ -161,7 +161,7 @@ class AxlClient:
|
||||
self._connected_at = _time.monotonic()
|
||||
self._last_error = None # operational state is now clean
|
||||
print(
|
||||
f"[mcp-cucm-axl] connected to {url} (TLS verify={verify_tls})",
|
||||
f"[mcaxl] connected to {url} (TLS verify={verify_tls})",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
@ -171,7 +171,7 @@ class AxlClient:
|
||||
# the last error for diagnostics.
|
||||
self._last_error = f"AXL connection failed: {e}"
|
||||
print(
|
||||
f"[mcp-cucm-axl] {self._last_error} (operational, will retry on next call)",
|
||||
f"[mcaxl] {self._last_error} (operational, will retry on next call)",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
@ -24,9 +24,11 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Default to the sibling docs index in this monorepo. Override with env var
|
||||
# if mcp-cucm-axl gets used outside this layout.
|
||||
_DEFAULT_INDEX_DIR = Path("/home/rpm/bingham/docs/src/assets/.cisco-docs-index")
|
||||
# No default path — operators set CISCO_DOCS_INDEX_PATH explicitly when
|
||||
# they have a sibling cisco-docs index they want prompts to draw from.
|
||||
# When unset, prompts gracefully degrade with a notice telling the LLM
|
||||
# to use the cisco-docs MCP server's search_docs tool instead.
|
||||
_DEFAULT_INDEX_DIR: Path | None = None
|
||||
|
||||
|
||||
# Doc-name multipliers — higher = preferred for conceptual prompts.
|
||||
@ -55,15 +57,27 @@ class DocsIndex:
|
||||
|
||||
@classmethod
|
||||
def load(cls, index_dir: Path | None = None) -> "DocsIndex | None":
|
||||
index_dir = index_dir or Path(
|
||||
os.environ.get("CISCO_DOCS_INDEX_PATH", _DEFAULT_INDEX_DIR)
|
||||
)
|
||||
# Resolution order:
|
||||
# 1. Explicit index_dir argument (test/programmatic use)
|
||||
# 2. CISCO_DOCS_INDEX_PATH env var
|
||||
# 3. _DEFAULT_INDEX_DIR (None upstream — set by downstream forks)
|
||||
# If none resolve to an existing index, prompts gracefully degrade.
|
||||
if index_dir is None:
|
||||
env_path = os.environ.get("CISCO_DOCS_INDEX_PATH")
|
||||
if env_path:
|
||||
index_dir = Path(env_path)
|
||||
elif _DEFAULT_INDEX_DIR is not None:
|
||||
index_dir = _DEFAULT_INDEX_DIR
|
||||
else:
|
||||
# No path configured — prompts will degrade with a notice
|
||||
return None
|
||||
|
||||
chunks_path = index_dir / "chunks.jsonl"
|
||||
meta_path = index_dir / "index_meta.json"
|
||||
|
||||
if not chunks_path.exists() or not meta_path.exists():
|
||||
print(
|
||||
f"[mcp-cucm-axl] cisco-docs index not found at {index_dir}; "
|
||||
f"[mcaxl] cisco-docs index not found at {index_dir}; "
|
||||
f"prompts will run without schema enrichment.",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
@ -77,7 +91,7 @@ class DocsIndex:
|
||||
if line.strip()
|
||||
]
|
||||
print(
|
||||
f"[mcp-cucm-axl] loaded {len(chunks)} doc chunks from {index_dir}",
|
||||
f"[mcaxl] loaded {len(chunks)} doc chunks from {index_dir}",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
@ -1,4 +1,4 @@
|
||||
"""Schema-grounded conversation seeds for `mcp-cucm-axl`.
|
||||
"""Schema-grounded conversation seeds for `mcaxl`.
|
||||
|
||||
Each module here defines a `render(docs, *args, **kwargs) -> str` function
|
||||
that produces the prompt body. The `@mcp.prompt` registration shims live
|
||||
@ -8,7 +8,7 @@ imports here) makes prompts unit-testable in isolation and keeps each
|
||||
prompt's content in its own file.
|
||||
|
||||
To add a new prompt:
|
||||
1. Create `src/mcp_cucm_axl/prompts/<name>.py` exporting a `render()`.
|
||||
1. Create `src/mcaxl/prompts/<name>.py` exporting a `render()`.
|
||||
2. Re-export it below.
|
||||
3. Add a thin `@mcp.prompt`-decorated shim in `server.py` that calls it.
|
||||
|
||||
@ -46,7 +46,7 @@ def docs_or_empty_msg() -> str:
|
||||
get equivalent info via the sibling cisco-docs MCP server."""
|
||||
return (
|
||||
"_The cisco-docs index is not loaded. Set CISCO_DOCS_INDEX_PATH or "
|
||||
"ensure /home/rpm/bingham/docs/src/assets/.cisco-docs-index/ exists. "
|
||||
"ensure `chunks.jsonl` + `index_meta.json` from the cisco-docs indexer. "
|
||||
"You can also use the sibling `cisco-docs` MCP server's `search_docs` "
|
||||
"tool for live semantic search._"
|
||||
)
|
||||
@ -17,7 +17,7 @@ def render(docs: "DocsIndex | None") -> str:
|
||||
return f"""# CUCM Route Plan Overview
|
||||
|
||||
You are auditing the routing configuration of a CUCM 15 cluster via the
|
||||
`mcp-cucm-axl` MCP server (read-only). Begin by gathering a high-level
|
||||
`mcaxl` MCP server (read-only). Begin by gathering a high-level
|
||||
snapshot, then drill in where anything looks wrong or surprising.
|
||||
|
||||
## Suggested first calls (in order)
|
||||
@ -303,7 +303,7 @@ class RisPortClient:
|
||||
|
||||
self._session = session
|
||||
print(
|
||||
f"[mcp-cucm-axl] RisPort client ready: {self._url}",
|
||||
f"[mcaxl] RisPort client ready: {self._url}",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
@ -362,7 +362,7 @@ def _to_int(v: object) -> int | None:
|
||||
except (TypeError, ValueError):
|
||||
import sys
|
||||
print(
|
||||
f"[mcp-cucm-axl] _to_int: unexpected non-numeric value {v!r} "
|
||||
f"[mcaxl] _to_int: unexpected non-numeric value {v!r} "
|
||||
f"(type {type(v).__name__}); returning None",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
@ -48,15 +48,6 @@ def _client() -> AxlClient:
|
||||
return _axl
|
||||
|
||||
|
||||
def _docs_or_empty_msg() -> str:
|
||||
return (
|
||||
"_The cisco-docs index is not loaded. Set CISCO_DOCS_INDEX_PATH or "
|
||||
"ensure /home/rpm/bingham/docs/src/assets/.cisco-docs-index/ exists. "
|
||||
"You can also use the sibling `cisco-docs` MCP server's `search_docs` "
|
||||
"tool for live semantic search._"
|
||||
)
|
||||
|
||||
|
||||
# ====================================================================
|
||||
# Foundational tools
|
||||
# ====================================================================
|
||||
@ -398,7 +389,7 @@ def route_filters(name: str | None = None, include_members: bool = False) -> dic
|
||||
# ====================================================================
|
||||
# Prompts — schema-grounded conversation seeds
|
||||
#
|
||||
# Bodies live in `mcp_cucm_axl.prompts.<name>`. The shims below are the
|
||||
# Bodies live in `mcaxl.prompts.<name>`. The shims below are the
|
||||
# FastMCP registration surface; FastMCP introspects each shim's signature
|
||||
# to expose parameters to the LLM, so the parameter contract lives here.
|
||||
# Each shim is a thin pass-through to the corresponding render() function.
|
||||
@ -512,12 +503,12 @@ def whoami(userid: str | None = None) -> str:
|
||||
|
||||
def _banner() -> None:
|
||||
try:
|
||||
v = _pkg_version("mcp-cucm-axl")
|
||||
v = _pkg_version("mcaxl")
|
||||
except Exception:
|
||||
v = "0.1.0"
|
||||
axl_url = os.environ.get("AXL_URL", "(unset)")
|
||||
print(f"[mcp-cucm-axl] v{v} starting", file=sys.stderr, flush=True)
|
||||
print(f"[mcp-cucm-axl] AXL_URL={axl_url}", file=sys.stderr, flush=True)
|
||||
print(f"[mcaxl] v{v} starting", file=sys.stderr, flush=True)
|
||||
print(f"[mcaxl] AXL_URL={axl_url}", file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@ -532,7 +523,7 @@ def main() -> None:
|
||||
|
||||
cache_dir = Path(
|
||||
os.environ.get("AXL_CACHE_DIR")
|
||||
or (Path(user_cache_dir("mcp-cucm-axl")) / "responses")
|
||||
or (Path(user_cache_dir("mcaxl")) / "responses")
|
||||
)
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
ttl = int(os.environ.get("AXL_CACHE_TTL", "3600"))
|
||||
@ -548,7 +539,7 @@ def main() -> None:
|
||||
cluster_id=cluster_id,
|
||||
)
|
||||
print(
|
||||
f"[mcp-cucm-axl] cache: {_cache.db_path} "
|
||||
f"[mcaxl] cache: {_cache.db_path} "
|
||||
f"(ttl={ttl}s, cluster_id={cluster_id})",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
Resolution chain:
|
||||
1. AXL_WSDL_PATH env var (explicit override) — use it
|
||||
2. ~/.cache/mcp-cucm-axl/wsdl/15.0/AXLAPI.wsdl — use cached copy
|
||||
2. ~/.cache/mcaxl/wsdl/15.0/AXLAPI.wsdl — use cached copy
|
||||
3. Auto-extract `schema/15.0/` from a Cisco AXL Toolkit zip:
|
||||
- $AXL_WSDL_ZIP if set
|
||||
- ./axlsqltoolkit.zip in the current working directory
|
||||
@ -28,8 +28,8 @@ WSDL_VERSION = "15.0"
|
||||
|
||||
|
||||
def cache_wsdl_dir(version: str = WSDL_VERSION) -> Path:
|
||||
"""Return ~/.cache/mcp-cucm-axl/wsdl/<version>/."""
|
||||
return Path(user_cache_dir("mcp-cucm-axl")) / "wsdl" / version
|
||||
"""Return ~/.cache/mcaxl/wsdl/<version>/."""
|
||||
return Path(user_cache_dir("mcaxl")) / "wsdl" / version
|
||||
|
||||
|
||||
def _has_complete_wsdl(directory: Path) -> bool:
|
||||
@ -59,7 +59,7 @@ def _try_extract_from_zip(version: str = WSDL_VERSION) -> Path | None:
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(
|
||||
f"[mcp-cucm-axl] extracting AXL schema {version} from {zip_path}",
|
||||
f"[mcaxl] extracting AXL schema {version} from {zip_path}",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
@ -71,7 +71,7 @@ def _try_extract_from_zip(version: str = WSDL_VERSION) -> Path | None:
|
||||
member = f"schema/{version}/{fname}"
|
||||
if member not in zf.namelist():
|
||||
print(
|
||||
f"[mcp-cucm-axl] zip missing member: {member}",
|
||||
f"[mcaxl] zip missing member: {member}",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
@ -80,13 +80,13 @@ def _try_extract_from_zip(version: str = WSDL_VERSION) -> Path | None:
|
||||
(target / fname).write_bytes(data)
|
||||
extracted.append(fname)
|
||||
print(
|
||||
f"[mcp-cucm-axl] extracted {len(extracted)} files into {target}",
|
||||
f"[mcaxl] extracted {len(extracted)} files into {target}",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return target
|
||||
except (zipfile.BadZipFile, OSError) as e:
|
||||
print(f"[mcp-cucm-axl] zip extraction failed: {e}", file=sys.stderr, flush=True)
|
||||
print(f"[mcaxl] zip extraction failed: {e}", file=sys.stderr, flush=True)
|
||||
return None
|
||||
|
||||
|
||||
@ -106,7 +106,7 @@ def resolve_wsdl_path() -> Path:
|
||||
if not _has_complete_wsdl(p.parent):
|
||||
missing = [f for f in WSDL_FILES if not (p.parent / f).exists()]
|
||||
print(
|
||||
f"[mcp-cucm-axl] warning: WSDL dir missing {missing} alongside {p.name}; "
|
||||
f"[mcaxl] warning: WSDL dir missing {missing} alongside {p.name}; "
|
||||
f"zeep may fail to resolve schema imports.",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
@ -1,5 +0,0 @@
|
||||
"""mcp-cucm-axl — read-only MCP server for CUCM 15 AXL."""
|
||||
|
||||
from .server import main
|
||||
|
||||
__all__ = ["main"]
|
||||
@ -5,7 +5,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_cucm_axl.cache import AxlCache
|
||||
from mcaxl.cache import AxlCache
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@ -11,8 +11,8 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_cucm_axl.cache import AxlCache
|
||||
from mcp_cucm_axl.client import AxlClient
|
||||
from mcaxl.cache import AxlCache
|
||||
from mcaxl.client import AxlClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -47,7 +47,7 @@ def test_operational_error_is_not_pinned(cache: AxlCache, monkeypatch):
|
||||
# Force the zeep Client constructor inside _ensure_connected to raise.
|
||||
# This simulates "WSDL fetch failed", "TLS handshake error", etc. —
|
||||
# transient operational failures.
|
||||
from mcp_cucm_axl import client as client_mod
|
||||
from mcaxl import client as client_mod
|
||||
|
||||
def boom(*args, **kwargs):
|
||||
raise ConnectionError("simulated transient network failure")
|
||||
@ -96,7 +96,7 @@ def test_retry_config_default_three_retries(cache: AxlCache, monkeypatch):
|
||||
monkeypatch.setenv("AXL_PASS", "test")
|
||||
monkeypatch.setenv("AXL_VERIFY_TLS", "false")
|
||||
# Stub Client construction so we exercise only the session/retry setup
|
||||
from mcp_cucm_axl import client as client_mod
|
||||
from mcaxl import client as client_mod
|
||||
|
||||
constructed = {}
|
||||
|
||||
@ -126,7 +126,7 @@ def test_retry_config_overridable_via_env(cache: AxlCache, monkeypatch):
|
||||
monkeypatch.setenv("AXL_PASS", "test")
|
||||
monkeypatch.setenv("AXL_RATE_LIMIT_RETRIES", "7")
|
||||
|
||||
from mcp_cucm_axl import client as client_mod
|
||||
from mcaxl import client as client_mod
|
||||
monkeypatch.setattr(client_mod, "Client", lambda *a, **kw: (_ for _ in ()).throw(ConnectionError("stub")))
|
||||
|
||||
client = AxlClient(cache)
|
||||
@ -144,7 +144,7 @@ def test_retry_config_zero_disables(cache: AxlCache, monkeypatch):
|
||||
monkeypatch.setenv("AXL_PASS", "test")
|
||||
monkeypatch.setenv("AXL_RATE_LIMIT_RETRIES", "0")
|
||||
|
||||
from mcp_cucm_axl import client as client_mod
|
||||
from mcaxl import client as client_mod
|
||||
monkeypatch.setattr(client_mod, "Client", lambda *a, **kw: (_ for _ in ()).throw(ConnectionError("stub")))
|
||||
|
||||
client = AxlClient(cache)
|
||||
|
||||
@ -14,7 +14,7 @@ state and act on it.
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_cucm_axl.route_plan import find_devices_using_css
|
||||
from mcaxl.route_plan import find_devices_using_css
|
||||
|
||||
|
||||
class FakeAxlClient:
|
||||
@ -140,7 +140,7 @@ def test_css_not_found_returns_error_not_partial():
|
||||
# referenced via these columns showed up as "0 references" — operator running
|
||||
# impact analysis would conclude safe-to-delete and break outbound transforms.
|
||||
|
||||
from mcp_cucm_axl.route_plan import _CSS_REFERENCE_QUERIES
|
||||
from mcaxl.route_plan import _CSS_REFERENCE_QUERIES
|
||||
|
||||
|
||||
def test_issue_1_cgpntransform_column_enumerated():
|
||||
|
||||
@ -5,7 +5,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_cucm_axl.docs_loader import DocsIndex
|
||||
from mcaxl.docs_loader import DocsIndex
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_cucm_axl.normalize import (
|
||||
from mcaxl.normalize import (
|
||||
BOOLEAN_COLUMNS,
|
||||
normalize_bool,
|
||||
normalize_row,
|
||||
|
||||
@ -10,8 +10,8 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_cucm_axl import prompts
|
||||
from mcp_cucm_axl.docs_loader import DocsIndex
|
||||
from mcaxl import prompts
|
||||
from mcaxl.docs_loader import DocsIndex
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -151,7 +151,7 @@ def test_all_prompts_registered_in_server():
|
||||
in server.py. Catches the case where a new module is added but the
|
||||
shim wasn't (the prompt would be invisible to the LLM)."""
|
||||
import asyncio
|
||||
from mcp_cucm_axl import server
|
||||
from mcaxl import server
|
||||
|
||||
async def _list():
|
||||
registered = await server.mcp.list_prompts()
|
||||
|
||||
@ -9,7 +9,7 @@ import xml.etree.ElementTree as ET
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_cucm_axl.risport import (
|
||||
from mcaxl.risport import (
|
||||
DEVICE_STATUS_VALUES,
|
||||
RisPortClient,
|
||||
_build_select_envelope,
|
||||
|
||||
@ -10,7 +10,7 @@ raise RuntimeError when their dependencies aren't initialized.
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_cucm_axl import server
|
||||
from mcaxl import server
|
||||
|
||||
|
||||
def test_cache_stats_raises_when_uninitialized(monkeypatch):
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_cucm_axl.sql_validator import validate_select, SqlValidationError
|
||||
from mcaxl.sql_validator import validate_select, SqlValidationError
|
||||
|
||||
|
||||
class TestSelectAccepted:
|
||||
|
||||
@ -10,7 +10,7 @@ doesn't silently rewrite real-zero into "no value."
|
||||
import sys
|
||||
from io import StringIO
|
||||
|
||||
from mcp_cucm_axl.route_plan import _to_int
|
||||
from mcaxl.route_plan import _to_int
|
||||
|
||||
|
||||
def test_to_int_passthrough_normal():
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp_cucm_axl.route_plan import _pattern_matches_number, _wildcard_to_regex
|
||||
from mcaxl.route_plan import _pattern_matches_number, _wildcard_to_regex
|
||||
|
||||
|
||||
class TestLiteralPatterns:
|
||||
|
||||
54
uv.lock
generated
54
uv.lock
generated
@ -792,33 +792,8 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.27.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "pyjwt", extra = ["crypto"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp-cucm-axl"
|
||||
version = "0.1.0"
|
||||
name = "mcaxl"
|
||||
version = "2026.4.27"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "fastmcp" },
|
||||
@ -846,6 +821,31 @@ requires-dist = [
|
||||
]
|
||||
provides-extras = ["test"]
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.27.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "pyjwt", extra = ["crypto"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user