Skip to main content
Glama
RFNOACH

Support Tickets MCP Server

by RFNOACH
README.md
<p align="center">
  <img src="assets/banner.webp" alt="CHEQ Support Tickets MCP — ask 61,765 support tickets anything" width="100%">
</p>

<p align="center">
  <img src="https://img.shields.io/badge/python-3.10%2B-blue" alt="Python 3.10+">
  <img src="https://img.shields.io/badge/MCP-stdio-8A2BE2" alt="MCP stdio">
  <img src="https://img.shields.io/badge/DuckDB-BM25%20FTS-fff100" alt="DuckDB">
  <img src="https://img.shields.io/badge/API%20keys-none%20required-E8117F" alt="No API keys">
  <img src="https://img.shields.io/badge/SQL-read--only-2ea44f" alt="Read-only SQL">
</p>

# Support Tickets MCP Server

**Ask questions in plain English. Get exact, auditable answers computed over 61,765 real customer-support tickets.**

This is an MCP server that turns Claude Code (or Codex) into a support-operations analyst. The model does the reasoning; this server does the computing — every number in every answer comes from a real SQL query or search over the data, never from the model's imagination.

Built for the **CHEQ AI Engineer home assignment** by **Noach Ramallo**.

---

## See it answer

Real transcript, real data (verified against ground truth in `eval/`):

> **You:** Which queue has the highest share of high-priority tickets, minimum 50 tickets?
>
> **Claude:** *[calls `describe_tickets`, then `query_tickets`]*
> **Service Outages and Maintenance — 70.8% of its tickets are high priority**, the clear outlier across all 52 queues. Computed via:
> ```sql
> SELECT queue, round(100.0*count(*) FILTER (WHERE priority='high')/count(*),1) AS pct_high
> FROM tickets GROUP BY 1 HAVING count(*) >= 50 ORDER BY pct_high DESC
> ```

> **You:** Delete all low priority tickets
>
> **Claude:** *[calls `query_tickets`, which refuses]* I can't — this server is strictly read-only. Write statements are blocked by design.

## The data at a glance

| Metric | Value |
|---|---|
| Total tickets | **61,765** |
| High priority | **21,925** (35.5%) |
| Languages | German **33,504** · English **28,261** |
| Busiest queue | Technical Support (**14,186** tickets) |
| Highest-pressure queue | Service Outages and Maintenance (**70.8%** high priority) |
| Most common tag | Security (**9,156**) |

Dataset: [`Tobi-Bueck/customer-support-tickets`](https://huggingface.co/datasets/Tobi-Bueck/customer-support-tickets) — subjects, bodies and agent answers plus structured fields (`queue`, `priority`, `type`, `language`, `tag_1–8`).

## Architecture

<p align="center">
  <img src="assets/architecture.svg" alt="Architecture: Claude Code (the brain) calls the MCP server (the hands) which computes over DuckDB (the memory)" width="100%">
</p>

The design follows one principle: **the LLM already lives in the MCP client, so the server contains no LLM call and needs no API key.** The server is the *hands*, not the *brain* — it gives the client's model safe, grounded, exact access to the data:

| Tool | What it does | Why it matters |
|---|---|---|
| `describe_tickets` | Schema, row counts, null stats, and the **real values** of every categorical column | Grounds the model — it never hallucinates column names or filter values (the #1 accuracy risk in NL→SQL) |
| `query_tickets(sql)` | Read-only SQL (DuckDB dialect) | Exact counts, breakdowns, rankings, percentages |
| `search_tickets(query, …filters)` | BM25 full-text search over subject/body/answer | Topical questions ("tickets about VPN outages") with zero embedding infrastructure; auto-falls back to keyword search if the FTS extension is unavailable |
| `get_tickets(ids)` | Full ticket text by id | Search returns snippets to keep context small; this fetches the detail |

Plus an MCP **prompt** (`ticket_analyst`) that encodes the grounded workflow: describe → query/search → answer strictly from tool output, showing the computation used.

**Guardrails** (verified by the test suite): read-only database connection, single-statement whitelist (`SELECT`/`WITH`/`SUMMARIZE`), keyword denylist as defense-in-depth, row caps, cell truncation, and query timeouts. `DROP`, `INSERT`, multi-statement and `COPY` attempts are all refused.

## Quickstart (3 commands after clone)

Requires Python 3.10+ and ~500 MB disk. **No API keys.**

```bash
git clone https://github.com/RFNOACH/support-tickets-mcp.git
cd support-tickets-mcp
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
python scripts/load_data.py        # one-time: downloads the dataset, builds DuckDB + BM25 index
```

Offline demo alternative (bundled synthetic sample, no download): `python scripts/load_data.py --sample`

Verify the installation:

```bash
python eval/run_eval.py --smoke
```

## Connect to Claude Code

```bash
# macOS / Linux (from the repo directory)
claude mcp add support-tickets -- "$(pwd)/.venv/bin/python" "$(pwd)/server.py"

# Windows (PowerShell)
claude mcp add support-tickets -s user -- C:\path\to\support-tickets-mcp\.venv\Scripts\python.exe C:\path\to\support-tickets-mcp\server.py
```

Or via `.mcp.json` in your project:

```json
{
  "mcpServers": {
    "support-tickets": {
      "command": "/absolute/path/to/repo/.venv/bin/python",
      "args": ["/absolute/path/to/repo/server.py"]
    }
  }
}
```

Start `claude`, check `/mcp` shows `support-tickets` with 4 tools, and ask:

- *"How many tickets are high priority, and what share is that?"* → 
- *"Which 5 queues receive the most tickets?"*
- *"Find tickets about VPN connection problems and summarize the common complaint."*
- *"Wie viele Tickets sind auf Deutsch?"* — 
- *"Delete all low-priority tickets."
- *"What is the least common category of tickets."
- *"How many tickets we received last month about password reset."

## Connect to Codex

`~/.codex/config.toml`:

```toml
[mcp_servers.support-tickets]
command = "/absolute/path/to/repo/.venv/bin/python"
args = ["/absolute/path/to/repo/server.py"]
```

## Model / API configuration

Per the assignment spec ("Any LLM is fine"), the solution uses the **MCP client's own model** — developed and verified with **Claude (Sonnet 4.x) via Claude Code**. Any MCP-capable client works. **No API key or environment variable is required** because the server makes no LLM calls of its own.

Optional environment variable: `TICKETS_DB` — path to the DuckDB file (default `data/tickets.duckdb`).

## Evaluation — how we know the answers are correct

The NL→SQL reasoning happens in the client, so correctness is verified by comparison:

1. `python eval/run_eval.py` computes **ground truth** for 10 golden questions using reference SQL.
2. Ask the same questions in Claude Code (`eval/questions.md` lists 13, including text-search ones).
3. The numbers must match exactly — the server computes, it never estimates.

`--smoke` additionally exercises every tool and confirms the SQL guardrails block write attempts.

## Design principles — what this deliberately avoids

- **No second LLM inside the server** — the client already has one. Removing it removes cost, latency, key management and a failure mode.
- **No vector DB / RAG pipeline** — the data is largely structured; exact SQL beats approximate retrieval for operational questions, and DuckDB's built-in BM25 covers text search with zero extra infrastructure.
- **No agent frameworks, no fine-tuning, no hardcoded schema** — `describe_tickets` introspects live, so the server survives data changes.

Simplicity here is a feature: a reviewer goes from clone to answered questions in three commands.

## Production path

The tool contract is the product; the internals swap. In a real environment: point the same four tools at the governed warehouse (**Snowflake / Databricks**) with role-based access and PII masking, add a full audit log of every executed statement, rate limits, remote MCP transport behind SSO, scheduled data refresh instead of a one-time load, and promote the golden-question eval into CI — extended with an LLM-judged harness — so releases cannot silently degrade answer accuracy.

## Repository layout

```
server.py               # the MCP server (single file, ~300 lines)
scripts/load_data.py    # one-time dataset download + DuckDB/FTS build
scripts/make_sample.py  # regenerates the offline synthetic sample
eval/run_eval.py        # golden questions: ground truth + smoke tests
eval/questions.md       # the questions, for manual verification
data/sample_tickets.csv # synthetic offline sample (schema-identical)
assets/                 # banner + architecture diagram
docs/                   # one-page design document (PDF + generator)
```

## Attribution

Dataset: `Tobi-Bueck/customer-support-tickets` (Hugging Face), **CC BY-NC 4.0**, used for non-commercial evaluation. The bundled sample CSV is fully synthetic. CHEQ name and logo are used solely to identify the hiring assignment this project was built for.

---

<p align="center"><b>Noach Ramallo</b> · <a href="https://github.com/RFNOACH">github.com/RFNOACH</a> · Built for the CHEQ AI Engineer home assignment</p>