MCP Server auf VPS installieren und betreiben
Lernen Sie, wie Sie MCP-Server via stdio oder remote HTTP auf einem VPS bereitstellen. Inklusive systemd, TLS und nginx für sichere AI-Agent-Tools.
Was Sie aufbauen
Zwei funktionierende MCP-Setups auf einem VPS. Zuerst ein stdio-Server — ein Filesystem- oder Datenbank-Tool, das Claude Code als Child-Prozess startet und über eine Pipe anspricht. Dann ein remote HTTP-Server, der als dauerhafter Netzwerkdienst unter systemd und einem nginx-Reverse-Proxy mit TLS läuft. Dieser ist über jeden MCP-Client erreichbar, den Sie darauf verweisen. Die Installation für beide ist geringfügig. Der Großteil dieser Anleitung befasst sich mit den kritischen Punkten: der sauberen Aufrechterhaltung des JSON-RPC-Streams und der Vermeidung von unauthentifizierten Tool-Endpoints im öffentlichen Internet.
Was MCP tatsächlich ist
Das Model Context Protocol ist ein Standard, mit dem ein AI-Client — Claude Code, Claude Desktop, der Gemini CLI auf einem VPS oder ein eigenes Skript — externe Tools aufrufen und externe Ressourcen lesen kann. Das Modell selbst führt keine Befehle aus. Es stellt dem Client eine Anfrage, der Client sendet JSON-RPC 2.0 an einen MCP server, und der Server führt das Tool aus und gibt das Ergebnis zurück. Durch das einheitliche Protokoll funktioniert ein einmal geschriebener Server mit jedem Client, der MCP unterstützt.
Es gibt zwei Transportprotokolle. Dieser Leitfaden ist danach unterteilt:
- stdio. Der Client startet den Server als Child-Prozess und tauscht JSON-RPC-Nachrichten mit Zeilenumbruch über standard input und standard output aus. Kein Netzwerk, kein Port, keine Authentifizierung — die Vertrauensgrenze ist der Prozess selbst. Fast jedes lokale Tool wird auf diese Weise bereitgestellt.
- Streamable HTTP (und die ältere Variante HTTP+SSE). Der Server ist ein dauerhaft laufender Webdienst. Der Client verbindet sich über HTTP, und der Server kann Antworten als Server-Sent Events streamen. Dies ermöglicht die Nutzung eines Servers durch viele Clients oder den Betrieb eines Tools, das dauerhaft auf einem System laufen muss.
Wählen Sie stdio, wenn das Tool nur für eine Maschine und einen Benutzer bestimmt ist. Wählen Sie HTTP, wenn es sich um einen gemeinsam genutzten Dienst handelt.
Voraussetzungen und bekannte Fallstricke
Gehen Sie von einem frischen Ubuntu 24.04 KVM VPS mit root- oder sudo-Rechten aus. Darüber hinaus:
- Eine Laufzeitumgebung, in der der Server geschrieben wurde. Die meisten Referenzserver nutzen Node oder Python. Ubuntu 24.04 enthält Node 18, aber viele aktuelle MCP-Pakete benötigen Node 20 oder neuer. Installieren Sie daher eine aktuelle LTS-Version von NodeSource oder nvm, anstatt sich auf
aptzu verlassen. Python 3.12 ist bereits vorinstalliert. - Eine Domain und ein DNS A-Record, jedoch nur für den entfernten HTTP-Server — TLS benötigt einen Namen, der auf diesen VPS auflöst. Das stdio-Beispiel benötigt kein DNS.
- 512 MB RAM sind ausreichend. MCP-Server sind schlanke JSON-RPC-Prozesse; der Speicherverbrauch hängt von dem Tool ab, das Sie verwenden (z. B. ein Datenbanktreiber oder ein Dateicache), nicht vom Protokoll.
- Die Spezifikation ist neu und in Entwicklung. Die Revision vom 26.03.2025 hat HTTP+SSE durch Streamable HTTP ersetzt und SSE als veraltet (deprecated) markiert. SSE funktioniert weiterhin und viele Server unterstützen es noch. Betrachten Sie feste Vorgaben zum Transportprotokoll daher als zu prüfende Informationen anhand der Release Notes des jeweiligen Servers und nicht als unumstößliche Fakten.
Schritt 1: Einen stdio-Server in Claude Code einbinden
Beginnen Sie mit dem Filesystem-Server – dieser ist offiziell, wird aktiv gepflegt und benötigt lediglich Node. Der folgende Befehl registriert den Server bei Claude Code und begrenzt den Zugriff auf das aktuelle Projekt, sodass die Konfiguration in einer committbaren Datei gespeichert wird:
cd /home/matt/projects/api
claude mcp add --scope project --transport stdio filesystem \
-- npx -y @modelcontextprotocol/server-filesystem /home/matt/projects/apiDer ---Separator ist wichtig: Alles nach diesem Trenner ist der Befehl, den Claude Code ausführt, und kein Flag für Claude Code. Dadurch wird eine .mcp.json im Projektverzeichnis erstellt:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/home/matt/projects/api"
]
}
}
}Es läuft noch nichts. Wenn Sie Claude Code als Nächstes in diesem Verzeichnis starten, liest der Agent die .mcp.json, startet npx -y @modelcontextprotocol/server-filesystem ... als Child-Prozess und führt den MCP-Handshake über die stdin/stdout des Prozesses aus. Überprüfen Sie die erfolgreiche Verbindung:
claude mcp listEin funktionierender Server gibt seinen Befehl und ein grünes Häkchen aus – filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected. Innerhalb der Session listet der /mcp-Slash-Befehl die vom Server bereitgestellten Tools auf (read_file, write_file, list_directory). Der Agent kann diese nun auf den von Ihnen erlaubten Pfaden aufrufen. Ein Datenbank-Tool folgt demselben Schema – tauschen Sie das Paket aus und übergeben Sie einen Connection-String als letzten Parameter. Prüfen Sie jedoch das Repository des Servers auf den aktuellen Paketnamen, da der Referenz-Postgres-Server bereits mehrfach aktualisiert wurde.
Dies ist der Hauptvorteil der Ausführung des Agenten auf dem Host: Die Claude Code Session läuft auf dem VPS innerhalb von tmux, und die stdio-Server laufen direkt daneben mit direktem Zugriff auf die Projektdateien und lokalen Dienste, ohne Netzwerk-Roundtrip.
Schritt 2: Erstellen eines Remote-HTTP-Servers
Ein stdio-Server wird beendet, wenn der Parent-Prozess endet. Wenn Sie ein Tool benötigen, das für jeden Client aktiv bleibt – etwa ein gemeinsames Ops-Tool, ein Datenbank-Gateway oder ein Dienst, den sowohl Ihr Laptop als auch Ihre CI aufrufen – benötigen Sie den HTTP-Transport und einen echten Service. Hier ist ein minimaler Python-Server unter Verwendung des offiziellen SDKs, der ein Tool bereitstellt:
# /opt/mcp-ops/server.py
from mcp.server.fastmcp import FastMCP
import subprocess
mcp = FastMCP("ops-tools", host="127.0.0.1", port=8000)
@mcp.tool()
def disk_free() -> str:
"""Return `df -h` for the server."""
out = subprocess.run(["df", "-h"], capture_output=True, text=True)
return out.stdout
if __name__ == "__main__":
# Serves Streamable HTTP at /mcp on 127.0.0.1:8000
mcp.run(transport="streamable-http")Beachten Sie host="127.0.0.1". Der Server bindet nur an localhost – von außerhalb des Systems ist er nicht direkt erreichbar. Dies ist vor der Implementierung einer Authentifizierung erwünscht. Installieren Sie den Server in einer eigenen virtualenv, damit systemd einen stabilen Interpreter-Pfad hat:
sudo useradd --system --home /opt/mcp-ops --shell /usr/sbin/nologin mcp
sudo install -d -o mcp -g mcp /opt/mcp-ops
sudo -H -u mcp python3 -m venv /opt/mcp-ops/.venv
sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install "mcp[cli]"Schritt 3: Dauerbetrieb mit systemd sicherstellen
Ein Tool, das nicht reagiert, wenn der Agent darauf zugreift, ist schlechter als gar kein Tool. Erstellen Sie /etc/systemd/system/mcp-ops.service:
[Unit]
Description=MCP ops-tools server
After=network.target
[Service]
Type=simple
User=mcp
WorkingDirectory=/opt/mcp-ops
ExecStart=/opt/mcp-ops/.venv/bin/python /opt/mcp-ops/server.py
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.targetDer absolute Pfad zum Python-Interpreter im venv in ExecStart ist obligatorisch — geben Sie /usr/bin/python3 an. Der Prozess startet sonst mit ModuleNotFoundError: No module named 'mcp', da der System-Interpreter Ihre pip install nicht erkennt. Aktivieren und prüfen Sie:
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-ops
sudo systemctl status mcp-ops
curl -si -H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
-X POST http://127.0.0.1:8000/mcpstatus sollte active (running) ausgeben. Die curl antwortet mit HTTP/1.1 400 Bad Request und einem JSON-RPC-Fehler im Body — die Anfrage enthielt keine Session und keinen gültigen JSON-Payload — und genau das ist das Ziel: Es beweist, dass der Port antwortet und das Protokoll versteht. Connection refused oder eine leere Antwort bedeutet, dass der Prozess nicht am erwarteten Port gebunden ist; lesen Sie journalctl -u mcp-ops -n 50.
Schritt 4: TLS und einen Reverse Proxy vorschalten
Der Server lauscht auf localhost. Um ihn von überall zu erreichen, wird TLS bei nginx terminiert und der Traffic intern weitergeleitet. Installieren Sie nginx, erhalten Sie ein Zertifikat mit Certbot und Let's Encrypt auf nginx und erstellen Sie anschließend den location-Block. Entscheidend ist das Deaktivieren von Buffering. Das Standardverhalten von nginx hält eine Antwort zurück, bis sie vollständig ist. Dies führt dazu, dass ein SSE-Stream dauerhaft blockiert wird:
server {
listen 443 ssl;
server_name mcp.example.com;
# ssl_certificate lines managed by Certbot
location /mcp {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
# The four lines that make SSE work through nginx:
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
chunked_transfer_encoding off;
}
}Mit sudo nginx -t && sudo systemctl reload nginx neu laden. Wenn Sie bereits eine Flotte von Containern betreiben, übernimmt dies ein Traefik reverse proxy mit automatischem TLS für Sie. Dieser stellt das Zertifikat aus und leitet Anfragen basierend auf dem Hostnamen weiter; Sie müssen lediglich Labels zum MCP-Container hinzufügen. In jedem Fall ist der Reverse Proxy nun das einzige Element auf einem öffentlichen Port, und er zeigt auf einen Dienst, der noch nicht abgesichert ist. Beheben Sie dies, bevor Sie die URL irgendwo registrieren.
Schritt 5: Die Sicherheitsregel, die dieses Thema bestimmt
Exponieren Sie niemals einen nicht authentifizierten MCP-Endpunkt. Ein MCP-Server ist keine schreibgeschützte API. Er gewährt Zugriff auf Tools — auf Ihre Dateien, Ihre Datenbank, teilweise auf eine Shell. Ein offener /mcp im öffentlichen Internet ist ein Fremder mit derselben Reichweite wie Ihr KI-Agent: Er listet Ihre Tools auf und führt sie anschließend aus. Behandeln Sie ihn exakt wie einen nicht authentifizierten Admin-Socket, denn genau das ist er.
Drei Abwehrmechanismen, nach Priorität sortiert:
- Veröffentlichen Sie ihn nicht. Behalten Sie den Server auf
127.0.0.1und greifen Sie über einen SSH-Tunnel von Ihrem Laptop aus darauf zu:ssh -L 8000:127.0.0.1:8000 matt@vps, und richten Sie den Client dann aufhttp://127.0.0.1:8000/mcpaus. Es wird nichts exponiert. - Platzieren Sie ihn in einem privaten Netzwerk. Binden Sie die Tunnel-Adresse eines selbst gehosteten WireGuard VPN und erlauben Sie nur VPN-Peers den Zugriff. Das öffentliche Internet sieht lediglich einen geschlossenen Port.
- Falls er öffentlich sein muss, fordern Sie ein Token an. Die korrekte Lösung ist der MCP OAuth-Flow, den der HTTP-Transport nativ unterstützt. Das pragmatische Minimum ist ein Shared Bearer Token, das am Proxy geprüft wird — kostengünstig und es verhindert Drive-by-Angriffe vollständig:
location /mcp {
if ($http_authorization != "Bearer REPLACE_WITH_LONG_RANDOM") {
return 401;
}
proxy_pass http://127.0.0.1:8000;
# ...buffering-off block from above...
}Generieren Sie das Token mit openssl rand -hex 32. Binden Sie den Server niemals ohne eine dieser Lösungen an 0.0.0.0. Der Client sendet das Token anschließend als Header. In Claude Code:
claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'Setzen Sie MCP_TOKEN in Ihrer Shell, damit das Secret niemals im Klartext in .mcp.json landet — Claude Code expandiert ${MCP_TOKEN} zur Laufzeit aus der Umgebungsvariable.
Schritt 6: Debugging mit dem MCP Inspector
Wenn ein Server fehlerhaft arbeitet, versuchen Sie nicht, die Ursache innerhalb des Agents zu erraten. Steuern Sie ihn direkt über den Inspector, den offiziellen webbasierten Testclient. Übergeben Sie einem stdio-Server denselben Befehl, den auch der Agent verwendet:
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmpDies startet eine Benutzeroberfläche auf http://localhost:6274 (neuere Versionen geben eine URL mit einem MCP_PROXY_AUTH_TOKEN Query-String aus – verwenden Sie genau diesen Link, da die UI sonst den Zugriff verweigert) und einen Proxy auf Port 6277. Klicken Sie auf Connect, dann auf List Tools und anschließend auf Call Tool mit den tatsächlichen Argumenten. Wenn der Aufruf im Inspector funktioniert, aber im Agent fehlschlägt, liegt der Fehler in Ihrer Client-Konfiguration und nicht am Server. Wählen Sie für einen Remote-HTTP-Server den Transporttyp Streamable HTTP, geben Sie https://mcp.example.com/mcp ein, fügen Sie den Authorization Header hinzu und klicken Sie auf Connect. Dies ist der schnellste Weg, um die Korrektheit der Authentifizierung und des Proxys ohne Beteiligung eines Agents zu prüfen.
Server aktuell halten
MCP entwickelt sich schnell; führen Sie Patches daher nach einem Zeitplan durch. Node-Server, die mit npx -y gestartet werden, laden bei jedem Start die neueste Version herunter. Das ist praktisch, aber nicht reproduzierbar. Fixieren Sie die exakte Version, die Sie getestet haben. Lesen Sie diese Version aus npm view @modelcontextprotocol/server-filesystem version aus und hängen Sie sie an den Paketnamen in .mcp.json (@modelcontextprotocol/server-filesystem@<version>) an. Dies ist wichtig, sobald ein Server produktiv genutzt wird; führen Sie Updates gezielt durch. Python-Server unter systemd werden durch sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]" gefolgt von sudo systemctl restart mcp-ops aktualisiert. Achten Sie bei einem Upgrade auf die Spezifikationsrevision Ihres SDKs. Ein Wechsel von SSE zu Streamable-HTTP kann das Transportprotokoll ändern, das Ihre Clients anfordern müssen.
Fehlerursachen und die entsprechenden Meldungen
Der Agent meldet einen Serverfehler. claude mcp list gibt ✗ Failed to connect aus und die TUI meldet MCP server 'filesystem' failed to start. Führen Sie claude --debug aus; meist erscheint Error: spawn npx ENOENT — der Befehl befindet sich nicht im PATH des Agents. Die Laufzeitumgebung fehlt oder ist nicht unter dem erwarteten Pfad zu finden: Node ist nicht installiert, npx fehlt oder ein Python-virtualenv wird nur über den Namen aufgerufen. Korrigieren Sie den Befehl zu einem absoluten Pfad oder installieren Sie die Laufzeitumgebung und verbinden Sie sich neu.
Ein stdio-Server stellt eine Verbindung her und bricht sofort ab. Der Client protokolliert einen JSON-Parse-Fehler — etwa Unexpected token 'S', "Server sta"... is not valid JSON oder Failed to parse message. Die Ursache ist immer dieselbe: Der Server hat eine Log-Zeile an stdout gesendet. Bei stdio ist stdout der JSON-RPC-Kanal; jeglicher zusätzliche Text korrumpiert den Stream und bricht den Handshake ab. In Node geht console.log an stdout — nutzen Sie console.error. In Python geht ein einfacher print() an stdout — schreiben Sie Logs mit logging konfiguriert auf sys.stderr oder übergeben Sie file=sys.stderr. Die Regel ist strikt: Bei stdio darf nur JSON-RPC an stdout gesendet werden; alle menschenlesbaren Daten müssen an stderr gesendet werden.
Ein Remote-Server läuft in ein Timeout oder schließt die Verbindung während des Handshakes. Der Client schlägt mit MCP error -32000: Connection closed fehl oder der Inspector bleibt bei Connect hängen und listet keine Tools auf. Hinter nginx liegt dies an Buffering: Der Proxy puffert den SSE-Stream, anstatt ihn sofort auszugeben, sodass der Client auf eine Antwort wartet, die nie ankommt. Fügen Sie proxy_buffering off; (und den restlichen Block aus Schritt 4) zur location hinzu. Überprüfen Sie dies mit curl -N gegen die öffentliche URL — die Event-Daten sollten inkrementell eintreffen und nicht gesammelt am Ende.
Die Authentifizierung wird abgelehnt. Der Client meldet Error POSTing to endpoint (HTTP 401) oder einfach 401 Unauthorized. Entweder fehlt der Header, der Token ist ungültig oder die Shell-Variable war leer, als der Client die Konfiguration gelesen hat — eine häufige Fehlerquelle, da ${MCP_TOKEN} zu nichts expandiert, wenn die Variable nicht gesetzt ist, und nginx dann Bearer ohne Wert sieht. Geben Sie die Variable mit echo aus, fügen Sie den Header erneut hinzu und prüfen Sie, ob die Bytes exakt mit dem Token in der nginx if übereinstimmen.
Der Dienst startet unter systemd nicht. journalctl -u mcp-ops zeigt ModuleNotFoundError: No module named 'mcp' — ExecStart verweist auf das System-Python anstatt auf den Interpreter des venv. Oder Address already in use — ein anderer Prozess belegt Port 8000; finden Sie diesen mit sudo ss -ltnp | grep 8000.
FAQ
Was genau ist ein MCP-Server?
Ein MCP-Server ist ein Programm, das Tools und Ressourcen über das Model Context Protocol mittels JSON-RPC 2.0 für einen KI-Client bereitstellt. Das KI-Modell führt das Tool nicht selbst aus. Es fordert den Client auf, der den MCP-Server aufruft, welcher die Ausführung übernimmt und das Ergebnis zurückgibt. Da das Protokoll standardisiert ist, ist ein Server mit jedem kompatiblen Client nutzbar, wie etwa Claude Code, Claude Desktop oder Gemini CLI.
Was ist der Unterschied zwischen stdio- und HTTP-Transport?
Ein stdio-Server wird vom Client als Child-Prozess gestartet und kommuniziert über stdin/stdout. Er ist an einen Client auf einer Maschine gebunden und benötigt keine Netzwerkverbindung oder Authentifizierung. Ein HTTP-Server ist ein dauerhaft laufender Netzwerkdienst, auf den mehrere Clients gleichzeitig zugreifen können. Daher sind TLS und Authentifizierung erforderlich. Verwenden Sie stdio für lokale Tools für einzelne Benutzer; verwenden Sie HTTP (Streamable HTTP auf aktuellen Servern) für gemeinsam genutzte oder persistente Dienste.
Wie sichere ich einen Remote-MCP-Server ab?
Ein MCP-Server gewährt Zugriff auf Dateien, Datenbanken oder die Shell. Stellen Sie ihn niemals ohne Authentifizierung bereit. Die sicherste Methode ist die Bindung an localhost und der Zugriff über einen SSH-Tunnel oder ein privates VPN. Falls der Server öffentlich erreichbar sein muss, platzieren Sie ihn hinter einem Reverse Proxy, der einen Bearer-Token oder den MCP-OAuth-Flow erzwingt. Generieren Sie den Token mit openssl rand -hex 32. Binden Sie den Server niemals ohne eine dieser Sicherheitsmaßnahmen an 0.0.0.0.
Wie debugge ich einen Server, der nicht startet?
Prüfen Sie zuerst claude mcp list. ✗ Failed to connect mit spawn ... ENOENT bedeutet, dass der Befehl oder die Laufzeitumgebung fehlt; korrigieren Sie den Pfad oder installieren Sie die Komponente. Wenn die Verbindung aufgebaut wird, aber dann ein JSON-Parse-Fehler auftritt, schreibt der Server Logs in den stdout-Stream und korrumpiert so den JSON-RPC-Stream. Leiten Sie alle Logs in den stderr-Stream um. Für alle anderen Fehler führen Sie den exakten Befehl im MCP Inspector aus. Dieser isoliert den Server, sodass zwischen einem Server-Bug und einem Client-Konfigurationsfehler unterschieden werden kann.