Metadata-Version: 2.4
Name: mcp-webexcalling
Version: 0.2.1
Summary: MCP server for interacting with Webex Calling APIs
License: MIT
Project-URL: Homepage, https://github.com/lutztalk/mcp-webexcalling
Project-URL: Issues, https://github.com/lutztalk/mcp-webexcalling/issues
Keywords: mcp,webex,webex-calling,telephony,model-context-protocol
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Communications :: Telephony
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp<3,>=2.0.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: pydantic>=2.0.0
Requires-Dist: pydantic-settings>=2.0.0
Provides-Extra: keyring
Requires-Dist: keyring>=24.0.0; extra == "keyring"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: jsonschema>=4.0.0; extra == "dev"
Requires-Dist: ruff<0.17,>=0.16; extra == "dev"
Dynamic: license-file

# MCP Webex Calling Server

[![CI](https://github.com/lutztalk/mcp-webexcalling/actions/workflows/ci.yml/badge.svg)](https://github.com/lutztalk/mcp-webexcalling/actions/workflows/ci.yml)

An MCP (Model Context Protocol) server for administering **Webex Calling**. It
exposes **90 tools** over MCP stdio, so an AI assistant such as Claude Desktop
can query and manage your Webex Calling organization in natural language:

- *"What call queues do we have configured?"*
- *"Generate an activation code for user john@example.com"*
- *"Get call statistics for the last 24 hours"*

The server talks to the official Webex REST APIs with your access token. It is
a thin layer over those APIs: every tool maps to documented Webex
endpoints, and where the public API has limits (see
[API limitations](#webex-api-limitations-worth-knowing)), the docs and the tool
descriptions say so.

## Quickstart

Requirements: **Python 3.10+**. Setup walks you through the Webex side.

**1. Install**

Once released on PyPI, the one-line route is [uv](https://docs.astral.sh/uv/):

```bash
uvx mcp-webexcalling --version          # runs without installing anything
```

Or install from a clone (works today):

```bash
git clone https://github.com/lutztalk/mcp-webexcalling.git
cd mcp-webexcalling
python3 -m venv venv
source venv/bin/activate   # Windows: venv\Scripts\activate
python -m pip install -U pip   # editable installs need a reasonably new pip
pip install -e .
```

**2. Sign in**

```bash
mcp-webexcalling setup
```

It offers two routes:

- **Sign in properly (recommended).** It walks you through creating a private
  Webex app — it prints the exact redirect address and the exact permissions
  to tick — then opens your browser to approve it. Takes about five minutes,
  **once**. After that the server renews its own access and you never sign in
  again.
- **Quick trial.** Paste a personal access token from developer.webex.com.
  Fastest way to look around, but it **stops working after about 12 hours**.

Either way, setup checks the connection before it finishes and tells you who
you signed in as and whether you have admin rights.

**3. Connect your MCP client**

```bash
mcp-webexcalling print-config
```

That prints the exact JSON block for Claude Desktop (with the absolute path
already filled in) and the one-line `claude mcp add` command for Claude Code.
**No token goes in the configuration** — credentials live in your own config
directory, not in a file you might share.

Restart your client, then ask it to run **`test_connection`**.

**Something not working?**

```bash
mcp-webexcalling doctor
```

One line per check: version, where credentials came from, when they expire,
whether renewal is working, whether you have admin rights, and which
permissions are missing — each with the fix.

For a step-by-step walkthrough, see
[SETUP.md](https://github.com/lutztalk/mcp-webexcalling/blob/main/SETUP.md).
For example conversations, see
[USAGE.md](https://github.com/lutztalk/mcp-webexcalling/blob/main/USAGE.md).

### Installing in Claude Desktop without a terminal

Each release also ships a **`.mcpb` bundle** on the
[releases page](https://github.com/lutztalk/mcp-webexcalling/releases).
Download it and double-click: Claude Desktop shows an install dialog and, if
you want to try it with a pasted token, a field to type it into. To get the
sign-in that renews itself, run `mcp-webexcalling setup` afterwards — the
bundle reads the same credentials.

## What can it do?

The server registers exactly **90 tools**. The complete reference — every tool
with its arguments and description, generated directly from the code so it
cannot drift — is in **[docs/TOOLS.md](https://github.com/lutztalk/mcp-webexcalling/blob/main/docs/TOOLS.md)**. By category:

| Category | Tools | Highlights |
| --- | --- | --- |
| Diagnostics | 1 | `test_connection` health/permission check |
| Organizations & Locations | 7 | list/create/update/delete locations, per-location feature summary |
| Users | 9 | CRUD, search by display name or email, extensions, calling settings |
| Licenses | 5 | list licenses, assign/remove per user |
| Phone Numbers | 6 | list/lookup, assign to users or locations, find unassigned numbers |
| Devices | 11 | list/details, activation codes, create by MAC, re-home, remove |
| Call Queues | 8 | CRUD plus agent management (location-scoped) |
| Auto Attendants | 5 | CRUD (location-scoped) |
| Hunt Groups | 7 | CRUD plus member management (location-scoped) |
| Trunks & Call Park | 4 | trunk groups, call park extensions, per-user park monitoring |
| User Calling Features | 5 | call forwarding, simultaneous ring, feature bundle updates |
| Voicemail | 5 | per-user settings; the authenticated user's messages |
| Call Recordings | 2 | list and fetch recording metadata (Converged Recordings API) |
| Call History, CDRs & Analytics | 10 | detailed call records, statistics, per-user/queue/state breakdowns, CSV/JSON export |
| Webhooks | 5 | CRUD for webhook registrations |

## Webex API limitations worth knowing

These are constraints of the public Webex APIs, not bugs in this server. The
tools work within them and say so in their output:

- **Call history / analytics windows are short.** Every CDR-based tool
  (`get_call_history`, `get_call_detail_records`, all `*_statistics_*` and
  analytics tools) uses the [Detailed Call History
  feed](https://developer.webex.com/calling/docs/api/v1/reports-detailed-call-history/get-detailed-call-history),
  which only accepts time ranges **between 48 hours and 5 minutes ago** — you
  cannot pull last month's calls. Tools whose times are optional default
  omitted bounds into that window. An explicitly supplied out-of-window range
  (e.g. "last month") is **rejected** by most tools: the API's 400 comes back
  annotated with exactly which bound is out of range. The exceptions are the
  statistics tools `get_call_statistics_from_cdr`, `get_user_call_statistics`,
  and `get_call_statistics_by_state`, which **clamp** a supplied range into
  the window instead. The feed also requires the **"Webex Calling Detailed
  Call History API access"** role on the token's (admin) user, granted in
  Control Hub.
- **No real-time metrics or server-side export.** `get_real_time_call_metrics`
  and `export_call_records` aggregate the same CDR feed locally (the feed lags
  real time by at least 5 minutes); Webex has no public real-time metrics or
  export endpoint.
- **Devices cannot be reassigned in place.** The Devices API has no "move to
  another user" operation. `associate_device_to_user` performs the documented
  flow instead: delete the device, then generate an activation code for the
  new owner. Activation codes are short-lived (days, not months) — the
  response's `expiryTime` is authoritative.
- **Voicemail messages are me-scoped.** `list_voicemail_messages`,
  `get_voicemail_message`, and `delete_voicemail_message` operate on the
  **authenticated user's** mailbox only; the Webex `voiceMessages` API cannot
  target another user's messages. (Per-user voicemail *settings* are
  admin-manageable.)
- **Number search is inventory, not shopping.**
  `search_available_phone_numbers` lists unassigned numbers your organization
  **already owns** at a location; the public API cannot purchase new numbers.
- **Webhook tools manage registrations only.** They create/update/delete
  webhook subscriptions pointing at *your* HTTPS receiver. A stdio MCP server
  has no HTTP listener and does not itself receive events.

## Authentication & tokens

`mcp-webexcalling setup` handles all of this for you. This section explains
what it does and why, for anyone who wants to know or needs to do it by hand.

### Why you create your own Webex app

Webex has no secret-free public OAuth client: every token-issuing call
requires an app's client secret. A public repository cannot ship one, so
"sign in with Webex" using *our* app is impossible — everybody gets their own
app instead. `setup` makes that a guided three-minute task: it prints the
exact redirect address to paste and the exact permissions to tick, then opens
the browser to approve it.

### The three routes

| Route | Lifetime | Good for |
| --- | --- | --- |
| **Your own integration** (`setup`, recommended) | Renews itself indefinitely | Everyday use |
| **Personal access token** (`setup --trial`) | ~12 hours, no renewal | Evaluating the server |
| **Service App** | Renews itself; org-owned | Unattended use that must survive its author leaving |

Access tokens from an integration last about 14 days and refresh tokens about
90 — and **the 90-day clock resets every time it is used**, so a server that
refreshes itself keeps working indefinitely (see Webex's
[integrations guide](https://developer.webex.com/docs/integrations)). That
renewal is now built in; it used to require an external process.

Service Apps are supported in the sense that their tokens work here, but
`setup` does not automate them: they need a full administrator to authorize
them in Control Hub, and it is unconfirmed whether call-history scopes work
under one.

### When renewal stops working

Renewal is not literally forever. It ends if the sign-in goes unused for 90
days, an administrator revokes the app, or the authorizing user's account
changes. When that happens every tool returns a plain-language error telling
you to run `mcp-webexcalling setup` again, and `doctor` reports what failed.

### Permissions (scopes)

This is the canonical list, generated from
[`mcp_webexcalling/scopes.py`](https://github.com/lutztalk/mcp-webexcalling/blob/main/mcp_webexcalling/scopes.py),
which is also what `setup` prints and what `doctor` checks against — the three
cannot drift apart.

| Scope | Needed for |
| --- | --- |
| `spark-admin:organizations_read` | organization info and the test_connection admin probe |
| `spark-admin:locations_read` / `spark-admin:locations_write` | location tools |
| `spark-admin:people_read` / `spark-admin:people_write` | user tools, number assignment, license assignment |
| `spark-admin:telephony_config_read` / `spark-admin:telephony_config_write` | queues, auto attendants, hunt groups, phone numbers, trunks, call park, and per-user calling features |
| `spark-admin:devices_read` / `spark-admin:devices_write` | device tools, activation codes, MAC provisioning |
| `spark-admin:licenses_read` | license listing and assignment lookups |
| `spark:webhooks_read` / `spark:webhooks_write` | webhook tools |
| `spark-admin:calling_cdr_read` | call detail records, analytics, and every call-statistics tool — additionally requires the 'Webex Calling Detailed Call History API access' role on the authorizing admin in Control Hub — a role, not a scope |
| `spark:calls_read` / `spark:calls_write` | the voicemail message tools (list, get, delete) — these are user-context scopes: the voicemail message API reads the authorizing user's own mailbox, not the organization's |
| `spark-admin:recordings_read` / `spark-admin:recordings_write` | the call recording tools — a compliance officer uses spark-compliance:recordings_read / _write instead |

`setup` reports any permission you did not grant, and `doctor` re-checks them
by probing one endpoint per family, so a missed checkbox is named rather than
surfacing later as unexplained failures.

Admin scopes only work when the person approving is a **full or read-only
administrator** of the organization. If someone else installs the server, send
them the sign-in address `setup` prints so an administrator can approve it.

### A warning about bot tokens

**Webex bot tokens do not work for Webex Calling administration.** Bots cannot
be granted `spark-admin:*` scopes or admin roles, so `/telephony/config/*`,
org-wide `/people`, `/licenses`, and the CDR feed all return **403** with a
bot token. A bot is only useful here for the unrelated
[push-notification workflow](#github-actions-webex-push-notifications). Don't
put a bot token in `WEBEX_ACCESS_TOKEN` expecting the telephony tools to work.

### Where credentials live, and how to revoke them

`setup` writes them to a file in your own configuration directory
(`~/.config/mcp-webexcalling/credentials.json`, or `%APPDATA%` on Windows),
which you can override with `WEBEX_CONFIG_DIR`. On Unix the file is created
mode `0600` and is never even briefly world-readable. On Windows those
permission bits do not exist; the file relies on the same per-user profile
permissions that protect the rest of `%APPDATA%`.

Be clear-eyed about what that file holds in the recommended mode: an access
token, a self-renewing refresh token, **and** the app's client secret. Anyone
who reads it can keep minting admin tokens. It protects against other local
users, casual file sharing, and backups of your MCP client's config — not
against malware running as you, or an unencrypted stolen disk. That is the
same posture as the `gcloud` and `aws` CLIs.

**Deleting the file does not cancel the access you granted.** Webex has no
public revoke endpoint, so to fully revoke:

```bash
mcp-webexcalling logout    # removes the local file and names the app to revoke
```

then delete (or regenerate the secret of) that integration at
[developer.webex.com/my-apps](https://developer.webex.com/my-apps), or have an
administrator remove its authorization in Control Hub under **Management →
Apps**. Do this whenever a laptop is lost or someone leaves the team.

### Precedence

The token is resolved in this order:

1. **`WEBEX_ACCESS_TOKEN` in the environment** — a client's `env` block, or
   your shell. Always wins, which keeps CI and scripted use predictable.
2. **The credential store** written by `setup` — the only source that renews
   itself.
3. **A `.env` file** — one in the working directory, else one beside the
   package (which only exists for an editable clone install).

Because the environment wins, a stale token left in a client's `env` block
will silently mask a working sign-in. `doctor` checks for exactly that and
says so.

## Reliability & robustness

Each of these is implemented in `mcp_webexcalling/webex_client.py` /
`config.py` and covered by tests:

- **Self-renewing sign-in** — with credentials from `setup`, the server
  refreshes its own access token before it expires and again if a call is
  rejected, so it keeps working without anyone re-authorizing it. Refreshes
  are serialised within the process and across processes with a lockfile, so
  several MCP clients sharing one sign-in cannot clobber each other's rotated
  token. A refresh that genuinely fails is recorded, so subsequent calls fail
  fast with an actionable message instead of hammering the token endpoint.
- **Flexible configuration** — credentials come from the environment, the
  credential store, or a `.env` file, in that order. The `env` block in
  `claude_desktop_config.json` works exactly as documented.
- **Automatic retries** — transient failures (HTTP 429, 5xx, network/timeout
  errors) are retried with exponential backoff and jitter, honoring the
  `Retry-After` header. Configurable via `WEBEX_MAX_RETRIES` /
  `WEBEX_RETRY_BACKOFF`.
- **Connection pooling** — one HTTP client is reused across all requests.
- **Automatic pagination** — list operations follow Webex `Link` headers, so
  `max_results` above the API's per-page cap returns the full set
  (`max_results=0` fetches everything available).
- **Concurrency-safe** — per-request base URLs (used for the analytics/CDR
  host) never mutate shared client state, so parallel tool calls don't
  interfere.
- **Actionable errors** — API errors carry their HTTP status and explanation;
  missing or wrongly-typed tool arguments are rejected with a clear message
  before any API call; `test_connection` diagnoses auth/permission problems.
- **Structured logging** — diagnostics go to stderr (configurable with
  `WEBEX_LOG_LEVEL`) and never corrupt the MCP stdio protocol on stdout.

## Using the client library directly

The underlying async client is usable on its own. This snippet is runnable as
written (see [examples/basic_usage.py](https://github.com/lutztalk/mcp-webexcalling/blob/main/examples/basic_usage.py) for a fuller,
guarded version — it calls the real Webex API, so it needs a real token):

```python
import asyncio

from mcp_webexcalling.webex_client import WebexClient


async def main() -> None:
    async with WebexClient(access_token="your_token") as client:
        # Who am I / does the token work?
        status = await client.test_connection()
        print(status)

        # List call queues
        queues = await client.list_call_queues()
        print(f"{len(queues)} call queues")

        # Call statistics — the CDR feed only accepts windows between
        # 48 hours and 5 minutes ago; omitted bounds default into that window.
        stats = await client.get_call_statistics_from_cdr()
        print(stats)

        # Generate a device activation code for a user
        code = await client.generate_activation_code(person_id="person_id_here")
        print(code["code"], "expires", code["expiryTime"])


asyncio.run(main())
```

`WebexClient` is an async context manager; if you don't use `async with`, call
`await client.aclose()` when done.

## Development

```bash
pip install -e ".[dev]"
pytest        # full test suite
ruff check .  # lint (config in pyproject.toml)
```

The test suite needs **no credentials and never touches the network**: unit
tests use `httpx.MockTransport`, and the end-to-end suite
(`tests/test_mcp_e2e.py`) spawns the real server as a subprocess over stdio,
pointed at a local mock Webex API, completes the MCP handshake, and invokes
**every one of the 90 tools** — with valid, missing, and wrongly-typed
arguments. The OAuth flow is exercised against a local loopback listener, and
every test runs with `WEBEX_CONFIG_DIR` pointed at a temporary directory, so
the suite can never read or write your real credentials.

CI (`.github/workflows/ci.yml`) runs lint + tests on Python 3.10–3.14 on
Linux, plus macOS and Windows, for every pull request and for pushes to
`main`/`master` (pushes to other branches don't trigger it). The extra
platforms matter here: file permissions, config paths, and the loopback
listener all behave differently off Linux.

To build the Claude Desktop bundle locally:

```bash
python scripts/build_bundle.py     # writes dist/mcp-webexcalling.mcpb
```

After adding or changing a tool, regenerate the tool reference:

```bash
python scripts/generate_tool_docs.py
```

Contributions are welcome — open a pull request. Please keep `pytest` and
`ruff check .` green, and remember that stdout is the MCP protocol channel:
any diagnostics must go to stderr.

## GitHub Actions: Webex push notifications

Separately from the MCP server, `.github/workflows/webex-notifications.yml`
posts a message to a Webex space when commits land on `main`. This is the one
place a **bot token** is the right tool. Setup: create a bot at
[developer.webex.com](https://developer.webex.com) (**My Apps** → **Bots**),
add the bot to the target space, then add repository secrets
`WEBEX_BOT_TOKEN` and `WEBEX_ROOM_ID`.

## License

MIT — see [LICENSE](https://github.com/lutztalk/mcp-webexcalling/blob/main/LICENSE).
