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.
Files changed (57) hide show
  1. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/PKG-INFO +91 -2
  2. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/README.md +89 -1
  3. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/pyproject.toml +3 -1
  4. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/mcp.py +31 -10
  5. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/core/client.py +4 -2
  6. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/core/services.py +347 -0
  7. lxw_cli-0.4.0/src/lxw_cli/feature_request.py +57 -0
  8. lxw_cli-0.4.0/src/lxw_cli/mcp_auth.py +586 -0
  9. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/mcp_server.py +223 -20
  10. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/test_cli.py +26 -0
  11. lxw_cli-0.4.0/tests/test_feature_request.py +32 -0
  12. lxw_cli-0.4.0/tests/test_mcp_auth.py +358 -0
  13. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/test_mcp_server.py +18 -1
  14. lxw_cli-0.4.0/tests/test_services.py +498 -0
  15. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/uv.lock +1 -1
  16. lxw_cli-0.1.2/tests/test_services.py +0 -257
  17. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/.env.example +0 -0
  18. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/.gitattributes +0 -0
  19. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/.github/workflows/publish.yml +0 -0
  20. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/.github/workflows/test.yml +0 -0
  21. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/.gitignore +0 -0
  22. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/LICENSE +0 -0
  23. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/docs/tui-screenshot.svg +0 -0
  24. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/__init__.py +0 -0
  25. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/__main__.py +0 -0
  26. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/cli.py +0 -0
  27. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/__init__.py +0 -0
  28. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/_common.py +0 -0
  29. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/articles.py +0 -0
  30. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/contacts.py +0 -0
  31. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/delivery_notes.py +0 -0
  32. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/invoices.py +0 -0
  33. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/orders.py +0 -0
  34. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/quotations.py +0 -0
  35. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/commands/vouchers.py +0 -0
  36. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/config.py +0 -0
  37. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/core/__init__.py +0 -0
  38. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/core/constants.py +0 -0
  39. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/core/errors.py +0 -0
  40. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/core/models.py +0 -0
  41. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/output.py +0 -0
  42. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/tui/__init__.py +0 -0
  43. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/tui/app.py +0 -0
  44. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/tui/detail.py +0 -0
  45. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/tui/entities.py +0 -0
  46. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/src/lxw_cli/tui/order_form.py +0 -0
  47. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/__init__.py +0 -0
  48. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/conftest.py +0 -0
  49. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/snapshots/contacts_flat.txt +0 -0
  50. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/snapshots/contacts_grouped.txt +0 -0
  51. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/snapshots/invoices_list.txt +0 -0
  52. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/test_cli_snapshots.py +0 -0
  53. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/test_client.py +0 -0
  54. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/test_config.py +0 -0
  55. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/test_output.py +0 -0
  56. {lxw_cli-0.1.2 → lxw_cli-0.4.0}/tests/test_resolve.py +0 -0
  57. {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.1.2
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
- 26 Tools mit identischem Verhalten wie die CLI-Befehle:
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
- 26 Tools mit identischem Verhalten wie die CLI-Befehle:
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.1.2"
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
- if sys.platform == "darwin":
35
- return Path.home() / "Library" / "Application Support" / "Claude" / (
36
- "claude_desktop_config.json"
37
- )
38
- if sys.platform == "win32":
39
- appdata = os.environ.get("APPDATA")
40
- root = Path(appdata) if appdata else Path.home() / "AppData" / "Roaming"
41
- return root / "Claude" / "claude_desktop_config.json"
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(self, path: str, json_body: Any) -> Any:
129
- return self._request("POST", path, json=json_body).json()
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()