diff --git a/.mcp.json b/.mcp.json index ede9adf..7dde4b6 100644 --- a/.mcp.json +++ b/.mcp.json @@ -6,7 +6,7 @@ "run", "--directory", "/home/rpm/bingham/axl", - "mcp-cucm-axl" + "mcaxl" ] } } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..943787d --- /dev/null +++ b/CHANGELOG.md @@ -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` +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."* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9957d3a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Ryan Malloy + +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. diff --git a/README.md b/README.md index 26f66ba..8cbba9c 100644 --- a/README.md +++ b/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: +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__`: - `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) diff --git a/docs/query-patterns/sip-trunk-report.md b/docs/query-patterns/sip-trunk-report.md index 5ece40e..789eaa5 100644 --- a/docs/query-patterns/sip-trunk-report.md +++ b/docs/query-patterns/sip-trunk-report.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`). diff --git a/pyproject.toml b/pyproject.toml index 6ed034d..9a0e0ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 diff --git a/src/mcaxl/__init__.py b/src/mcaxl/__init__.py new file mode 100644 index 0000000..288247b --- /dev/null +++ b/src/mcaxl/__init__.py @@ -0,0 +1,5 @@ +"""mcaxl — read-only MCP server for CUCM 15 AXL.""" + +from .server import main + +__all__ = ["main"] diff --git a/src/mcp_cucm_axl/cache.py b/src/mcaxl/cache.py similarity index 100% rename from src/mcp_cucm_axl/cache.py rename to src/mcaxl/cache.py diff --git a/src/mcp_cucm_axl/client.py b/src/mcaxl/client.py similarity index 98% rename from src/mcp_cucm_axl/client.py rename to src/mcaxl/client.py index f13e573..941d866 100644 --- a/src/mcp_cucm_axl/client.py +++ b/src/mcaxl/client.py @@ -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, ) diff --git a/src/mcp_cucm_axl/docs_loader.py b/src/mcaxl/docs_loader.py similarity index 81% rename from src/mcp_cucm_axl/docs_loader.py rename to src/mcaxl/docs_loader.py index 227de50..78b22d4 100644 --- a/src/mcp_cucm_axl/docs_loader.py +++ b/src/mcaxl/docs_loader.py @@ -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, ) diff --git a/src/mcp_cucm_axl/normalize.py b/src/mcaxl/normalize.py similarity index 100% rename from src/mcp_cucm_axl/normalize.py rename to src/mcaxl/normalize.py diff --git a/src/mcp_cucm_axl/prompts/__init__.py b/src/mcaxl/prompts/__init__.py similarity index 90% rename from src/mcp_cucm_axl/prompts/__init__.py rename to src/mcaxl/prompts/__init__.py index 7c72f30..ad06555 100644 --- a/src/mcp_cucm_axl/prompts/__init__.py +++ b/src/mcaxl/prompts/__init__.py @@ -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/.py` exporting a `render()`. +1. Create `src/mcaxl/prompts/.py` exporting a `render()`. 2. Re-export it below. 3. Add a thin `@mcp.prompt`-decorated shim in `server.py` that calls it. diff --git a/src/mcp_cucm_axl/prompts/_common.py b/src/mcaxl/prompts/_common.py similarity index 96% rename from src/mcp_cucm_axl/prompts/_common.py rename to src/mcaxl/prompts/_common.py index 8357355..b133476 100644 --- a/src/mcp_cucm_axl/prompts/_common.py +++ b/src/mcaxl/prompts/_common.py @@ -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._" ) diff --git a/src/mcp_cucm_axl/prompts/audit_routing.py b/src/mcaxl/prompts/audit_routing.py similarity index 100% rename from src/mcp_cucm_axl/prompts/audit_routing.py rename to src/mcaxl/prompts/audit_routing.py diff --git a/src/mcp_cucm_axl/prompts/cucm_sql_help.py b/src/mcaxl/prompts/cucm_sql_help.py similarity index 100% rename from src/mcp_cucm_axl/prompts/cucm_sql_help.py rename to src/mcaxl/prompts/cucm_sql_help.py diff --git a/src/mcp_cucm_axl/prompts/hunt_pilot_audit.py b/src/mcaxl/prompts/hunt_pilot_audit.py similarity index 100% rename from src/mcp_cucm_axl/prompts/hunt_pilot_audit.py rename to src/mcaxl/prompts/hunt_pilot_audit.py diff --git a/src/mcp_cucm_axl/prompts/inbound_did_audit.py b/src/mcaxl/prompts/inbound_did_audit.py similarity index 100% rename from src/mcp_cucm_axl/prompts/inbound_did_audit.py rename to src/mcaxl/prompts/inbound_did_audit.py diff --git a/src/mcp_cucm_axl/prompts/investigate_pattern.py b/src/mcaxl/prompts/investigate_pattern.py similarity index 100% rename from src/mcp_cucm_axl/prompts/investigate_pattern.py rename to src/mcaxl/prompts/investigate_pattern.py diff --git a/src/mcp_cucm_axl/prompts/phone_inventory_report.py b/src/mcaxl/prompts/phone_inventory_report.py similarity index 100% rename from src/mcp_cucm_axl/prompts/phone_inventory_report.py rename to src/mcaxl/prompts/phone_inventory_report.py diff --git a/src/mcp_cucm_axl/prompts/route_plan_overview.py b/src/mcaxl/prompts/route_plan_overview.py similarity index 96% rename from src/mcp_cucm_axl/prompts/route_plan_overview.py rename to src/mcaxl/prompts/route_plan_overview.py index 6bc19bb..07e8ce9 100644 --- a/src/mcp_cucm_axl/prompts/route_plan_overview.py +++ b/src/mcaxl/prompts/route_plan_overview.py @@ -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) diff --git a/src/mcp_cucm_axl/prompts/sip_trunk_report.py b/src/mcaxl/prompts/sip_trunk_report.py similarity index 100% rename from src/mcp_cucm_axl/prompts/sip_trunk_report.py rename to src/mcaxl/prompts/sip_trunk_report.py diff --git a/src/mcp_cucm_axl/prompts/user_audit.py b/src/mcaxl/prompts/user_audit.py similarity index 100% rename from src/mcp_cucm_axl/prompts/user_audit.py rename to src/mcaxl/prompts/user_audit.py diff --git a/src/mcp_cucm_axl/prompts/whoami.py b/src/mcaxl/prompts/whoami.py similarity index 100% rename from src/mcp_cucm_axl/prompts/whoami.py rename to src/mcaxl/prompts/whoami.py diff --git a/src/mcp_cucm_axl/risport.py b/src/mcaxl/risport.py similarity index 99% rename from src/mcp_cucm_axl/risport.py rename to src/mcaxl/risport.py index 8603ae7..9d7e3a1 100644 --- a/src/mcp_cucm_axl/risport.py +++ b/src/mcaxl/risport.py @@ -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, ) diff --git a/src/mcp_cucm_axl/route_plan.py b/src/mcaxl/route_plan.py similarity index 99% rename from src/mcp_cucm_axl/route_plan.py rename to src/mcaxl/route_plan.py index ca8a9c7..9db1e03 100644 --- a/src/mcp_cucm_axl/route_plan.py +++ b/src/mcaxl/route_plan.py @@ -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, diff --git a/src/mcp_cucm_axl/server.py b/src/mcaxl/server.py similarity index 96% rename from src/mcp_cucm_axl/server.py rename to src/mcaxl/server.py index 70e40e0..0ff194a 100644 --- a/src/mcp_cucm_axl/server.py +++ b/src/mcaxl/server.py @@ -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.`. The shims below are the +# Bodies live in `mcaxl.prompts.`. 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, diff --git a/src/mcp_cucm_axl/sql_validator.py b/src/mcaxl/sql_validator.py similarity index 100% rename from src/mcp_cucm_axl/sql_validator.py rename to src/mcaxl/sql_validator.py diff --git a/src/mcp_cucm_axl/wsdl_loader.py b/src/mcaxl/wsdl_loader.py similarity index 89% rename from src/mcp_cucm_axl/wsdl_loader.py rename to src/mcaxl/wsdl_loader.py index 98c7a6a..28c595c 100644 --- a/src/mcp_cucm_axl/wsdl_loader.py +++ b/src/mcaxl/wsdl_loader.py @@ -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//.""" - return Path(user_cache_dir("mcp-cucm-axl")) / "wsdl" / version + """Return ~/.cache/mcaxl/wsdl//.""" + 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, diff --git a/src/mcp_cucm_axl/__init__.py b/src/mcp_cucm_axl/__init__.py deleted file mode 100644 index 16dc8f1..0000000 --- a/src/mcp_cucm_axl/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""mcp-cucm-axl — read-only MCP server for CUCM 15 AXL.""" - -from .server import main - -__all__ = ["main"] diff --git a/tests/test_cache.py b/tests/test_cache.py index 335d99a..003e285 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -5,7 +5,7 @@ from pathlib import Path import pytest -from mcp_cucm_axl.cache import AxlCache +from mcaxl.cache import AxlCache @pytest.fixture diff --git a/tests/test_client_recovery.py b/tests/test_client_recovery.py index 3c31517..56c1193 100644 --- a/tests/test_client_recovery.py +++ b/tests/test_client_recovery.py @@ -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) diff --git a/tests/test_css_impact.py b/tests/test_css_impact.py index a64bb50..9f9ac33 100644 --- a/tests/test_css_impact.py +++ b/tests/test_css_impact.py @@ -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(): diff --git a/tests/test_docs_loader.py b/tests/test_docs_loader.py index 4a0b8bc..9780e02 100644 --- a/tests/test_docs_loader.py +++ b/tests/test_docs_loader.py @@ -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 diff --git a/tests/test_normalize.py b/tests/test_normalize.py index 6c462ac..57b0bbe 100644 --- a/tests/test_normalize.py +++ b/tests/test_normalize.py @@ -2,7 +2,7 @@ import pytest -from mcp_cucm_axl.normalize import ( +from mcaxl.normalize import ( BOOLEAN_COLUMNS, normalize_bool, normalize_row, diff --git a/tests/test_prompts_package.py b/tests/test_prompts_package.py index cd3245e..c6dcbfb 100644 --- a/tests/test_prompts_package.py +++ b/tests/test_prompts_package.py @@ -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() diff --git a/tests/test_risport.py b/tests/test_risport.py index 663b0ed..929afad 100644 --- a/tests/test_risport.py +++ b/tests/test_risport.py @@ -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, diff --git a/tests/test_server_consistency.py b/tests/test_server_consistency.py index 38a6e73..28672d8 100644 --- a/tests/test_server_consistency.py +++ b/tests/test_server_consistency.py @@ -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): diff --git a/tests/test_sql_validator.py b/tests/test_sql_validator.py index 505c130..32f5822 100644 --- a/tests/test_sql_validator.py +++ b/tests/test_sql_validator.py @@ -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: diff --git a/tests/test_to_int_diagnostic.py b/tests/test_to_int_diagnostic.py index 3c3f5dc..8f28cc2 100644 --- a/tests/test_to_int_diagnostic.py +++ b/tests/test_to_int_diagnostic.py @@ -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(): diff --git a/tests/test_wildcard.py b/tests/test_wildcard.py index d70d2b6..ddc7b0d 100644 --- a/tests/test_wildcard.py +++ b/tests/test_wildcard.py @@ -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: diff --git a/uv.lock b/uv.lock index 5249d8e..1a56fca 100644 --- a/uv.lock +++ b/uv.lock @@ -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"