TicketAI
by StanleyXisco
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.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues