Metadata-Version: 2.5
Name: lxw-cli
Version: 0.9.8
Summary: Inoffizielles CLI/TUI/MCP-Tool für die Lexware Office API (vormals lexoffice)
Project-URL: Homepage, https://github.com/OEM-Mrks/lxw-cli
Project-URL: Documentation, https://developers.lexware.io/
Author: Markus Oeffling
License: MIT
License-File: LICENSE
Keywords: api,cli,lexoffice,lexware,mcp,tui
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial :: Accounting
Requires-Python: >=3.11
Requires-Dist: cryptography>=42
Requires-Dist: fastmcp>=2.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.6
Requires-Dist: python-dotenv>=1.0
Requires-Dist: rich>=13.7
Requires-Dist: tenacity>=8.2
Requires-Dist: textual>=0.58
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# lxw-cli

Ein Kommandozeilen-Tool für die [Lexware Office API](https://developers.lexware.io/) (vormals lexoffice). Liest Rechnungen, Kontakte, Belege, Artikel, Angebote, Aufträge und Lieferscheine — und legt Drafts neu an.

> **Hinweis:** Inoffizielles Community-Tool — kein Produkt der Haufe-Lexware
> GmbH & Co. KG und nicht mit ihr verbunden. „Lexware" ist eine Marke der
> Haufe Group.

## Features

- Zwei Frontends auf einem UI-freien Core: **CLI** (skriptbar) und **interaktive TUI** (`lxw` ohne Argumente)
- Lesen: `list` (paginiert + Filter), `get` (Detail), `pdf` (Download)
- Zahlstatus: offener Betrag, bezahlt/offen/storniert und einzelne Zahlungen zu einer Rechnung (`invoices payment`)
- Mahnungen: aus einer überfälligen Rechnung erstellen (`dunnings create`), abrufen und als PDF laden (nur per id)
- Schreiben: `create-draft` für Belege (Rechnungen, Angebote, Aufträge, Lieferscheine, Belege), `create` für Stammdaten (Kontakte, Artikel)
- Beleg-Anhänge: Originalbelege (PDF/JPG/PNG) an einen Beleg anhängen und angehängte Dateien wieder herunterladen
- Buchungskategorien auflisten (die `id` referenzieren Beleg-Positionen als `categoryId`)
- Paginierung: `--limit N`, `--all`/`-a` bzw. `--limit 0` für **alle** Treffer über alle Seiten
- Artikel-Volltextsuche: `articles list --search/-q` (Bezeichnung, Beschreibung, Teil-Artikelnummer)
- Kontakte: `--customer` / `--vendor` (nur Kunden/Lieferanten), optional `--grouped`
- Ausgabe: Tabelle (Rich), JSON (pipe-fähig), CSV, Datei-Output
- PDF-Download nach Datei **oder** Verzeichnis (Auto-Dateiname); `-o` optional
- Komfort: API-Key-Abfrage beim ersten Start, Warte-Animation bei längeren Abrufen
- Eingebauter Rate-Limit-Schutz (2 req/s) + automatische Retries auf 429/5xx
- Mehrere Lexware-Installationen (z.B. Firma + Demo) über **Profile** — in CLI, TUI und MCP-Server (`lxw profiles`, `--profile`)
- Betriebsstatus: `lxw status` fragt status.lexware.de ab; bei Serverfehlern wird der Grund **automatisch** an die Fehlermeldung gehängt

## Installation

Läuft auf **macOS, Linux und Windows** (Python ≥ 3.11). Empfohlen als
isoliertes Tool mit [uv](https://docs.astral.sh/uv/) oder
[pipx](https://pipx.pypa.io/):

```bash
uv tool install lxw-cli      # oder: pipx install lxw-cli
lxw profile
```

Alternativ klassisch in ein Environment:

```bash
pip install lxw-cli
```

### Windows (cmd, ohne Vorinstallation)

Für ein frisches Windows 11 ohne uv und ohne Python — die vier Zeilen laufen
per Copy-Paste in `cmd.exe` durch (volle Pfade, damit kein Terminal-Neustart
nötig ist; uv lädt sich ein passendes Python selbst):

```cmd
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
"%USERPROFILE%\.local\bin\uv.exe" tool install lxw-cli
"%USERPROFILE%\.local\bin\uv.exe" tool update-shell
"%USERPROFILE%\.local\bin\lxw.exe" profile
```

Ab dem nächsten cmd-Fenster reicht dann überall einfach `lxw`. Der API-Key
wird beim ersten Start abgefragt und unter `%APPDATA%\lexware\.env` gespeichert.

### Update

Auf die neueste Version von PyPI:

```bash
uv tool upgrade lxw-cli        # oder: pipx upgrade lxw-cli
lxw --version                  # Kontrolle
```

Sieht uv eine frisch veröffentlichte Version noch nicht (das PyPI-CDN braucht
nach einem Release ein paar Minuten) oder ist das Tool auf eine exakte Version
gepinnt, hilft ein Neu-Install auf `latest`:

```bash
uv tool install --force lxw-cli@latest
```

Der MCP-Server für Claude nutzt das Update automatisch mit — bereits laufende
Claude-Sessions müssen dafür einmal neu gestartet werden.

### Aus dem Quellcode (Entwicklung)

```bash
git clone https://github.com/OEM-Mrks/lxw-cli
cd lxw-cli
uv venv && source .venv/bin/activate
uv pip install -e ".[dev]"
```

Lokalen Arbeitsstand ohne Release als globales `lxw` installieren:

```bash
uv tool install --force --reinstall .
```

(`--reinstall` ist nötig — bei unveränderter Versionsnummer nimmt uv sonst
den gecachten Build.)

## API-Key konfigurieren

Lege einen API-Key in deinem Lexware-Account an: <https://app.lexware.de/addons/public-api>

**Einfachste Variante:** Ruf `lxw` einfach auf. Findet das Tool keinen Key,
fragt es im Terminal danach, prüft ihn gegen die API und speichert ihn unter
`~/.config/lexware/.env` (Datei-Rechte `0600`, nur für dich lesbar; unter
Windows `%APPDATA%\lexware\.env`). Ab dann funktioniert `lxw` aus **jedem**
Verzeichnis, ohne weitere Einrichtung.

```bash
lxw profile
# → Kein Lexware API-Key gefunden. Lexware API-Key: ********
# → ✓ API-Key gespeichert in ~/.config/lexware/.env — angemeldet als Acme GmbH
```

Der Key wird in dieser Reihenfolge gesucht:

1. gesetzte Environment-Variable `LEXWARE_API_KEY`
2. projektlokale `.env` (vom aktuellen Verzeichnis aufwärts — praktisch für die Entwicklung)
3. globale `~/.config/lexware/.env` (bzw. `$XDG_CONFIG_HOME/lexware/.env`; unter Windows `%APPDATA%\lexware\.env`)

Wer es manuell vorziehen will, kann den Key auch direkt setzen:

```bash
cp .env.example .env   # projektlokal
# LEXWARE_API_KEY=… eintragen
```

In nicht-interaktiven Kontexten (Pipes, Cron, MCP-Server über stdio) wird **nicht**
gefragt — dort muss der Key vorab über eine der drei Quellen vorhanden sein.

### Mehrere Lexware-Installationen (Profile)

Wer mehrere Lexware-Installationen nutzt — etwa die eigene Firma und eine
Demo-Instanz —, legt pro Installation ein **Profil** mit eigenem API-Key an:

```bash
lxw profiles add oemedia      # fragt den Key ab, prüft ihn, zeigt den Firmennamen
lxw profiles add demo
lxw profiles list --check     # alle Profile + Firmenname je Key
lxw profiles remove demo
```

Jedes Profil liegt als eigene Datei unter `~/.config/lexware/profiles/<name>.env`
(chmod 600; Windows: `%APPDATA%\lexware\profiles\`). Nicht-interaktiv geht
auch `echo "$KEY" | lxw profiles add demo`.

Auswahl per `--profile`/`-p` (vor dem Befehl) oder `LEXWARE_PROFILE`:

```bash
lxw -p demo invoices list
LEXWARE_PROFILE=oemedia lxw contacts list
lxw -p demo                   # TUI für die Demo-Installation
```

- Ohne Profil bleibt alles wie bisher (Key-Suche wie oben) — der bisherige
  globale Key ist das Profil `default`.
- Ein benanntes Profil liest den Key **ausschließlich** aus seiner Datei. Ein
  exportiertes `LEXWARE_API_KEY` oder eine projektlokale `.env` kann es also
  nicht auf eine andere Installation umlenken; fehlt der Key, gibt es einen
  Fehler statt eines stillen Fallbacks.
- Optional kann ein Profil eine eigene `LEXWARE_API_BASE_URL` enthalten.

## Quickstart

```bash
# Auth-Test: zeigt Firmenprofil
lxw profile

# Rechnungen (Tabelle) — Standard: 25 Einträge
lxw invoices list --limit 10

# ALLE Rechnungen laden (paginiert automatisch über alle Seiten)
lxw invoices list --all
lxw invoices list --limit 0          # gleichbedeutend

# Archivierte sind standardmäßig ausgeblendet — bei Bedarf einblenden
lxw invoices list --include-archived
lxw contacts list --include-archived

# Rechnungen als JSON in jq pipen
lxw --json invoices list --status open | jq '.[].voucherNumber'

# CSV-Export von Kunden
lxw --csv --output kunden.csv contacts list --customer --limit 200

# Artikel suchen — Teiltreffer in Bezeichnung, Beschreibung und Artikelnummer
lxw articles list --search "schraube"
lxw articles list -q "SCH-"          # auch Teil-Artikelnummern
lxw articles list --number SCH-001   # exakte Artikelnummer (serverseitig, schnell)

# Rechnung per Belegnummer abrufen (UUID auch möglich)
lxw invoices get FB2600682
lxw --json invoices get FB2600682 | jq '.totalGrossAmount'

# PDF einer Rechnung herunterladen (UUID oder Belegnummer)
lxw invoices pdf FB2600682                       # → ./invoice-FB2600682.pdf
lxw invoices pdf FB2600682 --output ~/Rechnungen # Verzeichnis: Dateiname automatisch
lxw invoices pdf FB2600682 --output rechnung.pdf # exakter Dateipfad

# Kontakt anlegen (Stammdaten — kein Draft)
lxw contacts create --body '{
  "roles": {"customer": {}},
  "company": {"name": "Test GmbH"}
}'

# Komplexere Bodies aus Datei
lxw invoices create-draft --body @invoice-template.json
```

## Verfügbare Befehle

| Befehl | Zweck |
|---|---|
| `lxw profile` | Firmenprofil abrufen (Auth-Test) |
| `lxw status` | Betriebsstatus von Lexware Office (nicht zu verwechseln mit `lxw mcp status`) |
| `lxw invoices` | Rechnungen: `list`, `get`, `payment`, `pdf`, `create-draft` |
| `lxw contacts` | Kontakte: `list`, `get`, `create` |
| `lxw vouchers` | Belege: `list`, `get`, `create-draft`, `upload-file`, `download-file` |
| `lxw posting-categories` | Buchungskategorien: `list` (mit `--type`) |
| `lxw articles` | Artikel: `list`, `get`, `create` (mit Volltextsuche) |
| `lxw quotations` | Angebote: `list`, `get`, `pdf`, `create-draft` |
| `lxw orders` | Aufträge (Auftragsbestätigungen): `list`, `get`, `pdf`, `create-draft` |
| `lxw delivery-notes` | Lieferscheine: `list`, `get`, `pdf`, `create-draft` |
| `lxw dunnings` | Mahnungen: `create` (aus Rechnung), `get`, `pdf` (nur per id) |
| `lxw profiles` | Mehrere Lexware-Installationen: `add`, `list`, `remove` |

Hinweis: `create-draft` legt **Belege** als Entwurf an (Draft, nicht finalisiert);
**Stammdaten** (Kontakte, Artikel) kennen keinen Draft-Status und werden mit
`create` direkt angelegt.

Detail-Hilfe mit `lxw <command> --help` bzw. `lxw <command> list --help`.

## Betriebsstatus

`lxw status` liest die öffentliche Statusseite **status.lexware.de** — ohne
API-Key, und damit auch dann, wenn Lexware selbst nicht antwortet oder der
Schlüssel abgelaufen ist.

```bash
lxw status                  # Zustand + laufende Störungen + angekündigte Wartungen
lxw status --no-planned     # nur der aktuelle Zustand
lxw status --refresh        # Zwischenspeicher übergehen
lxw --json status           # maschinenlesbar
```

Exit-Codes für Monitoring: `0` alles normal · `1` Störung oder Wartung ·
`2` Statusseite nicht erreichbar (**nicht** dasselbe wie eine Störung).

Zusätzlich passiert das automatisch: schlägt ein Aufruf mit einem Serverfehler
fehl (5xx, Timeout, dauerhaftes 429), hängt die CLI — und im MCP-Server jedes
Tool — den Grund direkt an die Fehlermeldung:

```
API-Fehler: HTTP 503: Service Unavailable
Lexware meldet derzeit eine Störung: Technische Störung im Bereich
Belegerstellung (seit 10:14 Uhr, Ursache gefunden, Behebung läuft) — das ist
vermutlich die Ursache. Details: https://status.lexware.de
```

Bei Eingabe- oder Auth-Fehlern (400/401/404) bleibt die Meldung unverändert —
ein Störungshinweis wäre dort irreführend. Läuft die Statusseite grün, wird
ebenfalls nichts angehängt.

Das Ergebnis wird 5 Minuten zwischengespeichert (auf dem Fehlerpfad 1 Minute).
Die Statusseite liegt selbst hinter einem CDN mit 10-Minuten-Cache — häufiger
abzufragen liefert dieselbe Antwort.

## Suchen, Filtern & Paginierung

Alle `list`-Befehle teilen sich dasselbe Paginierungs-Verhalten und besitzen
befehlsspezifische Filter. Intern wird serverseitig seitenweise abgerufen —
immer mit dem API-Maximum von 250 Datensätzen/Request (mit Rate-Limit-Schutz),
damit so wenige Requests wie möglich nötig sind.

### Menge: --limit und --all (für jeden `list`-Befehl)

| Option | Wirkung |
|---|---|
| `--limit N`, `-n N` | Maximal N Einträge (Standard: 25). |
| `--limit 0` | Unbegrenzt — lädt **alle** Treffer über alle Seiten. |
| `--all`, `-a` | Wie `--limit 0`; überschreibt ein gesetztes `--limit`. |

```bash
lxw invoices list --all          # alle Rechnungen
lxw contacts list --customer -a  # alle Kunden
```

Am Listenende wird die **Gesamtzahl** der Datensätze ausgegeben (aus dem
`totalElements` der API) — z.B. `→ 25 von 1234 Rechnungen angezeigt (mehr mit
--all)` bzw. `→ 1234 Kontakte gesamt`, wenn alles geladen wurde. Diese Zeile geht
auf **stderr**, stört also `--json`/`--csv`-Pipes nicht. Bei der clientseitigen
Artikelsuche wird stattdessen die Trefferzahl gezeigt (`→ 12 Treffer`).

### Befehlsspezifische Filter

| Befehl | Filter-Optionen |
|---|---|
| `invoices list` | `--status`, `--number` (exakt), `--contact-id`, `--include-archived` |
| `vouchers list` | `--type`, `--status`, `--number` (exakt), `--contact-id`, `--include-archived` |
| `contacts list` | `--name` (≥3 Z.), `--email` (≥3 Z.), `--number`, `--customer`, `--vendor`, `--grouped/--flat`, `--include-archived` |
| `articles list` | `--search`/`-q` (Volltext), `--number` (exakt), `--type product\|service`, `--gtin` (exakt) |
| `quotations list` | `--status`, `--include-archived` |
| `orders list` | `--status`, `--include-archived` |
| `delivery-notes list` | `--status`, `--include-archived` |

`--status` und `--type` sind komma-separiert (z.B. `--status open,paid`).

### Archivierte ausblenden (Standard)

Archivierte Datensätze werden bei **Belegen** (Rechnungen, Belege, Angebote,
Lieferscheine) **und Kontakten** standardmäßig ausgeblendet — `--include-archived`
zeigt sie. Bei Belegen filtert die API serverseitig (`archived=false`), die
Gesamtzahl bleibt also exakt; bei Kontakten geschieht es clientseitig (siehe
unten). Der Footer weist mit `· ohne archivierte (--include-archived zeigt alle)`
darauf hin.

### Kontakte: nur Kunden oder nur Lieferanten

`contacts list` liefert standardmäßig **eine zusammenhängende Liste** (nicht
getrennt — gut für Pipes und Weiterverarbeitung durch LLMs). Auf Wunsch nur eine
Rolle oder optional gruppiert:

```bash
lxw contacts list                    # aktive Kontakte, eine Liste
lxw contacts list --customer         # nur Kunden (serverseitig gefiltert)
lxw contacts list --vendor           # nur Lieferanten (serverseitig gefiltert)
lxw contacts list --grouped          # Tabelle in Kunden-/Lieferanten-Abschnitte
lxw contacts list --include-archived # archivierte Kontakte einblenden
```

Bei `--grouped` zeigt jede Tabelle die passende Kunden- bzw. Lieferantennummer;
Kontakte mit beiden Rollen erscheinen in beiden Abschnitten (`role`-Spalte =
`customer+vendor`). `--json` und `--csv` sind immer eine flache Liste mit
`role`-Spalte.

**Archivierte Kontakte** werden standardmäßig ausgeblendet — `--include-archived`
zeigt sie wieder. Da die Kontakt-API archivierte nicht serverseitig filtern kann,
geschieht das clientseitig: `lxw` blättert durch die Seiten und überspringt
archivierte, bis genug aktive für das `--limit` zusammen sind. Der Footer zeigt
dann z.B. `→ 298 aktive Kontakte gesamt (14 archivierte ausgeblendet)`.

### Artikel-Volltextsuche: --search / -q

Die Lexware-API filtert Artikel serverseitig **nur exakt** (`--number`, `--gtin`,
`--type`). Für unscharfe Suche nach **Bezeichnung, Beschreibung oder
Teil-Artikelnummer** gibt es `--search`/`-q` — dabei werden Artikel clientseitig
durchsucht (Teiltreffer, Groß-/Kleinschreibung egal):

```bash
lxw articles list -q "schraube"        # Bezeichnung/Beschreibung
lxw articles list -q "SCH-"            # Teil-Artikelnummer
lxw articles list --number SCH-001     # exakte Nummer (serverseitig, schnell)
lxw articles list -q "kabel" --type product --all   # kombinierbar
```

Hinweis: `--search` scannt im Zweifel alle Artikel (bricht ab, sobald genug
Treffer für `--limit` gefunden sind). Bei sehr großen Beständen entsprechend
langsamer als die exakten Server-Filter.

## Globale Optionen

| Flag | Zweck |
|---|---|
| `--json` | JSON-Ausgabe (für Pipelines) |
| `--csv` | CSV-Ausgabe |
| `--output PATH`, `-o` | In Datei statt stdout schreiben |
| `--profile NAME`, `-p` | Lexware-Installation (Profil) wählen; auch per `LEXWARE_PROFILE` |
| `--version`, `-V` | Version anzeigen |

## Interaktive TUI

Neben der CLI gibt es eine **Terminal-UI** zum Durchstöbern deiner Daten
(Rechnungen, Kontakte, Belege, Artikel, Angebote, Aufträge, Lieferscheine) —
und zum Anlegen neuer Aufträge als Entwurf.

```bash
lxw            # ohne Argumente im Terminal → TUI startet automatisch
lxw-tui        # oder explizit
```

![TUI](docs/tui-screenshot.svg)

- Links die Entitäten wählen (Pfeile + Enter), rechts die Liste; **Enter** auf
  einer Zeile öffnet die Detailansicht, **Esc** schließt sie.
- Die Detailansicht zeigt die Felder **menschenlesbar** (deutsche Feldnamen,
  Datumsangaben als TT.MM.JJJJ, ja/nein, verschachtelte Strukturen als Pfad wie
  *Adressen › Rechnungsadresse › Ort*). Mit **j** wechselt man jederzeit auf das
  **rohe JSON** der API und zurück.
- Tastenkürzel: **Tab** Fokus wechseln · **r** aktualisieren · **n** neuer
  Auftrag · **m** mehr laden · **/** suchen · **q** beenden (immer als Leiste
  unten sichtbar).
- **Paginierung**: Jede Liste lädt zunächst eine volle API-Seite (250
  Datensätze — das Server-Maximum pro Request). Scrollt man auf die letzte
  Zeile (oder drückt **m**), wird automatisch der nächste Schwung nachgeladen;
  die Cursor-Position bleibt erhalten. Die Statuszeile zeigt den Stand, z.B.
  `250 von 1234 Rechnungen — ans Ende scrollen oder m lädt mehr`.
- **Suchen** (**/**): öffnet eine Suchleiste über der Tabelle. **Tippen
  filtert sofort** die bereits geladenen Zeilen (clientseitig, über alle
  sichtbaren Spalten). **Enter sucht über die API** — bei Kontakten per Name
  bzw. E-Mail (enthält die Eingabe ein `@`, wird die E-Mail-Suche verwendet;
  min. 3 Zeichen), bei Artikeln per Volltextsuche. In den API-Treffern kann
  anschließend weiter lokal gefiltert werden. **Esc** schließt die Suche und
  stellt die normale Liste wieder her.
- **Auftrag anlegen** (**n**): Kunde per Namenssuche wählen, Artikel suchen und
  mit Menge und (editierbarem) Netto-Einzelpreis als Positionen hinzufügen —
  der Preis wird aus dem Artikel vorbelegt und kann vor dem Hinzufügen
  überschrieben werden. Optional Einleitungs- und Schlusstext eintragen, dann
  *„Auftrag anlegen"* — der Auftrag wird als **Entwurf** gespeichert und die
  Auftragsliste geöffnet. Während Suchen und Anlegen laufen, zeigt das Formular
  eine Lade-Animation. Finalisieren/Versenden bleibt bewusst der Lexware-UI
  vorbehalten.
- Firmenname oben = Verbindungstest; Fehler aus der API werden als Hinweis und
  in der Statuszeile angezeigt (nie verschluckt).

Die TUI startet **nur im interaktiven Terminal**. Sobald `lxw` mit
Argumenten aufgerufen wird oder die Ausgabe in eine Pipe/Datei geht, verhält es
sich unverändert wie die CLI — Skripte und `… | jq` bleiben also unberührt.

Technisch spricht die TUI ausschließlich den UI-freien Core (`lxw_cli.core`)
— exakt dieselbe API wie CLI und MCP-Server.

## Claude-Integration (MCP-Server)

Das CLI bringt einen eingebauten MCP-Server mit, mit dem **Claude Code**, **Claude Desktop** und **Cowork** deine Lexware-Daten direkt abfragen können.

### Setup in einem Schritt

```bash
lxw mcp install-claude    # Claude Code
lxw mcp install-desktop   # Claude Desktop + Cowork
```

Die beiden Frontends haben getrennte Konfigurationen: `install-claude` registriert den Server bei **Claude Code** (via `claude mcp add`, Scope `user`), `install-desktop` trägt ihn in die `claude_desktop_config.json` von **Claude Desktop** ein — das deckt auch **Cowork** ab. **Wichtig:** `install-desktop` bei beendetem Claude Desktop ausführen (Quit, nicht nur Fenster schließen) — die laufende App schreibt ihre Konfiguration aus dem Arbeitsspeicher zurück und überschreibt externe Änderungen. Danach Desktop starten.

Der API-Key wird dabei **nicht** an Claude übergeben — der Server liest ihn selbst aus `~/.config/lexware/.env` (chmod 600; Windows: `%APPDATA%\lexware\.env`); falls er dort noch fehlt, legen die Install-Befehle ihn einmalig ab. Prüfen:

```bash
lxw mcp status    # zeigt Registrierung für Claude Code und Desktop
claude mcp list   # 'lexware' muss in der Liste erscheinen (Claude Code)
```

Öffne Claude Code in einem Projekt und frag z.B.:

> *"Wie viele offene Rechnungen habe ich?"*
> *"Lade die PDF von Rechnung FB2600682 herunter."*
> *"Zeig mir alle Kunden, deren Name mit 'A' anfängt."*

Claude erkennt die Tools automatisch und ruft sie auf.

### Verfügbare MCP-Tools

40 Tools mit identischem Verhalten wie die CLI-Befehle:

- **Info**: `version` (meldet die laufende Version + Build-Zeitstempel — nützlich, wenn eine Client-UI wie ChatGPT eine gecachte/alte Version anzeigt)
- **Betriebsstatus**: `service_status` beantwortet „ist Lexware gerade gestört?" bzw. „warum geht das nicht?" aus der öffentlichen Statusseite — ohne API-Key und damit auch dann, wenn Lexware selbst nicht antwortet. Zusätzlich hängt der Server den Grund bei Serverfehlern **automatisch** an jede Tool-Fehlermeldung (siehe [Betriebsstatus](#betriebsstatus)).
- **Belegkette (weiterführen)**: `continue_document` führt einen Beleg in den Folgebeleg fort und hält beide verknüpft (`precedingSalesVoucherId`). Der Quelltyp wird automatisch erkannt (Belegnummer oder id). Unterstützte Wege (wie in Lexware):
  - **Angebot** → Auftrag · Lieferschein · Rechnung
  - **Auftrag** → Lieferschein · Rechnung
  - **Lieferschein** → Rechnung
  - **Rechnung** → Lieferschein · Rechnungskorrektur · Mahnung

  Inhalte (Kunde, Positionen, Texte) werden übernommen, der Folgebeleg entsteht als Entwurf. Für Rechnung/Rechnungskorrektur/Mahnung muss der Ausgangsbeleg **festgeschrieben** sein (sonst klare Meldung statt HTTP 406). **Abschlagsrechnung** und **Serienrechnung** sind über die Lexware-API nicht erstellbar — das meldet das Tool klar.
- **Mahnungen**: `create_dunning` erstellt aus einer überfälligen (festgeschriebenen, noch offenen) Rechnung eine Mahnung als Entwurf — ein erneuter Aufruf erhöht die Mahnstufe. `get_dunning` und `download_dunning_pdf` rufen eine Mahnung ab. **Nur per id** ansprechbar: Mahnungen sind in der Lexware-API nicht auflistbar (kein Listing, keine Belegnummer-Suche); die id stammt aus dem Erstellen oder den verknüpften Belegen der Rechnung.
- **Lesen**: `profile`, `list_invoices`, `get_invoice`, `get_payment_status`, `download_invoice_pdf`, `list_contacts`, `get_contact`, `list_vouchers`, `get_voucher`, `list_posting_categories`, `list_articles`, `get_article`, `list_quotations`, `get_quotation`, `download_quotation_pdf`, `list_order_confirmations`, `get_order_confirmation`, `download_order_confirmation_pdf`, `list_delivery_notes`, `get_delivery_note`, `download_delivery_note_pdf`
- **Belege als Draft anlegen**: `create_invoice_draft`, `create_voucher_draft`, `create_quotation_draft`, `create_order_confirmation_draft`, `create_delivery_note_draft`
- **Beleg-Anhänge**: Lexware braucht zwei Schritte (erst Beleg anlegen, dann Datei an die neue Beleg-id). **Online (claude.ai, ChatGPT):** `create_voucher_draft` liefert zum angelegten Beleg einen **Upload-Link** (30 Minuten gültig, nur für diesen Beleg). Darüber kommt die **Originaldatei unverändert** zu Lexware — entweder lädt der Chat sie aus seiner Code-Umgebung per `curl -F file=@…` hoch, oder der Nutzer öffnet den Link und zieht die Datei hinein. Hintergrund: Muss ein Chat die Datei selbst als Base64-Text in den Aufruf schreiben, verkleinert er sie zu einem unlesbaren Vorschaubild; solche Mini-Bilder (< 800 px) lehnt der Server ab. `create_upload_link` erzeugt einen Link für einen bestehenden Beleg. **ChatGPT:** `attach_chat_file` bekommt die im Chat angehängte Datei direkt von ChatGPT (`openai/fileParams`: Download-Adresse + Datei-id); der Server holt das Original selbst ab und hängt es an den Beleg — kein Link, kein Klick. **Lokal (stdio):** `create_voucher_draft` / `upload_voucher_file` nehmen `file_path` und hängen die Datei direkt an. Scheitert nur das Anhängen, bleibt der Beleg bestehen und die Antwort sagt das klar (kein Fehler, der zum doppelten Buchen verleitet). `download_voucher_file` lädt eine angehängte Datei über ihre id herunter. Größe/Typ prüft Lexware (PDF/JPG/PNG/XML, max. 5 MB).
- **Stammdaten anlegen** (kein Draft): `create_contact`, `create_article`
- **Stammdaten bearbeiten** (Teil-Update mit Auto-Merge + automatischem `version`-Handling): `update_contact`, `update_article`. Es werden nur die übergebenen Felder geändert; verschachtelte Objekte werden feldweise zusammengeführt, Listen komplett ersetzt. (Kontakt-`archived` ist in der Lexware-API read-only — Lexware würde eine Änderung stillschweigend verwerfen; `update_contact` lehnt Archivieren/Wiederherstellen daher mit klarer Meldung ab und ändert nichts. Das geht nur in der Lexware-Weboberfläche. Firmen-Kontakte mit **mehr als einem Ansprechpartner** lassen sich per Lexware-API grundsätzlich nicht aktualisieren — `update_contact` meldet das klar, statt eines rohen HTTP 406.)
- **Funktionswunsch melden**: `request_feature` erstellt einen **unverbindlichen** Feature-Wunsch als fertigen Text (`subject`/`body`) samt Anbieter-Adresse (`to` = david@oemedia.de). Der Server **versendet nichts** — der Nutzer kopiert den Text und schickt ihn selbst per E-Mail. Dies ist ein Endkundenprodukt: der Assistent baut keine Funktionen selbst, sondern bietet bei fehlenden Funktionen nur diesen Weg an.

Alle document-Tools (`get_*`, `download_*_pdf`) akzeptieren UUID **oder** Belegnummer (z.B. `FB2600682`) — **Ausnahme Mahnungen**: die sind nur per id ansprechbar (s.o.). PDFs landen in `~/Downloads/lexware/`.
Zum Schutz vor Prompt-Injection gilt lokal: `output_dir` darf nicht in einem
versteckten Verzeichnis liegen (z.B. `~/.config`), Uploads per `file_path` sind
auf Belegdateien (pdf, jpg, jpeg, png, xml) außerhalb versteckter Verzeichnisse
beschränkt, und heruntergeladene Dateien mit anderer Endung bekommen `.bin`
angehängt.

Die `list_*`-Tools bieten dieselben Optionen wie die CLI:

- `limit` begrenzt die Trefferzahl; `limit=0` lädt **alle** (paginiert intern).
- `list_articles` hat `search` für die Volltextsuche (Bezeichnung/Beschreibung/Teil-Artikelnummer) zusätzlich zum exakten `article_number`.
- `list_contacts` filtert mit `customer` / `vendor` nach Rolle.
- Belege-Tools (`list_invoices`, `list_vouchers`, `list_quotations`, `list_delivery_notes`) und `list_contacts` blenden archivierte standardmäßig aus; `include_archived=true` zeigt sie.

### Mehrere Lexware-Installationen in Claude

Pro Profil wird ein **eigener MCP-Server** registriert (`lexware-<profil>`),
fest an dieses Profil gebunden. Claude sieht damit getrennte Werkzeuge je
Installation und kann Daten nicht versehentlich vermischen:

```bash
lxw profiles add oemedia
lxw profiles add demo

lxw mcp install-claude  --profile oemedia   # → Server 'lexware-oemedia'
lxw mcp install-claude  --profile demo      # → Server 'lexware-demo'
lxw mcp install-desktop --profile oemedia   # Claude Desktop/Cowork analog
lxw mcp install-desktop --profile demo
lxw mcp status                              # listet alle lexware-* Server
```

- Der Server startet als `lxw-mcp --profile <name>` — das Profil steht sichtbar
  in der Claude-Konfiguration, der **Key nicht** (der bleibt in der Profildatei).
- Fehlt das Profil, bricht der Server beim Start mit klarer Meldung ab, statt
  auf eine andere Installation auszuweichen.
- Der Server nennt Claude seine Installation (Server-Hinweise und
  `version` → `installation`), damit Antworten zuordenbar bleiben.
- Der klassische Server `lexware` (ohne Profil) kann parallel bestehen bleiben.
- `uninstall-claude`/`uninstall-desktop` nehmen ebenfalls `--profile`.

### Weitere mcp-Befehle

```bash
lxw mcp status                    # zeigt Registrierung (Claude Code + Desktop)
lxw mcp install-claude --force    # Re-Install Claude Code (z.B. nach Pfad-Wechsel)
lxw mcp install-desktop --force   # Re-Install Claude Desktop/Cowork
lxw mcp uninstall-claude          # aus Claude Code entfernen
lxw mcp uninstall-desktop         # aus Claude Desktop entfernen
lxw mcp serve                     # läuft direkt — wird von Claude intern aufgerufen
```

### Multi-User-Server (HTTP) — ein Server, jeder mit eigenem Key

Neben dem lokalen stdio-Modus gibt es `lxw-mcp-http`: einen zentralen
MCP-Server für mehrere Nutzer, bei dem **jeder seinen eigenen Lexware-API-Key
mitbringt**. Der Server speichert keine Keys — er ist ein zustandsloser Proxy.

```bash
LXW_MCP_PUBLIC_URL=https://mcp.example.com \
LXW_MCP_SECRET="$(python -c 'import secrets; print(secrets.token_urlsafe(48))')" \
lxw-mcp-http    # lauscht auf 127.0.0.1:8788 (LXW_MCP_HOST/LXW_MCP_PORT)
```

Zwei Wege, den eigenen Key mitzubringen:

1. **Direkt als Bearer-Header** (Claude Code, Cursor, Skripte — alles, was
   eigene Header kann). Der Key bleibt in der lokalen Client-Config:

   ```bash
   claude mcp add lexware --transport http https://mcp.example.com/mcp \
     --header "Authorization: Bearer ${LEXWARE_API_KEY}"
   ```

2. **OAuth** (claude.ai, ChatGPT — Clients ohne Header-Support): Beim
   Verbinden öffnet sich eine Consent-Seite des Servers, auf der man einmalig
   seinen Lexware-Key eingibt. Der Server prüft ihn gegen die Lexware-API und
   stellt ein Zugriffstoken aus, in dem der Key **verschlüsselt eingebettet**
   ist (Fernet, `LXW_MCP_SECRET`). Das Token liegt beim Client auf dem Gerät
   des Nutzers; der Server entschlüsselt es nur pro Request und persistiert
   ausschließlich die OAuth-Client-Registrierungen und eine Sperrliste
   widerrufener Tokens (`LXW_MCP_DATA_DIR`, Standard `~/.config/lexware/mcp/`).

Hinweise für den Betrieb:

- `LXW_MCP_SECRET` **fest setzen** — ohne festes Secret werden alle Tokens
  bei jedem Neustart ungültig. Das Secret muss ein Fernet-Key oder mindestens
  32 Zeichen lang sein, sonst startet der Server nicht.
- Laufzeiten: Zugriffstoken 24 Stunden, Refresh-Token 30 Tage. Bei jedem
  Erneuern gibt es neue Tokens, aktive Nutzer bleiben also angemeldet; wer
  30 Tage nichts tut, muss sich neu anmelden.
- **Secret tauschen, ohne dass sich jemand neu anmelden muss:** das neue
  Secret nach `LXW_MCP_SECRET`, das bisherige nach `LXW_MCP_SECRET_PREVIOUS`,
  Server neu starten. Neue Tokens werden nur noch mit dem neuen Secret
  versiegelt, alte werden weiter angenommen und beim nächsten Erneuern
  umgestellt. Nach 30 Tagen gibt es kein gültiges altes Token mehr, dann
  `LXW_MCP_SECRET_PREVIOUS` entfernen (oder beim nächsten Tausch einfach
  überschreiben). **Notfall** (Secret oder Tokens kompromittiert): nur das
  Secret tauschen, ohne `LXW_MCP_SECRET_PREVIOUS` — dann sind sofort alle
  ausgestellten Tokens ungültig.
- Refresh-Tokens sind einmalig gültig (Rotation). Wird ein bereits benutzter
  Refresh-Token erneut vorgelegt, wird die gesamte Verbindung gesperrt.
  Tokens lassen sich über `/revoke` (RFC 7009) widerrufen; die Sperrliste
  liegt in `revoked.json` und überlebt Neustarts.
- Grenzen: Client-Registrierung max. 10 pro IP in 10 Minuten und max. 2000
  gespeicherte Clients (älteste fallen raus); Request-Body max. 16 MB,
  Datei-Upload max. 10 MB. Der Reverse-Proxy muss `X-Forwarded-For` setzen;
  läuft er nicht auf demselben Host, seine Adresse in `FORWARDED_ALLOW_IPS`
  eintragen (uvicorn) — sonst zählt das Registrierungs-Limit alle Clients
  als eine IP.
- Vor den Server gehört ein HTTPS-Reverse-Proxy (Caddy, nginx, Cloudflare
  Tunnel, `tailscale serve`, …); `LXW_MCP_PUBLIC_URL` ist die von außen
  sichtbare Basis-URL (sie steht in den OAuth-Metadaten).
- PDF-Downloads liefern über HTTP das PDF als Binärinhalt zurück (statt eines
  Pfads auf der Server-Platte wie im stdio-Modus).
- OAuth-Clients dürfen sich nur mit Redirect-URIs auf vertrauenswürdige Ziele
  registrieren: Loopback (`127.0.0.1`, `localhost`, `[::1]`, beliebiger Port),
  `https://` auf `claude.ai`, `claude.com`, `chatgpt.com`, `chat.openai.com`,
  `vscode.dev`, sowie die Schemes `cursor:`, `vscode:`, `vscode-insiders:`.
  Weitere Ziele per `LXW_MCP_REDIRECT_ALLOW` (kommagetrennt; Host = https auf
  diesem Host, `schema:` = eigenes Schema). Registrierungen mit anderen Zielen
  werden abgelehnt, bereits gespeicherte ignoriert.
- Datei-Uploads laufen über HTTP per **Upload-Link** (`/upload/<token>`, Formular ohne
  JavaScript bzw. `curl -F file=@…`); `file_path` (eine Datei auf der Server-Platte)
  wird abgelehnt. Das Token ist wie die OAuth-Tokens mit `LXW_MCP_SECRET` versiegelt
  (Key + Beleg-id, 30 Minuten) — ein geleakter Link erlaubt nur das Anhängen an diesen
  einen Beleg. Damit der Chat die Datei **automatisch** hochladen kann, muss die
  Server-Domain in den Netzwerk-Einstellungen der Code-Umgebung erlaubt sein
  (bei claude.ai in den Einstellungen zur Code-Ausführung bzw. Netzwerkfreigabe);
  sonst bekommt der Nutzer den Link zum Anklicken.
- `attach_chat_file` lädt nur von den Datei-Hosts des Chats (Standard: `oaiusercontent.com`,
  `openai.com`, `chatgpt.com` samt Subdomains, nur https/443; ändern per
  `LXW_MCP_CHAT_FILE_HOSTS`, kommagetrennt). Weiterleitungen werden Schritt für Schritt
  gegen dieselbe Liste geprüft, es gehen keine eigenen Zugangsdaten mit, die Größe ist
  auf 10 MB begrenzt — der Server wird so nicht zum Proxy für beliebige Adressen.
- Der Server prüft im Hintergrund alle 10 Minuten den Lexware-Betriebsstatus
  (`LXW_MCP_STATUS_REFRESH`, Sekunden; `0` schaltet das ab). Das hält den
  Zwischenspeicher warm, damit der Fehlerpfad sofort antwortet, und schreibt
  Störungen ins Server-Log. Nur hier sinnvoll — der stdio-Server ist zu
  kurzlebig und füllt den Zwischenspeicher bei Bedarf.
- Tokens laufen nach 24 h ab; Clients erneuern sie automatisch per
  Refresh-Token. Ein geleaktes Token wird wirkungslos, sobald der zugehörige
  Lexware-Key unter <https://app.lexware.de/addons/public-api> widerrufen wird.

**Mehrere Installationen über HTTP** (z.B. oemedia + Demo): pro Installation
eine eigene Verbindung mit eigenem Key. Damit die Verbindungen unterscheidbar
sind (und Clients wie claude.ai dieselbe Adresse nicht doppelt ablehnen),
hängt man `?installation=<name>` an die URL:

```bash
# Header-fähige Clients
claude mcp add lexware-oemedia --transport http "https://mcp.example.com/mcp?installation=oemedia" \
  --header "Authorization: Bearer ${KEY_OEMEDIA}"
claude mcp add lexware-demo --transport http "https://mcp.example.com/mcp?installation=demo" \
  --header "Authorization: Bearer ${KEY_DEMO}"

# claude.ai / ChatGPT: zwei Connectoren
#   https://mcp.example.com/mcp?installation=oemedia
#   https://mcp.example.com/mcp?installation=demo
```

Der Parameter ist nur eine **Beschriftung** — welche Installation angesprochen
wird, entscheidet allein der Key. Die OAuth-Consent-Seite zeigt den Namen an
("gib den API-Key genau dieser Installation ein"), das Label wird ins Token
übernommen (übersteht auch den Refresh), und `version` meldet es als
`installation`. Der Server speichert weiterhin keine Keys.

`request_feature` braucht keine Konfiguration: es liefert nur einen fertigen
Text + die Empfänger-Adresse (`david@oemedia.de`) zurück, den der Nutzer selbst
per E-Mail sendet. Der Server verschickt nichts.

#### Zugang auf bekannte Installationen beschränken (KYC-Allow-List)

Standardmäßig darf **jeder mit einem gültigen Lexware-Key** den Server nutzen.
Soll er nur **bekannte** Lexware-Installationen bedienen (KYC, Basis für ein
Bezahlmodell), gibt es ein Allow-List-Gate. Es identifiziert eine Installation
über die `organizationId` aus dem Lexware-Profil (stabil pro Firma) und ist
standardmäßig **aus** — ohne Konfiguration ändert sich nichts.

Drei Modi über `LXW_MCP_ALLOWLIST_MODE`:

- `off` (Standard) — Gate inaktiv, kein zusätzlicher Profil-Abruf.
- `log` — jede anrufende Organisation wird in `<data_dir>/pending.json`
  protokolliert, **nichts wird blockiert**. Damit findet man ohne Aussperr-
  Risiko heraus, welche Installationen den Dienst tatsächlich nutzen, und baut
  daraus die Allow-List.
- `enforce` — Organisationen, die **nicht** freigeschaltet sind, werden
  abgewiesen; unbekannte landen weiterhin in `pending.json` (Onboarding-Queue).

Freischalten (beide Quellen werden gemischt):

- `LXW_MCP_ALLOWLIST` — kommaseparierte `organizationId`s inline, **oder**
- `<data_dir>/allowlist.json` — JSON-Array `["org-id", …]` oder Objekt
  `{"org-id": {"note": "Kunde X"}}`. Wird bei Änderung **automatisch neu
  geladen** (Kunde freischalten ohne Neustart).

**Einzelne Installation sperren:** `"blocked": true` am Eintrag, z. B.
`{"org-id": {"note": "Kunde X", "blocked": true}}`. Die Sperre wirkt in `log`
**und** `enforce`, ab dem nächsten Aufruf auch für bereits angemeldete Nutzer
und für Upload-Links. Der Nutzer sieht beim Neuverbinden „Der Zugang dieser
Lexware-Installation zum Dienst wurde beendet“ mit der Kontaktadresse.
Entsperren: `blocked` entfernen oder auf `false` setzen. Im Modus `off` gibt es
keine Prüfung und damit auch keine Sperre.

Weitere Schalter: `LXW_MCP_ALLOWLIST_CONTACT` (Adresse in der Ablehn-Meldung),
`LXW_MCP_ALLOWLIST_TTL` (Sekunden Cache für Key→Org-Auflösung, Standard 600),
`LXW_MCP_ALLOWLIST_FAIL_OPEN=1` (bei nicht erreichbarer Lexware-API —
Netzwerkfehler, HTTP 5xx/429 — durchlassen statt sperren; Standard ist im
`enforce`-Modus fail-closed). Ein von Lexware abgelehnter Key oder ein Profil
ohne Organisation bleibt auch mit Fail-Open gesperrt.

Empfohlener Rollout: erst `log` fahren, `pending.json` sichten, bekannte Orgs in
`allowlist.json` übernehmen, dann auf `enforce` umstellen.

## Out of Scope (v1)

Bewusst nicht enthalten:

- Finalisierung von Rechnungen (`/finalize`)
- Stornierungen / Credit Notes als eigene Workflows
- Webhooks / Event-Subscriptions
- Multi-Account-Profile (nutze stattdessen verschiedene `.env`-Dateien)

## Entwicklung

```bash
uv pip install -e ".[dev]"
pytest -v
ruff check src tests
```

## Lizenz

MIT
