SSD Nodes Learn Hosting plans →
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-28

MCP servers draaien op een VPS voor AI agents

Leer hoe u MCP servers op een VPS host via stdio en HTTP. Wij behandelen de configuratie van systemd, TLS en authenticatie om JSON-RPC fouten en beveiligingslekken te voorkomen.

Wat u bouwt

Twee werkende MCP-opstellingen op één VPS. De eerste is een stdio-server, een bestandssysteem- of databasetool die Claude Code als onderliggend proces start en waarmee via een pipe wordt gecommuniceerd. De tweede is een remote HTTP-server die als langlopende netwerkservice draait achter systemd en een nginx reverse proxy met TLS, bereikbaar voor elke MCP-client die u hierop instelt. De installatie voor beide is beperkt. Het grootste deel van deze handleiding richt zich op de twee onderdelen die in de praktijk voor problemen zorgen: het schoonhouden van de JSON-RPC-stroom en het nooit blootstellen van een niet-geauthenticeerd tooleindpunt aan het openbare internet.

Wat MCP in essentie is

Het Model Context Protocol is een standaardmethode voor een AI-client, zoals Claude Code, Claude Desktop, de Gemini CLI op een VPS, of uw eigen script, om externe tools aan te roepen en externe bronnen te lezen. Het model zelf voert niets uit. Het verzoekt de client om actie, de client communiceert via JSON-RPC 2.0 met een MCP-server, en de server voert de tool uit en geeft het resultaat terug. Die client is wat men bedoelt met een agent harness: de lus rondom het model die de lijst met tools, de toegangscontroles en de sessiestatus beheert. MCP is simpelweg de manier waarop u het tool-gedeelte hiervan uitbreidt. Het is één protocol, waardoor een server die u eenmaal schrijft, werkt met elke client die MCP ondersteunt. Als dit onderscheid nieuw voor u is, en zeker als u zich afvraagt hoe een model besluit om een tool te gebruiken, is een stapsgewijs pad door de basisprincipes van agents een uur van uw tijd waard voordat u een van deze servers voorziet van echte inloggegevens.

Er zijn twee transportmethoden, en de rest van deze handleiding is op basis daarvan opgesplitst:

  • stdio. De client start de server als een onderliggend proces en wisselt newline-delimited JSON-RPC-berichten uit via de standaardinvoer en standaarduitvoer. Er is geen netwerk, geen poort en geen authenticatie; de vertrouwensgrens is het proces zelf. Vrijwel elke lokale tool wordt op deze manier geleverd.
  • Streamable HTTP (en de oudere variant, HTTP+SSE). De server is een webdienst die continu actief is. 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 te draaien die permanent op de machine moet blijven draaien.

Kies stdio wanneer de tool bij één machine en één gebruiker hoort. Kies HTTP wanneer het een gedeelde dienst betreft.

Vereisten en de eerlijke valkuilen

Ga uit van een verse Ubuntu 24.04 KVM VPS met root- of sudo-toegang. Daarnaast geldt:

  • Een runtime waarin de server is geschreven. De meeste referentieservers zijn Node of Python. Ubuntu 24.04 levert Node 18, en diverse huidige MCP-pakketten vereisen Node 20 of nieuwer. Installeer daarom een actuele LTS-versie via NodeSource of nvm in plaats van te vertrouwen op apt. Python 3.12 is reeds aanwezig.
  • Een domein en DNS A-record, maar alleen voor de externe HTTP-server; TLS vereist een naam die naar deze VPS verwijst. Het stdio-voorbeeld heeft helemaal geen DNS nodig.
  • 512 MB RAM is ruim voldoende. MCP-servers zijn lichte JSON-RPC-processen; het geheugengebruik wordt bepaald door wat uw tool aanroept (een database-driver, een file-cache), niet door het protocol zelf.
  • De specificatie is jong en in beweging. De revisie van 2025-03-26 verving HTTP+SSE door Streamable HTTP en markeerde SSE als deprecated. SSE werkt nog steeds en veel servers ondersteunen het nog, dus beschouw elke transport-pin als iets dat u moet controleren aan de hand van de release notes van de server, in plaats van als een vaststaand feit.

Stap 1: een stdio-server koppelen aan Claude Code

Begin met de filesystem-server; deze is officieel, wordt actief onderhouden en vereist enkel Node. Het onderstaande commando registreert de server bij Claude Code en koppelt deze aan het huidige project, zodat de configuratie in een committeerbaar bestand wordt opgeslagen:

cd /home/matt/projects/api
claude mcp add --scope project --transport stdio filesystem \
  -- npx -y @modelcontextprotocol/server-filesystem /home/matt/projects/api

Het ---scheidingsteken is essentieel: alles wat daarna komt, is het commando dat Claude Code uitvoert, niet een vlag voor Claude Code zelf. Dit schrijft een .mcp.json naar de root van het project:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/home/matt/projects/api"
      ]
    }
  }
}

Er draait nog niets. Wanneer u de volgende keer Claude Code in deze map 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 list

Een correct werkende server toont het commando en een groen vinkje, filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected. Binnen de sessie toont het /mcp-slash-commando de tools die de server beschikbaar stelt (read_file, write_file, list_directory), en de agent kan deze nu aanroepen op de paden die u heeft toegestaan. Een database-tool werkt op dezelfde wijze; vervang het pakket en geef een connection string mee als laatste argument. Controleer echter de repository van de server voor de actuele pakketnaam, aangezien de referentie-Postgres-server inmiddels meerdere malen van beheerder is gewisseld.

Dit is precies het voordeel van het draaien van de agent op de server zelf: 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. Zodra de agent beschikt over zowel write_file als read_file, is het raadzaam om dit bereik te combineren met een vaardigheid die de agent dwingt tot de kleinst mogelijke werkende wijziging, omdat een filesystem-tool een ingrijpende herschrijving net zo eenvoudig maakt als een correctie van twee regels. Deze koppeling reikt verder dan lokale bestanden: als u al een zoekmachine op de VPS draait, kunt u de agent uw eigen SearXNG-instantie als zoektool geven. Hierdoor blijven de zoekopdrachten op uw eigen server, terwijl de tekst van onbetrouwbare pagina's direct in de context wordt geladen waar de agent vervolgens op acteert.

Stap 2: een externe HTTP-server bouwen

Een stdio-server stopt wanneer het ouderproces wordt beëindigd en wordt per client eenmaal gestart. Als u twee Claude Code-sessies op de machine draait die taken aan elkaar doorgeven, krijgt elk proces een eigen kopie van de tool. Wanneer u een tool wilt die voor elke client actief blijft, zoals een gedeelde ops-tool, een database-gateway of een voorziening die zowel door uw laptop als uw CI wordt aangeroepen, heeft u HTTP-transport en een echte service nodig. Hieronder volgt een minimale Python-server die gebruikmaakt van de officiële SDK en één tool beschikbaar stelt:

# /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 luistert uitsluitend op localhost; niets buiten de machine kan deze direct bereiken. Dit is de gewenste situatie zolang er nog geen authenticatie is geconfigureerd. Installeer de server in een eigen virtualenv zodat systemd over een stabiel pad naar de interpreter beschikt:

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 actief met systemd

Een tool die niet beschikbaar is wanneer de agent deze aanroept, is erger dan helemaal geen tool. Dit is vooral van belang wanneer de client zelf een langlopend proces is: een altijd actieve agent die zijn geheugen en planning behoudt na reboots zal deze tools volgens een schema aanroepen zonder toezicht, dus de server moet ook zelfstandig herstellen. 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.target

Het absolute pad naar de venv Python in ExecStart is niet optioneel; verwijs naar /usr/bin/python3 en het proces start met ModuleNotFoundError: No module named 'mcp', omdat de systeem-interpreter uw pip install nooit heeft gezien. Schakel in en controleer:

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/mcp

status hoort active (running) te tonen. De curl komt terug met HTTP/1.1 400 Bad Request met een JSON-RPC-fout in de body; het verzoek bevatte geen sessie en geen geldige JSON-payload, en dat is precies wat u wilt: het bewijst dat de poort antwoordt en het protocol spreekt. Connection refused of een leeg antwoord betekent dat het proces niet gebonden is aan de plek waar u denkt; lees journalctl -u mcp-ops -n 50.

Stap 4: TLS en een reverse proxy configureren

De server luistert op localhost. Om deze vanaf elke locatie bereikbaar te maken, beëindigt u TLS bij nginx en proxyt u het verkeer naar binnen. Installeer nginx, verkrijg een certificaat via Certbot en Let's Encrypt op nginx, en schrijf vervolgens het location-blok. Het cruciale onderdeel is het uitschakelen van buffering, omdat het standaardgedrag van nginx een antwoord vasthoudt totdat dit volledig is, wat een SSE-stream permanent blokkeert:

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 de configuratie met sudo nginx -t && sudo systemctl reload nginx. Als u al een reeks containers beheert, wordt hetzelfde werk voor u gedaan door een Traefik reverse proxy met automatische TLS; deze geeft het certificaat uit en routeert op basis van hostnaam, waarbij u enkel labels aan de MCP-container hoeft toe te voegen. Hoe dan ook, de reverse proxy is nu het enige onderdeel op een publieke poort en deze wijst naar een service die u nog niet heeft beveiligd. Los dit op voordat u de URL ergens registreert.

Stap 5: de beveiligingsregel die dit onderwerp domineert

Stel nooit een niet-geauthenticeerd MCP-endpoint bloot. 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 hetzelfde bereik als uw AI-agent: zij inventariseren uw tools en roepen deze vervolgens aan. Behandel het exact als een niet-geauthenticeerde admin-socket, want dat is wat het is. Hoeveel schade een gestolen token aanricht, hangt ook af van de server erachter: de read-only MCP-server die wordt meegeleverd met de openGym workout tracker kan alleen trainingsdata teruggeven, terwijl een filesystem- of shell-tool de volledige server overdraagt.

Drie verdedigingslinies, in volgorde van voorkeur:

  1. Publiceer het niet. Houd de server op 127.0.0.1 en bereik deze vanaf uw laptop met een SSH-tunnel: ssh -L 8000:127.0.0.1:8000 matt@vps, en wijs de client vervolgens naar http://127.0.0.1:8000/mcp. Niets wordt ooit blootgesteld.
  2. Plaats het op een privaat netwerk. Bind het tunneladres aan een self-hosted WireGuard VPN en laat alleen VPN-peers het bereiken. Het publieke internet ziet een gesloten poort.
  3. Als het publiek moet zijn, vereis dan een token. Het juiste antwoord is de MCP OAuth-flow die HTTP-transport native ondersteunt. Het pragmatische minimum is een gedeeld bearer-token dat bij de proxy wordt gecontroleerd; dit is goedkoop en stopt drive-by-aanvallen 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 het token met openssl rand -hex 32 en bind de server zelf nooit aan 0.0.0.0 zonder een van deze maatregelen ervoor. De client stuurt het 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 in plaintext in .mcp.json terechtkomt; Claude Code leest ${MCP_TOKEN} tijdens runtime uit de omgeving.

Elke bovenstaande verdediging beveiligt het endpoint in plaats van de agent die het token al bezit, wat de andere helft van het probleem is: als uw client de DeepSeek Harness is, dekken plugins die bepalen welke tools een agent mag aanroepen en tool-output scannen op geïnjecteerde instructies die kant af.

Stap 6: debuggen met de MCP Inspector

Wanneer een server niet naar behoren functioneert, ga dan niet af op aannames vanuit de agent, maar stuur deze direct aan met de Inspector, de officiële webgebaseerde testclient. Voor een stdio-server geeft u hetzelfde commando op als dat de agent uitvoert:

npx @modelcontextprotocol/inspector \
  npx -y @modelcontextprotocol/server-filesystem /tmp

Dit start een UI op http://localhost:6274 (recente versies tonen een URL met een MCP_PROXY_AUTH_TOKEN query-string; gebruik die exacte link, anders weigert de UI de verbinding) en een proxy op 6277. Klik op Connect, vervolgens op List Tools en daarna op Call Tool met de werkelijke argumenten. Als het in de Inspector werkt maar in de agent faalt, dan zit de fout in uw clientconfiguratie en niet in de server. Kies voor de externe HTTP-server het Streamable HTTP transport, voer https://mcp.example.com/mcp in, voeg de Authorization header toe en maak verbinding. Dit is de snelste manier om te verifiëren of authenticatie en de proxy correct zijn voordat er een agent bij betrokken wordt.

Servers up-to-date houden

MCP ontwikkelt zich snel, dus hanteer een vast patchschema. Node-servers die worden gestart met npx -y halen bij elke start de nieuwste versie op; dit is handig, maar niet reproduceerbaar. Pin de exacte versie die u heeft getest, lees deze af uit npm view @modelcontextprotocol/server-filesystem version en voeg deze toe aan de pakketnaam in .mcp.json (@modelcontextprotocol/server-filesystem@<version>) zodra een server in productie is. Verhoog deze versie vervolgens bewust. 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. Controleer bij het upgraden welke spec-revisie uw SDK hanteert; een overstap over de grens van SSE naar Streamable-HTTP kan het transportprotocol wijzigen dat uw clients moeten aanvragen.

Foutmodi en bijbehorende meldingen

De agent geeft aan dat de server is mislukt. claude mcp list print ✗ Failed to connect en de TUI rapporteert MCP server 'filesystem' failed to start. Voer claude --debug uit; u ziet doorgaans Error: spawn npx ENOENT, wat betekent dat het commando niet in het PATH van de agent staat. De runtime ontbreekt of staat niet op de locatie waar de agent zoekt: Node is niet geïnstalleerd, npx ontbreekt, of er wordt verwezen naar een virtualenv Python via een relatieve naam. Corrigeer het commando naar een absoluut pad of installeer de runtime en maak opnieuw verbinding.

Een stdio-server maakt verbinding, maar verbreekt deze direct. De client logt een JSON-parsefout, zoals Unexpected token 'S', "Server sta"... is not valid JSON of Failed to parse message. De oorzaak is altijd hetzelfde: de server schreef een logregel naar stdout. Bij stdio is stdout het JSON-RPC-kanaal; elke willekeurige 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 geef file=sys.stderr mee. De regel is absoluut: bij stdio mag alleen JSON-RPC naar stdout, alle menselijk leesbare tekst moet naar stderr.

Een externe server bereikt een timeout of sluit de verbinding tijdens de handshake. De client faalt met MCP error -32000: Connection closed, of de Inspector blijft hangen op Connect zonder tools te tonen. Achter nginx wordt dit veroorzaakt door buffering: de proxy houdt de SSE-stream vast in plaats van deze direct door te sturen, waardoor de client wacht op een antwoord dat nooit aankomt. Voeg proxy_buffering off; (en de rest van het blok in Stap 4) toe aan de location. Controleer dit met curl -N tegen de publieke URL; u hoort gebeurtenisgegevens incrementeel te zien binnenkomen, niet alles tegelijk aan het einde.

Authenticatie wordt geweigerd. De client rapporteert Error POSTing to endpoint (HTTP 401) of simpelweg 401 Unauthorized. Ofwel de header ontbreekt, het token is onjuist, of de shell-variabele was leeg toen de client de configuratie las. Dit is een veelvoorkomende valkuil, aangezien ${MCP_TOKEN} uitbreidt naar niets als de variabele niet is ingesteld, waardoor nginx Bearer ziet zonder waarde. Echo de variabele, voeg de header opnieuw toe en verifieer of de exacte bytes overeenkomen met het token in de nginx if.

De service start niet onder systemd. journalctl -u mcp-ops toont ModuleNotFoundError: No module named 'mcp', waarbij ExecStart naar de systeem-Python wijst in plaats van naar de venv-interpreter. Of Address already in use treedt op omdat een ander proces poort 8000 bezet houdt; zoek dit proces met sudo ss -ltnp | grep 8000.

FAQ

Wat is precies een MCP server?

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 verzoekt de client, de client roept de MCP server aan, en de server voert de actie uit en retourneert een resultaat. Omdat het protocol een standaard is, werkt één server met elke compatibele client, of dat nu Claude Code, Claude Desktop of de Gemini CLI is.

Wat is het verschil tussen stdio en HTTP transport?

Een stdio-server wordt door de client gestart als een onderliggend proces en communiceert via stdin/stdout. Deze leeft en sterft dus samen met één client op één machine en vereist geen netwerk of authenticatie. Een HTTP-server is een langlopende netwerkservice die door meerdere clients tegelijk kan worden bereikt; daarom vereist deze TLS en authenticatie. Gebruik stdio voor lokale tools voor één gebruiker; gebruik HTTP (Streamable HTTP op huidige servers) voor alles wat gedeeld of persistent is.

Hoe beveilig ik een externe MCP server?

Ga ervan uit dat de server toegang verleent tot uw bestanden, database of shell, en stel deze nooit bloot zonder authenticatie. Het is het beste om de server gebonden te houden aan localhost en deze te bereiken via een SSH-tunnel of een privé-VPN; 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 het token met openssl rand -hex 32 en bind de server nooit aan 0.0.0.0 zonder een van deze beveiligingslagen ervoor.

Hoe debug ik een server die niet wil starten?

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 tot stand komt maar direct wordt verbroken met een JSON-parsefout, logt de server naar stdout en raakt de JSON-RPC-stroom corrupt; verplaats alle logging naar stderr. Voor alle andere gevallen voert u het exacte commando uit in de MCP Inspector, die de server in isolatie aanstuurt, zodat u een serverfout kunt onderscheiden van een client-configuratiefout.