Jak uruchomić serwery MCP na VPS
Instrukcja konfiguracji serwerów stdio oraz remote HTTP przez systemd i nginx. Dowiedz się, jak zapewnić bezpieczeństwo TLS oraz autoryzację połączeń JSON-RPC.
Cel prac
Konfiguracja dwóch działających środowisk MCP na jednym VPS. Pierwszym jest serwer stdio — narzędzie do systemu plików lub bazy danych, które Claude Code uruchamia jako proces potomny i komunikuje się z nim za pomocą potoku (pipe). Drugim jest serwer remote HTTP, działający jako długotrwała usługa sieciowa w systemd, umieszczona za odwróconym proxy nginx z TLS; jest on dostępny dla każdego klienta MCP, który zostanie do niego skierowany. Proces instalacji obu rozwiązań jest krótki. Większość tego poradnika dotyczy dwóch kluczowych kwestii: utrzymania czystości strumienia JSON-RPC oraz unikania wystawiania nieautoryzowanych punktów końcowych narzędzi w publicznym internecie.
Czym w rzeczywistości jest MCP
Model Context Protocol to standardowy sposób, w jaki klient AI — Claude Code, Claude Desktop, Gemini CLI na VPS lub własny skrypt — wywołuje zewnętrzne narzędzia i odczytuje zewnętrzne zasoby. Sam model nie wykonuje żadnych operacji. Model wysyła zapytanie do klienta, klient komunikuje się za pomocą protokołu JSON-RPC 2.0 z serwerem MCP, a serwer wykonuje narzędzie i zwraca wynik. Dzięki jednemu protokołowi serwer napisany raz współpracuje z każdym klientem obsługującym MCP.
Występują dwa typy transportu, a dalsza część przewodnika jest podzielona zgodnie z tym podziałem:
- stdio. Klient uruchamia serwer jako proces potomny i wymienia komunikaty JSON-RPC oddzielone znakami nowej linii za pośrednictwem standardowego wejścia (stdin) i standardowego wyjścia (stdout). Brak sieci, portów oraz autoryzacji — granicą zaufania jest sam proces. Większość lokalnych narzędzi działa w ten sposób.
- Streamable HTTP (oraz starsza wersja, HTTP+SSE). Serwer jest działającym w tle serwisem webowym. Klient łączy się przez HTTP, a serwer może przesyłać odpowiedzi jako Server-Sent Events. Pozwala to na udostępnianie jednego serwera wielu klientom lub uruchamianie narzędzi wymagających stałego działania na maszynie.
Należy wybrać stdio, gdy narzędzie jest przypisane do jednej maszyny i jednego użytkownika. Należy wybrać HTTP, gdy narzędzie jest serwisem współdzielonym.
Wymagania wstępne i istotne uwagi
Należy przyjąć, że systemem jest świeża instancja Ubuntu 24.04 KVM VPS z uprawnieniami root lub sudo. Poza tym:
- Środowisko uruchomieniowe, w którym napisano serwer. Większość serwerów referencyjnych korzysta z Node lub Python. Ubuntu 24.04 zawiera Node 18, natomiast wiele aktualnych pakietów MCP wymaga wersji Node 20 lub nowszej. Zaleca się instalację aktualnej wersji LTS z NodeSource lub nvm zamiast polegania na
apt. Python 3.12 jest już zainstalowany. - Domena i rekord DNS typu A, lecz tylko dla zdalnego serwera HTTP — TLS wymaga nazwy rozwiązującej się na ten VPS. Przykład oparty na stdio nie wymaga konfiguracji DNS.
- 512 MB RAM jest wartością wystarczającą. Serwery MCP to lekkie procesy JSON-RPC; zużycie pamięci zależy od używanych narzędzi (sterownik bazy danych, pamięć podręczna plików), a nie od samego protokołu.
- Specyfikacja jest nowa i ulega zmianom. Rewizja z 2025-03-26 zastąpiła HTTP+SSE przez Streamable HTTP i oznaczyła SSE jako przestarzałe (deprecated). SSE nadal działa i wiele serwerów nadal go obsługuje, dlatego każdą sztywną konfigurację transportu należy weryfikować z wersją wydawniczą serwera, zamiast przyjmować ją jako ostateczną.
Krok 1: podłączenie serwera stdio do Claude Code
Należy zacząć od serwera filesystem — jest on oficjalny, aktywnie rozwijany i wymaga jedynie środowiska Node. Poniższa komenda rejestruje serwer w Claude Code i ogranicza jego zakres do bieżącego projektu, co skutkuje zapisaniem konfiguracji w pliku gotowym do zatwierdzenia (commit):
cd /home/matt/projects/api
claude mcp add --scope project --transport stdio filesystem \
-- npx -y @modelcontextprotocol/server-filesystem /home/matt/projects/apiSeparator -- jest istotny: wszystko po nim to komenda, którą uruchomi Claude Code, a nie flaga dla Claude Code. Operacja ta tworzy plik .mcp.json w głównym katalogu projektu:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/home/matt/projects/api"
]
}
}
}Żaden proces nie jest jeszcze uruchomiony. Przy następnym uruchomieniu Claude Code w tym katalogu, agent odczyta .mcp.json, uruchomi npx -y @modelcontextprotocol/server-filesystem ... jako proces potomny i przeprowadzi procedurę MCP handshake poprzez stdin/stdout tego procesu. Należy potwierdzić poprawność operacji:
claude mcp listPoprawnie działający serwer wypisuje swoją komendę oraz zielony znacznik — filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected. Wewnątrz sesji polecenie slash /mcp wyświetla narzędzia udostępnione przez serwer (read_file, write_file, list_directory), a agent może teraz wywoływać je w ścieżkach, które zostały zezwolone. Narzędzia bazodanowe działają w ten sam sposób — należy zmienić pakiet i przekazać ciąg połączenia jako ostatni argument — jednak należy sprawdzić aktualną nazwę pakietu w repozytorium serwera, ponieważ referencyjny serwer Postgres zmieniał twórców wielokrotnie.
To jest główny cel uruchamiania agenta na maszynie: sesja Claude Code działa na VPS wewnątrz tmux, a jego serwery stdio działają obok niego, mając bezpośredni dostęp do plików projektu i lokalnych usług, bez opóźnień sieciowych.
Krok 2: budowa zdalnego serwera HTTP
Serwer typu stdio kończy działanie wraz z procesem nadrzędnym. W przypadku narzędzi wymagających ciągłości działania dla każdego klienta — takich jak współdzielone narzędzia operacyjne, bramki bazodanowe czy usługi wywoływane zarówno przez laptopa, jak i CI — wymagany jest protokół HTTP oraz dedykowana usługa. Poniżej znajduje się minimalny serwer Python wykorzystujący oficjalny SDK, udostępniający jedno narzędzie:
# /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")Uwaga host="127.0.0.1". Serwer jest powiązany wyłącznie z localhost — żaden podmiot zewnętrzny nie ma do niego bezpośredniego dostępu, co jest pożądane przed wdrożeniem mechanizmów uwierzytelniania. Należy zainstalować serwer w dedykowanym środowisku virtualenv, aby zapewnić systemowi systemd stabilną ścieżkę do interpretera:
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]"Step 3: utrzymywanie działania za pomocą systemd
Narzędzie niedostępne w momencie próby połączenia przez agenta jest gorsze niż jego brak. Utwórz plik /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.targetPodanie pełnej ścieżki do interpretera Python wewnątrz venv w ExecStart jest obowiązkowe — należy wskazać /usr/bin/python3, aby proces uruchomił się z ModuleNotFoundError: No module named 'mcp'. Standardowy interpreter nie rozpoznaje bibliotek w pip install. Włącz usługę i sprawdź jej status:
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/mcpWynik polecenia status powinien brzmieć active (running). Komenda curl zwróci HTTP/1.1 400 Bad Request wraz z błędem JSON-RPC w treści — zapytanie nie zawierało sesji ani poprawnego ładunku JSON — jest to pożądany rezultat: potwierdza on, że port odpowiada i obsługuje protokół. Connection refused lub pusta odpowiedź oznaczają, że proces nie jest przypisany do oczekiwanego portu; sprawdź journalctl -u mcp-ops -n 50.
Krok 4: Konfiguracja TLS i reverse proxy
Serwer nasłuchuje na localhost. Aby uzyskać do niego dostęp z zewnątrz, należy zakończyć sesję TLS w nginx, a następnie przekazać ruch dalej (proxy). Należy zainstalować nginx, pobrać certyfikat za pomocą Certbot i Let's Encrypt na nginx, a następnie skonfigurować blok location. Kluczowym elementem jest wyłączenie buforowania (buffering). Domyślne zachowanie nginx polega na zatrzymywaniu odpowiedzi do momentu jej pełnego odebrania, co powoduje zawieszenie strumienia SSE:
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;
}
}Należy przeładować konfigurację poleceniem sudo nginx -t && sudo systemctl reload nginx. W przypadku korzystania z wielu kontenerów, zadanie to może wykonać reverse proxy Traefik z automatycznym TLS — rozwiązanie to wystawia certyfikat i przekierowuje ruch na podstawie nazwy hosta, a użytkownik musi jedynie dodać etykiety (labels) do kontenera MCP. W obu przypadkach reverse proxy jest jedynym procesem otwartym na porcie publicznym, a wskazuje ono na usługę, która nie została jeszcze zabezpieczona. Należy to naprawić przed rejestracją adresu URL w jakichkolwiek usługach.
Step 5: reguła bezpieczeństwa kluczowa dla tego zagadnienia
Nigdy nie udostępniaj nieautoryzowanego endpointu MCP. Serwer MCP nie jest API typu read-only. Przyznaje on dostęp do narzędzi — do plików, bazy danych, a czasem do powłoki shell. Otwarty /mcp w publicznym internecie to podmiot o takim samym zakresie uprawnień jak agent AI: może on listować narzędzia, a następnie je wywoływać. Należy traktować go identycznie jak nieautoryzowany socket administracyjny, ponieważ taki jest jego charakter.
Trzy metody obrony, w kolejności według preferencji:
- Nie publikuj serwera. Uruchom serwer na
127.0.0.1i łącz się z niego za pomocą tunelu SSH ze swojego laptopa:ssh -L 8000:127.0.0.1:8000 matt@vps, a następnie skieruj klienta nahttp://127.0.0.1:8000/mcp. Żadne dane nie są wystawione na zewnątrz. - Umieść serwer w sieci prywatnej. Przypisz adres tunelu self-hosted WireGuard VPN i zezwól na dostęp wyłącznie węzłom VPN. Publiczny internet widzi zamknięty port.
- Jeśli serwer musi być publiczny, wymagaj tokena. Prawidłowym rozwiązaniem jest przepływ MCP OAuth, który jest natywnie obsługiwany przez transport HTTP. Pragmatycznym minimum jest współdzielony bearer token sprawdzany przez proxy — rozwiązanie tanie, które całkowicie eliminuje ataki typu drive-by:
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...
}Wygeneruj token za pomocą openssl rand -hex 32 i nigdy nie przypisuj serwera bezpośrednio do 0.0.0.0 bez zastosowania jednej z powyższych metod. Klient przesyła token w nagłówku. W Claude Code:
claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'Ustaw MCP_TOKEN w powłoce shell, aby sekret nie został zapisany w .mcp.json w postaci tekstowej — Claude Code rozwija ${MCP_TOKEN} z zmiennych środowiskowych podczas odczytu.
Step 6: debug with the MCP Inspector
W przypadku błędnego działania serwera nie należy polegać na analizie wewnątrz agenta. Należy użyć Inspector, czyli oficjalnego klienta testowego w przeglądarce. W przypadku serwera stdio należy użyć tej samej komendy, którą uruchamia agent:
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmpInterfejs użytkownika zostanie uruchomiony na http://localhost:6274 (w nowszych wersjach wyświetlany jest adres URL z ciągiem zapytania MCP_PROXY_AUTH_TOKEN — należy użyć dokładnie tego linku, w przeciwnym razie interfejs odrzuci połączenie) oraz proxy na porcie 6277. Należy kliknąć Connect, następnie List Tools, a potem Call Tool z rzeczywistymi argumentami. Jeśli operacja kończy się sukcesem w Inspector, ale nie działa w agencie, błąd znajduje się w konfiguracji klienta, a nie w serwerze. W przypadku zdalnego serwera HTTP należy wybrać transport Streamable HTTP, wprowadzić https://mcp.example.com/mcp, dodać nagłówek Authorization i nawiązać połączenie. Jest to najszybsza metoda weryfikacji poprawności uwierzytelniania oraz proxy przed uruchomieniem agenta.
Aktualizacja serwerów
MCP rozwija się szybko, dlatego należy stosować harmonogram aktualizacji. Serwery Node uruchamiane z npx -y pobierają najnowszą wersję przy każdym uruchomieniu, co jest wygodne, ale uniemożliwia powtarzalność. Należy przypisać konkretną wersję, która została przetestowana — odczytać ją z npm view @modelcontextprotocol/server-filesystem version i dodać do nazwy pakietu w .mcp.json (@modelcontextprotocol/server-filesystem@<version>). Po ustabilizowaniu serwera należy dokonywać aktualizacji świadomie. Serwery Python działające pod systemd aktualizuje się za pomocą sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]", a następnie sudo systemctl restart mcp-ops. Podczas aktualizacji należy monitorować rewizję specyfikacji docelowej dla danego SDK — zmiana między SSE a Streamable-HTTP może zmienić protokół transportowy wymagany przez klientów.
Tryby awarii i widoczne komunikaty
Agent wskazuje na błąd serwera. claude mcp list wypisuje ✗ Failed to connect, a interfejs TUI raportuje MCP server 'filesystem' failed to start. Po uruchomieniu claude --debug zazwyczaj pojawia się Error: spawn npx ENOENT — polecenie nie znajduje się w zmiennej PATH agenta. Środowisko uruchomieniowe brakuje lub znajduje się w innej lokalizacji niż oczekuje agent: Node nie jest zainstalowany, brakuje npx, lub użyto nazwy bezwzględnej dla środowiska Python virtualenv. Należy podać pełną ścieżkę do polecenia lub zainstalować środowisko uruchomieniowe, a następnie nawiązać ponowne połączenie.
Serwer stdio łączy się, a następnie natychmiast zrywa połączenie. Klient loguje błąd parsowania JSON — na przykład Unexpected token 'S', "Server sta"... is not valid JSON lub Failed to parse message. Przyczyna jest zawsze taka sama: serwer wypisał linię logu do stdout. W trybie stdio stdout jest kanałem JSON-RPC, więc dowolny tekst niszczy strumień i przerywa proces handshake. W Node, console.log trafia do stdout — należy użyć console.error. W Python, zwykłe print() trafia do stdout — należy logować za pomocą logging skonfigurowanego na sys.stderr lub przekazać file=sys.stderr. Zasada jest bezwzględna: w trybie stdio na stdout może znajdować się wyłącznie JSON-RPC, a wszystkie komunikaty tekstowe muszą trafiać do stderr.
Serwer zdalny wygasa (timeout) lub zrywa połączenie podczas handshake. Klient zgłasza błąd MCP error -32000: Connection closed lub Inspector zawiesza się na etapie Connect i nie wyświetla listy narzędzi. W przypadku serwera za nginx przyczyną jest buforowanie: proxy zatrzymuje strumień SSE zamiast go wypłukiwać (flush), przez co klient czeka na odpowiedź, która nigdy nie dociera. Należy dodać proxy_buffering off; (oraz pozostałą część bloku z Kroku 4) do location. Należy to zweryfikować za pomocą curl -N pod publicznym adresem URL — dane zdarzeń powinny pojawiać się stopniowo, a nie wszystkie naraz na końcu.
Autoryzacja została odrzucona. Klient raportuje Error POSTing to endpoint (HTTP 401) lub 401 Unauthorized. Przyczyną jest brak nagłówka, błędny token lub pusta zmienna powłoki (shell) w momencie odczytu konfiguracji przez klienta — jest to częsty błąd, ponieważ ${MCP_TOKEN} rozwijają się do pustej wartości, jeśli zmienna nie jest ustawiona, co sprawia, że nginx otrzymuje Bearer bez wartości. Należy wyświetlić zawartość zmiennej, ponownie dodać nagłówek i zweryfikować, czy bajty są identyczne z tokenem w konfiguracji nginx if.
Usługa nie uruchamia się w systemd. journalctl -u mcp-ops wskazuje ModuleNotFoundError: No module named 'mcp' — ExecStart wskazuje na systemowy interpreter Python zamiast interpretera w venv. Lub Address already in use — inny proces zajmuje port 8000; należy go odnaleźć za pomocą sudo ss -ltnp | grep 8000.
FAQ
Czym dokładnie jest serwer MCP?
Jest to program udostępniający narzędzia i zasoby klientowi AI za pomocą Model Context Protocol przy użyciu JSON-RPC 2.0. Model AI nie uruchamia narzędzia bezpośrednio — wysyła zapytanie do klienta, klient wywołuje serwer MCP, a serwer wykonuje operację i zwraca wynik. Dzięki standaryzacji protokołu jeden serwer współpracuje z dowolnym zgodnym klientem, takim jak Claude Code, Claude Desktop lub Gemini CLI.
Jaka jest różnica między transportem stdio a HTTP?
Serwer stdio jest uruchamiany przez klienta jako proces potomny i komunikuje się przez stdin/stdout. Działa on tylko w ramach jednej sesji klienta na danej maszynie i nie wymaga sieci ani uwierzytelniania. Serwer HTTP to działająca w tle usługa sieciowa, do której może uzyskać dostęp wielu klientów jednocześnie, dlatego wymaga TLS oraz uwierzytelniania. Stosuj stdio dla lokalnych narzędzi przeznaczonych dla jednego użytkownika; stosuj HTTP (Streamable HTTP w obecnych serwerach) dla rozwiązań współdzielonych lub trwałych.
Jak zabezpieczyć zdalny serwer MCP?
Serwer może zapewniać dostęp do plików, baz danych lub powłoki (shell), dlatego nigdy nie należy udostępniać go bez uwierzytelniania. Najlepiej ograniczyć serwer do localhost i łączyć się z nim przez tunel SSH lub prywatną sieć VPN. Jeśli serwer musi być publiczny, należy umieścić go za reverse proxy wymuszającym użycie bearer token lub przepływu MCP OAuth. Token należy wygenerować za pomocą openssl rand -hex 32 i nigdy nie należy wiązać serwera z 0.0.0.0 bez zastosowania mechanizmu zabezpieczającego.
Jak debugować serwer, który nie chce się uruchomić?
Najpierw sprawdź claude mcp list — błąd ✗ Failed to connect z spawn ... ENOENT oznacza brak polecenia lub środowiska uruchomieniowego, należy zatem poprawić ścieżkę lub zainstalować brakujący komponent. Jeśli połączenie zostaje nawiązane, a następnie zostaje przerwane błędem JSON parse error, oznacza to, że serwer loguje dane na stdout, co niszczy strumień JSON-RPC; należy przenieść wszystkie logi na stderr. W pozostałych przypadkach należy uruchomić dokładnie tę samą komendę w MCP Inspector, który izoluje serwer, co pozwala odróżnić błąd serwera od błędu konfiguracji klienta.