MCP servers draaien op een VPS
Leer hoe u MCP servers via stdio en HTTP op een VPS installeert. Wij behandelen systemd, TLS en het beveiligen van JSON-RPC streams voor AI agents.
Wat u bouwt
Twee werkende MCP-installaties op één VPS. Eerst een stdio-server — een filesystem- of database-tool die Claude Code start als child process en waarmee wordt gecommuniceerd via een pipe. Daarna een remote HTTP-server die draait als een langdurige netwerkdienst via systemd en een nginx reverse proxy met TLS. Deze is bereikbaar voor elke MCP-client die u eraan koppelt. De installatie van beide is beperkt. Het grootste deel van deze handleiding behandelt de twee kritieke punten: het schoonhouden van de JSON-RPC-stream en het voorkomen dat een niet-geauthenticeerde tool-endpoint op het publieke internet wordt geplaatst.
Wat MCP precies is
Het Model Context Protocol is een standaardmethode waarmee een AI-client — zoals Claude Code, Claude Desktop, de Gemini CLI op een VPS of uw eigen script — externe tools aanroept en externe resources uitleest. Het model zelf voert niets uit. Het stelt een vraag aan de client, de client communiceert via JSON-RPC 2.0 met een MCP server, en de server voert de tool uit en geeft het resultaat terug. Door dit ene protocol werkt een eenmalig geschreven server met elke client die MCP ondersteunt.
Er zijn twee transportsystemen, en de rest van deze handleiding is hierop gebaseerd:
- stdio. De client start de server als een child process en wisselt JSON-RPC-berichten uit die door middel van newline-delimited worden gescheiden via de standard input en standard output. Geen netwerk, geen poort, geen authenticatie — de vertrouwensgrens is het proces zelf. Bijna elke lokale tool wordt op deze manier geleverd.
- Streamable HTTP (en de oudere variant, HTTP+SSE). De server is een webservice die continu draait. De client maakt verbinding via HTTP en de server kan antwoorden streamen als Server-Sent Events. Dit is de methode om één server met meerdere clients te delen, of om een tool uit te voeren die permanent op een systeem moet blijven draaien.
Gebruik stdio wanneer de tool specifiek voor één machine en één gebruiker is. Gebruik HTTP wanneer het een gedeelde service betreft.
Vereisten en belangrijke aandachtspunten
Ga uit van een schone Ubuntu 24.04 KVM VPS met root- of sudo-rechten. Verder het volgende:
- Een runtime waarin de server is geschreven. De meeste referentie-servers gebruiken Node of Python. Ubuntu 24.04 bevat Node 18, maar veel huidige MCP-packages vereisen Node 20 of nieuwer. Installeer daarom een huidige LTS via NodeSource of nvm in plaats van te vertrouwen op
apt. Python 3.12 is reeds aanwezig. - Een domein en een DNS A-record, maar alleen voor de remote HTTP-server — TLS vereist een naam die naar deze VPS verwijst. Het stdio-voorbeeld heeft geen DNS nodig.
- 512 MB RAM is voldoende. MCP-servers zijn lichte JSON-RPC-processen; het geheugengebruik wordt bepaald door de tools die u gebruikt (zoals een database driver of een file cache), niet door het protocol zelf.
- De specificatie is nieuw en in ontwikkeling. De revisie van 2025-03-26 heeft HTTP+SSE vervangen door Streamable HTTP en heeft SSE als deprecated gemarkeerd. SSE werkt nog steeds en veel servers ondersteunen het nog steeds. Controleer daarom transportmethoden altijd in de release notes van de server in plaats van blind te varen op de huidige status.
Stap 1: verbind een stdio-server met Claude Code
Begin met de filesystem-server — deze is officieel, wordt actief onderhouden en vereist alleen Node. Het onderstaande commando registreert de server bij Claude Code en beperkt de scope tot het huidige project, zodat de configuratie in een committable bestand terechtkomt:
cd /home/matt/projects/api
claude mcp add --scope project --transport stdio filesystem \
-- npx -y @modelcontextprotocol/server-filesystem /home/matt/projects/apiDe -- scheiding is belangrijk: alles na deze marker is het commando dat Claude Code uitvoert, geen flag voor Claude Code. Dit schrijft een .mcp.json naar de projectroot:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/home/matt/projects/api"
]
}
}
}Er draait nog niets. Wanneer u Claude Code de volgende keer in deze directory start, leest de agent .mcp.json, start npx -y @modelcontextprotocol/server-filesystem ... als een child process, en voert de MCP-handshake uit via de stdin/stdout van dat proces. Controleer of dit is gelukt:
claude mcp listEen werkende server printt het commando en een groen vinkje — filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected. Binnen de sessie vermeldt het /mcp slash-commando de tools die de server beschikbaar stelt (read_file, write_file, list_directory). De agent kan deze nu aanroepen op de paden die u heeft toegestaan. Een database-tool werkt op dezelfde manier — vervang het package en geef een connection string als laatste argument — maar controleer de repository van de server voor de huidige package-naam, aangezien de referentie Postgres-server meerdere malen is gewisseld.
Dit is het hoofddoel van het uitvoeren van de agent op de machine: de Claude Code sessie draait op de VPS binnen tmux, en de stdio-servers draaien direct daarnaast met directe toegang tot de projectbestanden en lokale services, zonder netwerkvertraging.
Stap 2: een remote HTTP-server bouwen
Een stdio-server stopt wanneer het parent-proces eindigt. Als u een tool nodig heeft die beschikbaar blijft voor elke client — zoals een gedeelde ops-tool, een database-gateway, of iets wat zowel uw laptop als uw CI aanroept — dan heeft u de HTTP-transportlaag en een echte service nodig. Hieronder vindt u een minimale Python-server die de officiële SDK gebruikt en één tool exposeert:
# /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")Let op host="127.0.0.1". De server is alleen gebonden aan localhost. Externe systemen kunnen de server niet direct bereiken, wat gewenst is voordat er authenticatie is ingesteld. Installeer de server in een eigen virtualenv zodat systemd een stabiel pad naar de interpreter heeft:
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]"Stap 3: houd het proces actief met systemd
Een tool die niet reageert wanneer de agent deze aanroept, is slechter dan geen tool. Schrijf /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.targetHet absolute pad naar de venv Python in ExecStart is verplicht — wijs naar /usr/bin/python3 en het proces start met ModuleNotFoundError: No module named 'mcp', omdat de systeem-interpreter uw pip install niet herkent. Inschakelen en controleren:
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 moet active (running) tonen. De curl geeft HTTP/1.1 400 Bad Request terug met een JSON-RPC error in de body — de aanvraag bevatte geen sessie en geen geldige JSON payload — en dat is precies wat u wilt: het bewijst dat de poort reageert en het protocol spreekt. Connection refused of een lege reactie betekent dat het proces niet gebonden is aan de verwachte locatie; lees journalctl -u mcp-ops -n 50.
Stap 4: voeg TLS en een reverse proxy toe
De server luistert op localhost. Om de server vanaf elke locatie te bereiken, moet u TLS beëindigen bij nginx en het verkeer intern doorsturen. Installeer nginx, verkrijg een certificaat met Certbot en Let's Encrypt op nginx, en configureer de location block. Het is essentieel om buffering uit te schakelen. Het standaardgedrag van nginx is namelijk om een response vast te houden tot deze volledig is. Dit blokkeert een SSE-stream permanent:
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;
}
}Herlaad met sudo nginx -t && sudo systemctl reload nginx. Als u al een vloot van containers gebruikt, kan dit werk automatisch worden uitgevoerd door een Traefik reverse proxy met automatische TLS — deze genereert het certificaat en routeert op basis van de hostname. U hoeft dan alleen labels toe te voegen aan de MCP container. In beide gevallen is de reverse proxy nu de enige component op een publieke port, en deze verwijst naar een service die nog niet beveiligd is. Los dit op voordat u de URL ergens registreert.
Stap 5: de beveiligingsregel die dit onderwerp domineert
Exposeer nooit een ongeauthenticeerd MCP-endpoint. Een MCP-server is geen read-only API. Het verleent toegang tot tools — tot uw bestanden, uw database, en soms een shell. Een open /mcp op het publieke internet is een vreemde met dezelfde reikwijdte als uw AI-agent: zij listar uw tools en roept deze vervolgens aan. Behandel dit exact als een ongeauthenticeerde admin socket, want dat is het.
Drie verdedigingsmethoden, in volgorde van voorkeur:
- Publiceer het niet. Houd de server op
127.0.0.1en bereik deze vanaf uw laptop via een SSH-tunnel:ssh -L 8000:127.0.0.1:8000 matt@vps, en wijs de client vervolgens naarhttp://127.0.0.1:8000/mcp. Er wordt niets blootgesteld. - Plaats het op een privénetwerk. Bind het tunneladres van een self-hosted WireGuard VPN en laat alleen VPN-peers er toegang toe hebben. Het publieke internet ziet een gesloten poort.
- Als het publiek moet zijn, vereis dan een token. De juiste oplossing is de MCP OAuth-flow die HTTP-transport natief ondersteunt. Het pragmatische minimum is een gedeelde bearer token die door de proxy wordt gecontroleerd — dit is eenvoudig en het voorkomt ongeautoriseerde toegang volledig:
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...
}Genereer de token met openssl rand -hex 32, en bind de server nooit direct aan 0.0.0.0 zonder een van deze methoden ervoor. De client stuurt de token vervolgens als een header. In Claude Code:
claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'Stel MCP_TOKEN in uw shell in, zodat het geheim nooit als plaintext in .mcp.json terechtkomt — Claude Code expandt ${MCP_TOKEN} vanuit de omgeving op het moment van lezen.
Stap 6: debuggen met de MCP Inspector
Wanneer een server niet correct functioneert, moet u niet gissen vanuit de agent. Gebruik de Inspector, de officiële webgebaseerde testclient, om de server direct aan te sturen. Gebruik voor een stdio-server exact dezelfde opdracht als de agent gebruikt:
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmpDit start een UI op http://localhost:6274 (recente versies tonen een URL met een MCP_PROXY_AUTH_TOKEN query string — gebruik exact die link, anders wordt de toegang geweigerd) en een proxy op 6277. Klik op Connect, selecteer vervolgens List Tools en gebruik daarna Call Tool met de juiste argumenten. Als de verbinding werkt in de Inspector maar faalt in de agent, dan zit de fout in uw clientconfiguratie en niet in de server. Kies voor een remote HTTP-server de transportoptie Streamable HTTP, voer https://mcp.example.com/mcp in, voeg de Authorization header toe en maak verbinding. Dit is de snelste methode om te verifiëren of de authenticatie en de proxy correct zijn voordat u de agent gebruikt.
Servers up-to-date houden
MCP ontwikkelt zich snel, dus werk volgens een schema met patches. Node servers die zijn gestart met npx -y halen bij elke spawn de nieuwste versie op. Dit is handig, maar niet reproduceerbaar. Leg de exacte versie vast die u heeft getest. Lees deze versie uit npm view @modelcontextprotocol/server-filesystem version en voeg deze toe aan de pakketnaam in .mcp.json (@modelcontextprotocol/server-filesystem@<version>). Dit is noodzakelijk zodra een server in productie is; voer updates bewust uit. Python servers onder systemd worden bijgewerkt met sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]" gevolgd door sudo systemctl restart mcp-ops. Let op de spec-revisie die uw SDK gebruikt bij een upgrade. Een overstap van de SSE-naar-Streamable-HTTP-grens kan de transportmethode veranderen die uw clients moeten aanvragen.
Foutmodi, met de strings die u zult zien
De agent geeft aan dat de server is mislukt. claude mcp list printt ✗ Failed to connect, en de TUI rapporteert MCP server 'filesystem' failed to start. Voer claude --debug uit; u ziet meestal Error: spawn npx ENOENT — het commando staat niet in het PATH van de agent. De runtime ontbreekt of bevindt zich niet op de locatie waar de agent zoekt: Node is niet geïnstalleerd, npx ontbreekt, of er wordt verwezen naar een Python virtualenv met alleen de naam. Gebruik een absoluut pad voor het commando of installeer de runtime, en verbind opnieuw.
Een stdio-server maakt verbinding, maar verbreekt deze direct. De client logt een JSON parse error — bijvoorbeeld Unexpected token 'S', "Server sta"... is not valid JSON of Failed to parse message. De oorzaak is altijd hetzelfde: de server heeft een logregel naar stdout geschreven. Bij stdio is stdout de JSON-RPC-kanaal; elke extra tekst corrumpeert de stream en de handshake mislukt. In Node gaat console.log naar stdout — gebruik console.error. In Python gaat een standaard print() naar stdout — schrijf logs met logging geconfigureerd naar sys.stderr, of gebruik file=sys.stderr. De regel is strikt: bij stdio mag alleen JSON-RPC naar stdout, alle tekst voor mensen moet naar stderr.
Een remote server geeft een timeout of sluit tijdens de handshake. De client faalt met MCP error -32000: Connection closed, of de Inspector blijft hangen op Connect en toont geen tools. Achter nginx is dit een buffering-probleem: de proxy houdt de SSE-stream vast in plaats van deze direct door te sturen, waardoor de client wacht op een antwoord dat nooit komt. Voeg proxy_buffering off; toe (en de rest van het blok in Stap 4) aan de location. Controleer dit met curl -N via de publieke URL — u moet de event-data stapsgewijs zien binnenkomen, niet allemaal tegelijk aan het einde.
Authenticatie wordt geweigerd. De client rapporteert Error POSTing to endpoint (HTTP 401) of simpelweg 401 Unauthorized. De header ontbreekt, de token is onjuist, of de shell-variabele was leeg toen de client de configuratie las — een veelvoorkomende fout, aangezien ${MCP_TOKEN} leeg blijft als de variabele niet is ingesteld en nginx vervolgens Bearer zonder waarde ziet. Controleer de variabele met echo, voeg de header opnieuw toe en verifieer of de bytes exact overeenkomen met de token in de nginx if.
De service start niet onder systemd. journalctl -u mcp-ops toont ModuleNotFoundError: No module named 'mcp' — ExecStart verwijst naar de systeem-Python in plaats van de venv-interpreter. Of Address already in use — een ander proces gebruikt poort 8000; zoek dit proces met sudo ss -ltnp | grep 8000.
FAQ
Wat is een MCP server precies?
Het is een programma dat tools en resources beschikbaar stelt aan een AI-client via het Model Context Protocol, gebruikmakend van JSON-RPC 2.0. Het AI-model voert de tool nooit zelf uit. Het vraagt de tool aan de client, de client roept de MCP server aan, en de server voert de actie uit en retourneert het resultaat. Omdat het protocol een standaard is, werkt één server met elke compatibele client, zoals Claude Code, Claude Desktop of de Gemini CLI.
Wat is het verschil tussen stdio en HTTP transport?
Een stdio server wordt door de client gestart als een child process en communiceert via stdin/stdout. De server is verbonden aan één client op één machine en heeft geen netwerk of authenticatie nodig. Een HTTP server is een netwerkdienst die continu draait en door meerdere clients tegelijk kan worden bereikt. Daarom is TLS en authenticatie vereist. Gebruik stdio voor lokale tools voor één gebruiker; gebruik HTTP (Streamable HTTP op huidige servers) voor gedeelde of persistente omgevingen.
Hoe beveilig ik een remote MCP server?
Een server heeft toegang tot uw bestanden, database of shell; exposeer deze daarom nooit zonder authenticatie. De beste methode is om de server te binden aan localhost en via een SSH-tunnel of een private VPN toegang te verlenen. Als de server publiek toegankelijk moet zijn, plaats deze dan achter een reverse proxy die een bearer token of de MCP OAuth flow afdwingt. Genereer de token met openssl rand -hex 32 en bind de server nooit aan 0.0.0.0 zonder een van deze beveiligingen.
Hoe debug ik een server die niet start?
Controleer eerst claude mcp list. ✗ Failed to connect met spawn ... ENOENT betekent dat het commando of de runtime ontbreekt; corrigeer het pad of installeer de software. Als de verbinding wordt verbroken met een JSON parse error, dan logt de server naar stdout en wordt de JSON-RPC stream corrupt gemaakt. Verplaats alle logging naar stderr. Gebruik voor andere problemen het exacte commando in de MCP Inspector. Deze tool draait de server in isolatie, zodat u een bug in de server kunt onderscheiden van een fout in de client-configuratie.