Skip to main content
Glama
README.md
# TicketAI

A small AI-powered triage tool for IT support tickets. You give it a subject and description, and it uses the Claude API to work out what category the issue falls into, how urgent it is, a one-line summary, a suggested first response, and which team should pick it up.

I built this to get real, hands-on experience with the Claude API and with MCP (Model Context Protocol), rather than just reading about them. It's small on purpose — the goal was to go end to end on something real: prompt design, API integration, persistence, a REST interface, and an MCP interface that lets an AI assistant call the same logic as a tool.

## What it does

Send it a ticket like:

> Subject: Can't connect to VPN
> Description: VPN client fails to connect since this morning, blocking remote work.

And it comes back with:

```json
{
  "category": "Network",
  "urgency": "High",
  "summary": "User unable to establish VPN connection, blocking remote work.",
  "suggested_response": "Thanks for flagging this — we're looking into your VPN connection now and will update you shortly.",
  "suggested_assignee_group": "Network Ops"
}
```

That result gets saved, so you can also ask for stats like "how many Network tickets came in, and what's the average urgency."

## Two ways to use it

There are two interfaces on top of the same core logic:

1. **A REST API** (FastAPI) — the kind of thing you'd wire into a ticket intake form or another system.
2. **An MCP server** — this is the part I was most interested in. It exposes the exact same triage logic as tools that an AI assistant (Claude Desktop, Claude Code) can call directly during a conversation. So instead of copy-pasting a ticket into a chat window, you can just say "analyze this ticket" and the assistant calls the tool itself.

Both talk to the same SQLite database, so a ticket analyzed through the API shows up in the MCP stats tool too, and vice versa.

## How it's put together

```
                    ┌───────────────────┐
                    │   Your input       │
                    │ (ticket subject +  │
                    │   description)     │
                    └─────────┬─────────┘
                              │
              ┌───────────────┴────────────────┐
              │                                 │
              ▼                                 ▼
   ┌─────────────────────┐         ┌──────────────────────┐
   │   FastAPI service     │         │    MCP server          │
   │   app/main.py          │         │  mcp_server/server.py  │
   │                       │         │                        │
   │ POST /tickets/analyze │         │  analyze_ticket_tool   │
   │ GET  /tickets/stats   │         │  get_ticket_tool       │
   │ GET  /healthz         │         │  get_category_stats    │
   └───────────┬───────────┘         └───────────┬────────────┘
               │                                  │
               └────────────────┬─────────────────┘
                                 ▼
                    ┌─────────────────────────┐
                    │  app/claude_client.py     │
                    │  - builds the prompt      │
                    │  - calls the Claude API   │
                    │  - parses + validates the │
                    │    JSON it returns        │
                    └────────────┬───────────────┘
                                 ▼
                    ┌─────────────────────────┐
                    │    app/storage.py         │
                    │  SQLite: tickets table    │
                    └─────────────────────────┘
```

The FastAPI app and the MCP server are both thin wrappers. Neither one contains any triage logic itself — they just call into `app/claude_client.py` and `app/storage.py`, which is where the actual work happens. I did it this way so the two interfaces can never drift out of sync with each other, and so I could test the triage logic once instead of twice.

## Getting it running

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env   # then paste in your own ANTHROPIC_API_KEY
```

### Running the REST API

```bash
export ANTHROPIC_API_KEY=sk-ant-...
uvicorn app.main:app --reload
```

Then, in another terminal:

```bash
curl -X POST http://localhost:8000/tickets/analyze \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Cannot connect to VPN",
    "description": "VPN client fails to connect since this morning, blocking remote work.",
    "requester": "jane@example.com"
  }'

curl http://localhost:8000/tickets/stats
```

FastAPI also gives you interactive docs for free at `http://localhost:8000/docs`, which is a fast way to poke at the endpoints without curl.

### Running the MCP server

```bash
export ANTHROPIC_API_KEY=sk-ant-...
python -m mcp_server.server
```

To actually connect it to Claude Desktop, add this to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "ticketai": {
      "command": "python",
      "args": ["-m", "mcp_server.server"],
      "cwd": "/absolute/path/to/ticketai",
      "env": { "ANTHROPIC_API_KEY": "sk-ant-..." }
    }
  }
}
```

Restart Claude Desktop, and you should be able to ask it things like "use ticketai to analyze this ticket: [paste text]" or "what does the ticketai category breakdown look like right now."

## Running the tests

The Claude API call is mocked throughout the test suite, so you can run everything without an API key or network access:

```bash
pytest -v
```

24 tests, covering the storage layer, the Claude client's JSON parsing and error handling, all three REST endpoints (including the error paths), and all three MCP tools.

## Decisions I made, and why

**Structured output via prompting, not the tool-use API.** I told Claude in the system prompt exactly which JSON keys to return and gave it a closed list of allowed values for category, urgency, and assignee group. I considered using the Anthropic API's formal tool-use/function-calling feature instead, but for something this size, a well-constrained prompt plus a parser was simpler to reason about and debug.

**One shared core, two thin interfaces.** `claude_client.py` and `storage.py` don't know FastAPI or MCP exist. That was a deliberate choice so I could test the actual triage logic in isolation, and so adding a third interface later (a Slack bot, say) wouldn't mean duplicating anything.

**SQLite, not a real database server.** For a project this size, running Postgres would be pure overhead. SQLite gives both interfaces a shared, persistent store with zero setup.

## What I'd change if this went further than a portfolio project

I want to be upfront about the corners I cut, since none of them are things I'd leave alone in production:

- **The MCP server only runs over stdio**, which means it's local-only. A team-wide deployment would need the HTTP/SSE transport instead.
- **There's no auth on any of it.** Anyone who can run the process can call the tools and rack up API cost. Fine for a demo on my own machine, not fine for anything shared.
- **The category/urgency/assignee-group taxonomy is one I made up**, not one pulled from a real ITSM tool. In a real deployment I'd want it mapped to whatever categories the actual ticketing system uses.
- **No schema migrations.** `init_db()` just does `CREATE TABLE IF NOT EXISTS`, so if I ever changed the schema, an existing `tickets.db` would need to be manually handled. I've used Alembic for exactly this in a previous role — just didn't feel worth pulling in for a single-table project.

## Repo history

This was built branch-by-branch rather than as one commit dump — `feature/claude-client` → `feature/storage` → `feature/fastapi-service` → `feature/mcp-server` → `feature/tests` → `feature/docs`, each merged into `main` once it worked. If you look at the git log you'll see that shape.