Skip to main content
Glama
Josh-E-S
by Josh-E-S
README.md
<p align="center">
  <img src="https://raw.githubusercontent.com/Josh-E-S/pexip-mgmt-mcp/main/.github/banner.png" alt="Infinity MCP" width="100%">
</p>

<h1 align="center">
MCP Server for Pexip Infinity
</h1>

[![CI](https://github.com/Josh-E-S/pexip-mgmt-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/Josh-E-S/pexip-mgmt-mcp/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)

<!-- mcp-name: io.github.josh-e-s/pexip-mgmt-mcp -->

MCP (Model Context Protocol) server for the **Pexip Infinity Management API**. It
gives an LLM-based agent everything it needs to manage and operate a Pexip
Infinity deployment, exposing **122 curated tools** and covering
all four admin API categories: Configuration, Status, History, and Command.

> **Disclaimer:** This is an independent, community-built project. It is **not
> affiliated with, endorsed by, or sponsored by Pexip**. It uses Pexip's public
> Management API and sends **no telemetry or analytics**. The server needs **no
> external network access** beyond the Pexip Management Node you configure, plus
> your own identity provider if you choose to use OIDC. It never sends your data
> anywhere else.

## Quick start

You'll need your Pexip **Management Node hostname** plus credentials. **OAuth2 is
recommended for production**; **basic auth** (admin username + password) is
recommended only for **dev/lab** environments. See
[Configuration](#configuration) to set up an OAuth2 client.

The server starts **read-only** by default: it can list and report, but every
create / update / delete / control tool is removed from the catalog until you
explicitly enable writes. Connect first, confirm it works, then decide.

### ① Claude Desktop: one-click install

The simplest path: no terminal, no JSON.

1. **Download** the `.mcpb` bundle for your OS (**macOS** or **Windows**) from the
   [latest release](https://github.com/Josh-E-S/pexip-mgmt-mcp/releases/latest).
2. **Double-click** it. Claude Desktop opens an install dialog showing the server
   and the permissions it requests.
3. **Fill in the form** with your Management Node **host** and either an **OAuth2
   client** (recommended) or an **admin username + password** (dev/lab only).
   Leave **read-only** on. Click install.
4. **Try it.** Ask Claude: _"List the VMRs on my Pexip node"_ or _"Show me the
   Pexip system status."_

To enable writes later, edit the server in Claude Desktop's settings and turn
**read-only** off.

<p align="center">
  <img src=".github/example-mcpb-config.png" alt="Claude Desktop Extensions settings for pexip-mgmt-mcp, showing the Management Node host, Auth mode, basic and OAuth2 credential fields, Verify TLS and Read-only toggles, and the per-tool permissions list" width="640">
</p>

### ② Manual config: Claude Code & other MCP hosts

For Claude Code, Cursor, or any MCP host that takes a JSON server config.

**1. Install the server** (needs Python 3.11+):

```bash
git clone https://github.com/Josh-E-S/pexip-mgmt-mcp.git
cd pexip-mgmt-mcp
pipx install .          # installs the `pexip-mgmt-mcp` command on your PATH
```

**2. Add it to your MCP host** by pointing its config at the `pexip-mgmt-mcp`
command. Use **OAuth2** for production:

```json
{
  "mcpServers": {
    "pexip-mgmt": {
      "command": "pexip-mgmt-mcp",
      "env": {
        "PEXIP_HOST": "pexip-mgr.example.com",
        "PEXIP_AUTH_MODE": "oauth2",
        "PEXIP_OAUTH2_CLIENT_ID": "your-client-id",
        "PEXIP_OAUTH2_PRIVATE_KEY": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
      }
    }
  }
}
```

For **dev/lab**, use basic auth instead: swap the `PEXIP_AUTH_MODE` /
`PEXIP_OAUTH2_*` keys for `"PEXIP_USERNAME": "admin"` and `"PEXIP_PASSWORD": "..."`.
See [Configuration](#configuration) for all options.

- **Enable writes:** add `"PEXIP_READ_ONLY": "false"` to `env` (read-only is the
  default).
- **Claude Code shortcut** (instead of hand-editing JSON):
  ```bash
  # OAuth2 (recommended for production)
  claude mcp add pexip-mgmt \
    -e PEXIP_HOST=pexip-mgr.example.com \
    -e PEXIP_AUTH_MODE=oauth2 \
    -e PEXIP_OAUTH2_CLIENT_ID=your-client-id \
    -e PEXIP_OAUTH2_PRIVATE_KEY="$(cat oauth2_private_key.pem)" \
    -- pexip-mgmt-mcp

  # basic auth (dev/lab only)
  claude mcp add pexip-mgmt \
    -e PEXIP_HOST=pexip-mgr.example.com \
    -e PEXIP_USERNAME=admin \
    -e PEXIP_PASSWORD=... \
    -- pexip-mgmt-mcp
  ```

<!-- Screenshot: Claude Code /mcp showing pexip-mgmt connected -->

### Other ways to run

- **Docker (self-hosted HTTP transport):** run the container alongside your
  Infinity, serving on `127.0.0.1:8000`. For **lab/dev**, fronting it with a
  tunnel or proxy is fine. For **production**, run it as a proper service behind
  a hardened reverse proxy (TLS, authentication, access control) on a managed
  host, not a dev tunnel. See [DEPLOY.md](DEPLOY.md).
- **From source (developers):** clone the repo, then:
  ```bash
  pip install -e .
  cp .env.example .env          # edit with your Management Node details
  python -m pexip_mcp --healthcheck
  # OK: connected to pexip-mgr.example.com as admin, schema fetched
  ```
  See [Testing](#testing) for the full dev workflow.

## Contents

- [Quick start](#quick-start)
- [Coverage](#coverage)
- [How it fits together](#how-it-fits-together)
- [Examples](#examples)
- [Configuration](#configuration)
- [Quality: the eval suite](#quality-the-eval-suite)
- [Skills SDK](#skills-sdk)
- [Testing](#testing)

## Coverage

| API | Tools | What it covers |
|---|---|---|
| **Configuration** | 46 | VMRs + aliases, end users, devices, gateway rules, automatic participants, IVR themes, LDAP sync, locations + Conferencing Nodes, global settings, schema introspection, plus **generic CRUD** (`list/get/create/update/delete_resource`) over ~70 registry resources: SIP/H.323/MS-SIP proxies, TURN/STUN, Teams Connectors, Azure tenants, Google Meet tokens, MJX (endpoints, integrations, deployments), DNS/NTP/SMTP/syslog/SNMP, certificates (CA/TLS/CSR), admin roles + identity providers, backups + upgrades, web app hosting, policy profiles, and more |
| **Status** | 39 | Live conferences + per-node shards, participants + media streams, per-participant call quality, registrations, node/location status + load stats, backplanes, alarms, licensing, cloud overflow, Exchange scheduler, MJX endpoints + meetings, Teams Connector nodes + calls |
| **History** | 14 | Conference + participant CDRs, server-side aggregation (`summarize_calls`), alarm history, backplane history, registration history, node event history |
| **Command** | 23 | Active call control: dial, disconnect, mute/unmute (participant or all guests), set role, lock/unlock, transfer, layout, messages, LDAP sync, provisioning emails, backup create/restore, certificate import, snapshot, upgrade |

The full catalog with per-tool annotations and parameters is in
[TOOLS.md](TOOLS.md) (regenerate with `uv run python scripts/generate_tools_md.py`).

**This MCP server is designed to optimize tool use.** High-traffic resources
(VMRs, users, devices, rules) get dedicated typed tools; the ~70 remaining
configuration resources share five generic CRUD tools backed by a resource
registry. That keeps the catalog at 122 tools, which lowers per-request token use
and improves the agent's tool selection (verified by the
[eval suite](#quality-the-eval-suite)).

## How it fits together

<!-- Diagram source: .github/architecture.mmd
     Regenerate: npx -y @mermaid-js/mermaid-cli -i .github/architecture.mmd -o .github/architecture.png -b white -s 2 -->
<p align="center">
  <img src=".github/architecture.png" alt="An MCP client talks to pexip-mgmt-mcp over stdio or HTTP; the server resolves names, enforces read-only mode, and calls the four Pexip Management APIs: Configuration, Status, History, and Command" width="100%">
</p>

Source layout:

```
src/pexip_mcp/
├── client.py                 # Async PexipClient (all 4 API categories), 429-aware retry
├── config.py                 # PexipSettings (env-driven, pydantic-settings)
├── mcp_app.py                # FastMCP instance + lifespan
├── server.py                 # Imports tool modules to trigger registration
├── __main__.py               # Entry point + --healthcheck + --http
└── tools/
    ├── _helpers.py           # get_client, resolve_id_by_field, paginate_all, annotation presets
    ├── command.py            # Active call control + name→UUID resolvers
    ├── status.py             # Live state + per-participant quality
    ├── history.py            # CDRs + summarize_calls aggregation
    ├── resource_crud.py      # Generic CRUD + resource registry (~70 resources)
    ├── schema.py             # Live schema introspection
    └── conference.py, end_user.py, device.py, gateway_rule.py, alias.py,
        automatic_participant.py, infrastructure.py, ldap.py,
        ivr_theme.py, global_settings.py   # dedicated typed CRUD per resource
```

## Examples

Real Claude Desktop conversations against a lab Pexip node.

**A status question, answered directly:**

<p align="center">
  <img src=".github/example-call-summary.png" alt="Claude Desktop: user asks how many calls this week; Claude reports 12 calls, all inbound, with total and average duration, and offers to break it down further" width="800">
</p>

**Config inventory across multiple resources in one ask:**

<p align="center">
  <img src=".github/example-inventory.png" alt="Claude Desktop: user asks for a count and details of VMRs, users, and devices; Claude lists each VMR with aliases, description, tag, and view settings" width="800">
</p>

**CDRs turned into an interactive dashboard, not a data dump:**

<p align="center">
  <img src=".github/example-dashboard.png" alt="Claude Desktop: user asks for interactive graphs of Infinity usage; Claude builds a dashboard with stat cards, a call-quality donut chart, a disconnect-reasons bar chart, a call timeline, and a written analysis of the traffic pattern" width="800">
</p>

## Configuration

All env vars are `PEXIP_*` prefixed and loaded from `.env` (or the process
environment). See [.env.example](.env.example) for an annotated template.

| Variable | Default | Purpose |
|---|---|---|
| `PEXIP_HOST` | required | Management Node hostname or IP, no scheme |
| `PEXIP_AUTH_MODE` | `basic` | `oauth2` (recommended for production) or `basic` (dev/lab) |
| `PEXIP_OAUTH2_CLIENT_ID` | oauth2 only | OAuth2 Client ID from the Pexip UI |
| `PEXIP_OAUTH2_PRIVATE_KEY` | oauth2 only | OAuth2 client "Private key" (ES256, shown once) |
| `PEXIP_OAUTH2_TOKEN_URL` | `https://<host>/oauth/token/` | Override the token endpoint |
| `PEXIP_OAUTH2_SCOPE` | `is_admin use_api` | OAuth2 scopes requested |
| `PEXIP_USERNAME` | basic only | Admin username (typically `admin`) |
| `PEXIP_PASSWORD` | basic only | Admin password |
| `PEXIP_VERIFY_TLS` | `true` | Set `false` for self-signed lab nodes |
| `PEXIP_TIMEOUT` | `30` | HTTP timeout in seconds |
| `PEXIP_MAX_RETRIES` | `3` | Retries on 429 rate-limit responses |
| `PEXIP_READ_ONLY` | `true` | Expose only read tools; remove all write/control tools. **On by default;** set `false` to enable writes |
| `PEXIP_ALLOW_SECURITY_RESOURCES` | `false` | Allow generic CRUD to mutate security-critical resources (SSH keys, roles, auth, certs). Only relevant when writes are enabled |
| `PEXIP_ALLOW_PLATFORM_TOOLS` | `false` | Expose platform-lifecycle command tools (backup/restore, upgrade, cert import, software upload, cloud-node start, snapshot). Removed at startup unless enabled, even with writes on |
| `PEXIP_MCP_AUTH_MODE` | `token` | How HTTP clients authenticate to this server: `token` (static bearer) or `oauth` (OIDC). Only affects `--http`. See [docs/identity.md](docs/identity.md) |
| `PEXIP_MCP_TOKEN` | unset | Bearer token for the `--http` transport (required for non-loopback binds; min 32 chars). Run `pexip-mgmt-mcp --generate-token` |
| `PEXIP_OIDC_ISSUER` | unset | Issuer URL; required when `PEXIP_MCP_AUTH_MODE=oauth` (validate OIDC JWTs from your own IdP: Entra/Google/Okta/on-prem) |
| `PEXIP_OIDC_AUDIENCE` | unset | Expected token audience; required when `PEXIP_MCP_AUTH_MODE=oauth` |
| `PEXIP_OIDC_REQUIRED_SCOPES` | unset | Space-separated scopes the JWT must carry (optional) |
| `PEXIP_OIDC_JWKS_URI` | discovered | Override the JWKS endpoint (default: discovered from the issuer) |

### Read-only mode (default)

The server runs in **read-only mode by default**: only the read tools
(list / get / schema) are exposed. Every create, update, delete, and Command-API
control tool is removed from the catalog at startup, so the LLM cannot mutate the
deployment even if it tries. This is enforced server-side: the tools are gone
from the catalog, not merely flagged with an advisory `readOnlyHint`.

To enable the mutating admin surface, set `PEXIP_READ_ONLY=false` (logged loudly
at startup). Even then, generic CRUD refuses to touch security-critical
resources (SSH keys, admin roles/permissions, authentication/SSO, TLS/CA certs)
unless you also set `PEXIP_ALLOW_SECURITY_RESOURCES=true`. Pair writes with a
least-privilege Pexip Administrator Role for defense in depth.

### Authentication: OAuth2 vs basic

Two modes, selected by `PEXIP_AUTH_MODE`:

- **`oauth2`** (recommended for production): an OAuth2 **JWT bearer assertion**
  (ES256). The server signs a short-lived JWT with the client's private key and
  exchanges it at `https://<host>/oauth/token/` for a 1-hour bearer token (cached
  and auto-refreshed). It authenticates once per hour instead of on every request
  and keeps a reusable admin password out of the server's environment.
- **`basic`** (dev/lab only): a local Management Node admin username + password.
  Works out of the box on every Infinity deployment and is the quickest way to
  get started, but sends admin credentials on each request. Not recommended for
  production.

**Set up an OAuth2 client** in the Pexip admin UI under **Users & Devices >
OAuth2 Clients**: add a client, then copy its **Client ID** and **Private key**
(the private key is shown only once). You also enable Management API OAuth2 and
attach an Administrator Role, per Pexip's
[Managing API access via OAuth2](https://docs.pexip.com/admin/managing_API_oauth.htm).
Then set `PEXIP_AUTH_MODE=oauth2`, `PEXIP_OAUTH2_CLIENT_ID`, and
`PEXIP_OAUTH2_PRIVATE_KEY`.

<p align="center">
  <img src=".github/oauth2-client-setup.png" alt="Pexip admin UI: Users & Devices > OAuth2 Clients, showing the Client ID, Client name, Role, and Private key fields" width="640">
</p>

## Quality: the eval suite

The [`evals/`](evals/) suite measures whether an LLM can drive these tools
correctly from natural language. It defines **156 scenarios** written as real
admin requests ("mute all the guests in AllHands", "point the syslog server at
10.0.0.99"), graded automatically across three layers:

| Layer | What runs | Cost |
|---|---|---|
| Deterministic (332 checks) | Every eval case is validated against the live tool registry (tools exist, parameter names match signatures) | free, in CI |
| LLM-graded (`--llm`) | Each prompt goes to Claude with the full tool catalog; multi-turn conversations with mocked API responses; graded on tool choice, parameters, and chain order | ~$2 / full run (tool catalog is prompt-cached) |
| Live (`--live`, 33 tests) | CRUD, status reads, and call commands against a real Infinity node, including auto-dialing a test call and moderating it by name | free (your lab) |

Scoring supports exact / subset / ordered-subset / any-of tool matching,
per-step parameter checks with acceptable-alternative values, and optional
steps for chains where server-side name resolution makes a lookup legitimate
but unnecessary. See [evals/README.md](evals/README.md) for the case format
and how to add scenarios.

Current state: the full suite passes: **698 passed, 12 skipped, 0 failures**
across all four layers (208 unit + 332 deterministic eval checks + LLM-graded +
33 live), 86% coverage. The 12 skips are env-gated live cases (e.g. no dial
target set). Reproduce the full run with `uv run pytest tests/ evals/ --llm
--live`, or the free deterministic subset (540 checks) with
`uv run pytest tests/ evals/`.

## Skills SDK

[`pexip-mgmt-skills/`](./pexip-mgmt-skills/) is a self-contained
[Agent Skills](https://agentskills.io) (open standard) + Claude Code plugin
package that wraps this MCP server with operator runbooks and
developer-reference skills. Built to be extractable: `cp -r pexip-mgmt-skills/`
out and you have a complete, plug-installable SDK that loads in Claude Code,
Gemini CLI, Codex CLI, Cursor, or any other compliant host.

Currently ships 9 skills across 6 domains:

| Domain | Skill | Audience |
|---|---|---|
| router | `pexip-mgmt-intake` | both, start here for open-ended requests |
| operations | `pexip-operations` | operator: kick / lock / report / configure |
| management-api | `pexip-config-api` | developer: modify Configuration API tool code |
| management-api | `pexip-status-api` | developer: Status API |
| management-api | `pexip-history-api` | developer: History API |
| management-api | `pexip-command-api` | developer: Command API |
| events | `pexip-event-sinks` | both, configure Pexip's webhook push-event destinations |
| policy | `pexip-external-policy` | developer: external policy server config (via generic CRUD) |
| room-integration | `pexip-mjx` | both, One-Touch Join |

See [`pexip-mgmt-skills/README.md`](./pexip-mgmt-skills/README.md) for the
install instructions and
[`pexip-mgmt-skills/ARCHITECTURE.md`](./pexip-mgmt-skills/ARCHITECTURE.md)
for the design rules. The companion
[awesome-pexip-skills](https://github.com/Josh-E-S/awesome-pexip-skills)
covers the **client-side** (webapp3, `@pexip/infinity`, `@pexip/media`).
Install both for full Pexip coverage.

## Testing

```bash
uv run pytest                  # 208 unit + 332 deterministic eval checks, 86% coverage
uv run pytest tests/           # 208 unit tests (mocked HTTP via respx), ~2s
uv run pytest evals/           # 332 deterministic eval checks, free
uv run pytest evals/ --llm     # LLM-graded evals (needs ANTHROPIC_API_KEY, ~$2)
uv run pytest evals/ --live    # integration against a real node (needs .env)
uv run ruff check src tests evals
```

Every run prints a coverage report, but the **80% gate is enforced only in CI on the
full suite** (`--cov-fail-under=80` in `.github/workflows/ci.yml`), so subset runs
like `uv run pytest evals/` show partial coverage without failing. Unit tests mock
the Pexip Management API with [respx](https://github.com/lundberg/respx); the retry
suite monkeypatches `asyncio.sleep` so backoff tests run instantly.

## License

[MIT](LICENSE)

TDQS

B3.2/5.0

Scored across 76 tools

Disambiguation4/5

Tools clearly distinguish between active and historical states, and different resource types (participants vs conferences vs nodes). However, the high number of similar get/list pairs (e.g., get_node_statistics vs get_node_status) could confuse agents, though descriptions help.

Naming Consistency5/5

Almost all tools follow a consistent verb_noun pattern with get_ and list_ prefixes in snake_case. Exceptions like summarize_calls and get_resource_schema are minor and still descriptive. No mixing of conventions.

Tool Count2/5

With 76 tools, the count far exceeds the 25+ threshold considered 'too many.' While the domain is complex, this many tools risks overwhelming agents and increases cognitive load. A more curated set or consolidated tools could improve usability.

Completeness4/5

The tool set covers a wide range of read and monitoring operations across conferences, participants, nodes, configurations, and history. The catch-all list_resources fills many gaps. Missing write operations (create/update/delete) are acceptable given the read-only intent, but a few minor read gaps exist (e.g., no get_alarm for active alarms).

Maintenance

ActivityStale
ResponsivenessNo issues