Jak uruchomić serwer MCP na VPS dla agentów AI
Instrukcja konfiguracji serwerów MCP na VPS z wykorzystaniem stdio oraz HTTP. Dowiedz się, jak zarządzać procesami przez systemd, zabezpieczyć TLS i uniknąć błędów JSON-RPC.
Co jest budowane
Dwie działające konfiguracje MCP na jednym VPS. Pierwsza to serwer stdio, narzędzie do obsługi systemu plików lub bazy danych, które Claude Code uruchamia jako proces potomny i z którym komunikuje się przez potok. Druga to zdalny serwer HTTP, działający jako długotrwała usługa sieciowa zarządzana przez systemd oraz odwrotny serwer proxy nginx z TLS, dostępny dla każdego klienta MCP wskazanego w konfiguracji. Instalacja obu rozwiązań jest niewielka. Większość tego przewodnika skupia się na dwóch kluczowych kwestiach: utrzymaniu czystości strumienia JSON-RPC oraz unikaniu udostępniania 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, taki jak Claude Code, Claude Desktop, Gemini CLI na VPS lub własny skrypt, wywołuje zewnętrzne narzędzia i odczytuje zasoby zewnętrzne. Sam model nie uruchamia żadnego kodu. Wysyła on zapytanie do klienta, klient komunikuje się za pomocą JSON-RPC 2.0 z serwerem MCP, a serwer wykonuje narzędzie i zwraca wynik. Klient jest tym elementem, który określa się mianem agent harness: to pętla wokół modelu, która zarządza listą narzędzi, kontrolą uprawnień oraz stanem sesji, a MCP jest po prostu sposobem na rozszerzenie części narzędziowej. Jeden protokół sprawia, że serwer napisany raz współpracuje z każdym klientem obsługującym MCP. Jeśli ten podział jest nowością, a w szczególności kwestia tego, w jaki sposób model decyduje o użyciu narzędzia, warto poświęcić godzinę na etapową ścieżkę przez podstawy działania agentów, zanim przekaże się takiemu serwerowi rzeczywiste dane uwierzytelniające.
Istnieją dwa rodzaje transportu, według których podzielona jest reszta tego przewodnika:
- stdio. Klient uruchamia serwer jako proces potomny i wymienia komunikaty JSON-RPC rozdzielane znakami nowej linii za pośrednictwem standardowego wejścia i wyjścia. Brak sieci, brak portów, brak autoryzacji; granicą zaufania jest sam proces. Prawie każde lokalne narzędzie działa w ten sposób.
- Streamable HTTP (oraz jego starszy odpowiednik, HTTP+SSE). Serwer jest długo działającą usługą sieciową. Klient łączy się przez HTTP, a serwer może przesyłać odpowiedzi strumieniowo za pomocą Server-Sent Events. Jest to sposób na współdzielenie jednego serwera przez wielu klientów lub uruchomienie narzędzia, które musi działać na maszynie w sposób ciągły.
Wybierz stdio, gdy narzędzie przypisane jest do jednej maszyny i jednego użytkownika. Wybierz HTTP, gdy jest to usługa współdzielona.
Wymagania wstępne i istotne uwagi
Przyjęto założenie korzystania z czystej instancji Ubuntu 24.04 na VPS typu KVM z dostępem root lub sudo. Ponadto:
- Środowisko uruchomieniowe dla serwera. Większość serwerów referencyjnych jest napisana w Node lub Python. Ubuntu 24.04 dostarcza Node 18, jednak wiele aktualnych pakietów MCP wymaga wersji Node 20 lub nowszej. Należy zainstalować bieżące wydanie LTS z NodeSource lub nvm, zamiast polegać na
apt. Python 3.12 jest dostępny domyślnie. - Domena i rekord DNS A, ale tylko w przypadku zdalnego serwera HTTP; TLS wymaga nazwy, która wskazuje na ten VPS. Przykład stdio nie wymaga żadnej konfiguracji DNS.
- 512 MB pamięci RAM jest wystarczające. Serwery MCP to lekkie procesy JSON-RPC; zużycie pamięci zależy od wykorzystywanych narzędzi (sterownik bazy danych, pamięć podręczna plików), a nie od samego protokołu.
- Specyfikacja jest młoda i dynamiczna. Wersja z 2025-03-26 zastąpiła HTTP+SSE protokołem Streamable HTTP i oznaczyła SSE jako przestarzałe. SSE nadal działa i wiele serwerów wciąż go obsługuje, dlatego wszelkie ograniczenia dotyczące transportu należy weryfikować w informacjach o wydaniu serwera, zamiast traktować je jako ostateczne.
Krok 1: podłączenie serwera stdio do Claude Code
Należy rozpocząć od serwera systemu plików; jest on oficjalny, aktywnie utrzymywany i wymaga jedynie środowiska Node. Poniższe polecenie rejestruje go w Claude Code i ogranicza jego zasięg do bieżącego projektu, dzięki czemu zapisuje się on w pliku nadającym się do zatwierdzenia w systemie kontroli wersji:
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, co znajduje się po nim, stanowi polecenie wykonywane przez Claude Code, a nie flagę dla samego narzędzia. Powoduje to zapisanie pliku .mcp.json w katalogu głównym projektu:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/home/matt/projects/api"
]
}
}
}W tym momencie nic jeszcze nie działa. Przy kolejnym uruchomieniu Claude Code w tym katalogu, agent odczyta plik .mcp.json, uruchomi npx -y @modelcontextprotocol/server-filesystem ... jako proces potomny i przeprowadzi uzgadnianie MCP za pośrednictwem standardowego wejścia/wyjścia (stdin/stdout) tego procesu. Należy potwierdzić poprawność konfiguracji:
claude mcp listPoprawnie działający serwer wyświetla swoje polecenie oraz zielony znacznik filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected. Wewnątrz sesji polecenie ukośnikowe /mcp wyświetla listę narzędzi udostępnianych przez serwer (read_file, write_file, list_directory), a agent może teraz wywoływać je w ramach dozwolonych ścieżek. Narzędzie bazodanowe działa w ten sam sposób; należy zamienić pakiet i przekazać ciąg połączeniowy jako ostatni argument. Warto jednak sprawdzić repozytorium danego serwera w celu uzyskania aktualnej nazwy pakietu, ponieważ referencyjny serwer Postgres zmieniał już właściciela.
To jest główny cel uruchamiania agenta na serwerze: sesja Claude Code działa na VPS wewnątrz tmux, a serwery stdio pracują bezpośrednio obok niej, mając dostęp do plików projektu i usług lokalnych bez opóźnień sieciowych. Gdy agent dysponuje zarówno write_file, jak i read_file, warto połączyć te możliwości z umiejętnością dążenia do najmniejszej działającej zmiany, ponieważ narzędzie do obsługi systemu plików sprawia, że rozległe przepisanie kodu jest równie łatwe, co poprawka dwuliniowa. To samo połączenie wykracza poza pliki lokalne: jeśli na VPS uruchomiona jest wyszukiwarka, można przekazać agentowi własną instancję SearXNG jako narzędzie wyszukiwania. Dzięki temu zapytania pozostają na serwerze, a tekst z niezaufanych stron trafia bezpośrednio do kontekstu, na którym operuje agent.
Krok 2: budowa zdalnego serwera HTTP
Serwer stdio kończy działanie wraz z procesem nadrzędnym i jest uruchamiany osobno dla każdego klienta. Jeśli uruchomisz dwie sesje Claude Code na tej samej maszynie, które przekazują sobie zadania, każda z nich otrzyma własną, prywatną kopię narzędzia. Jeśli potrzebujesz narzędzia, które działa stale dla każdego klienta, współdzielonego narzędzia operacyjnego, bramy bazodanowej lub rozwiązania wywoływanego zarówno z laptopa, jak i z CI, wymagany jest transport HTTP oraz rzeczywista usługa. Poniżej znajduje się minimalny serwer w języku Python, wykorzystujący oficjalny SDK i 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")Zwróć uwagę na host="127.0.0.1". Serwer wiąże się wyłącznie z localhost, dzięki czemu żaden podmiot spoza maszyny nie może uzyskać do niego bezpośredniego dostępu, co jest pożądanym stanem przed wdrożeniem uwierzytelniania. Zainstaluj go w osobnym środowisku virtualenv, aby systemd dysponował 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]"Krok 3: utrzymanie ciągłości działania za pomocą systemd
Narzędzie, które jest niedostępne w momencie, gdy agent próbuje z niego skorzystać, jest bezużyteczne. Ma to kluczowe znaczenie, gdy klientem jest proces długotrwały: zawsze aktywny agent, który zachowuje stan pamięci i harmonogramy po restartach, będzie wywoływał te narzędzia zgodnie z harmonogramem bez nadzoru, dlatego serwer musi automatycznie powracać do pracy. 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.targetBezwzględna ścieżka do interpretera Python wewnątrz venv w ExecStart nie jest opcjonalna; należy wskazać /usr/bin/python3, aby proces uruchamiał się z ModuleNotFoundError: No module named 'mcp', ponieważ systemowy interpreter nie posiada dostępu do Twojego pip install. Włącz usługę i sprawdź jej stan:
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 powinno zwrócić active (running). curl powróci z HTTP/1.1 400 Bad Request zawierającym błąd JSON-RPC w treści, ponieważ żądanie nie zawierało sesji ani poprawnego ładunku JSON, co jest pożądanym zachowaniem: potwierdza to, że port odpowiada i obsługuje protokół. Connection refused lub pusta odpowiedź oznaczają, że proces nie jest powiązany z portem w oczekiwanym miejscu; sprawdź journalctl -u mcp-ops -n 50.
Krok 4: wdrożenie TLS i reverse proxy
Serwer nasłuchuje na localhost. Aby uzyskać dostęp z zewnątrz, należy zakończyć połączenie TLS na nginx i przekierować ruch do wewnątrz. Zainstaluj nginx, uzyskaj certyfikat zgodnie z Certbot i Let's Encrypt na nginx, a następnie utwórz blok location. Kluczowe jest wyłączenie buforowania, ponieważ domyślne zachowanie nginx polega na wstrzymaniu odpowiedzi do momentu jej pełnego odebrania, co powoduje trwałe zablokowanie 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;
}
}Przeładuj konfigurację za pomocą sudo nginx -t && sudo systemctl reload nginx. Jeśli zarządzasz już zestawem kontenerów, to samo zadanie wykona za Ciebie Traefik reverse proxy z automatycznym TLS, który wystawi certyfikat i skieruje ruch na podstawie nazwy hosta; wystarczy dodać odpowiednie etykiety do kontenera MCP. Niezależnie od wybranej metody, reverse proxy jest teraz jedynym elementem dostępnym na publicznym porcie i wskazuje na usługę, która nie została jeszcze zabezpieczona. Należy to naprawić przed zarejestrowaniem adresu URL w jakimkolwiek publicznym miejscu.
Krok 5: zasada bezpieczeństwa kluczowa dla tego zagadnienia
Nigdy nie udostępniaj nieuwierzytelnionego punktu końcowego MCP. Serwer MCP nie jest interfejsem API typu tylko do odczytu. Przyznaje on dostęp do narzędzi, plików, bazy danych, a czasem nawet do powłoki systemowej. Otwarty /mcp w publicznym Internecie to obca osoba z takimi samymi uprawnieniami, jak Twój agent AI: może wyświetlić listę narzędzi, a następnie je wywołać. Traktuj to dokładnie tak samo, jak nieuwierzytelniony gniazdo administracyjne, ponieważ tym właśnie jest. Zakres szkód wynikający z kradzieży tokena zależy również od serwera: serwer MCP typu tylko do odczytu, dostarczany z trackerem treningów openGym może jedynie zwrócić dane treningowe, podczas gdy narzędzie do obsługi systemu plików lub powłoki przejmuje kontrolę nad maszyną.
Trzy metody ochrony, w kolejności od najbardziej zalecanej:
- Nie publikuj go. Utrzymuj serwer na
127.0.0.1i łącz się z nim ze swojego laptopa za pomocą tunelu SSH:ssh -L 8000:127.0.0.1:8000 matt@vps, a następnie skieruj klienta nahttp://127.0.0.1:8000/mcp. Nic nie jest wystawione na zewnątrz. - Umieść go w sieci prywatnej. Powiąż adres tunelu z samodzielnie hostowanym VPN WireGuard i pozwól na dostęp tylko uczestnikom sieci VPN. Publiczny Internet widzi zamknięty port.
- Jeśli musi być publiczny, wymagaj tokena. Właściwym rozwiązaniem jest przepływ OAuth MCP, który jest natywnie obsługiwany przez transport HTTP. Pragmatycznym minimum jest współdzielony token bearer weryfikowany na poziomie proxy; jest to rozwiązanie tanie i całkowicie blokuje 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 wiąż samego serwera z 0.0.0.0 bez zastosowania jednego z powyższych zabezpieczeń. Klient przesyła wtedy 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 swojej powłoce, aby sekret nigdy nie trafił do .mcp.json w postaci jawnej; Claude Code rozwija ${MCP_TOKEN} ze środowiska w momencie odczytu.
Każda z powyższych metod ochrony zabezpiecza punkt końcowy, a nie agenta, który już posiada token. To druga strona problemu: jeśli Twoim klientem jest DeepSeek Harness, wtyczki ograniczające dostęp agenta do narzędzi oraz skanujące dane wyjściowe narzędzi pod kątem wstrzykniętych instrukcji zabezpieczają ten aspekt.
Krok 6: debugowanie za pomocą MCP Inspector
Gdy serwer działa nieprawidłowo, nie należy zgadywać przyczyn z poziomu agenta; należy sterować nim bezpośrednio za pomocą Inspector, oficjalnego klienta testowego opartego na przeglądarce. W przypadku serwera stdio należy przekazać mu to samo polecenie, które uruchamia agent:
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmpUruchamia to interfejs użytkownika na http://localhost:6274 (nowsze wersje wyświetlają adres URL z ciągiem zapytań 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 narzędzie działa w Inspector, a zawodzi w agencie, błąd leży 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 najszybszy sposób na zweryfikowanie poprawności autoryzacji i proxy przed zaangażowaniem agenta.
Utrzymywanie serwerów w aktualnym stanie
MCP rozwija się szybko, dlatego aktualizacje należy przeprowadzać zgodnie z harmonogramem. Serwery Node uruchamiane za pomocą npx -y pobierają najnowszą wersję przy każdym uruchomieniu, co jest wygodne, ale nie zapewnia powtarzalności. Po ustabilizowaniu serwera należy przypiąć dokładnie przetestowaną wersję, odczytując ją z npm view @modelcontextprotocol/server-filesystem version i dopisując do nazwy pakietu w .mcp.json (@modelcontextprotocol/server-filesystem@<version>), a następnie aktualizować ją w sposób kontrolowany. Serwery Python działające pod kontrolą 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ć wersję specyfikacji, do której odwołuje się SDK; przejście przez granicę między SSE a Streamable-HTTP może zmienić protokół transportowy wymagany przez klientów.
Tryby awarii i towarzyszące im komunikaty
Agent zgłasza błąd serwera. claude mcp list wyświetla ✗ Failed to connect, a TUI raportuje MCP server 'filesystem' failed to start. Uruchom claude --debug; zazwyczaj zobaczysz Error: spawn npx ENOENT, co oznacza, że polecenie nie znajduje się w zmiennej PATH agenta. Środowisko uruchomieniowe jest nieobecne lub znajduje się w innej lokalizacji niż oczekiwana: Node nie jest zainstalowany, brakuje npx lub odwołano się do środowiska virtualenv Python za pomocą samej nazwy. Popraw polecenie na ścieżkę bezwzględną lub zainstaluj środowisko, a następnie połącz się ponownie.
Serwer stdio łączy się i natychmiast rozłącza. 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 zapisał linię logu do stdout. W przypadku stdio, stdout jest kanałem JSON-RPC, więc każdy przypadkowy tekst uszkadza strumień i przerywa handshake. W Node console.log kieruje dane do stdout; użyj console.error. W Pythonie zwykłe print() kieruje dane do stdout; zapisuj logi za pomocą logging skonfigurowanego na sys.stderr lub przekaż file=sys.stderr. Zasada jest bezwzględna: na stdio tylko JSON-RPC na stdout, wszelkie komunikaty dla człowieka na stderr.
Zdalny serwer przekracza limit czasu lub zamyka połączenie w trakcie handshake. Klient zgłasza błąd MCP error -32000: Connection closed lub Inspector zawiesza się na etapie Connect i nie wyświetla narzędzi. Za nginx przyczyną jest buforowanie: proxy przetrzymuje strumień SSE zamiast go wysyłać, więc klient czeka na odpowiedź, która nigdy nie dociera. Dodaj proxy_buffering off; (oraz resztę bloku z kroku 4) do location. Zweryfikuj działanie za pomocą curl -N względem publicznego adresu URL; dane zdarzeń powinny napływać stopniowo, a nie wszystkie naraz na końcu.
Uwierzytelnianie odrzucone. Klient zgłasza Error POSTing to endpoint (HTTP 401) lub po prostu 401 Unauthorized. Nagłówek jest nieobecny, token jest błędny lub zmienna powłoki była pusta w momencie odczytu konfiguracji przez klienta. To częsty błąd, ponieważ ${MCP_TOKEN} rozwija się do zera, jeśli zmienna nie jest ustawiona, a nginx widzi wtedy Bearer bez wartości. Wyświetl zmienną za pomocą echo, dodaj nagłówek ponownie i sprawdź, czy bajty dokładnie odpowiadają tokenowi w pliku if serwera nginx.
Usługa nie uruchamia się pod systemd. journalctl -u mcp-ops pokazuje ModuleNotFoundError: No module named 'mcp', a ExecStart wskazuje na systemowego Pythona zamiast na interpreter z venv. Ewentualnie Address already in use oznacza, że inny proces zajmuje port 8000; znajdź go za pomocą sudo ss -ltnp | grep 8000.
FAQ
Czym dokładnie jest serwer MCP?
Jest to program, który udostępnia narzędzia i zasoby klientowi AI za pośrednictwem protokołu Model Context Protocol, wykorzystując JSON-RPC 2.0. Model AI nigdy nie uruchamia narzędzia samodzielnie; wysyła zapytanie do klienta, klient wywołuje serwer MCP, a serwer wykonuje zadanie i zwraca wynik. Ponieważ protokół jest standardowy, jeden serwer współpracuje z każdym zgodnym klientem, niezależnie od tego, czy jest to Claude Code, Claude Desktop, czy 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, dzięki czemu działa i kończy pracę wraz z jednym klientem na jednej maszynie, nie wymagając sieci ani uwierzytelniania. Serwer HTTP to długo działająca usługa sieciowa, z której może korzystać wielu klientów jednocześnie, dlatego wymaga TLS i uwierzytelniania. Używaj stdio dla lokalnych narzędzi dla jednego użytkownika; używaj HTTP (w obecnych serwerach Streamable HTTP) dla wszystkiego, co jest współdzielone lub trwałe.
Jak zabezpieczyć zdalny serwer MCP?
Należy założyć, że serwer zapewnia dostęp do plików, bazy danych lub powłoki, dlatego nigdy nie należy go udostępniać bez uwierzytelniania. Najlepszą praktyką jest powiązanie go z localhost i dostęp przez tunel SSH lub prywatną sieć VPN. Jeśli serwer musi być publiczny, należy umieścić go za reverse proxy, które wymusza token bearer lub przepływ MCP OAuth. Wygeneruj token za pomocą openssl rand -hex 32 i nigdy nie wiąż serwera z 0.0.0.0 bez zastosowania jednego z tych zabezpieczeń.
Jak debugować serwer, który nie chce się uruchomić?
Najpierw sprawdź claude mcp list, ✗ Failed to connect; jeśli otrzymasz spawn ... ENOENT, oznacza to, że brakuje polecenia lub środowiska uruchomieniowego, więc popraw ścieżkę lub zainstaluj brakujący komponent. Jeśli połączenie zostaje nawiązane, a następnie przerwane z błędem parsowania JSON, serwer prawdopodobnie zapisuje logi do stdout, co uszkadza strumień JSON-RPC; przenieś całe logowanie do stderr. W każdym innym przypadku uruchom dokładnie to samo polecenie w MCP Inspector, który uruchamia serwer w izolacji, co pozwala odróżnić błąd serwera od błędu konfiguracji klienta.