TicketDesk MCP
by bhargavlukka
README.md
# TicketDesk MCP — Production-Style MCP Server & Client
A Model Context Protocol (MCP) server exposing an internal support-ticket queue, paired
with a client that discovers and invokes its tools, resources, and prompts. Built for the
"Build a Production MCP Server & Client" lab (Module 4, Model Context Protocol).
## Contents
- [Architecture](#architecture)
- [Setup & running the demo](#setup--running-the-demo)
- [`docs/tools-resources-prompts.md`](docs/tools-resources-prompts.md) — what the server exposes
- [`docs/architecture.md`](docs/architecture.md) — transport choice & justification
- [`docs/security-summary.md`](docs/security-summary.md) — authentication, least privilege, error handling
- [`demo/session_log.txt`](demo/session_log.txt) — a real captured discovery-and-invocation run
## Architecture
```mermaid
flowchart TD
HOST["Host application\n(e.g. an AI assistant / chatbot)"] --> CLIENT
subgraph Client["client.py — fastmcp.Client"]
CLIENT["Discovery: list_tools / list_resources /\nlist_resource_templates / list_prompts"]
ELICIT["elicitation_handler\n(client-side safety gate for\nhigh-impact actions)"]
CLIENT <--> ELICIT
end
CLIENT <-->|"Streamable HTTP\nAuthorization: Bearer <token>"| SERVER
subgraph Server["server.py — FastMCP('TicketDesk')"]
AUTH["StaticTokenVerifier\n(read:tickets / write:tickets scopes)"]
TOOLS["Tools: search_tickets, get_ticket,\nadd_ticket_note, close_ticket"]
RES["Resources: ticket://{ticket_id},\npolicy://sla"]
PROMPT["Prompt: triage_ticket"]
AUTH --> TOOLS
AUTH --> RES
AUTH --> PROMPT
end
SERVER --> DATA[("data/tickets.json,\ndata/sla_policy.md")]
```
**Transport: Streamable HTTP** — justified in [`docs/architecture.md`](docs/architecture.md)
(short version: this is meant to be a shared internal capability multiple teams' AI
applications connect to, not a single local process pair, which is what stdio is suited
for).
## What the server exposes
- **4 tools**: `search_tickets`, `get_ticket`, `add_ticket_note`, `close_ticket` (the last
is a high-impact action gated by client-side elicitation)
- **2 resources**: `ticket://{ticket_id}` (templated) and `policy://sla` (static)
- **1 prompt**: `triage_ticket`
Full documentation of each: [`docs/tools-resources-prompts.md`](docs/tools-resources-prompts.md).
## Setup & running the demo
```bash
pip install -r requirements.txt
# Terminal 1
python server.py
# Terminal 2
python client.py # interactive: real confirmation prompts via input()
python client.py --auto-confirm # non-interactive demo mode (used to produce demo/session_log.txt)
```
`client.py` connects with `dev-agent-token` (read + write scopes), runs discovery, invokes
one of each tool/resource/prompt, demonstrates both error-handling failure modes
(Requirement 8), performs the high-impact `close_ticket` action through client-side
elicitation (Requirement 6), and finishes with a least-privilege demo showing
`dev-readonly-token` correctly rejected from a write-scoped tool.
## Security
Full write-up: [`docs/security-summary.md`](docs/security-summary.md). Short version:
bearer-token auth via `StaticTokenVerifier`, explicit per-tool scope checks (a real gap —
connection-level scope requirements alone do **not** restrict individual tool calls — was
found and fixed during development; see the security doc for the honest account), and
`ToolError` + `mask_error_details=True` to distinguish user-actionable errors from
internal failures that must never reach the client verbatim.
## Requirements mapping
| Requirement | Where it's satisfied |
|---|---|
| ≥3 tools | `search_tickets`, `get_ticket`, `add_ticket_note`, `close_ticket` (`server.py`) |
| ≥2 resources, each a URI | `ticket://{ticket_id}`, `policy://sla` (`server.py`) |
| ≥1 reusable prompt | `triage_ticket` (`server.py`) |
| Client discovers + invokes each | `client.py` `run_discovery_and_demo()` |
| Transport choice justified | `docs/architecture.md` |
| Client-side safety feature | Elicitation gating `close_ticket` (`client.py` `elicitation_handler`) |
| Security design summary | `docs/security-summary.md` |
| Error handling for a realistic failure mode | Invalid ticket ID (`ToolError`) + simulated backing-store outage (`mask_error_details`) — both in `server.py`, demonstrated in `client.py` |
| Demonstration logs | `demo/session_log.txt` (real captured run) |