lxw-cli 0.1.2__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/PKG-INFO +91 -2
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/README.md +89 -1
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/pyproject.toml +3 -1
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/mcp.py +31 -10
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/core/client.py +4 -2
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/core/services.py +347 -0
- lxw_cli-0.4.0/src/lxw_cli/feature_request.py +57 -0
- lxw_cli-0.4.0/src/lxw_cli/mcp_auth.py +586 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/mcp_server.py +223 -20
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/test_cli.py +26 -0
- lxw_cli-0.4.0/tests/test_feature_request.py +32 -0
- lxw_cli-0.4.0/tests/test_mcp_auth.py +358 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/test_mcp_server.py +18 -1
- lxw_cli-0.4.0/tests/test_services.py +498 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/uv.lock +1 -1
- lxw_cli-0.1.2/tests/test_services.py +0 -257
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/.env.example +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/.gitattributes +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/.github/workflows/publish.yml +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/.github/workflows/test.yml +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/.gitignore +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/LICENSE +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/docs/tui-screenshot.svg +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/__init__.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/__main__.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/cli.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/__init__.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/_common.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/articles.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/contacts.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/delivery_notes.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/invoices.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/orders.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/quotations.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/vouchers.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/config.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/core/__init__.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/core/constants.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/core/errors.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/core/models.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/output.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/tui/__init__.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/tui/app.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/tui/detail.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/tui/entities.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/tui/order_form.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/__init__.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/conftest.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/snapshots/contacts_flat.txt +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/snapshots/contacts_grouped.txt +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/snapshots/invoices_list.txt +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/test_cli_snapshots.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/test_client.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/test_config.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/test_output.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/test_resolve.py +0 -0
- {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/test_tui.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lxw-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Inoffizielles CLI/TUI/MCP-Tool für die Lexware Office API (vormals lexoffice)
|
|
5
5
|
Project-URL: Homepage, https://github.com/OEM-Mrks/lxw-cli
|
|
6
6
|
Project-URL: Documentation, https://developers.lexware.io/
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: cryptography>=42
|
|
20
21
|
Requires-Dist: fastmcp>=2.10
|
|
21
22
|
Requires-Dist: httpx>=0.27
|
|
22
23
|
Requires-Dist: pydantic>=2.6
|
|
@@ -87,6 +88,26 @@ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | ie
|
|
|
87
88
|
Ab dem nächsten cmd-Fenster reicht dann überall einfach `lxw`. Der API-Key
|
|
88
89
|
wird beim ersten Start abgefragt und unter `%APPDATA%\lexware\.env` gespeichert.
|
|
89
90
|
|
|
91
|
+
### Update
|
|
92
|
+
|
|
93
|
+
Auf die neueste Version von PyPI:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
uv tool upgrade lxw-cli # oder: pipx upgrade lxw-cli
|
|
97
|
+
lxw --version # Kontrolle
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Sieht uv eine frisch veröffentlichte Version noch nicht (das PyPI-CDN braucht
|
|
101
|
+
nach einem Release ein paar Minuten) oder ist das Tool auf eine exakte Version
|
|
102
|
+
gepinnt, hilft ein Neu-Install auf `latest`:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
uv tool install --force lxw-cli@latest
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Der MCP-Server für Claude nutzt das Update automatisch mit — bereits laufende
|
|
109
|
+
Claude-Sessions müssen dafür einmal neu gestartet werden.
|
|
110
|
+
|
|
90
111
|
### Aus dem Quellcode (Entwicklung)
|
|
91
112
|
|
|
92
113
|
```bash
|
|
@@ -96,6 +117,15 @@ uv venv && source .venv/bin/activate
|
|
|
96
117
|
uv pip install -e ".[dev]"
|
|
97
118
|
```
|
|
98
119
|
|
|
120
|
+
Lokalen Arbeitsstand ohne Release als globales `lxw` installieren:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
uv tool install --force --reinstall .
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
(`--reinstall` ist nötig — bei unveränderter Versionsnummer nimmt uv sonst
|
|
127
|
+
den gecachten Build.)
|
|
128
|
+
|
|
99
129
|
## API-Key konfigurieren
|
|
100
130
|
|
|
101
131
|
Lege einen API-Key in deinem Lexware-Account an: <https://app.lexware.de/addons/public-api>
|
|
@@ -377,11 +407,21 @@ Claude erkennt die Tools automatisch und ruft sie auf.
|
|
|
377
407
|
|
|
378
408
|
### Verfügbare MCP-Tools
|
|
379
409
|
|
|
380
|
-
|
|
410
|
+
31 Tools mit identischem Verhalten wie die CLI-Befehle:
|
|
411
|
+
|
|
412
|
+
- **Info**: `version` (meldet die laufende Version + Build-Zeitstempel — nützlich, wenn eine Client-UI wie ChatGPT eine gecachte/alte Version anzeigt)
|
|
413
|
+
- **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):
|
|
414
|
+
- **Angebot** → Auftrag · Lieferschein · Rechnung
|
|
415
|
+
- **Auftrag** → Lieferschein · Rechnung
|
|
416
|
+
- **Lieferschein** → Rechnung
|
|
417
|
+
- **Rechnung** → Lieferschein · Rechnungskorrektur
|
|
381
418
|
|
|
419
|
+
Inhalte (Kunde, Positionen, Texte) werden übernommen, der Folgebeleg entsteht als Entwurf. Für Rechnung/Rechnungskorrektur 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.
|
|
382
420
|
- **Lesen**: `profile`, `list_invoices`, `get_invoice`, `download_invoice_pdf`, `list_contacts`, `get_contact`, `list_vouchers`, `get_voucher`, `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`
|
|
383
421
|
- **Belege als Draft anlegen**: `create_invoice_draft`, `create_voucher_draft`, `create_quotation_draft`, `create_order_confirmation_draft`, `create_delivery_note_draft`
|
|
384
422
|
- **Stammdaten anlegen** (kein Draft): `create_contact`, `create_article`
|
|
423
|
+
- **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 und kann nicht per API gesetzt werden. 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.)
|
|
424
|
+
- **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.
|
|
385
425
|
|
|
386
426
|
Alle document-Tools (`get_*`, `download_*_pdf`) akzeptieren UUID **oder** Belegnummer (z.B. `FB2600682`). PDFs landen in `~/Downloads/lexware/`.
|
|
387
427
|
|
|
@@ -403,6 +443,55 @@ lxw mcp uninstall-desktop # aus Claude Desktop entfernen
|
|
|
403
443
|
lxw mcp serve # läuft direkt — wird von Claude intern aufgerufen
|
|
404
444
|
```
|
|
405
445
|
|
|
446
|
+
### Multi-User-Server (HTTP) — ein Server, jeder mit eigenem Key
|
|
447
|
+
|
|
448
|
+
Neben dem lokalen stdio-Modus gibt es `lxw-mcp-http`: einen zentralen
|
|
449
|
+
MCP-Server für mehrere Nutzer, bei dem **jeder seinen eigenen Lexware-API-Key
|
|
450
|
+
mitbringt**. Der Server speichert keine Keys — er ist ein zustandsloser Proxy.
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
LXW_MCP_PUBLIC_URL=https://mcp.example.com \
|
|
454
|
+
LXW_MCP_SECRET='ein-langes-zufaelliges-secret' \
|
|
455
|
+
lxw-mcp-http # lauscht auf 127.0.0.1:8788 (LXW_MCP_HOST/LXW_MCP_PORT)
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Zwei Wege, den eigenen Key mitzubringen:
|
|
459
|
+
|
|
460
|
+
1. **Direkt als Bearer-Header** (Claude Code, Cursor, Skripte — alles, was
|
|
461
|
+
eigene Header kann). Der Key bleibt in der lokalen Client-Config:
|
|
462
|
+
|
|
463
|
+
```bash
|
|
464
|
+
claude mcp add lexware --transport http https://mcp.example.com/mcp \
|
|
465
|
+
--header "Authorization: Bearer ${LEXWARE_API_KEY}"
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
2. **OAuth** (claude.ai, ChatGPT — Clients ohne Header-Support): Beim
|
|
469
|
+
Verbinden öffnet sich eine Consent-Seite des Servers, auf der man einmalig
|
|
470
|
+
seinen Lexware-Key eingibt. Der Server prüft ihn gegen die Lexware-API und
|
|
471
|
+
stellt ein Zugriffstoken aus, in dem der Key **verschlüsselt eingebettet**
|
|
472
|
+
ist (Fernet, `LXW_MCP_SECRET`). Das Token liegt beim Client auf dem Gerät
|
|
473
|
+
des Nutzers; der Server entschlüsselt es nur pro Request und persistiert
|
|
474
|
+
ausschließlich die OAuth-Client-Registrierungen (`LXW_MCP_DATA_DIR`,
|
|
475
|
+
Standard `~/.config/lexware/mcp/`).
|
|
476
|
+
|
|
477
|
+
Hinweise für den Betrieb:
|
|
478
|
+
|
|
479
|
+
- `LXW_MCP_SECRET` **fest setzen** — ohne festes Secret werden alle Tokens
|
|
480
|
+
bei jedem Neustart ungültig. Rotieren des Secrets invalidiert bewusst alle
|
|
481
|
+
ausgestellten Tokens.
|
|
482
|
+
- Vor den Server gehört ein HTTPS-Reverse-Proxy (Caddy, nginx, Cloudflare
|
|
483
|
+
Tunnel, `tailscale serve`, …); `LXW_MCP_PUBLIC_URL` ist die von außen
|
|
484
|
+
sichtbare Basis-URL (sie steht in den OAuth-Metadaten).
|
|
485
|
+
- PDF-Downloads liefern über HTTP das PDF als Binärinhalt zurück (statt eines
|
|
486
|
+
Pfads auf der Server-Platte wie im stdio-Modus).
|
|
487
|
+
- Tokens laufen nach 24 h ab; Clients erneuern sie automatisch per
|
|
488
|
+
Refresh-Token. Ein geleaktes Token wird wirkungslos, sobald der zugehörige
|
|
489
|
+
Lexware-Key unter <https://app.lexware.de/addons/public-api> widerrufen wird.
|
|
490
|
+
|
|
491
|
+
`request_feature` braucht keine Konfiguration: es liefert nur einen fertigen
|
|
492
|
+
Text + die Empfänger-Adresse (`david@oemedia.de`) zurück, den der Nutzer selbst
|
|
493
|
+
per E-Mail sendet. Der Server verschickt nichts.
|
|
494
|
+
|
|
406
495
|
## Out of Scope (v1)
|
|
407
496
|
|
|
408
497
|
Bewusst nicht enthalten:
|
|
@@ -52,6 +52,26 @@ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | ie
|
|
|
52
52
|
Ab dem nächsten cmd-Fenster reicht dann überall einfach `lxw`. Der API-Key
|
|
53
53
|
wird beim ersten Start abgefragt und unter `%APPDATA%\lexware\.env` gespeichert.
|
|
54
54
|
|
|
55
|
+
### Update
|
|
56
|
+
|
|
57
|
+
Auf die neueste Version von PyPI:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv tool upgrade lxw-cli # oder: pipx upgrade lxw-cli
|
|
61
|
+
lxw --version # Kontrolle
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Sieht uv eine frisch veröffentlichte Version noch nicht (das PyPI-CDN braucht
|
|
65
|
+
nach einem Release ein paar Minuten) oder ist das Tool auf eine exakte Version
|
|
66
|
+
gepinnt, hilft ein Neu-Install auf `latest`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv tool install --force lxw-cli@latest
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Der MCP-Server für Claude nutzt das Update automatisch mit — bereits laufende
|
|
73
|
+
Claude-Sessions müssen dafür einmal neu gestartet werden.
|
|
74
|
+
|
|
55
75
|
### Aus dem Quellcode (Entwicklung)
|
|
56
76
|
|
|
57
77
|
```bash
|
|
@@ -61,6 +81,15 @@ uv venv && source .venv/bin/activate
|
|
|
61
81
|
uv pip install -e ".[dev]"
|
|
62
82
|
```
|
|
63
83
|
|
|
84
|
+
Lokalen Arbeitsstand ohne Release als globales `lxw` installieren:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uv tool install --force --reinstall .
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
(`--reinstall` ist nötig — bei unveränderter Versionsnummer nimmt uv sonst
|
|
91
|
+
den gecachten Build.)
|
|
92
|
+
|
|
64
93
|
## API-Key konfigurieren
|
|
65
94
|
|
|
66
95
|
Lege einen API-Key in deinem Lexware-Account an: <https://app.lexware.de/addons/public-api>
|
|
@@ -342,11 +371,21 @@ Claude erkennt die Tools automatisch und ruft sie auf.
|
|
|
342
371
|
|
|
343
372
|
### Verfügbare MCP-Tools
|
|
344
373
|
|
|
345
|
-
|
|
374
|
+
31 Tools mit identischem Verhalten wie die CLI-Befehle:
|
|
375
|
+
|
|
376
|
+
- **Info**: `version` (meldet die laufende Version + Build-Zeitstempel — nützlich, wenn eine Client-UI wie ChatGPT eine gecachte/alte Version anzeigt)
|
|
377
|
+
- **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):
|
|
378
|
+
- **Angebot** → Auftrag · Lieferschein · Rechnung
|
|
379
|
+
- **Auftrag** → Lieferschein · Rechnung
|
|
380
|
+
- **Lieferschein** → Rechnung
|
|
381
|
+
- **Rechnung** → Lieferschein · Rechnungskorrektur
|
|
346
382
|
|
|
383
|
+
Inhalte (Kunde, Positionen, Texte) werden übernommen, der Folgebeleg entsteht als Entwurf. Für Rechnung/Rechnungskorrektur 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.
|
|
347
384
|
- **Lesen**: `profile`, `list_invoices`, `get_invoice`, `download_invoice_pdf`, `list_contacts`, `get_contact`, `list_vouchers`, `get_voucher`, `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`
|
|
348
385
|
- **Belege als Draft anlegen**: `create_invoice_draft`, `create_voucher_draft`, `create_quotation_draft`, `create_order_confirmation_draft`, `create_delivery_note_draft`
|
|
349
386
|
- **Stammdaten anlegen** (kein Draft): `create_contact`, `create_article`
|
|
387
|
+
- **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 und kann nicht per API gesetzt werden. 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.)
|
|
388
|
+
- **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.
|
|
350
389
|
|
|
351
390
|
Alle document-Tools (`get_*`, `download_*_pdf`) akzeptieren UUID **oder** Belegnummer (z.B. `FB2600682`). PDFs landen in `~/Downloads/lexware/`.
|
|
352
391
|
|
|
@@ -368,6 +407,55 @@ lxw mcp uninstall-desktop # aus Claude Desktop entfernen
|
|
|
368
407
|
lxw mcp serve # läuft direkt — wird von Claude intern aufgerufen
|
|
369
408
|
```
|
|
370
409
|
|
|
410
|
+
### Multi-User-Server (HTTP) — ein Server, jeder mit eigenem Key
|
|
411
|
+
|
|
412
|
+
Neben dem lokalen stdio-Modus gibt es `lxw-mcp-http`: einen zentralen
|
|
413
|
+
MCP-Server für mehrere Nutzer, bei dem **jeder seinen eigenen Lexware-API-Key
|
|
414
|
+
mitbringt**. Der Server speichert keine Keys — er ist ein zustandsloser Proxy.
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
LXW_MCP_PUBLIC_URL=https://mcp.example.com \
|
|
418
|
+
LXW_MCP_SECRET='ein-langes-zufaelliges-secret' \
|
|
419
|
+
lxw-mcp-http # lauscht auf 127.0.0.1:8788 (LXW_MCP_HOST/LXW_MCP_PORT)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Zwei Wege, den eigenen Key mitzubringen:
|
|
423
|
+
|
|
424
|
+
1. **Direkt als Bearer-Header** (Claude Code, Cursor, Skripte — alles, was
|
|
425
|
+
eigene Header kann). Der Key bleibt in der lokalen Client-Config:
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
claude mcp add lexware --transport http https://mcp.example.com/mcp \
|
|
429
|
+
--header "Authorization: Bearer ${LEXWARE_API_KEY}"
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
2. **OAuth** (claude.ai, ChatGPT — Clients ohne Header-Support): Beim
|
|
433
|
+
Verbinden öffnet sich eine Consent-Seite des Servers, auf der man einmalig
|
|
434
|
+
seinen Lexware-Key eingibt. Der Server prüft ihn gegen die Lexware-API und
|
|
435
|
+
stellt ein Zugriffstoken aus, in dem der Key **verschlüsselt eingebettet**
|
|
436
|
+
ist (Fernet, `LXW_MCP_SECRET`). Das Token liegt beim Client auf dem Gerät
|
|
437
|
+
des Nutzers; der Server entschlüsselt es nur pro Request und persistiert
|
|
438
|
+
ausschließlich die OAuth-Client-Registrierungen (`LXW_MCP_DATA_DIR`,
|
|
439
|
+
Standard `~/.config/lexware/mcp/`).
|
|
440
|
+
|
|
441
|
+
Hinweise für den Betrieb:
|
|
442
|
+
|
|
443
|
+
- `LXW_MCP_SECRET` **fest setzen** — ohne festes Secret werden alle Tokens
|
|
444
|
+
bei jedem Neustart ungültig. Rotieren des Secrets invalidiert bewusst alle
|
|
445
|
+
ausgestellten Tokens.
|
|
446
|
+
- Vor den Server gehört ein HTTPS-Reverse-Proxy (Caddy, nginx, Cloudflare
|
|
447
|
+
Tunnel, `tailscale serve`, …); `LXW_MCP_PUBLIC_URL` ist die von außen
|
|
448
|
+
sichtbare Basis-URL (sie steht in den OAuth-Metadaten).
|
|
449
|
+
- PDF-Downloads liefern über HTTP das PDF als Binärinhalt zurück (statt eines
|
|
450
|
+
Pfads auf der Server-Platte wie im stdio-Modus).
|
|
451
|
+
- Tokens laufen nach 24 h ab; Clients erneuern sie automatisch per
|
|
452
|
+
Refresh-Token. Ein geleaktes Token wird wirkungslos, sobald der zugehörige
|
|
453
|
+
Lexware-Key unter <https://app.lexware.de/addons/public-api> widerrufen wird.
|
|
454
|
+
|
|
455
|
+
`request_feature` braucht keine Konfiguration: es liefert nur einen fertigen
|
|
456
|
+
Text + die Empfänger-Adresse (`david@oemedia.de`) zurück, den der Nutzer selbst
|
|
457
|
+
per E-Mail sendet. Der Server verschickt nichts.
|
|
458
|
+
|
|
371
459
|
## Out of Scope (v1)
|
|
372
460
|
|
|
373
461
|
Bewusst nicht enthalten:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lxw-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Inoffizielles CLI/TUI/MCP-Tool für die Lexware Office API (vormals lexoffice)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -30,6 +30,7 @@ dependencies = [
|
|
|
30
30
|
"python-dotenv>=1.0",
|
|
31
31
|
"fastmcp>=2.10",
|
|
32
32
|
"textual>=0.58",
|
|
33
|
+
"cryptography>=42",
|
|
33
34
|
]
|
|
34
35
|
|
|
35
36
|
[project.optional-dependencies]
|
|
@@ -44,6 +45,7 @@ dev = [
|
|
|
44
45
|
[project.scripts]
|
|
45
46
|
lxw = "lxw_cli.cli:_run"
|
|
46
47
|
lxw-mcp = "lxw_cli.mcp_server:run"
|
|
48
|
+
lxw-mcp-http = "lxw_cli.mcp_server:run_http"
|
|
47
49
|
lxw-tui = "lxw_cli.tui.app:run"
|
|
48
50
|
|
|
49
51
|
[project.urls]
|
|
@@ -21,25 +21,46 @@ app = typer.Typer(no_args_is_help=True)
|
|
|
21
21
|
MCP_NAME = "lexware"
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def _desktop_config_dirs() -> list[Path]:
|
|
25
|
+
"""Candidate directories for Claude Desktop's configuration, per platform.
|
|
26
|
+
|
|
27
|
+
On Windows the classic install uses %APPDATA%\\Claude; the Microsoft-Store
|
|
28
|
+
build (MSIX) virtualizes AppData under
|
|
29
|
+
%LOCALAPPDATA%\\Packages\\Claude_<hash>\\LocalCache\\Roaming\\Claude.
|
|
30
|
+
"""
|
|
31
|
+
if sys.platform == "darwin":
|
|
32
|
+
return [Path.home() / "Library" / "Application Support" / "Claude"]
|
|
33
|
+
if sys.platform == "win32":
|
|
34
|
+
appdata = os.environ.get("APPDATA")
|
|
35
|
+
root = Path(appdata) if appdata else Path.home() / "AppData" / "Roaming"
|
|
36
|
+
dirs = [root / "Claude"]
|
|
37
|
+
local = os.environ.get("LOCALAPPDATA")
|
|
38
|
+
lroot = Path(local) if local else Path.home() / "AppData" / "Local"
|
|
39
|
+
dirs += sorted((lroot / "Packages").glob("Claude_*/LocalCache/Roaming/Claude"))
|
|
40
|
+
return dirs
|
|
41
|
+
return [Path.home() / ".config" / "Claude"]
|
|
42
|
+
|
|
43
|
+
|
|
24
44
|
def desktop_config_path() -> Path:
|
|
25
45
|
"""Location of Claude Desktop's `claude_desktop_config.json`.
|
|
26
46
|
|
|
27
47
|
Claude Desktop (and Cowork) read MCP servers from this file — registering
|
|
28
48
|
via `claude mcp add` only covers Claude Code. `CLAUDE_DESKTOP_CONFIG`
|
|
29
|
-
overrides the path (mostly for tests).
|
|
49
|
+
overrides the path (mostly for tests). Among the platform candidates the
|
|
50
|
+
one already holding a config file wins, then the first existing directory,
|
|
51
|
+
then the classic default.
|
|
30
52
|
"""
|
|
31
53
|
override = os.environ.get("CLAUDE_DESKTOP_CONFIG")
|
|
32
54
|
if override:
|
|
33
55
|
return Path(override).expanduser()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
|
|
56
|
+
candidates = _desktop_config_dirs()
|
|
57
|
+
for d in candidates:
|
|
58
|
+
if (d / "claude_desktop_config.json").is_file():
|
|
59
|
+
return d / "claude_desktop_config.json"
|
|
60
|
+
for d in candidates:
|
|
61
|
+
if d.is_dir():
|
|
62
|
+
return d / "claude_desktop_config.json"
|
|
63
|
+
return candidates[0] / "claude_desktop_config.json"
|
|
43
64
|
|
|
44
65
|
|
|
45
66
|
def _load_desktop_config(path: Path) -> dict[str, Any]:
|
|
@@ -125,8 +125,10 @@ class LexwareClient:
|
|
|
125
125
|
def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
126
126
|
return self._request("GET", path, params=params).json()
|
|
127
127
|
|
|
128
|
-
def post(
|
|
129
|
-
|
|
128
|
+
def post(
|
|
129
|
+
self, path: str, json_body: Any, params: dict[str, Any] | None = None
|
|
130
|
+
) -> Any:
|
|
131
|
+
return self._request("POST", path, params=params, json=json_body).json()
|
|
130
132
|
|
|
131
133
|
def put(self, path: str, json_body: Any) -> Any:
|
|
132
134
|
return self._request("PUT", path, json=json_body).json()
|