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:
Ryan Malloy 2026-04-27 12:53:54 -06:00
parent 39d4b29392
commit ca6956e826
41 changed files with 358 additions and 210 deletions

View File

@ -6,7 +6,7 @@
"run", "run",
"--directory", "--directory",
"/home/rpm/bingham/axl", "/home/rpm/bingham/axl",
"mcp-cucm-axl" "mcaxl"
] ]
} }
} }

67
CHANGELOG.md Normal file
View 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
View 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
View File

@ -1,86 +1,99 @@
# mcp-cucm-axl # mcaxl
Read-only MCP server for **Cisco Unified CM 15** AXL — built for LLM-driven Read-only MCP server for **Cisco Unified Communications Manager (CUCM)**
cluster auditing, with a particular focus on the **Route Plan Report**: exposes the AXL SOAP API and RisPort70 real-time registration state to
partitions, calling search spaces, route patterns, translation patterns, LLMs for dial-plan analysis, configuration auditing, and impact analysis.
called/calling party transformations, and digit-discard instructions.
> Tested against CUCM 15.0.1.12900. Should work on any CUCM 12.5+.
## Why this exists ## Why this exists
CUCM's admin UI is great for one-config-at-a-time work but painful for CUCM's admin UI is great for one-config-at-a-time work but painful for
audit / discovery questions like: audit / discovery questions like:
- "Which translation patterns rewrite the calling party number, and why?" - *"Which translation patterns rewrite the calling party number, and why?"*
- "Which CSSs include the `Internal_PT` partition, in what order?" - *"Which CSSs include the `Internal-PT` partition, in what order?"*
- "Show me every route pattern targeting the SIP trunk to the carrier." - *"Show me every route pattern targeting our PSTN carrier."*
- "Are there partitions defined but unreachable from any CSS?" - *"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, `mcaxl` gives an LLM SQL access to CUCM's Informix data dictionary,
plus focused tools that bake in the right joins for routing-audit work. schema-aware joins for common audit questions, and RisPort70
Pair it with the sibling [`mcp-cisco-docs`](../docs/) server and the LLM cross-reference for live registration state. Then a set of curated
gets vendor documentation alongside live cluster state — answering prompts orchestrates the tools toward audit *findings*, not just data.
"is our config consistent with Cisco's recommended baseline?" in a single
conversation.
## Read-only by structural guarantee ## Read-only by structural guarantee
The server **never registers** AXL write methods. There is no The server **never registers** AXL write methods. There is no
`executeSQLUpdate`, no `add*`/`update*`/`remove*`/`apply*`/`reset*`/ `executeSQLUpdate`, no `add*` / `update*` / `remove*` / `apply*` /
`restart*` tool. Read-only is enforced by *absence* of write operations, `reset*` / `restart*` tool. Read-only is enforced by *absence* of write
not by runtime sanitization. Defense-in-depth: SQL queries are also operations, not by runtime sanitization. Defense-in-depth: SQL queries
client-side validated to begin with `SELECT` or `WITH`. 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 # Or as a pinned dev install:
AXL_URL=https://cucm-pub:8443/axl pip install mcaxl
AXL_USER=AxlUser
AXL_PASS=... # Or via Claude Code's MCP registry:
AXL_VERIFY_TLS=false # CUCM ships self-signed certs; default off claude mcp add cucm-axl -- uvx mcaxl
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
``` ```
### 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 > Application → Plugins → Find → "Cisco AXL Toolkit" → Download
Drop the resulting `axlsqltoolkit.zip` into the project directory. On first Drop the resulting `axlsqltoolkit.zip` into your working directory. On
launch, the server auto-extracts `schema/15.0/` into `~/.cache/mcp-cucm-axl/wsdl/15.0/`. first launch, the server auto-extracts `schema/15.0/` (or whichever
The zip is gitignored (Cisco-licensed; not redistributable). version matches your cluster) into `~/.cache/mcaxl/wsdl/15.0/`.
Alternatives (in resolution order):
Alternative resolution paths (in order):
```bash ```bash
# A: explicit zip elsewhere export AXL_WSDL_ZIP=/path/to/axlsqltoolkit.zip # explicit zip
export AXL_WSDL_ZIP=/path/to/axlsqltoolkit.zip export AXL_WSDL_PATH=/path/to/schema/15.0/AXLAPI.wsdl # explicit WSDL
# Or pre-populate the cache:
# B: explicit WSDL file mkdir -p ~/.cache/mcaxl/wsdl/15.0/
export AXL_WSDL_PATH=/path/to/schema/15.0/AXLAPI.wsdl cp /path/to/schema/15.0/* ~/.cache/mcaxl/wsdl/15.0/
# 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/
``` ```
### 3. Install + run ## Tool surface (19 total)
```bash
uv sync
uv run mcp-cucm-axl
```
Or via the bundled `.mcp.json`, automatically registered when Claude Code
opens this directory.
## Tool surface
### Foundational ### Foundational
@ -93,17 +106,6 @@ opens this directory.
| `cache_stats()`, `cache_clear(pattern=None)` | Cache plumbing | | `cache_stats()`, `cache_clear(pattern=None)` | Cache plumbing |
| `health()` | Subsystem self-check (cache / AXL / docs / RisPort init state) | | `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 ### Route plan
| Tool | Purpose | | 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_calling_search_spaces(name=None)` | CSS list with ordered partitions |
| `route_patterns(kind=None, partition=None, filter=None)` | Route Plan Report — patterns + transformations | | `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_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_lists_and_groups(name=None)` | Route list → route group → gateway chain |
| `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_translation_chain(number, css_name=None)` | Wildcard-aware pattern matcher |
| `route_digit_discard_instructions()` | DDI catalog | | `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_device_pool_route_groups(device_pool_name=None)` | Local Route Group resolution |
| `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_devices_using_css(css_name)` | Impact analysis across 71 known fk-CSS columns |
| `route_filters(name=None)` | Route filter clauses + member rules (composed with @-pattern routes) | | `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 | Tool | Purpose |
sibling `cisco-docs` index and embed them inline: |---|---|
| `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 - `route_plan_overview` — fresh audit conversation seed
- `investigate_pattern(pattern, partition=None)` — deep-dive a specific pattern - `investigate_pattern(pattern, partition=None)`single-pattern deep dive
- `audit_routing(focus="full")` — comprehensive audit walkthrough - `audit_routing(focus="full")` — comprehensive walkthrough with checklist
- `cucm_sql_help(question)` — catch-all for arbitrary SQL questions - `cucm_sql_help(question)` — catch-all SQL helper
- `sip_trunk_report(name_filter=None)` — SIP trunk inventory + findings - `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 - `phone_inventory_report(filter=None)` — phone fleet aggregates with anomaly findings (cross-references RisPort)
- `user_audit(focus="full")` — end users + application users + role assignments - `user_audit(focus="full")` — end users + app users + role assignments
- `inbound_did_audit()` — XFORM-Inbound-DNIS inventory + screening pipeline - `inbound_did_audit()` — XFORM-Inbound-DNIS inventory + screening pipeline
- `hunt_pilot_audit()` — hunt pilots, queue settings, line group membership - `hunt_pilot_audit()` — hunt pilots, queue settings, line group membership
- `whoami(userid=None)` — single-user role chain (defaults to AXL service account) - `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 Set `CISCO_DOCS_INDEX_PATH` to a directory containing `chunks.jsonl`
RisPort cross-reference for registration state. It does *not* cover and `index_meta.json` (produced by the
operational debugging (logs, packet capture, perfmon counters, [`mcp-cisco-docs`](https://github.com/...) indexer or any compatible
service control, certificates, backups). embedding pipeline) to have prompts pull relevant Cisco documentation
chunks inline. Without this, prompts gracefully degrade to a fallback
For those, install [`@calltelemetry/cisco-cucm-mcp`](https://github.com/calltelemetry/cisco-cucm-mcp) notice instructing the LLM to use the sibling cisco-docs server's
alongside this server: `search_docs` tool.
```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."*
## Cache ## Cache
Responses are cached in SQLite at `~/.cache/mcp-cucm-axl/responses/axl_responses.sqlite`. Responses are cached in SQLite at
Cache survives restarts. Clear with `cache_clear()` after a known config change. `~/.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 - `route_translation_chain` evaluates CUCM wildcards (`X`, `!`, `[0-9]`,
matcher evaluates wildcards (`X`, `!`, `[0-9]`, etc.) and selects the `@`, `\+`) but does *not* model route-filter constraints on `@`
longest match. Treat results as "patterns to investigate" rather than patterns. Use as guidance, not authoritative.
"definitive route." - The package's `recordingprofile` / `usageprofile` / `vipre164transformation`
- Pattern type codes (`tkpatternusage`) used by `route_patterns(kind=...)` are reference categories were schema-verified against CUCM 15. If a future
stable across CUCM versions but enumerated against the `typepatternusage` CUCM version adds new `fkcallingsearchspace_*` columns,
table at query time, so any cluster-specific custom types still work. `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)

View File

@ -7,7 +7,7 @@ post-migration cleanup, and identifying single-points-of-failure on
specific trunks. specific trunks.
**Status:** Validated against CUCM 15.0.1.12900-234 on 2026-04-25. **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 intended home for extracting this into a `@mcp.prompt` function. For
now, prompts live inline in `server.py` (see `route_plan_overview`, now, prompts live inline in `server.py` (see `route_plan_overview`,
`investigate_pattern`, `audit_routing`). `investigate_pattern`, `audit_routing`).

View File

@ -1,11 +1,29 @@
[project] [project]
name = "mcp-cucm-axl" name = "mcaxl"
version = "0.1.0" version = "2026.04.27"
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." 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"}] authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
readme = "README.md" readme = "README.md"
license = {text = "MIT"} license = {text = "MIT"}
requires-python = ">=3.11" 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 = [ dependencies = [
"fastmcp>=3.2", "fastmcp>=3.2",
@ -22,14 +40,36 @@ test = [
] ]
[project.scripts] [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] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel] [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] [tool.ruff]
line-length = 100 line-length = 100

5
src/mcaxl/__init__.py Normal file
View File

@ -0,0 +1,5 @@
"""mcaxl — read-only MCP server for CUCM 15 AXL."""
from .server import main
__all__ = ["main"]

View File

@ -136,7 +136,7 @@ class AxlClient:
# zeep's own WSDL cache (separate from our response cache) keeps # zeep's own WSDL cache (separate from our response cache) keeps
# repeat startups fast — it parses the WSDL once and reuses # repeat startups fast — it parses the WSDL once and reuses
from platformdirs import user_cache_dir 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) zeep_cache_path.parent.mkdir(parents=True, exist_ok=True)
transport = Transport( transport = Transport(
@ -161,7 +161,7 @@ class AxlClient:
self._connected_at = _time.monotonic() self._connected_at = _time.monotonic()
self._last_error = None # operational state is now clean self._last_error = None # operational state is now clean
print( print(
f"[mcp-cucm-axl] connected to {url} (TLS verify={verify_tls})", f"[mcaxl] connected to {url} (TLS verify={verify_tls})",
file=sys.stderr, file=sys.stderr,
flush=True, flush=True,
) )
@ -171,7 +171,7 @@ class AxlClient:
# the last error for diagnostics. # the last error for diagnostics.
self._last_error = f"AXL connection failed: {e}" self._last_error = f"AXL connection failed: {e}"
print( 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, file=sys.stderr,
flush=True, flush=True,
) )

View File

@ -24,9 +24,11 @@ import sys
from pathlib import Path from pathlib import Path
# Default to the sibling docs index in this monorepo. Override with env var # No default path — operators set CISCO_DOCS_INDEX_PATH explicitly when
# if mcp-cucm-axl gets used outside this layout. # they have a sibling cisco-docs index they want prompts to draw from.
_DEFAULT_INDEX_DIR = Path("/home/rpm/bingham/docs/src/assets/.cisco-docs-index") # 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. # Doc-name multipliers — higher = preferred for conceptual prompts.
@ -55,15 +57,27 @@ class DocsIndex:
@classmethod @classmethod
def load(cls, index_dir: Path | None = None) -> "DocsIndex | None": def load(cls, index_dir: Path | None = None) -> "DocsIndex | None":
index_dir = index_dir or Path( # Resolution order:
os.environ.get("CISCO_DOCS_INDEX_PATH", _DEFAULT_INDEX_DIR) # 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" chunks_path = index_dir / "chunks.jsonl"
meta_path = index_dir / "index_meta.json" meta_path = index_dir / "index_meta.json"
if not chunks_path.exists() or not meta_path.exists(): if not chunks_path.exists() or not meta_path.exists():
print( 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.", f"prompts will run without schema enrichment.",
file=sys.stderr, file=sys.stderr,
flush=True, flush=True,
@ -77,7 +91,7 @@ class DocsIndex:
if line.strip() if line.strip()
] ]
print( 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, file=sys.stderr,
flush=True, flush=True,
) )

View File

@ -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 Each module here defines a `render(docs, *args, **kwargs) -> str` function
that produces the prompt body. The `@mcp.prompt` registration shims live 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. prompt's content in its own file.
To add a new prompt: 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. 2. Re-export it below.
3. Add a thin `@mcp.prompt`-decorated shim in `server.py` that calls it. 3. Add a thin `@mcp.prompt`-decorated shim in `server.py` that calls it.

View File

@ -46,7 +46,7 @@ def docs_or_empty_msg() -> str:
get equivalent info via the sibling cisco-docs MCP server.""" get equivalent info via the sibling cisco-docs MCP server."""
return ( return (
"_The cisco-docs index is not loaded. Set CISCO_DOCS_INDEX_PATH or " "_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` " "You can also use the sibling `cisco-docs` MCP server's `search_docs` "
"tool for live semantic search._" "tool for live semantic search._"
) )

View File

@ -17,7 +17,7 @@ def render(docs: "DocsIndex | None") -> str:
return f"""# CUCM Route Plan Overview return f"""# CUCM Route Plan Overview
You are auditing the routing configuration of a CUCM 15 cluster via the 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. snapshot, then drill in where anything looks wrong or surprising.
## Suggested first calls (in order) ## Suggested first calls (in order)

View File

@ -303,7 +303,7 @@ class RisPortClient:
self._session = session self._session = session
print( print(
f"[mcp-cucm-axl] RisPort client ready: {self._url}", f"[mcaxl] RisPort client ready: {self._url}",
file=sys.stderr, file=sys.stderr,
flush=True, flush=True,
) )

View File

@ -362,7 +362,7 @@ def _to_int(v: object) -> int | None:
except (TypeError, ValueError): except (TypeError, ValueError):
import sys import sys
print( 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", f"(type {type(v).__name__}); returning None",
file=sys.stderr, file=sys.stderr,
flush=True, flush=True,

View File

@ -48,15 +48,6 @@ def _client() -> AxlClient:
return _axl 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 # Foundational tools
# ==================================================================== # ====================================================================
@ -398,7 +389,7 @@ def route_filters(name: str | None = None, include_members: bool = False) -> dic
# ==================================================================== # ====================================================================
# Prompts — schema-grounded conversation seeds # 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 # FastMCP registration surface; FastMCP introspects each shim's signature
# to expose parameters to the LLM, so the parameter contract lives here. # to expose parameters to the LLM, so the parameter contract lives here.
# Each shim is a thin pass-through to the corresponding render() function. # 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: def _banner() -> None:
try: try:
v = _pkg_version("mcp-cucm-axl") v = _pkg_version("mcaxl")
except Exception: except Exception:
v = "0.1.0" v = "0.1.0"
axl_url = os.environ.get("AXL_URL", "(unset)") axl_url = os.environ.get("AXL_URL", "(unset)")
print(f"[mcp-cucm-axl] v{v} starting", file=sys.stderr, flush=True) print(f"[mcaxl] 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] AXL_URL={axl_url}", file=sys.stderr, flush=True)
def main() -> None: def main() -> None:
@ -532,7 +523,7 @@ def main() -> None:
cache_dir = Path( cache_dir = Path(
os.environ.get("AXL_CACHE_DIR") 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) cache_dir.mkdir(parents=True, exist_ok=True)
ttl = int(os.environ.get("AXL_CACHE_TTL", "3600")) ttl = int(os.environ.get("AXL_CACHE_TTL", "3600"))
@ -548,7 +539,7 @@ def main() -> None:
cluster_id=cluster_id, cluster_id=cluster_id,
) )
print( print(
f"[mcp-cucm-axl] cache: {_cache.db_path} " f"[mcaxl] cache: {_cache.db_path} "
f"(ttl={ttl}s, cluster_id={cluster_id})", f"(ttl={ttl}s, cluster_id={cluster_id})",
file=sys.stderr, file=sys.stderr,
flush=True, flush=True,

View File

@ -2,7 +2,7 @@
Resolution chain: Resolution chain:
1. AXL_WSDL_PATH env var (explicit override) use it 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: 3. Auto-extract `schema/15.0/` from a Cisco AXL Toolkit zip:
- $AXL_WSDL_ZIP if set - $AXL_WSDL_ZIP if set
- ./axlsqltoolkit.zip in the current working directory - ./axlsqltoolkit.zip in the current working directory
@ -28,8 +28,8 @@ WSDL_VERSION = "15.0"
def cache_wsdl_dir(version: str = WSDL_VERSION) -> Path: def cache_wsdl_dir(version: str = WSDL_VERSION) -> Path:
"""Return ~/.cache/mcp-cucm-axl/wsdl/<version>/.""" """Return ~/.cache/mcaxl/wsdl/<version>/."""
return Path(user_cache_dir("mcp-cucm-axl")) / "wsdl" / version return Path(user_cache_dir("mcaxl")) / "wsdl" / version
def _has_complete_wsdl(directory: Path) -> bool: 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) target.mkdir(parents=True, exist_ok=True)
print( print(
f"[mcp-cucm-axl] extracting AXL schema {version} from {zip_path}", f"[mcaxl] extracting AXL schema {version} from {zip_path}",
file=sys.stderr, file=sys.stderr,
flush=True, flush=True,
) )
@ -71,7 +71,7 @@ def _try_extract_from_zip(version: str = WSDL_VERSION) -> Path | None:
member = f"schema/{version}/{fname}" member = f"schema/{version}/{fname}"
if member not in zf.namelist(): if member not in zf.namelist():
print( print(
f"[mcp-cucm-axl] zip missing member: {member}", f"[mcaxl] zip missing member: {member}",
file=sys.stderr, file=sys.stderr,
flush=True, flush=True,
) )
@ -80,13 +80,13 @@ def _try_extract_from_zip(version: str = WSDL_VERSION) -> Path | None:
(target / fname).write_bytes(data) (target / fname).write_bytes(data)
extracted.append(fname) extracted.append(fname)
print( print(
f"[mcp-cucm-axl] extracted {len(extracted)} files into {target}", f"[mcaxl] extracted {len(extracted)} files into {target}",
file=sys.stderr, file=sys.stderr,
flush=True, flush=True,
) )
return target return target
except (zipfile.BadZipFile, OSError) as e: 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 return None
@ -106,7 +106,7 @@ def resolve_wsdl_path() -> Path:
if not _has_complete_wsdl(p.parent): if not _has_complete_wsdl(p.parent):
missing = [f for f in WSDL_FILES if not (p.parent / f).exists()] missing = [f for f in WSDL_FILES if not (p.parent / f).exists()]
print( 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.", f"zeep may fail to resolve schema imports.",
file=sys.stderr, file=sys.stderr,
flush=True, flush=True,

View File

@ -1,5 +0,0 @@
"""mcp-cucm-axl — read-only MCP server for CUCM 15 AXL."""
from .server import main
__all__ = ["main"]

View File

@ -5,7 +5,7 @@ from pathlib import Path
import pytest import pytest
from mcp_cucm_axl.cache import AxlCache from mcaxl.cache import AxlCache
@pytest.fixture @pytest.fixture

View File

@ -11,8 +11,8 @@ from pathlib import Path
import pytest import pytest
from mcp_cucm_axl.cache import AxlCache from mcaxl.cache import AxlCache
from mcp_cucm_axl.client import AxlClient from mcaxl.client import AxlClient
@pytest.fixture @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. # Force the zeep Client constructor inside _ensure_connected to raise.
# This simulates "WSDL fetch failed", "TLS handshake error", etc. — # This simulates "WSDL fetch failed", "TLS handshake error", etc. —
# transient operational failures. # transient operational failures.
from mcp_cucm_axl import client as client_mod from mcaxl import client as client_mod
def boom(*args, **kwargs): def boom(*args, **kwargs):
raise ConnectionError("simulated transient network failure") 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_PASS", "test")
monkeypatch.setenv("AXL_VERIFY_TLS", "false") monkeypatch.setenv("AXL_VERIFY_TLS", "false")
# Stub Client construction so we exercise only the session/retry setup # 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 = {} constructed = {}
@ -126,7 +126,7 @@ def test_retry_config_overridable_via_env(cache: AxlCache, monkeypatch):
monkeypatch.setenv("AXL_PASS", "test") monkeypatch.setenv("AXL_PASS", "test")
monkeypatch.setenv("AXL_RATE_LIMIT_RETRIES", "7") 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"))) monkeypatch.setattr(client_mod, "Client", lambda *a, **kw: (_ for _ in ()).throw(ConnectionError("stub")))
client = AxlClient(cache) client = AxlClient(cache)
@ -144,7 +144,7 @@ def test_retry_config_zero_disables(cache: AxlCache, monkeypatch):
monkeypatch.setenv("AXL_PASS", "test") monkeypatch.setenv("AXL_PASS", "test")
monkeypatch.setenv("AXL_RATE_LIMIT_RETRIES", "0") 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"))) monkeypatch.setattr(client_mod, "Client", lambda *a, **kw: (_ for _ in ()).throw(ConnectionError("stub")))
client = AxlClient(cache) client = AxlClient(cache)

View File

@ -14,7 +14,7 @@ state and act on it.
import pytest import pytest
from mcp_cucm_axl.route_plan import find_devices_using_css from mcaxl.route_plan import find_devices_using_css
class FakeAxlClient: 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 # referenced via these columns showed up as "0 references" — operator running
# impact analysis would conclude safe-to-delete and break outbound transforms. # 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(): def test_issue_1_cgpntransform_column_enumerated():

View File

@ -5,7 +5,7 @@ from pathlib import Path
import pytest import pytest
from mcp_cucm_axl.docs_loader import DocsIndex from mcaxl.docs_loader import DocsIndex
@pytest.fixture @pytest.fixture

View File

@ -2,7 +2,7 @@
import pytest import pytest
from mcp_cucm_axl.normalize import ( from mcaxl.normalize import (
BOOLEAN_COLUMNS, BOOLEAN_COLUMNS,
normalize_bool, normalize_bool,
normalize_row, normalize_row,

View File

@ -10,8 +10,8 @@ from pathlib import Path
import pytest import pytest
from mcp_cucm_axl import prompts from mcaxl import prompts
from mcp_cucm_axl.docs_loader import DocsIndex from mcaxl.docs_loader import DocsIndex
@pytest.fixture @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 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).""" shim wasn't (the prompt would be invisible to the LLM)."""
import asyncio import asyncio
from mcp_cucm_axl import server from mcaxl import server
async def _list(): async def _list():
registered = await server.mcp.list_prompts() registered = await server.mcp.list_prompts()

View File

@ -9,7 +9,7 @@ import xml.etree.ElementTree as ET
import pytest import pytest
from mcp_cucm_axl.risport import ( from mcaxl.risport import (
DEVICE_STATUS_VALUES, DEVICE_STATUS_VALUES,
RisPortClient, RisPortClient,
_build_select_envelope, _build_select_envelope,

View File

@ -10,7 +10,7 @@ raise RuntimeError when their dependencies aren't initialized.
import pytest import pytest
from mcp_cucm_axl import server from mcaxl import server
def test_cache_stats_raises_when_uninitialized(monkeypatch): def test_cache_stats_raises_when_uninitialized(monkeypatch):

View File

@ -2,7 +2,7 @@
import pytest import pytest
from mcp_cucm_axl.sql_validator import validate_select, SqlValidationError from mcaxl.sql_validator import validate_select, SqlValidationError
class TestSelectAccepted: class TestSelectAccepted:

View File

@ -10,7 +10,7 @@ doesn't silently rewrite real-zero into "no value."
import sys import sys
from io import StringIO 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(): def test_to_int_passthrough_normal():

View File

@ -2,7 +2,7 @@
import pytest 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: class TestLiteralPatterns:

54
uv.lock generated
View File

@ -792,33 +792,8 @@ wheels = [
] ]
[[package]] [[package]]
name = "mcp" name = "mcaxl"
version = "1.27.0" version = "2026.4.27"
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"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "fastmcp" }, { name = "fastmcp" },
@ -846,6 +821,31 @@ requires-dist = [
] ]
provides-extras = ["test"] 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]] [[package]]
name = "mdurl" name = "mdurl"
version = "0.1.2" version = "0.1.2"