Skip to main content
Glama
wernerth-cs

ticketsystem-mcp

by wernerth-cs
README.md
# Ticketsystem-MCP

MCP server for the ticket system of a fictional German health insurer. Built for
demos: it serves a small, self-consistent case load and lets an agent look up
tickets and hand them over — without any real data being involved.

The code is English, everything the server delivers is German: titles, statuses,
history entries and error messages read the way a case worker would see them.

## What the server can do

- **Six tickets across two case workers**, Nadine Krohn and Adrian Osei. Each
  ticket is one claim with its own history.
- **A case load that stays current.** History entries are dated relative to
  today and pulled back onto working days, so nothing looks stale in a demo half
  a year from now.
- **Real reassignment.** `ticket_zuweisen` moves a ticket, appends a handover
  entry and is visible in every later `tickets_auflisten` call.

`TCK-48213` is the centrepiece: the only open ticket of Nadine Krohn, waiting on
an insured person's reply that came in yesterday and has not been read.

## Tools

| Tool                                       | Purpose                                                                |
| ------------------------------------------ | ---------------------------------------------------------------------- |
| `tickets_auflisten(bearbeiter, nur_offene?)`      | Tickets of one case worker, most recently updated first, without history |
| `ticket_abrufen(ticket_nummer)`                   | One ticket including its full history                                   |
| `ticket_zuweisen(ticket_nummer, neuer_bearbeiter)` | Hand a ticket over to someone else                                     |

`bearbeiter` and `neuer_bearbeiter` accept either the full name (`Nadine Krohn`)
or the email address (`nadine.krohn@musterkasse.de`). `nur_offene` defaults to
`true`, so closed tickets stay out of the way unless asked for.

Tool names, parameters and the field names of every response are German on
purpose: MCP clients render an identifier by splitting it on underscores and
capitalising it, so `tickets_auflisten` shows up as "Tickets Auflisten". English
identifiers would be visible to the audience of a German demo.

## Quickstart

The [justfile](justfile) bundles the common flows; `just` without arguments lists
every recipe.

### Local

```sh
just setup                                    # uv sync
TICKETSYSTEM_API_KEY=demo-schluessel just run
```

The endpoint is then at `http://localhost:8001/mcp`.

### Docker

```sh
cp .env.example .env          # and adjust the key
just up                        # docker compose up -d --build
just health                    # query the health endpoint
just tools                     # list the tools of the running server
just down
```

## Connecting an MCP client

`.mcp.json` in the project directory:

```json
{
  "mcpServers": {
    "ticketsystem": {
      "type": "http",
      "url": "http://localhost:8001/mcp",
      "headers": { "X-API-KEY": "demo-schluessel" }
    }
  }
}
```

Port 8001 is the default so this server and `speiseplan-mcp` can run side by
side.

## Authentication

Every call needs the header `X-API-KEY` with one of the keys configured in
`TICKETSYSTEM_API_KEY`; otherwise the server answers HTTP 401. Only `/health` is
exempt, for the container health check.

Without `TICKETSYSTEM_API_KEY` the server does not start.

## Configuration

All values come from environment variables, see `.env.example`:

| Variable                | Default         | Meaning                                 |
| ----------------------- | --------------- | --------------------------------------- |
| `TICKETSYSTEM_API_KEY`  | — (mandatory)   | valid keys, several separated by commas  |
| `TICKETSYSTEM_HOST`     | `0.0.0.0`       | bind address                            |
| `TICKETSYSTEM_PORT`     | `8001`          | port                                    |
| `TICKETSYSTEM_PATH`     | `/mcp`          | path of the MCP endpoint                |
| `TICKETSYSTEM_TIMEZONE` | `Europe/Berlin` | basis for "today"                       |

## Storage

Reassignments live in the memory of the process only. There is no database and no
volume — a restart returns the case load to its initial state, which is exactly
what you want between two demo runs.

## Example: the demo run

```
tickets_auflisten(bearbeiter="Nadine Krohn")
  TCK-48213  Rückfrage an Versicherte  ungelesene Antwort

ticket_abrufen(ticket_nummer="TCK-48213")
  ...
  gestern  Antwort der Versicherten mit Klinik-Bescheinigung eingegangen (ungelesen)

ticket_zuweisen(ticket_nummer="TCK-48213", neuer_bearbeiter="adrian.osei@musterkasse.de")
  Ticket TCK-48213 ist jetzt Adrian Osei zugewiesen, zuvor Nadine Krohn.

tickets_auflisten(bearbeiter="Nadine Krohn")   -> leer
tickets_auflisten(bearbeiter="Adrian Osei")    -> enthält TCK-48213
```

## Development

```sh
just check                        # linter, formatting, mypy and tests
just tests                        # tests only; arguments are passed through
just fix                          # format and fix what can be fixed
```

Without `just` the tools work directly:

```sh
.venv/bin/pytest                  # tests, no external services needed
.venv/bin/ruff check src tests    # linter
.venv/bin/ruff format src tests   # formatting
.venv/bin/mypy                    # type check in strict mode
```

## Publishing

The image goes to the registry `registry.florian-sattler-apps.de`:

```sh
just login                        # once: docker login
just publish-preview              # shows the target tags without pushing
just publish                      # checks, builds and pushes after a prompt
```

More on the architecture and the decisions behind it is in [CLAUDE.md](CLAUDE.md).