custsupport
# CustSupport
An MCP server for a customer support workflow — **monitoring, reporting,
and resolving** — built as a portfolio project. A local LLM (Ollama)
generates a synthetic ticket dataset, which is validated, stored in
BigQuery, and served to any MCP-compatible client (Claude Desktop,
Claude Code) through a set of read-oriented monitoring/reporting tools
and a resolving tool set that drafts suggestions for human review rather
than acting autonomously.
## Architecture
```
generate_batch.py → validator.py → load_to_bigquery.py → BigQuery
(Ollama) (QA / flagging) (load job) │
▼
mcp_server/server.py
(monitoring / reporting /
resolving tools)
│
▼
Claude Desktop / Claude Code
```
- **Data**: synthetic support tickets (Zendesk-shaped schema — category,
persona, priority, channel, subject, body, SLA hours) generated locally
via Ollama (llama3.2), chosen over a real support-platform API to keep
cost/friction low while controlling for specific edge cases (SLA
breaches, ambiguous sentiment) that make the monitoring/reporting logic
worth testing.
- **Generation**: category/persona/priority/channel/scenario are sampled
*before* generation (weighted, conditionally correlated — e.g. urgent
tickets skew frustrated, technical/account issues skew higher priority
than shipping/feature requests), then a parameterized prompt is built
per combination. Persona is fully decoupled from category — any persona
can occur with any category.
- **Validation**: a QA pass built directly from failure patterns observed
across real generation batches — full-caps "shouting" bodies (~30-40%
of frustrated-persona tickets, a rate prompt tuning couldn't fully
eliminate), leftover bracket placeholders, stale absolute dates,
third-person voice drift, and category/ID mismatches (e.g. a feature
request referencing an order number). Flagged tickets are separated
out for review rather than silently dropped or silently kept.
- **Storage**: BigQuery, loaded via a load job (not streaming insert) so
rows are immediately eligible for the UPDATE used by the resolving
tool set's status changes.
- **MCP server**: exposes the tool set below over stdio, for use by
Claude Desktop or Claude Code.
## Tool set
| Category | Tool | Read/Write |
|---|---|---|
| Monitoring | `list_open_tickets`, `get_ticket`, `sla_breaches` | Read |
| Reporting | `category_breakdown`, `sla_risk_summary`, `daily_digest` | Read |
| Resolving | `draft_reply`, `suggest_escalation` | Read (suggestions only) |
| Resolving | `update_ticket_status` | **Write** |
**Human-approval design**: `draft_reply` and `suggest_escalation` never
modify data — they return a suggestion for a human to review.
`update_ticket_status` is the only tool that writes, and is intended to
be called only after that review. This is a convention carried by tool
descriptions and expected usage, not something the server can technically
enforce — the MCP protocol gives a calling agent the tools, not a way to
prove a human looked at the output first.
## Project layout
```
src/custsupport/
├── schema.py # SyntheticTicket dataclass, category/persona/priority enums
├── config.py # env-based config (Ollama, BigQuery) — loads .env via python-dotenv
├── generator/
│ ├── prompts.py # build_prompt() — tuned, parameterized, category/persona decoupled
│ ├── sampler.py # sample_ticket_params() / sample_batch() — weighted, conditional sampling
│ ├── ollama_client.py # Ollama /api/generate wrapper
│ ├── batch.py # wires sampler+prompts+ollama_client into SyntheticTicket objects
│ └── validator.py # QA pass — full-caps, placeholders, dates, voice, ID mismatches
├── storage/
│ └── bigquery_client.py # dataset/table creation, insert/load, monitoring queries, status updates
└── mcp_server/
├── draft.py # draft_reply's Ollama-backed prompt/generation
└── server.py # MCP tool registration (monitoring/reporting/resolving)
scripts/
├── sanity_check_tickets.py # manual prompt-tuning harness (exploratory, not a test)
├── generate_batch.py # generate + validate a batch, write clean/flagged JSONL
└── load_to_bigquery.py # load a generated JSONL batch into BigQuery
tests/
├── test_sampler.py # sampler distribution checks, no external deps
├── test_build_prompt.py # prompt cross-pairing checks, needs local Ollama
├── test_batch_generator.py # parser + pipeline checks, Ollama mocked
├── test_validator.py # validator checks against real historical failure tickets
└── test_mcp_server.py # tool registration + pure-logic checks, BigQuery/Ollama mocked
```
## Setup (fresh clone)
This repo excludes both the generated dataset (`data/`) and any
environment-specific credentials — a fresh clone needs its own GCP
project and its own generated tickets.
```bash
# 1. Install dependencies
uv sync
uv pip install -e .
# 2. Configure environment
cp .env.example .env
# edit .env: set BQ_PROJECT_ID to your own GCP project
# 3. GCP auth (local dev)
gcloud auth application-default login
gcloud auth application-default set-quota-project <your-project-id>
# 4. Ollama
ollama pull llama3.2
# 5. Generate and validate a batch of synthetic tickets
uv run python scripts/generate_batch.py --n 50 --seed 1
# 6. Load into BigQuery (creates dataset/table on first run)
uv run python scripts/load_to_bigquery.py --input data/tickets_batch.jsonl
# 7. Run the test suite
uv run python tests/test_sampler.py --n 2000
uv run python tests/test_batch_generator.py
uv run python tests/test_validator.py
uv run python tests/test_mcp_server.py
```
## Registering the MCP server
**Claude Desktop** — add to `claude_desktop_config.json`:
```json
{
"mcpServers": {
"custsupport": {
"command": "uv",
"args": ["run", "--directory", "<absolute path to this repo>", "python", "-m", "custsupport.mcp_server.server"]
}
}
}
```
**Claude Code**:
```bash
claude mcp add custsupport -- uv run --directory "<absolute path to this repo>" python -m custsupport.mcp_server.server
```
Ollama must be running locally for `draft_reply` to work once connected.
## Status
Fully functional end-to-end, verified against live Ollama, a live
BigQuery project, and the real MCP Python SDK (`mcp` 2.x — note the SDK
renamed `FastMCP` to `MCPServer` in 2.0). Not yet verified: the full
round trip through an actual Claude Desktop/Code session (the tool
registration itself was tested via the SDK directly, not via a live
client connection).
## Possible next steps
- `search_similar_resolved_tickets` — RAG over past resolutions. Not
built yet since the generator has only ever produced `status="open"`
tickets; would need a small resolved-ticket generation pass first
(populating `ground_truth_resolution`).
- Auto-regeneration of validator-flagged tickets rather than manual review.
## License
MIT — see [LICENSE](LICENSE).TDQS
Scored across 9 tools
Tools are largely distinct, but aggregation tools like daily_digest, category_breakdown, and sla_risk_summary overlap in the data they summarize, which could lead to misselection. Descriptions clarify their differences, but the boundary between daily_digest and category_breakdown is subtle.
Names follow snake_case and are generally clear, but mix verb-first (list_open_tickets, get_ticket) with noun-first patterns (sla_breaches, category_breakdown). Despite the mix, the naming is predictable and readable, with no camelCase or chaotic variations.
9 tools is well within the ideal 3-15 range for a focused domain. Each tool covers a distinct part of the support workflow—listing, detail, aggregation, SLA monitoring, draft replies, escalation, and status updates—without redundancy or bloat.
The surface covers the main support lifecycle: read (list, get), aggregate (breakdowns, SLA risk), assist (draft reply, escalation), and write (update status). Minor gaps exist, such as no tool to create or assign tickets, but these may be handled externally. The core workflow is well supported with no dead ends.