Skip to main content
Glama
ssolanky

cxone-wfm-intraday-mcp

by ssolanky
README.md
# CXone WFM Intraday MCP (Sparkathon Prototype)

An MCP server that exposes governed CXone WFM **Intraday** tools to an AI assistant.
It proves one Real-Time Analyst workflow — *"Why is Billing Support at risk today,
and what can we do to recover SLA?"* — grounded entirely in structured tool calls
over deterministic synthetic data. Simulation only; no live systems, no write-back.

Design spec: `docs/superpowers/specs/2026-07-15-cxone-wfm-intraday-mcp-design.md`.

## Tools

| Tool | Purpose |
|---|---|
| `list_intraday_metrics` | Catalog of queryable Intraday metrics |
| `resolve_intraday_entity` | Fuzzy name → canonical queue |
| `get_intraday_snapshot` | Current state of all/selected queues + highest-risk queue |
| `get_forecast_vs_actual` | Per-interval forecast vs actual variance |
| `get_intraday_risk_drivers` | Ranked reasons a queue is at risk |
| `get_available_capacity` | Donor queues with spare capacity + trade-offs |
| `simulate_recovery_action` | Before/after impact of moving N agents for Y minutes |
| `summarise_intraday_recommendation` | Plain-English what / why / action / impact / trade-offs |
| `load_intraday_data` | Replace the dataset with your own queues (bring-your-own-data) |
| `reset_intraday_data` | Restore the built-in demo dataset |

## Ways to run it

Pick the path that fits — all three end with the same 10 tools available to your assistant:

- **Fastest, no install** → [Quick start (uv)](#quick-start-no-clone-via-uv): one `uvx` command, nothing to clone.
- **Local dev / offline / to change the data** → [Setup](#setup): clone + venv + `pip install`.
- **Share via a URL (teammates install nothing)** → [Run as a remote HTTP MCP](#run-as-a-remote-http-mcp-optional): deploy to Cloud Run.

Then, in your assistant, ask *"Which queues are at risk right now?"* and follow the [Demo script](#demo-script).

## Quick start (no clone, via uv)

If you have [uv](https://docs.astral.sh/uv/) installed and access to this repo, register
the server with a single command — no `git clone`, no venv, no `pip install`. `uv` fetches,
installs, and runs it in an ephemeral environment straight from the repo.

**Claude Code:**
```bash
claude mcp add cxone-wfm-intraday -- uvx --from git+https://github.com/ssolanky/cxone-wfm-intraday-mcp cxone-wfm-intraday
```

**Claude Desktop** — add to `claude_desktop_config.json` (Settings → Developer → Edit Config):
```json
{
  "mcpServers": {
    "cxone-wfm-intraday": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/ssolanky/cxone-wfm-intraday-mcp", "cxone-wfm-intraday"]
    }
  }
}
```

Notes: this is a **local** server — `uv` runs it on your machine, it just removes the manual
clone/install steps. Because the repo is private, `uv` needs Git access to it (a credential
helper via `gh auth setup-git`, or a token). No `uv`? Use the full **Setup** below instead.

## Setup

```bash
git clone https://github.com/ssolanky/cxone-wfm-intraday-mcp.git
cd cxone-wfm-intraday-mcp
python -m venv .venv
# Windows:  .venv\Scripts\activate       macOS/Linux:  source .venv/bin/activate
python -m pip install -e ".[dev]"
python -m pytest -q          # all tests should pass (64 tests)
```

> **NICE machines:** `pip` here defaults to a private CodeArtifact index that 401s and
> lacks `mcp`. Install from public PyPI instead:
> `python -m pip install --index-url https://pypi.org/simple/ -e ".[dev]"`
> (or add `index-url = https://pypi.org/simple/` under `[global]` in `.venv/pip.ini`).

## Run against Claude

**Claude Code:**
```bash
claude mcp add cxone-wfm-intraday -- python -m cxone_wfm_intraday_mcp.server
```
(run from this project directory, using the interpreter where the package is installed).

**Claude Desktop:** copy the block from `claude_desktop_config.example.json` into your
`claude_desktop_config.json` (Settings → Developer → Edit Config), fixing `command`
to your Python interpreter, then restart Claude Desktop.

## Run as a remote HTTP MCP (optional)

The same server can serve the **Streamable HTTP** transport instead of stdio, so teammates
add just a URL — nothing installed locally. It's **zero config**: the server serves HTTP
automatically when a `PORT` env var is present (as on Google Cloud Run) or `MCP_TRANSPORT=http`
is set, and otherwise stays stdio for local use. HTTP endpoint path is `/mcp`.

**Test it locally:**
```bash
# macOS/Linux
MCP_TRANSPORT=http PORT=8080 python -m cxone_wfm_intraday_mcp.server
# Windows PowerShell
$env:MCP_TRANSPORT="http"; $env:PORT="8080"; python -m cxone_wfm_intraday_mcp.server
# now serving at http://127.0.0.1:8080/mcp
```

**Deploy to Google Cloud Run** (free tier, scales to zero) — from the repo root, using the
included `Dockerfile`:
```bash
gcloud run deploy cxone-wfm-intraday --source . --region <region>
```
Cloud Run builds the image, injects `PORT`, and the server serves HTTP automatically. It
prints a service URL; register it with any assistant:
```bash
claude mcp add --transport http cxone-wfm-intraday https://<service-url>/mcp
```

> **Auth:** a public URL serving even mock data should be protected. Cloud Run requires IAM
> auth by default (omit `--allow-unauthenticated`); for other hosts put a token/proxy in
> front. Authentication was deliberately deferred by the Sparkathon spec — add it before any
> non-demo use.

## Demo script

1. "Which queues are at risk right now?" → snapshot flags **Billing Support**.
2. "Why is Billing Support at risk?" → volume +18%, AHT +11%, staffing −3.
3. "What can we do to recover?" → move 2 agents from **General Enquiries**.
4. "What would the impact be?" → SLA **72% → ~81%**, General Enquiries stays above target.

## What you can ask (and what you'll get back)

You never call tools by name — just ask in plain language and the assistant picks the right
tool(s). The tables below list example prompts, the tool(s) that fire, and the answer you can
expect **on the built-in demo data** (swap in your own data and the same questions work — see
[Bring your own data](#bring-your-own-data)).

### Reading current state

| Ask something like… | Tool(s) | Expected answer (demo data) |
|---|---|---|
| "What metrics can I query?" / "What data do you have?" | `list_intraday_metrics` | The 11 Intraday metrics with definitions/units: SLA, SLA target, forecast/actual volume, forecast/actual AHT, planned/actual staffing, occupancy, backlog, risk status. |
| "Which queues are at risk right now?" / "Give me the intraday snapshot" | `get_intraday_snapshot` | All 5 queues with SLA/target, volume, AHT, staffing, occupancy, backlog, risk — and **highest risk: Billing Support (72% vs 80% target)**. |
| "How is Sales doing?" / "Show me just Billing and General Enquiries" | `resolve_intraday_entity` → `get_intraday_snapshot` | The selected queue(s) only. Sales → 83% vs 80% target, **watch**. |
| "Does 'GE' / 'the billing queue' / 'tech support' map to a real queue?" | `resolve_intraday_entity` | Resolves fuzzy names/aliases → General Enquiries / Billing Support / Technical Support (with a confidence level). |

### Explaining risk

| Ask something like… | Tool(s) | Expected answer (demo data) |
|---|---|---|
| "Why is Billing Support at risk?" | `resolve_intraday_entity` → `get_intraday_risk_drivers` | **Primary: volume +18% above forecast.** Also AHT +11%, staffing 3 below plan, occupancy 92%, backlog 47. Severity: **high**. |
| "How are actuals tracking vs forecast for Billing this morning?" | `get_forecast_vs_actual` | Six 30-min intervals (08:00–10:30); variance **widens** from ~0% to **+18% volume / +11% AHT**, staffing drifts 15 → 12. |
| "Which queue is under the most pressure and why?" | `get_intraday_snapshot` → `get_intraday_risk_drivers` | Billing Support, driven by the volume spike + AHT + staffing gap (as above). |

### Finding capacity & recovering

| Ask something like… | Tool(s) | Expected answer (demo data) |
|---|---|---|
| "Is there spare capacity to help Billing?" / "Who can lend agents?" | `get_available_capacity` | **General Enquiries can spare 2** (SLA 94% → ~92%, still above target); **Complaints can spare 1**. Sales & Technical Support: none (staffed to plan). |
| "What's the best recovery action?" / "How do we recover Billing's SLA?" | `summarise_intraday_recommendation` | **Move 2 agents from General Enquiries to Billing Support for 60 min → Billing SLA 72% → ~81.2%**, GE stays above target. Trade-off + "monitor AHT and backlog" included. |
| "What happens if I move 2 agents from General Enquiries to Billing for 60 minutes?" | `simulate_recovery_action` | Billing **72% → 81.2%** (occupancy 92 → 79); GE **94% → 91.9%** (occupancy 71 → 81), **stays above its 80% target**; no new risk. |
| "What if I move 3 instead?" | `simulate_recovery_action` | Billing **→ 84.6%**, GE **→ 90.6%** — but **flagged**: 3 exceeds GE's spare capacity of 2, so GE drops below its own plan. |
| "Can I pull 2 from Sales instead?" | `simulate_recovery_action` | Billing → 81.2%, but **Sales falls 83% → ~71%, below its 80% target** — not a safe donor (risk created). |

### Bring-your-own-data & housekeeping

| Ask something like… | Tool(s) | Expected answer |
|---|---|---|
| "Load this intraday data, then tell me which queue is at risk: `{…}`" | `load_intraday_data` | "Loaded N queues; highest risk: `<id>`." Every later question then analyzes **your** data. |
| "Reset to the demo data" | `reset_intraday_data` | Restores the built-in 5-queue demo for your session. |

### A full worked conversation (demo data)

> **You:** Which queues are at risk right now?
> **Assistant:** Five queues are live. **Billing Support is the highest risk** — SLA 72% against an 80% target. General Enquiries (94%), Technical Support (88%) and Complaints (91%) are healthy; Sales (83%) is on watch.
>
> **You:** Why is Billing Support at risk?
> **Assistant:** Three things are compounding: **volume is 18% above forecast**, **AHT is 11% higher than expected**, and **staffing is 3 agents below plan** — pushing occupancy to 92% with a backlog of 47. The variance has widened steadily since 08:00.
>
> **You:** What can we do to recover?
> **Assistant:** **General Enquiries has 2 agents of spare capacity** (it's 2 over plan at 94% SLA). Complaints could lend 1; Sales and Technical Support have none.
>
> **You:** What would moving 2 from General Enquiries do?
> **Assistant:** **Billing Support SLA improves from 72% to ~81.2%** and its occupancy eases from 92% to ~79%. **General Enquiries dips to ~91.9% — still comfortably above its 80% target.** No new risk is created. Recommend the move for 60 minutes and keep watching AHT and backlog.

### When something's off (error responses)

| Ask something like… | What you'll get |
|---|---|
| "Analyze the Refunds queue" | An error naming the valid queues: *"Could not resolve 'Refunds'… Valid queues: Billing Support, Complaints, General Enquiries, Sales, Technical Support."* |
| "Move 50 agents from General Enquiries to Billing" | *"Cannot move 50 agents from General Enquiries; only 16 are staffed."* |
| Loading data with a missing/invalid field | A clear message (e.g. *"queue 'x': missing required field 'sla_current'"* or *"risk_status must be one of ('at_risk','watch','healthy')"*), and the current dataset is left untouched. |

## Where the data comes from

All data is **synthetic and hardcoded** in `cxone_wfm_intraday_mcp/data.py` — a fixed
in-memory list of 5 queues loaded once at startup. There is **no database, API, CXone
tenant, network call, or randomness**, so every run returns identical numbers (a
repeatable demo by design). This matches the spec: for the prototype, mocked data is
acceptable; the point is proving the pattern of grounded, tool-driven answers.

What's **stored by hand** vs. **computed at call time**:

- **Stored** (the demo "facts"), per queue in `data.py`: `sla_current`, `sla_target`,
  `volume_actual/forecast`, `aht_actual/forecast`, `staffing_planned/actual`,
  `occupancy`, `backlog`, a `risk_status` label (`at_risk` / `watch` / `healthy`), and
  a 6-interval forecast-vs-actual history.
- **Computed** from those stored fields at call time — this is the real "analysis":
  - `get_intraday_snapshot` ranks queues by stored `risk_status` (`analysis.py`).
  - `get_forecast_vs_actual` / `get_intraday_risk_drivers` compute volume/AHT/staffing
    variance from the stored actual-vs-forecast values and flag drivers over thresholds
    (`analysis.py`).
  - `get_available_capacity` derives spare agents (surplus over plan, validated against
    the SLA curve).
  - `simulate_recovery_action` recomputes SLA for both queues via the formula below
    (`simulation.py`).

So *"Billing Support is at risk"* is a hand-authored label, but *"…because volume is
+18%, AHT +11%, staffing −3, and moving 2 agents lifts SLA 72→81.2%"* is genuinely
calculated from the stored fields — never invented by the assistant.

**Change the scenario** (make a different queue at risk, tweak SLA/volume/staffing,
add a queue) by editing the literals in `data.py` — nothing else needs to change. In a
real product, `data.py` would be replaced by live CXone WFM as the source of truth; the
tool contracts stay identical.

## Bring your own data

Consumers can analyze **their own** Intraday data without editing any code, via two tools:

- **`load_intraday_data`** — replace the dataset with your own queues; every other tool then
  analyzes your data.
- **`reset_intraday_data`** — restore the built-in demo.

Just ask the assistant (e.g. *"Load this Intraday data and tell me which queue is at risk"*)
and provide queues shaped like:

```json
{
  "queues": [
    {"id": "payments", "name": "Payments", "sla_current": 64, "sla_target": 85,
     "volume_forecast": 200, "volume_actual": 260, "aht_forecast": 280, "aht_actual": 320,
     "staffing_planned": 18, "staffing_actual": 14, "occupancy": 94, "backlog": 40,
     "risk_status": "at_risk", "aliases": ["pay"]},
    {"id": "onboarding", "name": "Onboarding", "sla_current": 97, "sla_target": 80,
     "volume_forecast": 90, "volume_actual": 80, "aht_forecast": 300, "aht_actual": 290,
     "staffing_planned": 7, "staffing_actual": 10, "occupancy": 55, "backlog": 0,
     "risk_status": "healthy"}
  ]
}
```

Per-queue fields: `id`, `name`, `sla_current`, `sla_target`, `volume_forecast`,
`volume_actual`, `aht_forecast`, `aht_actual`, `staffing_planned`, `staffing_actual`
(**≥ 1** — the SLA curve divides by it), `occupancy`, `backlog`, `risk_status`
(`at_risk` | `watch` | `healthy`). Optional: `aliases` (list) and `intervals` (list of
`{interval, volume_forecast, volume_actual, aht_forecast, aht_actual, staffing_planned,
staffing_actual}`). Percentages are 0–100, times are seconds, staffing is agent counts.
Invalid input is rejected with a clear message and leaves the current dataset untouched.

> **Isolation:** loaded data is scoped to your **MCP session** — on a shared HTTP server each
> conversation gets its own dataset (one client's load never affects another). `reset_intraday_data`,
> or the session ending, restores the demo for that session only. stdio has a single session (just
> you). Nothing is persisted to disk; the dataset lives in memory for the life of the session.

## The simulation model

Rule-based and deterministic. Each queue has a staffing→SLA response curve
`sla(n) = 100 − (100 − sla_current)·decay^(n − staff_actual)`, with
`decay = exp(−2.4 / staff_actual)`, anchored so `sla(actual) = current SLA`. Every
number the assistant reports comes from this formula given tool inputs — not from
the model's own reasoning. See §6 of the design spec.

TDQS

A3.6/5.0

Scored across 10 tools

Disambiguation5/5

Each tool has a distinct purpose: snapshot, forecast comparison, risk analysis, capacity query, simulation, recommendation, data loading/reset, entity resolution, metrics listing. No overlapping functionality.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern in snake_case (e.g., get_available_capacity, simulate_recovery_action, reset_intraday_data). No mixing of conventions.

Tool Count5/5

10 tools is well-scoped for a WFM intraday server. Each tool serves a needed function without being excessive or too sparse.

Completeness4/5

Covers snapshot, forecast comparison, risk analysis, capacity, simulation, recommendation, and data management. Minor gap: no direct tool for updating individual queue attributes, but load_intraday_data allows bulk replacement.

Maintenance

ActivityStale
ResponsivenessNo issues