Anleitungen
Fernzugriff (HTTP)
Auf dieser Seite
- Konzepte
- Bearer oder OAuth
- Standardmäßig nur für Ihre Konten
- Stufen je Zugang
- Was der Server speichert und was nie
- Tokens und Laufzeiten
- Eine Instanz
- Schnellstart: Bearer-Modus mit Docker
- Schnellstart: OAuth mit einem benutzerdefinierten Connector in claude.ai
- Bereitstellung
- Docker Compose mit Caddy (deploy/compose/)
- Fly.io (deploy/fly/fly.toml)
- Render (deploy/render/render.yaml)
- Railway
- Ein Tunnel, zum Testen
- Konfigurationsreferenz
- HTTP-Variablen
- Andere Variablen im HTTP-Modus
- Abbrüche und Warnungen beim Start
- Clients verbinden
- claude.ai, Claude Desktop und die mobilen Apps (OAuth)
- Claude Code
- Cursor (Bearer)
- VS Code (Bearer)
- Andere Clients
- Das Freigabe-Postfach
- Dateien: Hochladen und Herunterladen
- Den Server betreiben
- Befehle
- Zugriff entziehen
- Das Server-Geheimnis wechseln
- Logs
- Zustandsprüfung und Herunterfahren
- Fehlersuche
- Das Image prüfen
- Grenzen
Mit --http spricht der Server MCP über Streamable HTTP statt über stdio. Dann können ihn auch
Clients nutzen, die keinen lokalen Prozess starten können: benutzerdefinierte Connectoren in
claude.ai (und die Claude-Apps, die mit demselben Konto angemeldet sind), Claude Code, Cursor oder
VS Code auf einem anderen Rechner oder ein kleines Team, das sich einen Server teilt. Eine gehostete
Instanz gibt es nicht: Sie betreiben den Server selbst, auf Ihrem eigenen Rechner, einer VM oder
einer Plattform wie Fly.io. Für eine Person an einem Rechner bleibt stdio die einfachere Wahl.
Diese Seite ist die Anleitung für den Betrieb. Der Entwurf steht in architecture.md und in den ADRs 0006, 0007 und 0008; das Bedrohungsmodell in SECURITY.de.md.
Konzepte#
Bearer oder OAuth#
SEVDESK_HTTP_AUTH wählt einen von zwei Modi. Ohne die Variable startet --http nicht.
bearer | oauth | |
|---|---|---|
| Wer das sevDesk-Token hält | der Server (SEVDESK_API_TOKEN) | der Client jedes Benutzers, verschlüsselt in den Tokens, die der Server ihm ausgestellt hat |
| Konten | eines | jedes Konto in SEVDESK_OAUTH_ACCOUNTS |
| Was ein Client sendet | ein gemeinsames Geheimnis: Authorization: Bearer <secret> | OAuth-2.1-Access-Tokens, nach einer Zustimmungsseite im Browser |
| Stufen | SEVDESK_ALLOW | je Verbindung auf der Zustimmungsseite gewählt, höchstens SEVDESK_ALLOW |
| Clients | alles, was einen Header senden kann | Clients mit Client ID Metadata Documents: claude.ai, Claude Code |
| Zugriff entziehen | das Geheimnis ändern | je Verbindung, je Konto oder alles (grants revoke) |
bearer ist der einfache Modus: Ein gemeinsames Geheimnis (SEVDESK_HTTP_BEARER_TOKEN)
schützt ein sevDesk-Konto. Verwenden Sie ihn in einem privaten Netz, hinter einer anderen
Zugangskontrolle oder für Clients, die Sie selbst einrichten. Wer das Geheimnis hat, kann die
Stufen des Servers für dieses Konto nutzen; behandeln Sie es also wie das sevDesk-Token.
oauth macht den Server zu seinem eigenen OAuth-2.1-Autorisierungsserver. Ein Client, der sich
verbindet, wird auf eine Zustimmungsseite auf Ihrem Server geleitet. Dort wählt der Benutzer die
Stufen für diese Verbindung und fügt sein sevDesk-API-Token einmal ein. Der Server prüft das Token
bei sevDesk, verschlüsselt es (AES-256-GCM) in den Autorisierungscode und in die Access- und
Refresh-Tokens, die er dem Client gibt, und behält nur einen Vermerk, dass die Verbindung besteht:
einen Zugang (Grant). Benutzerdefinierte Connectoren in claude.ai brauchen diesen Modus.
Clients weisen sich mit einem Client ID Metadata Document (CIMD) aus: Ihre client_id ist eine
https-URL, die ihren Namen und ihre Redirect-URIs liefert. Dynamic Client Registration wird nicht
angeboten; ein Client, der nur die Registrierung unterstützt, kann den OAuth-Modus also nicht
nutzen. Verwenden Sie für ihn den Bearer-Modus.
Standardmäßig nur für Ihre Konten#
Im OAuth-Modus können sich nur die sevDesk-Konten in SEVDESK_OAUTH_ACCOUNTS verbinden. Ihre
Konto-ID finden Sie mit account-id; ein abgelehnter Benutzer sieht seine eigene
Konto-ID auf der Zustimmungsseite. SEVDESK_OAUTH_ACCOUNTS=any lässt jeden mit einem
sevDesk-Token Ihren Server nutzen, und der Start protokolliert eine Warnung: Sie würden dann die
Buchhaltungsdaten anderer Leute verarbeiten (siehe PRIVACY.de.md).
Auch Clients sind beschränkt. SEVDESK_OAUTH_CLIENTS führt die Hosts auf, die das
Metadaten-Dokument eines Clients liefern dürfen (Standard claude.ai). Der Server prüft das, bevor
er etwas abruft. Ohne diese Liste könnte ein Angreifer sein eigenes Metadaten-Dokument
bereitstellen und Ihnen einen Link auf Ihre eigene Zustimmungsseite schicken, der echt aussieht
(Consent-Phishing).
Beide Listen gelten für jede Anfrage und jede Erneuerung, nicht nur bei der Zustimmung. Entfernen Sie ein Konto oder einen Client-Host und starten neu, werden dessen bestehende Zugänge bei der nächsten Nutzung widerrufen (fügen Sie den Eintrag später wieder hinzu, stellt das sie nicht wieder her; der Benutzer verbindet sich erneut), und die Browser-Sitzungen eines entfernten Kontos enden.
Stufen je Zugang#
Jede OAuth-Verbindung hat ihre eigenen Stufen. Die Zustimmungsseite gewährt immer read und bietet
für jede weitere Stufe, die SEVDESK_ALLOW aktiviert, ein Kästchen an, alle nicht angehakt; einem
Client, der bestimmte sevdesk:<tier>-Scopes anfragt, werden nur diese angeboten. Eine Anfrage
bekommt die Stufen des Zugangs, die SEVDESK_ALLOW noch aktiviert (nur read mit
SEVDESK_READ_ONLY=1), und das Modell sieht nur die Werkzeuge dieser Stufen. Ein engeres
SEVDESK_ALLOW gilt sofort für alle Zugänge; ein weiteres erweitert bestehende Zugänge nicht: Der
Benutzer verbindet sich neu und hakt die neue Stufe an.
Im Bearer-Modus sind die Stufen SEVDESK_ALLOW, wie über stdio.
Was der Server speichert und was nie#
| Wo | Was |
|---|---|
grants.jsonl in SEVDESK_HTTP_STATE_DIR (oder Speicher) | Je Zugang (OAuth): eine zufällige ID, die sevDesk-Konto-ID, die client_id-URL des Clients und ihr Host, die Stufen, Zeitstempel (angelegt, Ablauf, letzte Rotation, widerrufen), der Status, die Generation des Refresh-Tokens und der Grund eines Widerrufs. Je Browser-Sitzung (die Anmeldung des Freigabe-Postfachs und der Seiten zum Hoch- und Herunterladen): eine zufällige ID, die Konto-ID, der Zugang, aus dem sie stammt, und wie sie geöffnet wurde. Nur angehängt, Modus 0600, beim Start und von Zeit zu Zeit im laufenden Betrieb verdichtet. Auch der Bearer-Modus hält seine Browser-Sitzungen hier, sobald das Postfach oder die Dateiübertragung aktiv ist. |
grants.lock im Zustandsverzeichnis | Kennzeichnet die laufende Instanz (ein Lebenszeichen alle 10 Sekunden). |
files/ im Zustandsverzeichnis (oder Speicher) | Hochgeladene Dateien und die Dateien hinter Download-Links, solange sie gültig sind (Dateien): verschlüsselt mit AES-256-GCM unter einem Schlüssel, der beim Start des Prozesses zufällig erzeugt und nirgends gespeichert wird, unter zufälligen Namen (Ordner 0700, Dateien 0600). Gelöscht, wenn sie ablaufen, und beim Herunterfahren; der Ordner wird bei jedem Start geleert, sodass nichts den Prozess überdauert. |
SEVDESK_AUDIT_LOG, falls gesetzt | Jeder schreibende Aufruf jedes Kontos, mit Konto und Zugang, sowie jeder empfangene Upload, jeder angelegte Download-Link und jeder Download (Referenz, Art, Größe und SHA-256, kein Name und kein Inhalt). Siehe SECURITY.de.md. |
| Nur im Speicher (beim Neustart verloren) | Ausstehende Freigaben, ausstehende Bestätigungen (ihre Einmal-Nonces), Upload-Seiten und Download-Links (Eigentümer, Referenz, Zweck, der bereinigte Dateiname, Größe, SHA-256, Ablauf, verbleibende Downloads), zwischengespeicherte Metadaten-Dokumente von Clients, Caches je Konto und Zähler der Ratenbegrenzung. |
Nie gespeichert, weder in Dateien noch in Logs: sevDesk-API-Tokens, Autorisierungscodes, Access- und Refresh-Tokens, Cookies, das Server-Geheimnis, die Bearer-Geheimnisse und der Schlüssel der verschlüsselten Dateien. Die Geheimnisse kommen nur aus der Umgebung. Upload- und Download-IDs und Dateinamen erscheinen nie in einem Log.
Tokens und Laufzeiten#
| Was | Laufzeit | Hinweise |
|---|---|---|
| Autorisierungscode | 60 Sekunden | Einmal verwendbar. Ein zweiter Austausch widerruft den Zugang. |
| Access-Token | SEVDESK_OAUTH_ACCESS_TTL (1 Stunde) | Nur gültig für <public URL>/mcp. Jede Anfrage wird gegen den Zugangsspeicher geprüft. |
| Refresh-Token | SEVDESK_OAUTH_REFRESH_IDLE_TTL (14 Tage ungenutzt) | Bei jeder Nutzung ersetzt; nie länger gültig als der Zugang. |
| Zugang | SEVDESK_OAUTH_GRANT_TTL (90 Tage ab Zustimmung) | Danach stimmt der Benutzer erneut zu. |
| Cookie der Browser-Sitzung | 12 Stunden | Freigabe-Postfach, Seiten zum Hoch- und Herunterladen. Enthält kein sevDesk-Token. Endet mit seinem Zugang. |
| Upload-Seite | 15 Minuten | Nimmt eine Datei an. |
| Hochgeladene Datei, Download-Link | SEVDESK_HTTP_FILES_TTL (30 Minuten) | Ein Link liefert höchstens 5 Downloads. Ein Neustart macht beide ungültig. |
Jedes Token ist mit Schlüsseln verschlüsselt, die aus SEVDESK_HTTP_SECRET abgeleitet sind. Jede
Erneuerung liefert ein neues Refresh-Token. Das vorige funktioniert noch 30 Sekunden, weil Clients
manchmal zweimal gleichzeitig erneuern; danach gilt seine Vorlage als gestohlenes Token und widerruft
den Zugang. Bei jeder Erneuerung fragt der Server außerdem bei sevDesk, ob das sevDesk-Token noch
funktioniert: Antwortet sevDesk mit 401 (das Token wurde neu erzeugt oder sein Benutzer entfernt)
oder gehört das Token jetzt zu einem anderen Konto, wird der Zugang widerrufen. Ist sevDesk nicht
erreichbar, gelingt die Erneuerung trotzdem: Das neue Access-Token ist ohne funktionierendes
sevDesk-Token ohnehin nutzlos.
Eine Instanz#
MCP-Anfragen werden zustandslos bearbeitet, aber betreiben Sie genau eine Instanz. Zugänge,
Browser-Sitzungen, ausstehende Freigaben, Uploads und Download-Links, die Einmal-Nonces der
Bestätigungen und die Ratenbegrenzungen leben im Prozess und seinem Zustandsverzeichnis, und eine
zweite Instanz auf demselben Zustandsverzeichnis startet nicht. Das gilt auch im Bearer-Modus,
sobald Browser-Seiten aktiv sind (die Dateiübertragung ist standardmäßig an). Mehrere Instanzen
bräuchten eine gemeinsame Datenbank, die dieses Projekt bewusst vermeidet
(ADR 0006). Eine Instanz bedient ein
kleines Team problemlos: Der Engpass ist das Tempo von sevDesk selbst (SEVDESK_RATE_LIMIT, je
Konto).
Schnellstart: Bearer-Modus mit Docker#
Für einen ersten Versuch auf Ihrem eigenen Rechner:
# 1. Ein zufälliges Client-Geheimnis (43 Zeichen).docker run --rm ghcr.io/jaylann/mcp-server-sevdesk:latest generate-secret# 2. Die Einstellungen, in einer Datei, die nur Sie lesen können (außerhalb jedes Repositorys).umask 077cat > ~/sevdesk-mcp.env <<'EOF'SEVDESK_HTTP_AUTH=bearerSEVDESK_HTTP_PUBLIC_URL=http://localhost:8080SEVDESK_HTTP_BEARER_TOKEN=<the secret from step 1>SEVDESK_API_TOKEN=<your sevDesk API token>EOF# 3. Starten, nur von diesem Rechner aus erreichbar.docker run --rm --read-only -p 127.0.0.1:8080:8080 --env-file ~/sevdesk-mcp.env \ -v sevdesk-mcp-data:/data ghcr.io/jaylann/mcp-server-sevdesk:latestDer Server schreibt den MCP-Endpunkt, den er bereitstellt, nach stderr. Verbinden Sie einen Client
mit http://localhost:8080/mcp und dem Header Authorization: Bearer <the secret>
(Clients verbinden).
Warum hier SEVDESK_HTTP_PUBLIC_URL: Das Image lauscht im Container auf 0.0.0.0, und ein
Lauschen außerhalb von Loopback braucht die URL, die Clients verwenden. Einfaches http ist
erlaubt, weil der Host localhost ist. Verwenden Sie localhost auch im Client: Andere Hostnamen
werden abgelehnt, sofern sie nicht in SEVDESK_HTTP_ALLOWED_HOSTS stehen. Behalten Sie das
127.0.0.1: in -p: Auf allen Schnittstellen veröffentlicht, nähme der Port Anfragen über
einfaches http aus Ihrem Netz an, wo das Geheimnis unverschlüsselt übertragen wird und jeder
Rechner Host: localhost senden kann. Der Start warnt genau vor dieser Kombination (ein Server,
der auf allen Schnittstellen lauscht, mit einer öffentlichen Loopback-URL); ist der Port nur auf
127.0.0.1 veröffentlicht, trifft die Warnung nicht zu.
Einfaches http auf localhost ist nur für einen kurzen Test gedacht. Browser trennen Cookies
nicht nach Port, daher geht das Cookie der Browser-Sitzung (der Seiten zum Hoch- und Herunterladen,
standardmäßig an, und mit SEVDESK_CONFIRM=browser des Freigabe-Postfachs) auch an jeden anderen
Webserver, den der Browser auf localhost erreicht; dieser könnte dann Ihre Dateien abrufen, in
Ihrem Namen hochladen oder Ihre Freigaben lesen und entscheiden. Der Start warnt davor, solange die
Dateiübertragung aktiv ist. Verwenden Sie https (eine der Bereitstellungen unten) oder zumindest
ein Browserprofil, das Sie für nichts anderes nutzen.
Für alles, was länger als ein Test läuft, legen Sie eine Version (:<version>) oder einen Digest
statt latest fest und übergeben Geheimnisse als Dateien (SEVDESK_HTTP_BEARER_TOKEN_FILE,
SEVDESK_API_TOKEN_FILE) statt als Umgebungswerte, die docker inspect anzeigt.
Schnellstart: OAuth mit einem benutzerdefinierten Connector in claude.ai#
claude.ai verbindet sich von den Servern von Anthropic aus, nicht aus Ihrem Browser; der Server braucht daher eine öffentliche https-URL.
-
Wählen Sie, wo er läuft (Bereitstellung): eine Domain hinter Caddy, Fly.io, Render oder Railway. Für einen kurzen Test geht auch ein Tunnel:
cloudflared tunnel --url http://localhost:8080gibt einehttps://….trycloudflare.com-URL aus. -
Erzeugen Sie das Server-Geheimnis mit
generate-secret. Es schützt jede Verbindung: Bewahren Sie es wie ein Passwort auf, im Secret-Speicher Ihrer Plattform oder in einer Datei. -
Ermitteln Sie Ihre Konto-ID:
docker run --rm -e SEVDESK_API_TOKEN ghcr.io/jaylann/mcp-server-sevdesk:latest account-id(mitSEVDESK_API_TOKENin Ihrer Shell exportiert;-e NAMEohne Wert reicht die Variable weiter, ohne sie auf die Kommandozeile zu schreiben). -
Konfigurieren Sie den Server:
TerminalSEVDESK_HTTP_AUTH=oauthSEVDESK_HTTP_PUBLIC_URL=https://mcp.example.comSEVDESK_HTTP_SECRET_FILE=/run/secrets/sevdesk_http_secretSEVDESK_OAUTH_ACCOUNTS=<your account id># Ein Reverse Proxy vor dem Server.SEVDESK_HTTP_TRUST_PROXY=1# Das Höchste, was eine Verbindung bekommen darf.SEVDESK_ALLOW=read,write,commit# Freigaben im Postfach: claude.ai kann im Chat nicht fragen.SEVDESK_CONFIRM=browserSEVDESK_AUDIT_LOG=/data/audit.jsonlEs gibt kein
SEVDESK_API_TOKEN: Im OAuth-Modus bringt jeder Benutzer sein eigenes auf der Zustimmungsseite mit. -
Fügen Sie den Connector in claude.ai hinzu: Einstellungen → Connectors → Benutzerdefinierten Connector hinzufügen, mit der URL
https://mcp.example.com/mcp. claude.ai öffnet die Zustimmungsseite Ihres Servers:- Prüfen Sie, dass der Client
claude.aiist und dass die Seite Sie zuclaude.aizurückleitet; - haken Sie die Stufen an, die diese Verbindung nutzen darf (
readist immer enthalten); - fügen Sie Ihr sevDesk-API-Token ein; der Server prüft es bei sevDesk und gegen
SEVDESK_OAUTH_ACCOUNTSund zeigt dann das Konto, zu dem es gehört (nie wieder das Token); - bestätigen Sie dieses Konto. Sie sind zurück in claude.ai, und dieser Browser ist jetzt für
12 Stunden an den eigenen Seiten des Servers angemeldet: den
Seiten zum Hoch- und Herunterladen (standardmäßig an)
und, mit
SEVDESK_CONFIRM=browser, dem Freigabe-Postfach.
- Prüfen Sie, dass der Client
Bereitstellung#
Das Image ist ghcr.io/jaylann/mcp-server-sevdesk, für amd64 und arm64, mit den Tags <version>,
<major>.<minor> und latest. Es läuft als uid 65532 auf einer distroless-Basis mit Node.js (ohne
Shell), startet --http, lauscht auf 0.0.0.0:8080, hält seinen Zustand im Volume /data
(SEVDESK_HTTP_STATE_DIR=/data) und hat eine Zustandsprüfung, die den Befehl healthcheck
ausführt. Sein Root-Dateisystem kann schreibgeschützt sein: Nur /data wird beschrieben. Jeder
andere Befehl ersetzt --http, zum Beispiel docker run --rm <image> generate-secret.
Die Vorlagen unten betreiben das Image im OAuth-Modus, als eine Instanz mit einem Volume unter
/data, SEVDESK_CONFIRM=browser, dem Audit-Log in /data/audit.jsonl und SEVDESK_ALLOW=read:
Erhöhen Sie SEVDESK_ALLOW auf das Höchste, was eine Verbindung bekommen soll. release-please
aktualisiert ihren Image-Tag mit jedem Release. Jede Bereitstellung braucht mindestens
SEVDESK_HTTP_PUBLIC_URL und entweder SEVDESK_HTTP_SECRET plus SEVDESK_OAUTH_ACCOUNTS (OAuth)
oder SEVDESK_HTTP_BEARER_TOKEN plus SEVDESK_API_TOKEN (Bearer).
Docker Compose mit Caddy (deploy/compose/)#
Für eine VM oder einen Heimserver mit Domainnamen.
deploy/compose/compose.yaml startet zwei Dienste:
mcp, der Server: schreibgeschütztes Root-Dateisystem, alle Linux-Capabilities entzogen,no-new-privileges, Grenzen für Speicher und Prozesse, das Server-Geheimnis aus einer Datei über Compose-Secrets (SEVDESK_HTTP_SECRET_FILE), ein benanntes Volume unter/dataund kein Port, der auf dem Host veröffentlicht ist;caddy(per Digest festgelegt): holt und erneuert das TLS-Zertifikat für Ihre Domain, behält denHost-Header bei, setztX-Forwarded-Forund gibt gestreamte Antworten sofort weiter (flush_interval -1). Er schreibt bewusst kein Access-Log: OAuth-Redirect-URLs enthalten Autorisierungscodes.
Richten Sie das DNS der Domain auf den Rechner und öffnen Sie die Ports 80 und 443. Dann, in
deploy/compose/:
cp .env.example .env # SEVDESK_DOMAIN, SEVDESK_OAUTH_ACCOUNTS, SEVDESK_ALLOWinstall -d -m 0700 secretsdocker run --rm ghcr.io/jaylann/mcp-server-sevdesk:latest generate-secret > secrets/http_secretsudo chown 65532:65532 secrets/http_secret && sudo chmod 0400 secrets/http_secretdocker compose up -dCompose bindet eine Secret-Datei so ein, wie sie auf dem Host liegt; sie muss daher für uid 65532
lesbar sein (das chown). secrets/ ist in Git ignoriert. Caddy ist der eine Proxy davor, daher
setzt die Vorlage SEVDESK_HTTP_TRUST_PROXY=1. Für den Bearer-Modus folgen Sie den
auskommentierten Zeilen in compose.yaml. Die Betriebsbefehle führen Sie in einem einmaligen
Container desselben Dienstes aus, zum Beispiel docker compose run --rm mcp grants list.
Fly.io (deploy/fly/fly.toml)#
deploy/fly/fly.toml betreibt eine Machine in Frankfurt mit einem
1-GB-Volume (sevdesk_data) unter /data. Fly terminiert TLS und erzwingt https; die Machine
pausiert im Leerlauf (auto_stop_machines = "suspend", das ihren Speicher behält) und startet bei
der nächsten Anfrage.
fly apps create <app># In fly.toml: `app`, SEVDESK_HTTP_PUBLIC_URL (https://<app>.fly.dev oder Ihre Domain) und# SEVDESK_OAUTH_ACCOUNTS setzen.fly secrets set --app <app> --stage \ SEVDESK_HTTP_SECRET="$(docker run --rm ghcr.io/jaylann/mcp-server-sevdesk:latest generate-secret)"fly deploy --config deploy/fly/fly.toml --ha=falseBleiben Sie bei einer Machine (--ha=false): Eine zweite hätte ihr eigenes Volume und ihre
eigenen Zugänge. Der Fly-Proxy ist ein Hop (SEVDESK_HTTP_TRUST_PROXY=1).
Render (deploy/render/render.yaml)#
deploy/render/render.yaml ist ein Blueprint: ein Webdienst aus
dem Image in Frankfurt, mit einer 1-GB-Disk unter /data und /healthz als Zustandsprüfung. Wählen
Sie im Render-Dashboard New → Blueprint, dieses Repository (oder Ihren Fork) und als
Blueprint-Pfad deploy/render/render.yaml. Render erzeugt SEVDESK_HTTP_SECRET selbst
(generateValue, nie in der Datei sichtbar) und fragt beim Anlegen nach SEVDESK_HTTP_PUBLIC_URL
(https://<service>.onrender.com oder Ihre Domain) und SEVDESK_OAUTH_ACCOUNTS. Eine Disk braucht
einen kostenpflichtigen Instanztyp und hält den Dienst bei einer Instanz. Die Vorlage setzt
PORT=8080, den Port, an den Render weiterleitet.
Railway#
Railway liest railway.json für neue Dienste nicht mehr (seine Konfiguration als Code ist
abgekündigt und wird ab dem 2026-12-01 nicht mehr gelesen). Legen Sie den Dienst im Dashboard aus
dem Image an, oder beschreiben Sie ihn in einer Railway-Datei für Infrastructure as Code
(.railway/railway.ts) in Ihrem eigenen Projekt: deploy/README.md
enthält beides, mit einem Volume unter /data, /healthz als Zustandsprüfung und einer Replica.
Railway bindet Volumes mit root als Eigentümer ein, daher läuft der Dienst mit
RAILWAY_RUN_UID=0. Das Image lauscht auf Port 8080.
Ein Tunnel, zum Testen#
cloudflared tunnel --url http://localhost:8080 gibt einem lokalen Server eine vorübergehende
öffentliche https-URL. Setzen Sie sie als SEVDESK_HTTP_PUBLIC_URL (Neustart, wenn sie sich ändert)
und SEVDESK_HTTP_TRUST_PROXY=1. Der Tunnelanbieter terminiert TLS und sieht den Verkehr,
einschließlich der Tokens; verwenden Sie ihn für Tests, nicht für Ihre Buchhaltung.
Konfigurationsreferenz#
Alle Variablen werden beim Start gelesen. Der Start meldet alle Probleme auf einmal und verweigert
sich, bis alle behoben sind. Variablen, die auf _FILE enden, lesen den Wert stattdessen aus einer
Datei (Docker- und Kubernetes-Secrets); setzen Sie die eine oder die andere.
HTTP-Variablen#
| Variable | Standard | Bedeutung |
|---|---|---|
SEVDESK_HTTP_AUTH | (Pflicht) | bearer oder oauth. |
SEVDESK_HTTP_HOST | 127.0.0.1 (Image: 0.0.0.0) | Adresse, auf der der Server lauscht. Alles außer einer Loopback-Adresse braucht SEVDESK_HTTP_PUBLIC_URL. |
SEVDESK_HTTP_PORT | $PORT, sonst 8080 (Image: 8080) | Port, 0–65535 (0 wählt einen freien Port, für Tests). |
SEVDESK_HTTP_PUBLIC_URL | Der Origin, den Clients verwenden, zum Beispiel https://mcp.example.com: nur Schema, Host und Port, ohne Pfad, Query, Fragment oder Zugangsdaten. https, außer der Host ist localhost, 127.0.0.1 oder [::1]. Pflicht für OAuth und wenn der Server nicht nur auf Loopback lauscht. Der MCP-Endpunkt (und die OAuth-Ressource) ist <origin>/mcp, der OAuth-Issuer ist der Origin. Jede URL, die der Server ausgibt oder auf die er weiterleitet, wird daraus gebaut, nie aus Headern der Anfrage. | |
SEVDESK_HTTP_ALLOWED_HOSTS | Weitere Hostnamen, die im Host-Header angenommen werden, durch Kommas getrennt. Der Host der öffentlichen URL wird immer angenommen (bei Loopback ohne öffentliche URL die Loopback-Namen). | |
SEVDESK_HTTP_ALLOWED_ORIGINS | Weitere Werte, die im Origin-Header von /mcp-Anfragen angenommen werden, durch Kommas getrennt; die Prüfung vergleicht nur Hostnamen (beliebiges Schema, beliebiger Port). Der öffentliche Origin wird immer angenommen, und Anfragen ohne Origin passieren. /mcp sendet keine CORS-Header und beantwortet kein Preflight (OPTIONS); MCP-Clients, die in einer Webseite auf einem anderen Origin laufen, werden also nicht unterstützt: Das lässt nur Clients zu, die selbst einen solchen Origin-Header senden. | |
SEVDESK_HTTP_TRUST_PROXY | 0 | Reverse Proxys vor dem Server, 0–5. Die Client-IP (Ratenbegrenzung, Logs) ist der Eintrag, der so viele Stellen von rechts in X-Forwarded-For steht. |
SEVDESK_HTTP_BEARER_TOKEN (_FILE) | Bearer-Modus, Pflicht: das Client-Geheimnis, oder zwei durch Komma getrennte Geheimnisse während eines Wechsels. Jedes mindestens 32 Zeichen mit mindestens 16 verschiedenen, ohne Leerraum und nicht das sevDesk-Token. | |
SEVDESK_HTTP_SECRET (_FILE) | OAuth, Pflicht: das Server-Geheimnis, mit dem alle Tokens und Cookies verschlüsselt werden; mindestens 32 zufällige Bytes, hex oder base64/base64url (generate-secret). Optional im Bearer-Modus, wo es Browser-Anmeldungen über einen Neustart hinweg gültig hält. | |
SEVDESK_HTTP_SECRET_PREVIOUS (_FILE) | Das vorige Server-Geheimnis, während Sie es wechseln: Damit verschlüsselte Tokens werden weiter angenommen und bei der Erneuerung neu verschlüsselt. | |
SEVDESK_HTTP_STATE_DIR | Speicher (Image: /data) | Absolutes Verzeichnis für den Zugangsspeicher und die verschlüsselten Dateien der Dateiübertragung (files/); angelegt mit Modus 0700 (ein bestehendes wird auf 0700 verschärft) und beim Start auf Schreibrechte geprüft. Ohne die Variable enden Zugänge und Browser-Sitzungen mit dem Prozess (der OAuth-Start warnt), und Dateien werden im Speicher gehalten. |
SEVDESK_HTTP_FILES | on | Dateiübertragung: Upload-Seiten für Belege und E-Rechnungen und Download-Links für die Dateien, die Werkzeuge erzeugen. on/off, 1/0, true/false oder yes/no. Aus: kein sevdesk_upload_request, und im OAuth-Modus gar keine Dateiwerkzeuge. |
SEVDESK_HTTP_FILES_MAX_MB | 256 mit Zustandsverzeichnis, sonst 64 | Mebibyte (MiB), die die hochgeladenen Dateien und Download-Links aller Konten zusammen belegen dürfen, 32 bis 4096. Ein Konto darf ein Viertel davon nutzen (mindestens 20 MiB, höchstens die Hälfte); Platz für einen Upload bleibt für Konten frei, die noch keine Dateien halten. Mit Zustandsverzeichnis liegen die Dateien auf der Festplatte, sonst im Speicher. |
SEVDESK_HTTP_FILES_TTL | 30m | Wie lange eine hochgeladene Datei und ein Download-Link gültig bleiben, 5 Minuten bis 24 Stunden. |
SEVDESK_OAUTH_ACCOUNTS | OAuth, Pflicht: die sevDesk-Konto-IDs, die sich verbinden dürfen, durch Kommas getrennt, oder any (der Start warnt). | |
SEVDESK_OAUTH_CLIENTS | claude.ai | Hosts, die das Metadaten-Dokument eines Clients (seine client_id-URL) liefern dürfen, durch Kommas getrennt, oder any (der Start warnt). |
SEVDESK_OAUTH_ACCESS_TTL | 1h | Laufzeit des Access-Tokens, 5 Minuten bis 24 Stunden. Unter 10 Minuten warnt der Start: Claude erneuert 5 Minuten vor Ablauf und würde dann bei fast jeder Anfrage erneuern. |
SEVDESK_OAUTH_REFRESH_IDLE_TTL | 14d | Ein Refresh-Token, das so lange ungenutzt bleibt, läuft ab; 1 bis 90 Tage. |
SEVDESK_OAUTH_GRANT_TTL | 90d | Laufzeit eines Zugangs ab der Zustimmung; 1 bis 365 Tage. |
Dauern schreiben Sie als ganze Zahl mit der Einheit s, m, h oder d (90s, 30m, 1h,
14d) oder als bloße Anzahl Sekunden.
Andere Variablen im HTTP-Modus#
Die Variablen des stdio-Modus behalten ihre Bedeutung, mit diesen Unterschieden:
SEVDESK_API_TOKEN(_FILE): Pflicht im Bearer-Modus; dort ist es das Konto, das der Server bedient, und das Token, mit dem sich ein Browser an den Seiten des Servers anmeldet (Freigabe-Postfach, Hoch- und Herunterladen). Im OAuth-Modus nicht verwendet (der Start warnt, wenn es gesetzt ist): Jeder Benutzer fügt sein eigenes Token auf der Zustimmungsseite ein.SEVDESK_API_TOKEN_KEYCHAIN,SEVDESK_PROFILE,SEVDESK_PROFILES_PATH: abgelehnt. Der Schlüsselbund des Betriebssystems und lokale Profile sind für lokale Installationen über stdio; ein Container hat keinen Schlüsselbund.SEVDESK_ALLOW,SEVDESK_READ_ONLY: das Höchste, was eine Verbindung bekommen darf. Im OAuth-Modus bekommt jeder Zugang die Schnittmenge mit den Stufen, die auf der Zustimmungsseite angehakt wurden.SEVDESK_CONFIRM:elicit(der Standard) funktioniert mit Clients, die das Protokoll 2026-07-28 sprechen und Formular-Elicitation unterstützen. Clients aus der Zeit von 2025 können über zustandsloses HTTP nicht gefragt werden; dazu gehört zum Zeitpunkt des Schreibens auch claude.ai. Der Start warnt daher, wennelicitmit der Stufecommitoderexternalkombiniert ist.browsernutzt das Freigabe-Postfach des Servers.SEVDESK_CONFIRM_OPEN: im HTTP-Modus aus, sofern nicht ausdrücklich gesetzt, und dort ohne Wirkung: Freigaben warten im Postfach, das Sie selbst öffnen.1zusammen mit einem Lauschen außerhalb von Loopback wird abgelehnt.SEVDESK_FILE_DIRS,SEVDESK_OUTPUT_DIR: im OAuth-Modus abgelehnt, weil sich alle Verbindungen die Festplatte des Servers teilen würden. Dateien erreichen die Werkzeuge stattdessen über Upload-Seiten und Download-Links (SEVDESK_HTTP_FILES, standardmäßig an). Der Bearer-Modus behält sie; die Ordner liegen auf dem Server, nicht auf dem Rechner des Clients. Dort kann ein Dateiargument ein Pfad inSEVDESK_FILE_DIRSoder ein Upload sein, undSEVDESK_OUTPUT_DIRhat, wenn gesetzt, Vorrang vor Download-Links: Dateien werden dann auf dem Server gespeichert.SEVDESK_AUDIT_LOG: ein Log für alle Konten; jede Zeile nennt das Konto und den Zugang. Es hält außerdem jeden empfangenen Upload, jeden angelegten Download-Link und jeden Download fest. Im Image ist das Root-Dateisystem schreibgeschützt; legen Sie es also unter/dataab.SEVDESK_RATE_LIMIT: gilt je sevDesk-Konto, geteilt von allen seinen Verbindungen.
Abbrüche und Warnungen beim Start#
Der Start verweigert sich unter anderem bei:
--httpohneSEVDESK_HTTP_AUTH;- Bearer-Modus ohne gültiges Bearer-Geheimnis oder ohne sevDesk-Token;
- OAuth ohne
SEVDESK_HTTP_SECRET,SEVDESK_HTTP_PUBLIC_URLoderSEVDESK_OAUTH_ACCOUNTSoder mit einem Geheimnis, das zu kurz oder nicht zufällig genug ist; - einem Lauschen außerhalb von Loopback ohne öffentliche URL oder einer öffentlichen
http://-URL, deren Host nicht Loopback ist; - OAuth zusammen mit
SEVDESK_FILE_DIRSoderSEVDESK_OUTPUT_DIR(die Meldung verweist auf die Upload-Seiten und Download-Links); SEVDESK_API_TOKEN_KEYCHAIN,SEVDESK_PROFILEoderSEVDESK_PROFILES_PATH(nur stdio);SEVDESK_HTTP_FILESmit einem Wert, der kein Schalterwert ist (siehe Tabelle),SEVDESK_HTTP_FILES_MAX_MBaußerhalb von 32–4096 oderSEVDESK_HTTP_FILES_TTLaußerhalb von 5 Minuten bis 24 Stunden;SEVDESK_CONFIRM_OPEN=1bei einem Lauschen außerhalb von Loopback;- einem Zustandsverzeichnis, das nicht beschreibbar ist, einer beschädigten
grants.jsonl, einem Zustandsverzeichnis, das eine andere laufende Instanz nutzt, oder einem Ordnerfiles/darin, der sich nicht leeren lässt; - jedem anderen Wert außerhalb dessen, was die Tabellen oben erlauben, etwa einer Variablen, die
zusammen mit ihrer
_FILE-Variante gesetzt ist,SEVDESK_HTTP_SECRET_PREVIOUSohneSEVDESK_HTTP_SECRET, mehr als zwei Bearer-Geheimnissen, einem relativen Zustandsverzeichnis, einer OAuth-Lebensdauer außerhalb ihres Bereichs, einem ungültigen Host- oder Origin-Eintrag oderanygemischt mit IDs oder Hosts.
Der Start warnt (und startet) bei SEVDESK_CONFIRM=elicit mit der Stufe commit oder external,
SEVDESK_OAUTH_ACCOUNTS=any (mit aktiver Dateiübertragung auch, weil der Server dann kurzzeitig
die Belege und Exporte jedes verbundenen Kontos hält und einige Konten zusammen seinen Platz für
Dateien füllen können), SEVDESK_OAUTH_CLIENTS=any, OAuth ohne Zustandsverzeichnis, SEVDESK_API_TOKEN oder einem Bearer-Geheimnis im OAuth-Modus, einem Server-
oder Bearer-Geheimnis, das eher wie eine Passphrase als wie zufällige Bytes aussieht (verwenden Sie
generate-secret), einem Lauschen außerhalb von Loopback mit einer öffentlichen Loopback-URL (jeder,
der den Port erreicht, kann den Server nutzen; siehe den
Docker-Schnellstart), Dateiübertragung über einfaches
http (das Cookie der Browser-Sitzung erreicht jeden Server unter diesem Hostnamen),
Dateiübertragung ohne Zustandsverzeichnis (Dateien werden im Speicher gehalten, bis zu
SEVDESK_HTTP_FILES_MAX_MB) sowie SEVDESK_HTTP_FILES_MAX_MB oder SEVDESK_HTTP_FILES_TTL
zusammen mit SEVDESK_HTTP_FILES=off (sie haben dann keine Wirkung).
Clients verbinden#
Der MCP-Endpunkt ist immer <public URL>/mcp, zum Beispiel https://mcp.example.com/mcp.
claude.ai, Claude Desktop und die mobilen Apps (OAuth)#
Einstellungen → Connectors → Benutzerdefinierten Connector hinzufügen, mit der URL
https://<host>/mcp. claude.ai öffnet die Zustimmungsseite
(Schnellstart). Der
Connector steht dann in den Claude-Apps zur Verfügung, die mit demselben Konto angemeldet sind.
Entfernen Sie den Connector in claude.ai, verwirft claude.ai seine Tokens;
den Zugang zu widerrufen beendet ihn auch auf dem Server.
Claude Code#
Mit OAuth:
claude mcp add --transport http sevdesk https://<host>/mcpFühren Sie dann /mcp in Claude Code aus und wählen Sie die Authentifizierung: Die
Zustimmungsseite öffnet sich in Ihrem Browser und leitet Sie zu Claude Code zurück. Claude Code
weist sich mit einem eigenen Metadaten-Dokument aus, das wie das von claude.ai unter claude.ai
liegt (der Standardwert von SEVDESK_OAUTH_CLIENTS deckt es also ab), und mit einer
Loopback-Weiterleitung, auf die die Zustimmungsseite hinweist.
Mit einem Bearer-Geheimnis (lesen Sie es aus einer Datei oder Ihrem Passwortmanager, statt es einzutippen):
claude mcp add --transport http sevdesk https://<host>/mcp \ --header "Authorization: Bearer $MCP_BEARER_SECRET"Cursor (Bearer)#
In ~/.cursor/mcp.json (oder .cursor/mcp.json in einem Projekt):
{ "mcpServers": { "sevdesk": { "url": "https://<host>/mcp", "headers": { "Authorization": "Bearer ${env:MCP_BEARER_SECRET}" } } }}VS Code (Bearer)#
In .vscode/mcp.json, mit einer Passwortabfrage, damit das Geheimnis nicht in der Datei steht:
{ "inputs": [ { "type": "promptString", "id": "sevdesk-mcp-bearer", "description": "Bearer secret of your sevDesk MCP server", "password": true } ], "servers": { "sevdesk": { "type": "http", "url": "https://<host>/mcp", "headers": { "Authorization": "Bearer ${input:sevdesk-mcp-bearer}" } } }}Andere Clients#
Jeder Client, der Streamable HTTP spricht und einen Header senden kann, funktioniert im
Bearer-Modus. Im OAuth-Modus braucht ein Client Client ID Metadata Documents; nehmen Sie den Host
seiner client_id-URL in SEVDESK_OAUTH_CLIENTS auf.
Das Freigabe-Postfach#
Mit SEVDESK_CONFIRM=browser warten Aufrufe der Stufen commit und external auf eine
Entscheidung im Freigabe-Postfach des Servers unter <public URL>/approvals. Es ersetzt die lokale
Freigabeseite des stdio-Modus, die ein entfernter Server nicht in Ihrem Browser öffnen kann.
So läuft eine Bestätigung ab:
- Das Modell ruft etwa das Werkzeug auf, das eine Rechnung fertigstellt. Der Server bereitet den
Aufruf vor und antwortet mit
approval_pending, einem kurzen Referenzcode, dem Ablauf (10 Minuten) und der URL des Postfachs. - Sie öffnen das Postfach, suchen die Anfrage mit diesem Referenzcode und lesen die Vorschau: die Aktion, Warnungen, Beträge, Empfänger, Texte.
- Sie drücken Approve oder Reject.
- Sie bitten das Modell, es erneut zu versuchen. Es ruft das Werkzeug mit denselben Argumenten auf; der Server bereitet die Aktion noch einmal vor und führt sie nur aus, wenn Sie freigegeben haben und die Vorschau unverändert ist. Jede Freigabe wird einmal verwendet.
Anmelden. Das Postfach zeigt nichts außer einem Anmeldeformular, bis der Browser nachweist, dass er für das Konto handeln darf:
- nach der OAuth-Zustimmung automatisch: Die Zustimmungsseite meldet den Browser an, der sie abgeschlossen hat (sobald das Postfach oder die Dateiübertragung aktiv ist; dieselbe Sitzung öffnet beides);
- durch Einfügen des sevDesk-API-Tokens in das Anmeldeformular. Im OAuth-Modus prüft der Server
das Token bei sevDesk und gegen
SEVDESK_OAUTH_ACCOUNTS. Im Bearer-Modus muss es das eigeneSEVDESK_API_TOKENdes Servers sein; das Bearer-Geheimnis meldet nicht an, weil der Client es hält.
Eine Anmeldung gilt 12 Stunden oder bis Sie sich abmelden, der Zugang, aus dem sie stammt,
widerrufen wird oder grants revoke --account <id> alle Sitzungen des Kontos beendet. Eine
Anmeldung mit eingefügtem Token ist nicht an dieses Token gebunden: Wird das Token in sevDesk neu
erzeugt, endet sie nicht. Anmeldeversuche sind je Client-Adresse begrenzt, IPv6-Adressen je
/64-Netz gezählt (5 pro Minute, 20 pro Stunde). Setzen Sie im Bearer-Modus zusätzlich
SEVDESK_HTTP_SECRET, wenn Anmeldungen einen Neustart überdauern sollen.
Was das Modell sieht. Den Referenzcode, den Ablauf und die URL des Postfachs. Die URL ist nicht geheim: Ohne Anmeldung zeigt sie nur das Formular. Das Modell sieht nie die eigene ID einer Freigabe, ein Cookie oder irgendetwas, das freigeben könnte.
Wer was sieht. Freigaben gehören der Verbindung, die gefragt hat: Ein erneuter Versuch findet nur Freigaben, die sein eigener Zugang angelegt hat. Das Postfach listet die Freigaben des ganzen Kontos auf; jeder, der an diesem Konto angemeldet ist, sieht sie also und kann über sie entscheiden. Höchstens fünf Freigaben je Konto warten gleichzeitig, und die Anfragen eines Kontos können die Freigaben eines anderen Kontos nicht aus dem Speicher des Servers verdrängen.
Grenzen. Ein Agent, der Ihren Browser steuern kann, während er angemeldet ist
(Browser-Automatisierung, ein Werkzeug zur Computersteuerung), könnte selbst freigeben. Und das
Postfach fragt nach Ihrem sevDesk-Token; eine nachgeahmte Seite könnte also versuchen, es
abzugreifen: Fügen Sie es nur in eine Seite ein, deren Adresse /approvals Ihres Servers ist. Bei
einer Einrichtung mit einfachem http auf localhost erhalten auch andere lokale Webserver das
Sitzungs-Cookie (Schnellstart). Ausstehende Freigaben
enden, wenn der Server neu startet; das Modell fragt dann einfach erneut. Siehe
ADR 0007.
Dateien: Hochladen und Herunterladen#
Ein entfernter Server kann keine Dateien auf Ihrem Rechner lesen, und die Dateien, die er erzeugt,
gelangen nicht von selbst dorthin. Mit SEVDESK_HTTP_FILES=on (der Standard) wandern Dateien über
Seiten des Servers, die Sie in Ihrem Browser öffnen. Das Modell sieht und sendet nie den Inhalt
einer Datei.
Einen Beleg oder eine E-Rechnung hochladen:
- Das Modell ruft
sevdesk_upload_requestauf (Zweckreceiptodereinvoice) und gibt Ihnen einen Link,<public URL>/uploads/<id>, mit einem kurzen Referenzcode und einem Ablauf. - Sie öffnen den Link. Die Seite zeigt nur, was der Server weiß: was sie annimmt, die größte Datei, bis wann sie gültig ist, das Konto und den Referenzcode. Das Modell kann keinen Text darauf setzen.
- Sie wählen die Datei und drücken Upload. Der Server prüft den Dateityp am Inhalt, nicht am
Namen, und behält einen bereinigten Namen (Buchstaben, Ziffern,
.,_,-; Umlaute ausgeschrieben; höchstens 100 Zeichen; die Endung aus dem Inhalt). - Sie sagen dem Modell, dass der Upload erledigt ist. Es übergibt die erhaltene
uploadId(upload:<id>) alsfileansevdesk_voucher_create_draft,sevdesk_voucher_update_draft,sevdesk_einvoice_parseodersevdesk_voucher_import_einvoice. Der Upload bleibt bis zu seinem Ablauf nutzbar, auch für einen erneuten Versuch nach einem Fehler, aber nur für die Verbindung, die ihn angefordert hat.
Herunterladen. sevdesk_document_save, sevdesk_csv_export und sevdesk_datev_export geben
statt eines Dateipfads einen Download-Link zurück, <public URL>/files/<id>. Der Server hat die
Datei während des Werkzeugaufrufs von sevDesk geholt; das Öffnen des Links sendet nichts an
sevDesk. Der Browser speichert die Datei als Download; sie wird nie auf der Seite des Servers
angezeigt.
Was eine Browser-Sitzung braucht. Jede Upload-Seite und jeder Download-Link. Die Links sind nicht geheim: Ohne Sitzung zeigen sie nur das Anmeldeformular, und ein Link eines anderen Kontos sieht genauso aus wie einer, den es nicht gibt. Die Sitzung ist die des Freigabe-Postfachs:
- Der Browser, der die OAuth-Zustimmung abgeschlossen hat, ist 12 Stunden angemeldet, auch wenn das Postfach aus ist;
- jeder andere Browser meldet sich im Formular mit dem sevDesk-API-Token des Kontos an
(OAuth-Modus: bei sevDesk und gegen
SEVDESK_OAUTH_ACCOUNTSgeprüft; Bearer-Modus: dasSEVDESK_API_TOKENdes Servers, nie das Bearer-Geheimnis) und landet wieder auf der Seite, von der er kam.
Das Formular sendet an /approvals/login, auch wenn das Freigabe-Postfach aus ist. An einem Konto
angemeldet, sieht ein Browser die Upload-Seiten und Links aller Verbindungen dieses Kontos; die
Werkzeuge verwenden nur die Uploads ihrer eigenen Verbindung.
Auf dem Telefon. Die Claude-Apps öffnen einen Link unter Umständen in einem Browser, der die Cookies desjenigen nicht teilt, in dem Sie zugestimmt haben: Sie sehen dann das Anmeldeformular. Melden Sie sich dort mit dem sevDesk-API-Token an oder öffnen Sie den Link in dem Browser, in dem Sie zugestimmt haben.
Grenzen und Laufzeiten:
| Was | Grenze |
|---|---|
| Upload-Seite | 15 Minuten gültig, nimmt eine Datei an. |
| Angenommene Dateien | Belege: PDF, PNG oder JPEG. E-Rechnungen: XML (XRechnung) oder PDF (ZUGFeRD, Factur-X). Am Inhalt geprüft. |
| Ein Upload | 20 MiB, oder die Hälfte von SEVDESK_HTTP_FILES_MAX_MB, falls das weniger ist. |
| Hochgeladene Datei, Link | Gültig für SEVDESK_HTTP_FILES_TTL (30 Minuten). Ein Link liefert höchstens 5 Downloads. |
| Je Konto | 5 Upload-Seiten, die auf eine Datei warten, 10 Uploads und Links zusammen, ein Viertel von SEVDESK_HTTP_FILES_MAX_MB (mindestens 20 MiB, höchstens die Hälfte). |
| Alle Konten | SEVDESK_HTTP_FILES_MAX_MB (256 MiB mit Zustandsverzeichnis, 64 MiB ohne), 1.000 Uploads und Links. Platz für einen Upload bleibt für Konten frei, die noch keine Dateien halten. |
| Anfragen je Client | 60 pro Minute an die Upload- und Download-Seiten zusammen; 10 Uploads pro Minute und 60 pro Stunde (IPv6 je /64-Netz). |
| Gleichzeitige Uploads | 2 für den ganzen Server, 1 je Konto; ein weiterer bekommt „Server busy“ oder „Upload in progress“ (503): Versuchen Sie es gleich noch einmal. |
| Upload-Tempo | Die Datei muss innerhalb von 90 Sekunden eintreffen, ohne 10 Sekunden Pause und nach den ersten 10 Sekunden mit durchschnittlich mindestens 16 KB/s; sonst 408. |
| Ein Neustart | Macht jede Upload-Seite, jeden Upload und jeden Link ungültig. Das Modell fordert einen neuen an. |
Nichts wird verdrängt, um Platz zu schaffen: Ist eine Grenze erreicht, werden neue Uploads und
Links abgelehnt, bis ältere ablaufen. Download-Links zählen gegen denselben Platz wie Uploads; ein
großer DATEV-Export passt daher unter Umständen nicht (mit 64 MiB im Speicher hat ein Konto 20 MiB).
Das Werkzeug sagt dann, dass sevDesk den Export gestartet hat, und Sie laden ihn stattdessen in
sevDesk herunter; oder Sie erhöhen SEVDESK_HTTP_FILES_MAX_MB, am besten mit einem
Zustandsverzeichnis, damit die Dateien auf die Festplatte gehen.
Speicher. Ohne Zustandsverzeichnis werden Dateien im Speicher gehalten. Während ein Upload eintrifft, liegt er etwa dreifach im Speicher; deshalb werden höchstens zwei gleichzeitig angenommen. Auf einem kleinen Rechner (512 MB) behalten Sie ein Zustandsverzeichnis und die Standardgrenzen bei.
Reverse Proxys. Der Body eines Uploads ist bis zu 20 MiB groß plus etwas Formular-Overhead, muss
innerhalb von 90 Sekunden ohne 10 Sekunden Pause eintreffen und Content-Length tragen; ein Upload
in Chunks ohne diesen Header wird mit 411 abgelehnt. nginx braucht client_max_body_size 25m; (sein Standard von 1 MB
lehnt die meisten Belege ab), und seine standardmäßige Pufferung von Anfragen sendet
Content-Length. Caddy, wie in der Compose-Vorlage, braucht nichts.
Abschalten. SEVDESK_HTTP_FILES=off entfernt sevdesk_upload_request und die Seiten. Im
OAuth-Modus wird dann kein Dateiwerkzeug angeboten; der Bearer-Modus nutzt weiterhin
SEVDESK_FILE_DIRS und SEVDESK_OUTPUT_DIR, wenn sie gesetzt sind.
Grenzen des Entwurfs. Ein Agent, der Ihren angemeldeten Browser steuern kann, könnte selbst
hoch- oder herunterladen, so wie er im Postfach freigeben könnte. Eine heruntergeladene Datei ist
das, was sevDesk geliefert hat; öffnen Sie sie wie jedes Dokument aus Ihrer Buchhaltung. Bei einer
Einrichtung mit einfachem http auf localhost erhalten auch andere lokale Webserver das
Sitzungs-Cookie. Siehe ADR 0008.
Den Server betreiben#
Befehle#
Das Programm hat außer dem Bereitstellen einige Befehle. Mit Docker hängen Sie sie an
docker run --rm <image> an (der Standardbefehl des Images ist --http).
| Befehl | Was er tut |
|---|---|
generate-secret | Gibt 32 zufällige Bytes aus, base64url: ein Wert für SEVDESK_HTTP_SECRET oder ein Bearer-Geheimnis. |
account-id | Fragt sevDesk, zu welchem Konto das konfigurierte Token gehört (SEVDESK_API_TOKEN, seine _FILE-Variante, der Schlüsselbund oder ein Profil), und gibt dessen ID aus, für SEVDESK_OAUTH_ACCOUNTS. |
grants list [--json] | Listet die Zugänge in SEVDESK_HTTP_STATE_DIR auf, die neuesten zuerst: ID, Konto, Client-Host, Stufen, Status (mit dem Grund eines Widerrufs), angelegt, Ablauf und letzte Erneuerung. --json gibt JSON mit der vollständigen client_id aus (Felder unten). |
grants revoke <grant-id> | Widerruft einen Zugang und seine Browser-Sitzungen. |
grants revoke --account <id> / --all | Widerruft alle Zugänge eines Kontos oder alle Zugänge. |
healthcheck | Fragt den Server unter SEVDESK_HTTP_HOST und seinem Port nach /healthz (unter 127.0.0.1, wenn er auf allen Schnittstellen lauscht) und endet mit 0, wenn er {"status":"ok"} antwortet; die Zustandsprüfung des Images nutzt ihn. Ohne SEVDESK_HTTP_AUTH (ein Image, das mit --stdio startet) endet er mit 0, ohne zu fragen; bei Port 0 schlägt er fehl. |
grants list --json gibt ein Array mit einem Objekt je Zugang aus, die neuesten zuerst: id,
account (die sevDesk-Konto-ID), client (die vollständige client_id-URL), clientHost,
tiers, status (pending, bis der Client seinen Autorisierungscode eingelöst hat, danach
active, revoked oder expired), reason (warum er widerrufen wurde, ein kurzer Code wie
user, operator, superseded oder sevdesk_401; sonst null) sowie die Zeitpunkte
createdAt, expiresAt, lastRefreshAt und revokedAt (ISO 8601, null, solange es nicht
geschehen ist).
Die grants-Befehle brauchen dasselbe SEVDESK_HTTP_STATE_DIR wie der Server und funktionieren,
während er läuft: Sie hängen an grants.jsonl an, und der Server übernimmt die Änderung innerhalb
von etwa einer Sekunde. Mit dem Image brauchen sie das Volume, zum Beispiel
docker run --rm -v sevdesk-mcp-data:/data ghcr.io/jaylann/mcp-server-sevdesk:<version> grants list.
Ohne Zustandsverzeichnis gibt es Zugänge nur im Serverprozess, und die Befehle sehen sie nicht. Das
Image hat keine Shell; eine Plattform-Konsole, die eine braucht, kann sie also nicht ausführen. Dort
entziehen Sie den Zugriff, indem Sie das Server-Geheimnis oder das sevDesk-Token ersetzen
(Zugriff entziehen).
Zugriff entziehen#
| Um zu beenden | Tun Sie |
|---|---|
| Eine Verbindung | grants revoke <grant-id>. Ihre nächste Anfrage bekommt 401, und der Client bittet um eine neue Verbindung. Clients, die den Widerruf von Tokens unterstützen (RFC 7009, /revoke), tun dasselbe, wenn Sie sie trennen. |
| Alle Verbindungen eines Kontos | grants revoke --account <id>. Das beendet auch jede Browser-Sitzung des Kontos, einschließlich Anmeldungen mit eingefügtem Token. |
| Alles | grants revoke --all, oder ersetzen Sie SEVDESK_HTTP_SECRET, ohne das alte als _PREVIOUS zu behalten: Kein zuvor ausgestelltes Token lässt sich dann noch öffnen. |
| Zugriff mit einem sevDesk-Token | Erzeugen Sie das Token in sevDesk neu (Einstellungen → Benutzer → API-Token). sevDesk lehnt das alte sofort ab, Werkzeugaufrufe funktionieren also nicht mehr, und die nächste Erneuerung jedes Zugangs, der es hält (innerhalb der Laufzeit des Access-Tokens), widerruft diesen Zugang. Das wirkt auch, wenn das Server-Geheimnis oder die Tokens eines Clients nach außen gelangt sind. Browser-Sitzungen sind nicht an das Token gebunden: Eine Anmeldung mit dem alten Token bleibt bis zu 12 Stunden gültig und eine Sitzung aus einer Zustimmung, bis ihr Zugang widerrufen wird. Führen Sie zusätzlich grants revoke --account <id> aus, um sie sofort zu beenden. |
| Bearer-Zugriff | Ersetzen Sie SEVDESK_HTTP_BEARER_TOKEN und starten Sie neu. Browser-Anmeldungen überdauern einen Neustart, wenn SEVDESK_HTTP_SECRET und das Zustandsverzeichnis erhalten bleiben; grants revoke --account <id> beendet sie (die ID ist die, die der Server beim Start protokolliert hat, oder bearer, falls diese Abfrage fehlschlug). |
Uploads und Download-Links werden nicht mit einem Zugang widerrufen: Das Ende einer Sitzung sperrt
den Browser, der sie hielt, und die Dateien laufen innerhalb von SEVDESK_HTTP_FILES_TTL ab. Ein
Neustart macht sie alle auf einmal ungültig.
Das Server-Geheimnis wechseln#
- Erzeugen Sie mit
generate-secretein neues Geheimnis. - Setzen Sie es als
SEVDESK_HTTP_SECRET, verschieben Sie das alte nachSEVDESK_HTTP_SECRET_PREVIOUSund starten Sie neu. Mit dem alten Geheimnis verschlüsselte Tokens werden weiter angenommen; ihre Erneuerung verschlüsselt die neuen mit dem neuen Geheimnis. - Behalten Sie
_PREVIOUSso lange wie die Leerlaufzeit der Erneuerung (SEVDESK_OAUTH_REFRESH_IDLE_TTL, standardmäßig 14 Tage), entfernen Sie es dann und starten Sie neu. Ein Client, der in dieser Zeit nicht erneuert hat, verbindet sich neu.
Wenn das Geheimnis nach außen gelangt sein könnte, überspringen Sie die Wartezeit aus Schritt 3:
Ersetzen Sie es ohne _PREVIOUS. Jeder Client verbindet sich neu. Wenn auch Tokens nach außen
gelangt sein könnten (das Geheimnis öffnet sie und das sevDesk-Token darin), erzeugen Sie außerdem
die sevDesk-Tokens neu und führen für jedes Konto grants revoke --account <id> aus, was auch seine
Browser-Sitzungen beendet.
Bearer-Geheimnisse wechseln Sie genauso: Setzen Sie SEVDESK_HTTP_BEARER_TOKEN=<new>,<old>,
aktualisieren Sie die Clients und entfernen Sie dann den alten Wert.
Logs#
Der Server schreibt nach stderr: beim Start den MCP-Endpunkt, den er bereitstellt, dann ein
JSON-Objekt pro Anfrage mit der Route (als Vorlage, nicht der vollständige Pfad), dem Status, der
Dauer, der Client-IP und den ersten 8 Zeichen der Zugangs-ID. Header, Bodys, Cookies und
Query-Strings werden nie protokolliert, und jede Zeile wird vom sevDesk-Token, den Bearer- und
Server-Geheimnissen, allem, was wie ein verschlüsseltes Token aussieht (sdmcp_…), und jeder Folge
von 32 oder mehr Hex-Ziffern bereinigt (die Form eines sevDesk-Tokens, das der Server im
OAuth-Modus nicht im Voraus kennt). Sicherheitsereignisse werden als Warnungen mit Namen
protokolliert: refresh_replay und code_replay (ein Token wurde zweimal verwendet; der Zugang
wurde widerrufen), account_refused (Zustimmung mit einem Konto, das nicht auf der Liste steht),
cimd_blocked (ein Metadaten-Dokument eines Clients wurde abgelehnt) und grant_revoked (mit
seinem Grund, zum Beispiel account_removed, wenn eine Erlaubnisliste den Zugang nicht mehr
abdeckt).
Die Browser-Seiten protokollieren eigene Ereignisse. Anmeldungen (gemeinsam für das Postfach und
die Dateiseiten und nach dem Postfach benannt): approval_login, approval_login_refused,
approval_login_limited, approval_login_unavailable (sevDesk konnte nicht gefragt werden),
approval_logout und approval_session_refused (eine Sitzung eines Kontos, das nicht mehr erlaubt
ist). Dateiübertragung:
| Ereignis | Stufe | Felder |
|---|---|---|
upload_received | info | account, reference, kind, bytes |
upload_refused | warn | reason (rate_limited, foreign_origin, signed_out, unknown, expired, already_received, unsupported_type, length_required, too_large, bad_form, csrf, empty, wrong_kind, storage_full, failed), account nach der Anmeldung |
download_served | info | account, reference, kind, bytes |
download_refused | warn | reason (signed_out, unknown, expired, exhausted), account nach der Anmeldung |
audit_failed | warn | event: ein Dateiereignis konnte nicht in SEVDESK_AUDIT_LOG geschrieben werden |
Ihre Anfragezeilen zeigen die Routen als /uploads/* und /files/*. Upload- und Download-IDs und
Dateinamen werden nie protokolliert; der Referenzcode verbindet eine Logzeile mit der Seite, dem
Ergebnis des Werkzeugs und dem Audit-Log.
Zustandsprüfung und Herunterfahren#
GET /healthz antwortet {"status":"ok"} ohne Authentifizierung und ohne Host-Prüfung, für die
Zustandsprüfungen von Plattformen. Bei SIGTERM antwortet der Server dort mit 503, gibt laufenden
Anfragen 10 Sekunden, löscht dann die Dateien der Uploads und Download-Links, schreibt den
Zugangsspeicher und gibt die Sperre frei.
Fehlersuche#
claude.ai meldet, den Server nicht erreichen oder sich nicht verbinden zu können. claude.ai verbindet sich aus dem Internet; prüfen Sie also von außerhalb Ihres Netzes:
curl https://<host>/healthzantwortet{"status":"ok"};curl https://<host>/.well-known/oauth-protected-resource/mcpliefert JSON mit Ihrer öffentlichen URL;- die URL des Connectors ist genau
SEVDESK_HTTP_PUBLIC_URLplus/mcp; - der Server läuft im OAuth-Modus (im Bearer-Modus antworten die Metadaten-Dokumente mit 404, und claude.ai kann sich nicht verbinden);
- das Zertifikat gilt für den Host, und nichts vor dem Server verlangt eine Anmeldung.
Eine 401-Schleife: Die Zustimmung klappt, dann bittet claude.ai erneut um Verbindung. Das
Access-Token gilt nur für <public URL>/mcp, ist mit dem aktuellen Geheimnis verschlüsselt und
wird gegen den Zugangsspeicher geprüft.
- Die URL des Connectors weicht von der öffentlichen URL ab (anderer Host,
httpstatthttps, ein abschließender Schrägstrich oder ein anderer Pfad): Korrigieren Sie die URL oderSEVDESK_HTTP_PUBLIC_URL. - Das Geheimnis hat sich seit der Zustimmung geändert, oder zwei Instanzen mit verschiedenen Geheimnissen bedienen dieselbe URL.
- Das Zustandsverzeichnis ist nicht dauerhaft, ein Neustart hat die Zugänge also vergessen. Binden Sie dort ein Volume ein.
- Mehr als eine Instanz: Ein Zugang existiert nur im Speicher der Instanz, die ihn angelegt hat.
Anfragen werden wegen Host oder Origin abgelehnt, hinter einem Proxy. Der Server nimmt nur
den Host seiner öffentlichen URL an. Die meisten Proxys (Caddy, Fly, Render, Railway, cloudflared)
reichen den ursprünglichen Host weiter; sendet Ihrer einen eigenen (einen internen Namen wie
localhost:8080), stellen Sie ihn so ein, dass er den ursprünglichen Host weiterreicht, oder
nehmen Sie diesen Namen in SEVDESK_HTTP_ALLOWED_HOSTS auf. Ein Client, der einen anderen
Origin-Header als den öffentlichen Origin sendet, braucht ihn in SEVDESK_HTTP_ALLOWED_ORIGINS;
ein MCP-Client, der in einer Webseite auf einem anderen Origin läuft, kann gar nicht funktionieren,
weil /mcp kein CORS unterstützt. /healthz wird nie wegen seines Hosts abgelehnt. Beide
Ablehnungen antworten mit 403.
Viele Antworten mit 429, oder jede Anfrage mit der IP des Proxys protokolliert. Setzen Sie
hinter einem Proxy SEVDESK_HTTP_TRUST_PROXY auf die Anzahl der Proxys, die etwas an
X-Forwarded-For anhängen; sonst teilen sich alle Clients die Ratenbegrenzung der Adresse des
Proxys. Setzen Sie den Wert nie höher als die tatsächliche Anzahl: Clients könnten sonst ihre
eigene IP-Adresse wählen.
Antworten hängen oder kommen alle auf einmal. Ein Proxy puffert die gestreamten Antworten.
Schalten Sie die Pufferung für den Server ab: Caddy flush_interval -1 (die Compose-Vorlage tut
das), nginx proxy_buffering off.
Uploads scheitern hinter einem Proxy. Die Upload-Seite antwortet mit den eigenen Worten des Servers; alles andere kommt vom Proxy.
- „Size missing“ (411): Der Proxy hat den Upload ohne
Content-Lengthweitergeleitet (in Chunks). Lassen Sie ihn dasContent-Lengthdes Browsers weiterreichen, oder lassen Sie ihn Request-Bodys puffern: nginx tut das standardmäßig (proxy_request_buffering on); Caddy reicht es unverändert weiter. - Eine 413-Seite des Proxys, nicht das „File too large“ des Servers: Die Grenze des Proxys für den
Body liegt unter der des Servers (etwas über 20 MiB). nginx:
client_max_body_size 25m;(Standard 1 MB). Caddy hat keine Grenze, sofern Sie keine setzen. - Eine Zeitüberschreitung (408) bei großen Dateien über eine langsame Verbindung: Der Body muss innerhalb von 90 Sekunden eintreffen, mit durchschnittlich mindestens 16 KB/s und nie 10 Sekunden lang pausieren. Ein Proxy, der Anfragen puffert (nginx standardmäßig), reicht sie mit voller Geschwindigkeit weiter.
Ein Link aus dem Chat zeigt das Anmeldeformular. Der Browser hat keine Sitzung dieses Kontos: Es ist nicht der Browser, in dem Sie zugestimmt haben (die Claude-Apps öffnen Links unter Umständen in ihrem eigenen Browser), die 12 Stunden sind vorbei, Sie haben sich abgemeldet, oder er ist an einem anderen Konto angemeldet. Melden Sie sich in diesem Formular mit dem sevDesk-API-Token des Kontos an; es bringt Sie zur Seite zurück.
„Upload page not found“ oder „File not found“. Der Link ist unbekannt, gehört zu einem anderen sevDesk-Konto, oder der Server wurde inzwischen neu gestartet (ein Neustart macht jeden Upload und jeden Link ungültig). „Expired“ und „used up“ bedeuten, was sie sagen. Bitten Sie das Modell um einen neuen Upload-Link oder darum, die Datei erneut zu erzeugen.
„No space for the file“ (507), oder das Modell meldet, dass die Grenze erreicht ist. Die
Uploads und Links des Kontos füllen sein Viertel von SEVDESK_HTTP_FILES_MAX_MB, die Konten
zusammen füllen den Platz, oder 10 davon sind gültig. Warten Sie, bis ältere ablaufen
(SEVDESK_HTTP_FILES_TTL), oder erhöhen Sie die Grenze. „Server busy“ (503) heißt, dass gerade
zwei andere Uploads eintreffen, „Upload in progress“ (503), dass ein anderer Upload desselben
Kontos eintrifft; versuchen Sie es gleich noch einmal.
Das Modell sagt, der Client könne nicht um Bestätigung gebeten werden. Der Client hat über HTTP
keine Formular-Elicitation (claude.ai zum Zeitpunkt des Schreibens). Setzen Sie
SEVDESK_CONFIRM=browser und verwenden Sie das Freigabe-Postfach.
Die Zustimmungsseite lehnt das Konto oder den Client ab. Sie nennt die Konto-ID oder den
Client-Host; nehmen Sie ihn in SEVDESK_OAUTH_ACCOUNTS oder SEVDESK_OAUTH_CLIENTS auf und
starten Sie neu, wenn Sie ihn zulassen wollen. Einen Eintrag zu entfernen und neu zu starten
widerruft auch die bestehenden Zugänge dieses Kontos oder Clients bei ihrer nächsten Nutzung und
beendet die Browser-Sitzungen des Kontos.
Die Zustimmungsseite meldet zu viele Verbindungen. Der Zugangsspeicher ist voll.
grants list zeigt die Zugänge; widerrufen Sie die, die niemand nutzt.
Das Zustandsverzeichnis ist nicht beschreibbar. Das Image läuft als uid 65532. Ein benanntes
Docker-Volume übernimmt seinen Eigentümer aus dem Image und funktioniert einfach. Ein Bind-Mount
braucht ein Verzeichnis, in das uid 65532 schreiben kann:
sudo install -d -o 65532 -g 65532 -m 700 ./data. Eine Plattform, die Volumes mit root als
Eigentümer einbindet, braucht ihre eigene Lösung: Railway dokumentiert RAILWAY_RUN_UID=0, das den
Container als root ausführt
(deploy/README.md). Ohne beschreibbares
Volume setzen Sie SEVDESK_HTTP_STATE_DIR auf einen leeren Wert: Der Zustand wird dann im
Speicher gehalten, und jeder Neustart meldet alle Clients ab.
Der Start meldet, dass eine andere Instanz das Zustandsverzeichnis nutzt. Nur ein laufender Server darf es nutzen. Nach einem Absturz gilt die alte Sperre nach 30 Sekunden als veraltet, und der nächste Start übernimmt sie. Laufen wirklich zwei (eine zweite Replica oder Machine), reduzieren Sie auf eine. Ein laufender Server, der seine Sperre von einem anderen übernommen findet, vertraut seinem Zugangsspeicher nicht mehr: OAuth-Anfragen scheitern mit einem Serverfehler (Clients behalten ihre Tokens), und der Prozess beendet sich, sodass die Plattform ihn neu startet.
Das Image prüfen#
Jedes Release-Image trägt eine Build-Provenance-Attestierung von GitHub Actions sowie die SBOM und Provenance von BuildKit. So prüfen Sie, dass ein Image aus diesem Repository von seinem Release-Workflow gebaut wurde:
gh attestation verify oci://ghcr.io/jaylann/mcp-server-sevdesk:<version> --owner jaylannSBOM und Provenance lesen:
docker buildx imagetools inspect ghcr.io/jaylann/mcp-server-sevdesk:<version> --format '{{ json .SBOM }}'docker buildx imagetools inspect ghcr.io/jaylann/mcp-server-sevdesk:<version> --format '{{ json .Provenance }}'Legen Sie dann den geprüften Digest (ghcr.io/jaylann/mcp-server-sevdesk@sha256:…) in Ihrer
Bereitstellung fest.
Grenzen#
- Eine Instanz je Zustandsverzeichnis; keine horizontale Skalierung (Eine Instanz).
- Ein Neustart macht ausstehende Freigaben und Bestätigungen sowie jeden Upload und
Download-Link ungültig. Das Modell fragt erneut. Zugänge und Browser-Sitzungen überdauern ihn
mit einem Zustandsverzeichnis (und im Bearer-Modus mit
SEVDESK_HTTP_SECRET); ohne meldet ein Neustart jeden Client ab. - Höchstens 500 Zugänge insgesamt: Darüber hinaus werden neue Zustimmungen abgelehnt, bis
Zugänge widerrufen werden oder ablaufen. Jedes Konto hat außerdem eine kleinere Grenze für gültige
Zugänge: Eine neue Zustimmung darüber hinaus widerruft den ältesten Zugang des Kontos (Grund
superseded), damit ein Konto den Speicher nicht füllen kann. Widerrufene Zugänge bleiben 7 Tage gelistet. Höchstens 1.000 Browser-Sitzungen: Darüber hinaus endet die älteste. - Dateien im OAuth-Modus nur über den Browser. Keine Server-Ordner (
SEVDESK_FILE_DIRS,SEVDESK_OUTPUT_DIR); Belege werden auf den Seiten des Servers hochgeladen und Dateien dort heruntergeladen, was eine Browser-Sitzung des Kontos braucht, und leben höchstensSEVDESK_HTTP_FILES_TTL(Dateien). Ein Upload ist höchstens 20 MiB groß. - Clients ohne Client ID Metadata Documents können den OAuth-Modus nicht nutzen; für sie funktioniert der Bearer-Modus.
- Keine gehostete Instanz. Sie betreiben, was Sie bereitstellen.